diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4127fe8 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +OLLAMARK_API=https://ollamark.com +API_KEY= +PUBLIC_KEY= +KEY= \ No newline at end of file diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 16027bd..5636e7a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,11 +2,13 @@ name: Go on: push: - branches: [ "main" ] + tags: + - 'v*' jobs: build: runs-on: ubuntu-latest + environment: main steps: - uses: actions/checkout@v4 @@ -20,6 +22,13 @@ jobs: sudo apt-get update sudo apt-get install -y libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libgl1-mesa-dev libxi-dev libxext-dev libglu1-mesa-dev xorg-dev + - name: Create .env file + run: | + echo "OLLAMARK_API=${{ secrets.OLLAMARK_API }}" > .env + echo "PUBLIC_KEY=${{ secrets.PUBLIC_KEY }}" >> .env + echo "KEY=${{ secrets.KEY }}" >> .env + shell: bash + - name: Build run: go build -v ./... diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48d2c23..2258074 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,12 +3,12 @@ name: Release on: push: - tags: - - 'v*' + branches: [ "main" ] jobs: release: runs-on: ${{ matrix.os }} + environment: main strategy: matrix: os: [ubuntu-latest, macos-latest] @@ -21,7 +21,9 @@ jobs: go-version: '1.20' # Ensure this is the version you want to use - name: Install fyne-cross - run: go install github.com/fyne-io/fyne-cross@latest + run: | + go install github.com/fyne-io/fyne-cross@latest + fyne-cross -v - name: Install X11 development libraries if: matrix.os == 'ubuntu-latest' @@ -30,28 +32,35 @@ jobs: sudo apt-get install -y libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libgl1-mesa-dev libxi-dev libxext-dev libglu1-mesa-dev xorg-dev - name: Download macOS SDK - if: matrix.os == 'macos-latest' run: | curl -L -o MacOSX13.3.sdk.tar.xz "https://github.com/joseluisq/macosx-sdks/releases/download/13.3/MacOSX13.3.sdk.tar.xz" sudo mkdir -p /opt/MacOSX13.3.sdk sudo tar -xJf MacOSX13.3.sdk.tar.xz -C /opt/MacOSX13.3.sdk + - name: Create .env file + run: | + echo "OLLAMARK_API=${{ secrets.OLLAMARK_API }}" > .env + echo "PUBLIC_KEY=${{ secrets.PUBLIC_KEY }}" >> .env + echo "KEY=${{ secrets.KEY }}" >> .env + shell: bash + - name: Build run: | if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then fyne-cross windows -arch=amd64 -icon icon.png fyne-cross linux -arch=amd64 -icon icon.png - elif [ "${{ matrix.os }}" == "macos-latest" ]; then + # elif [ "${{ matrix.os }}" == "macos-latest" ]; then fyne-cross darwin -arch=arm64 -icon icon.png -app-id com.contextlabs.ollamark fi + shell: bash env: FYNE_CROSS_TARGETS: windows/amd64 linux/amd64 darwin/arm64 FYNE_CROSS_DARWIN_SDK: /opt/MacOSX13.3.sdk SDKROOT: /opt/MacOSX13.3.sdk + PATH: ${{ github.workspace }}/go/bin:$PATH - name: Release uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') with: files: | fyne-cross/bin/windows-amd64/Ollamark.exe diff --git a/.gitignore b/.gitignore index 5df6512..e656432 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,11 @@ ollamark ollamark.exe ollamark.app -.env \ No newline at end of file +.env +private_key.pem +public_key.pem +server/server.exe +server/server + +node_modules +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index e955199..36b9997 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,30 @@ -# Ollamark README +# Ollamark + +### By Carsen Klock (https://twitter.com/carsenklock) for Ollama (https://ollama.com/) Benchmarking! ## Overview -Ollamark and Ollamark CLI is a command-line/UI interface for benchmarking models using the Ollama API. It allows users to specify the model to benchmark, whether to submit the benchmark results, and the API endpoint. +Ollamark and Ollamark CLI is a command-line/UI interface for benchmarking models using the Ollama API. It allows users to specify the model to benchmark, whether to submit and share the benchmark results, the API endpoint, and number of iterations. -**This is a WIP and is subject to change! (Submitting benchmarks is not completed yet.)** +**This is a WIP and is subject to change! Ollamark.com coming soon!** -## Installation +## Building Ensure you have Go installed on your system. Clone the repository and build the project using: ```bash go build ``` +## Installing +- Download and Install Ollama from https://ollama.com/download +- Ollama will start automatically in the background +- Run Ollamark with flags to start the benchmarking process in CLI mode or without flags to run in GUI mode + ## Usage Run the Ollamark CLI using the following flags to customize the benchmarking process: ### Flags - `-m`: Model name to benchmark. Default is `"llama3"`. - `-s`: Submit benchmark results. It accepts a boolean value. Default is `false`. -- `-o`: Ollama API endpoint. Default is `"http://localhost:11434/api/generate"`. +- `-o`: Ollama API endpoint. Default is `"http://localhost:11434"`. - `-i`: Number of iterations to run the benchmark. Default is `2`. - `-h` or `-help`: Display the help message below. @@ -29,7 +36,7 @@ Options: -m string Model name to benchmark (default "llama3") -o string - Ollama API endpoint (default "http://localhost:11434/api/generate") + Ollama API endpoint (default "http://localhost:11434") -s Submit benchmark results to Ollamark (default false) Examples: For Ollamark GUI mode: @@ -37,12 +44,12 @@ Examples: For Ollamark CLI mode: ollamark -m llama3 -i 10 ollamark -m phi3 - ollamark -m phi3 -s -o http://localhost:11434/api/generate + ollamark -m phi3 -s -o http://localhost:11434 ``` ### Example ```bash -./ollamark -m llama3 -s true -i 5 -o "http://localhost:11434/api/generate" +./ollamark -m llama3 -s -i 5 -o "http://localhost:11434" ``` This command will benchmark the model "llama3" for 5 iterations, submit the results, and use the specified API endpoint to interface with Ollama. @@ -50,22 +57,21 @@ This command will benchmark the model "llama3" for 5 iterations, submit the resu ## Configuration The CLI checks for command-line arguments and if provided, Ollamark runs in CLI mode. If no arguments are provided, it defaults to the Ollamark GUI application. - -## Additional Information +## Additional Information for Building/Forking - Ensure the `.env` file is correctly configured as it loads environment variables crucial for the application. - The application can also be run as a Fyne GUI application if no CLI flags are provided. -Example .env file: -``` -API_ENDPOINT=http://localhost:11434/api/generate -API_KEY= -``` ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. ## License This project is licensed under the MIT License - see the LICENSE file for details. +## Author +- Carsen Klock (https://twitter.com/carsenklock) + ## Acknowledgments -- [Ollama](https://ollama.com/) for providing the Ollama API. +- [Ollama](https://ollama.com/) for providing Ollama. +- [GoLang](https://golang.org/) for providing the Go programming language. - [Fyne](https://fyne.io/) for providing the Fyne GUI framework. +- [Gin](https://gin.github.io/) for providing the Gin HTTP web framework. diff --git a/go.mod b/go.mod index df3fa2b..ebf128d 100644 --- a/go.mod +++ b/go.mod @@ -7,32 +7,92 @@ require ( fyne.io/fyne/v2 v2.4.4 // indirect fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect fyne.io/x/fyne v0.0.0-20240326131024-3ba9170cc3be // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/carsenk/gotribushash v0.0.0-20171104193954-ddd27ce24bbf // indirect + github.com/charmbracelet/bubbletea v0.26.4 // indirect + github.com/charmbracelet/lipgloss v0.11.0 // indirect + github.com/charmbracelet/x/ansi v0.1.2 // indirect + github.com/charmbracelet/x/input v0.1.0 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.1.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/didip/tollbooth/v6 v6.1.2 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fredbi/uri v1.0.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 // indirect github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/cors v1.7.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.10.0 // indirect github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-pkgz/expirable-cache v0.0.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-redis/redis v6.15.9+incompatible // indirect github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 // indirect github.com/go-text/typesetting v0.1.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/snappy v0.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect - github.com/stretchr/testify v1.8.4 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/tevino/abool v1.2.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/yuin/goldmark v1.5.5 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.mongodb.org/mongo-driver v1.15.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect golang.org/x/image v0.11.0 // indirect golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect ) diff --git a/go.sum b/go.sum index 94f9cf2..50b6605 100644 --- a/go.sum +++ b/go.sum @@ -53,13 +53,39 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/carsenk/gotribushash v0.0.0-20171104193954-ddd27ce24bbf h1:7f0RKgGSXFqZSwR9PFsnAeG/ehXZ3d8aP++YCVdtkF0= +github.com/carsenk/gotribushash v0.0.0-20171104193954-ddd27ce24bbf/go.mod h1:P0KFshtCcEflPd5mmzFwOV/uS5P+gS5Uf6ySjFOvB/o= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/charmbracelet/bubbletea v0.26.4 h1:2gDkkzLZaTjMl/dQBpNVtnvcCxsh/FCkimep7FC9c40= +github.com/charmbracelet/bubbletea v0.26.4/go.mod h1:P+r+RRA5qtI1DOHNFn0otoNwB4rn+zNAzSj/EXz6xU0= +github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= +github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= +github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= +github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/ansi v0.1.2 h1:6+LR39uG8DE6zAmbu023YlqjJHkYXDF1z36ZwzO4xZY= +github.com/charmbracelet/x/ansi v0.1.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= +github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= +github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -69,6 +95,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/didip/tollbooth/v6 v6.1.2 h1:Kdqxmqw9YTv0uKajBUiWQg+GURL/k4vy9gmLCL01PjQ= +github.com/didip/tollbooth/v6 v6.1.2/go.mod h1:xjcse6CTHCLuOkzsWrEgdy9WPJFv+p/x6v+MyfP+O9s= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -76,6 +106,8 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fredbi/uri v1.0.0 h1:s4QwUAZ8fz+mbTsukND+4V5f+mJ/wjaTokwstGUAemg= github.com/fredbi/uri v1.0.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0= @@ -89,7 +121,15 @@ github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504/go.mod h1:gLRWYfYn github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk= github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0= github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= +github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= @@ -100,10 +140,24 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc= +github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 h1:VkKnvzbvHqgEfm351rfr8Uclu5fnwq8HP2ximUzJsBM= github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8/go.mod h1:h29xCucjNsDcYb7+0rJokxVwYAq+9kQ19WiFuBKkYtc= github.com/go-text/typesetting v0.1.0 h1:vioSaLPYcHwPEPLT7gsjCGDCoYSbljxoHJzMnKwVvHw= github.com/go-text/typesetting v0.1.0/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= @@ -139,6 +193,8 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -170,6 +226,8 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -207,6 +265,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e h1:LvL4XsI70QxOGHed6yhQtAU34Kx3Qq2wwBzGFKY8zKk= @@ -214,14 +274,30 @@ github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e/go.mod h1:kLgvv7o6U github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -232,28 +308,49 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -276,6 +373,8 @@ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqd github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -284,11 +383,28 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -298,9 +414,13 @@ github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU= github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= +go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -311,6 +431,9 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -320,6 +443,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -405,6 +530,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -429,7 +556,10 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -441,6 +571,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -480,8 +611,11 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -494,13 +628,18 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -664,6 +803,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -686,6 +827,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main.go b/main.go index 9ac4138..5faa435 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,34 @@ +// Ollamark By Carsen Klock 2024 under the MIT license +// https://github.com/context-labs/ollamark +// https://ollamark.com +// Ollamark Client + package main import ( "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" "encoding/json" + "encoding/pem" "flag" "fmt" "image/color" "io" "net/http" + "net/url" "os" + "os/exec" + "runtime" + "strconv" + "strings" "time" "fyne.io/fyne/v2" @@ -19,17 +39,27 @@ import ( "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" xwidget "fyne.io/x/fyne/widget" + "github.com/dgrijalva/jwt-go" + "github.com/google/uuid" "github.com/joho/godotenv" + "github.com/shirou/gopsutil/mem" ) type BenchmarkResult struct { - ModelName string `json:"model_name"` - Timestamp int64 `json:"timestamp"` - Duration float64 `json:"duration"` - TokensPerSecond float64 `json:"tokens_per_second"` - EvalCount int `json:"eval_count"` - EvalDuration int64 `json:"eval_duration"` - Iterations int `json:"iterations"` + ModelName string `json:"model_name"` + Timestamp int64 `json:"timestamp"` + Duration float64 `json:"duration"` + TokensPerSecond float64 `json:"tokens_per_second"` + EvalCount int `json:"eval_count"` + EvalDuration int64 `json:"eval_duration"` + Iterations int `json:"iterations"` + SysInfo *SysInfo `json:"sys_info"` + GPUInfo *GPUInfo `json:"gpu_info"` + OllamaVersion string `json:"ollama_version"` + ClientType string `json:"client_type"` + ClientVersion string `json:"client_version"` + IP string `json:"ip"` + ProofOfWork ProofOfWorkSolution `json:"proof_of_work"` } type OllamaRequest struct { @@ -37,6 +67,10 @@ type OllamaRequest struct { Prompt string `json:"prompt"` } +type ModelRequest struct { + Name string `json:"name"` +} + type OllamaResponse struct { Model string `json:"model"` CreatedAt string `json:"created_at"` @@ -46,18 +80,496 @@ type OllamaResponse struct { EvalDuration int64 `json:"eval_duration"` } -// Models supported -var MODELS = []string{ - "llama3", - "phi3", - "mistral", - "mixtral:8x22b", - "command-r", - "command-r-plus", - "dolphin-llama3", - "dolphin-mixtral:8x22b", - "llama3:70b", - "llama3-gradient", +type SysInfo struct { + OS string `json:"os"` + Arch string `json:"arch"` + Version string `json:"version"` + Kernel string `json:"kernel"` + CPU string `json:"cpu"` + CPUName string `json:"cpu_name"` + Memory string `json:"memory"` +} + +type GPUInfo struct { + Name string `json:"name"` + Vendor string `json:"vendor"` + Memory string `json:"memory"` + DriverVersion string `json:"driver_version"` + Count int `json:"count"` +} + +var ( + globalModels []ModelInfo + apiEndpoint string + clientVersion = "0.0.1" +) + +// ProofOfWorkChallenge represents a proof-of-work challenge +type ProofOfWorkChallenge struct { + Challenge string `json:"challenge"` + Difficulty int `json:"difficulty"` + Timestamp int64 `json:"timestamp"` +} + +// ProofOfWorkSolution represents a solution to a proof-of-work challenge +type ProofOfWorkSolution struct { + Challenge string `json:"challenge"` + Nonce string `json:"nonce"` + Timestamp int64 `json:"timestamp"` + Difficulty int `json:"difficulty"` +} + +// requestProofOfWorkChallenge requests a new proof-of-work challenge from the server +func requestProofOfWorkChallenge(apiEndpoint string) (ProofOfWorkChallenge, error) { + resp, err := http.Get(apiEndpoint + "/api/pow-challenge") + if err != nil { + return ProofOfWorkChallenge{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return ProofOfWorkChallenge{}, err + } + + var challenge ProofOfWorkChallenge + if err := json.Unmarshal(body, &challenge); err != nil { + return ProofOfWorkChallenge{}, err + } + + return challenge, nil +} + +// solveProofOfWork solves the proof-of-work challenge +func solveProofOfWork(challenge ProofOfWorkChallenge) (string, error) { + prefix := strings.Repeat("0", challenge.Difficulty) + for i := 0; ; i++ { + nonce := strconv.Itoa(i) + hash := sha256.Sum256([]byte(challenge.Challenge + nonce)) + if strings.HasPrefix(hex.EncodeToString(hash[:]), prefix) { + return nonce, nil + } + } +} + +type ModelInfo struct { + Name string `json:"name"` + Parameters string `json:"parameters"` + Quantization string `json:"quantization"` +} + +func fetchModels() ([]ModelInfo, error) { + mainURL := os.Getenv("OLLAMARK_API") + resp, err := http.Get(mainURL + "/api/model-list") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // Read the raw JSON response + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Unmarshal the JSON response + var result struct { + Models []ModelInfo `json:"models"` + } + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + return result.Models, nil +} + +func initModels() error { + models, err := fetchModels() + if err != nil { + return err + } + globalModels = models + return nil +} + +func LoadPublicKey() (*rsa.PublicKey, error) { + publicKeyData := os.Getenv("PUBLIC_KEY") + block, _ := pem.Decode([]byte(publicKeyData)) + if block == nil { + return nil, fmt.Errorf("failed to parse PEM block containing the public key") + } + + publicKey, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + rsaPublicKey, ok := publicKey.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("failed to cast public key to RSA public key") + } + + return rsaPublicKey, nil +} + +func EncryptData(publicKey *rsa.PublicKey, data []byte) ([]byte, error) { + return rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, data, nil) +} + +// Generate a random AES key +func generateAESKey() ([]byte, error) { + key := make([]byte, 32) // AES-256 + _, err := rand.Read(key) + if err != nil { + return nil, err + } + return key, nil +} + +// Encrypt data using AES-GCM +func encryptAESGCM(key, plaintext []byte) ([]byte, []byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, nil, err + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return nil, nil, err + } + + nonce := make([]byte, aesGCM.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, nil, err + } + + ciphertext := aesGCM.Seal(nil, nonce, plaintext, nil) + return nonce, ciphertext, nil +} + +// Encrypt the AES key using RSA +func encryptRSA(publicKey *rsa.PublicKey, data []byte) ([]byte, error) { + return rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, data, nil) +} + +// Generate a random UUID +func generateUUID() string { + return uuid.New().String() +} + +// Sign the UUID with HMAC-SHA256 +func signUUID(uuid string, secretKey string) string { + h := hmac.New(sha256.New, []byte(secretKey)) + h.Write([]byte(uuid)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func getCPUName() string { + // get windows cpu name + if runtime.GOOS == "windows" { + cmd := exec.Command("wmic", "cpu", "get", "name") + output, err := cmd.Output() + if err != nil { + return "Unknown" + } + lines := strings.Split(string(output), "\n") + if len(lines) > 1 { + return strings.TrimSpace(lines[1]) + } + return "Unknown" + } + + // get linux cpu name + if runtime.GOOS == "linux" { + cmd := exec.Command("lshw", "-C", "cpu") + output, err := cmd.Output() + if err != nil { + return "Unknown" + } + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "product:") { + return strings.TrimSpace(strings.Split(line, ":")[1]) + } + } + return "Unknown" + } + + return "Unknown" +} + +func getKernelVersion() (string, error) { + if runtime.GOOS == "windows" { + cmd := exec.Command("wmic", "os", "get", "Version", "/value") + output, err := cmd.Output() + if err != nil { + return "", err + } + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "Version=") { + return strings.TrimSpace(strings.Split(line, "=")[1]), nil + } + } + return "", fmt.Errorf("failed to parse Windows version") + } + + cmd := exec.Command("uname", "-r") + output, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(output)), nil +} + +func getSysInfo() (*SysInfo, error) { + v, _ := mem.VirtualMemory() + // s, _ := mem.SwapMemory() + + totalMemory := v.Total / 1024 / 1024 / 1024 + // usedMemory := v.Used + // availableMemory := v.Available + // swapTotal := s.Total + // swapUsed := s.Used + + sysInfo := &SysInfo{} + sysInfo.OS = runtime.GOOS + sysInfo.Arch = runtime.GOARCH + sysInfo.Version = "0.0.1" + kernelVersion, err := getKernelVersion() + if err != nil { + return nil, err + } + sysInfo.Kernel = kernelVersion + sysInfo.CPU = strconv.Itoa(runtime.NumCPU()) + // get CPU Name for Windows and Linux + + sysInfo.CPUName = getCPUName() + + sysInfo.Memory = strconv.Itoa(int(totalMemory)) + " GB" + + // Get system information if macOS (darwin) and aarch64 (arm64) then get the info with apple silicon only command: TODO (Test) + if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { + cmd := exec.Command("system_profiler", "SPHardwareDataType") + output, err := cmd.Output() + if err != nil { + return nil, err + } + // Extract the CPU information from the output + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "Chip:") { + sysInfo.CPUName = strings.TrimSpace(strings.Split(line, ":")[1]) + break + } + } + } + + return sysInfo, nil +} + +func getMacGPUInfo() (*GPUInfo, error) { + cmd := exec.Command("system_profiler", "SPDisplaysDataType") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + gpuInfo := &GPUInfo{} + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "Chipset Model:") { + gpuInfo.Name = strings.TrimSpace(strings.Split(line, ":")[1]) + gpuInfo.Vendor = "Apple" + break + } + } + + // If we couldn't find GPU info, it's likely integrated with the CPU + if gpuInfo.Name == "" { + cpuCmd := exec.Command("system_profiler", "SPHardwareDataType") + cpuOutput, err := cpuCmd.Output() + if err != nil { + return nil, err + } + cpuLines := strings.Split(string(cpuOutput), "\n") + for _, line := range cpuLines { + if strings.Contains(line, "Chip:") { + gpuInfo.Name = strings.TrimSpace(strings.Split(line, ":")[1]) + " GPU" + gpuInfo.Vendor = "Apple" + break + } + } + } + + // Memory information isn't easily available for integrated GPUs + gpuInfo.Memory = "Shared" + gpuInfo.DriverVersion = "N/A" + gpuInfo.Count = 1 + + return gpuInfo, nil +} + +func getGPUInfo() (*GPUInfo, error) { + // First, attempt to use nvidia-smi to fetch Nvidia GPU info + nvidiaGPU, err := getNvidiaGPUInfo() + if err == nil { + return nvidiaGPU, nil + } + + // If Nvidia GPU info fetching fails, attempt to fetch AMD GPU info + amdGPU, err := getAMDGPUInfo() + if err == nil { + return amdGPU, nil + } + + // Check if we're on macOS (darwin) and arm64 architecture + if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { + return getMacGPUInfo() + } + + // If both methods fail, return the last error + return nil, err +} + +func getNvidiaGPUInfo() (*GPUInfo, error) { + cmd := exec.Command("nvidia-smi", "--query-gpu=name,memory.total,driver_version", "--format=csv,noheader") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + outputStr := strings.TrimSpace(string(output)) + lines := strings.Split(outputStr, "\n")[0] // Assuming single GPU + fields := strings.Split(lines, ",") + + if len(fields) < 2 { + return nil, fmt.Errorf("failed to parse Nvidia GPU information") + } + + return &GPUInfo{ + Name: strings.TrimSpace(fields[0]), + Vendor: "NVIDIA", + Memory: strings.TrimSpace(fields[1]), + DriverVersion: strings.TrimSpace(fields[2]), + Count: len(lines), + }, nil +} + +func getAMDGPUInfo() (*GPUInfo, error) { + switch runtime.GOOS { + case "windows": + return getAMDGPUInfoWindows() + case "linux": + return getAMDGPUInfoLinux() + case "darwin": + return nil, fmt.Errorf("macOS, Skipping AMD GPU info") + default: + return nil, fmt.Errorf("AMD GPU unsupported operating system") + } +} + +func getAMDGPUInfoWindows() (*GPUInfo, error) { + cmd := exec.Command("wmic", "path", "win32_VideoController", "get", "Name,DriverVersion", "/format:list") + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed to execute wmic command: %v", err) + } + + outputStr := string(output) + return parseWMICOutput(outputStr) +} + +func parseWMICOutput(output string) (*GPUInfo, error) { + lines := strings.Split(output, "\n") + info := GPUInfo{} + gpuNames := make(map[string]bool) // To track unique GPU names + + for _, line := range lines { + if strings.HasPrefix(line, "Name=") { + name := strings.TrimSpace(strings.Split(line, "=")[1]) + // Skip integrated and virtual GPUs + if strings.Contains(name, "Integrated") || strings.Contains(name, "Display Adapter") || strings.Contains(name, "AMD Radeon(TM) Graphics") { + continue + } + if !gpuNames[name] { + gpuNames[name] = true + info.Name = name + info.Vendor = "AMD" // Assuming AMD if we are parsing this on an AMD system check + info.Count++ + } + } else if strings.HasPrefix(line, "DriverVersion=") { + info.DriverVersion = strings.TrimSpace(strings.Split(line, "=")[1]) + info.Memory = "Unknown" // Placeholder for memory, as WMIC does not provide it directly + } + } + + if info.Name == "" { + return nil, fmt.Errorf("no dedicated AMD GPUs found") + } + + return &info, nil +} + +func getAMDGPUInfoLinux() (*GPUInfo, error) { + cmd := exec.Command("lshw", "-C", "display") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + outputStr := string(output) + // Example of parsing, adjust according to actual output + if strings.Contains(outputStr, "Radeon") || strings.Contains(outputStr, "AMD") { + name := extractField(outputStr, "product") + // vendor := "AMD" + memory := extractField(outputStr, "size") + + return &GPUInfo{ + Name: name, + // Vendor: vendor, + Memory: memory, + }, nil + } + + return nil, fmt.Errorf("no AMD GPU detected") +} + +func getIPAddress() string { + resp, err := http.Get("https://icanhazip.com") + if err != nil { + return "Unknown" + } + defer resp.Body.Close() + ip, err := io.ReadAll(resp.Body) + if err != nil { + return "Unknown" + } + return strings.TrimSpace(string(ip)) +} + +func getOllamaVersion() string { + cmd := exec.Command("ollama", "--version") + output, err := cmd.Output() + if err != nil { + return "Unknown" + } + + // remove "ollama version is " from the output + return strings.TrimSpace(strings.Split(string(output), "ollama version is ")[1]) +} + +func extractField(data, fieldName string) string { + // Simple parsing logic, needs to be adjusted based on actual output + start := strings.Index(data, fieldName+":") + if start == -1 { + return "" + } + start += len(fieldName) + 1 + end := strings.Index(data[start:], "\n") + if end == -1 { + return data[start:] + } + return strings.TrimSpace(data[start : start+end]) } func main() { @@ -67,6 +579,22 @@ func main() { fmt.Println("Error loading .env file:", err) } + fmt.Println("Loading Ollamark...") + + fmt.Println("Checking Ollama Version...") + ollamaVersion := getOllamaVersion() + if ollamaVersion == "Unknown" { + fmt.Println("Ollama not found, please install Ollama from https://ollama.com/download to Ollamark 😎") + return + } + fmt.Println("Ollama Version:", ollamaVersion) + + err = initModels() + if err != nil { + fmt.Println("Failed to initialize models:", err) + return + } + flag.Usage = func() { fmt.Println("Usage: ollamark [options]") fmt.Println("Options:") @@ -77,16 +605,20 @@ func main() { fmt.Println(" For Ollamark CLI mode:") fmt.Println(" ollamark -m llama3 -i 10") fmt.Println(" ollamark -m phi3") + fmt.Println(" ollamark -m phi3 -s") fmt.Println(" ollamark -m phi3 -s -o http://localhost:11434/api/generate") } // Parse command-line arguments (Ollamark CLI) - modelPtr := flag.String("m", "llama3", "Model name to benchmark") - submitPtr := flag.Bool("s", false, "Submit benchmark results to Ollamark (default false)") - ollamaPtr := flag.String("o", "http://localhost:11434/api/generate", "Ollama API endpoint") + modelPtr := flag.String("m", "llama3", "Model name to benchmark (default: llama3)") + submitPtr := flag.Bool("s", false, "Submit benchmark results to Ollamark.com (default false)") + ollamaPtr := flag.String("o", "http://localhost:11434", "Ollama API endpoint (default http://localhost:11434)") iterationsPtr := flag.Int("i", 2, "Number of benchmark iterations (Min 2, Max 20)") flag.Parse() + // Set the global API endpoint + apiEndpoint = *ollamaPtr + // Check if CLI arguments are provided if flag.NFlag() > 0 { @@ -106,7 +638,7 @@ func main() { } // Run ollamark in CLI mode - runBenchmarkCLI(*modelPtr, *submitPtr, *ollamaPtr, *iterationsPtr) + runBenchmarkCLI(*modelPtr, *submitPtr, apiEndpoint, *iterationsPtr) return } @@ -117,7 +649,7 @@ func main() { w := a.NewWindow("Ollamark - Ollama Benchmark") // set window size - w.Resize(fyne.NewSize(300, 200)) + w.Resize(fyne.NewSize(400, 300)) w.CenterOnScreen() // create a logo @@ -135,9 +667,13 @@ func main() { a.SetIcon(icon) } + sysinfo, _ := getSysInfo() + gpuinfo, _ := getGPUInfo() + ollamaVersion = getOllamaVersion() + // create an api entry field apiEntry := widget.NewEntry() - apiEntry.SetText("http://localhost:11434/api/generate") + apiEntry.SetText(apiEndpoint) // create a title label titleLabel := widget.NewLabel("Ollama API Endpoint") @@ -146,10 +682,27 @@ func main() { title2Label := widget.NewLabel("Select a model to benchmark") title2Label.TextStyle = fyne.TextStyle{Bold: true} - modelSelect := widget.NewSelect(MODELS, func(value string) { - // do nothing + // Create a slice of model names for the dropdown + modelNames := make([]string, len(globalModels)) + for i, model := range globalModels { + modelNames[i] = model.Name + } + + // Create the select widget with model names + modelSelect := widget.NewSelect(modelNames, func(value string) { + // You can add logic here if needed when a model is selected }) - modelSelect.SetSelected("llama3") + + // Set the default selected model + // Find the index of "llama3" in the modelNames slice + defaultIndex := 0 + for i, name := range modelNames { + if name == "llama3" { + defaultIndex = i + break + } + } + modelSelect.SetSelected(modelNames[defaultIndex]) resultLabel := widget.NewLabel("") resultLabel.Alignment = fyne.TextAlignCenter @@ -168,6 +721,15 @@ func main() { tpsText.Alignment = fyne.TextAlignCenter tpsText.Hide() + sysText := widget.NewLabel("") + sysText.Hide() + + gpuText := widget.NewLabel("") + gpuText.Hide() + + ollamaVersionText := widget.NewLabel("") + ollamaVersionText.Hide() + iterationsSlider := widget.NewSlider(2, 20) iterationsSlider.SetValue(2) iterationsSlider.Step = 1 @@ -177,6 +739,22 @@ func main() { iterationsLabel.SetText(fmt.Sprintf("Iterations: %d", int(value))) } + sysText.SetText(fmt.Sprintf("CPU: %s\nMemory: %s\nOS: %s\nKernel: %s", sysinfo.CPUName, sysinfo.Memory, sysinfo.OS, sysinfo.Kernel)) + sysText.Show() + sysText.Refresh() + + // if gpu Info is available, show it + if gpuinfo != nil { + gpuText.SetText(fmt.Sprintf("GPU Name: %s\nDriver Version: %s", gpuinfo.Name, gpuinfo.DriverVersion)) + gpuText.Show() + gpuText.Refresh() + } + + // set ollama version text make version bold + ollamaVersionText.SetText(fmt.Sprintf("Ollama Version: %s", ollamaVersion)) + ollamaVersionText.Show() + ollamaVersionText.Refresh() + // create a progress bar progressBar := widget.NewProgressBarInfinite() progressBar.Hide() @@ -192,9 +770,11 @@ func main() { var benchmarkResult *BenchmarkResult var submitButton *widget.Button + var linkButton *widget.Button benchmarkButton := widget.NewButton("Benchmark", nil) benchmarkButton.OnTapped = func() { + linkButton.Hide() benchmarkButton.SetText("Benchmarking...") benchmarkButton.Disable() submitButton.Disable() @@ -205,6 +785,8 @@ func main() { tokensPerSecondText.Hide() tpsText.Hide() + // sysText.Hide() + // gpuText.Hide() go func() { progressBar.Show() @@ -215,7 +797,47 @@ func main() { modelName := modelSelect.Selected iterations := int(iterationsSlider.Value) + modelRequest := ModelRequest{ + Name: modelName, + } + jsonData, _ := json.Marshal(modelRequest) + fullURL := apiEndpoint + "/api/pull" + resultLabel.SetText("Pulling model " + modelName + ", Please wait...") + resultLabel.Refresh() + resp, err := http.Post(fullURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + resultLabel.SetText("Error: " + err.Error()) + benchmarkButton.SetText("Benchmark") + benchmarkButton.Enable() + progressBar.Hide() + progressBar.Refresh() + gif.Hide() + return + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + resultLabel.SetText(fmt.Sprintf("Error pulling model: %s", body)) + benchmarkButton.SetText("Benchmark") + benchmarkButton.Enable() + progressBar.Hide() + progressBar.Refresh() + gif.Hide() + return + } + + // fmt.Println("Model pull response:", string(body)) // Debug print + resultLabel.SetText("Model pulled successfully") + resultLabel.Refresh() + resultLabel.SetText("Benchmarking...") + resultLabel.Refresh() + var totalTokensPerSecond float64 + var evalCount int + var evalDuration float64 + + start := time.Now() for i := 0; i < iterations; i++ { requestBody := OllamaRequest{ @@ -224,7 +846,7 @@ func main() { } jsonData, _ := json.Marshal(requestBody) - resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonData)) + resp, err := http.Post(apiURL+"/api/generate", "application/json", bytes.NewBuffer(jsonData)) if err != nil { resultLabel.SetText("Error: " + err.Error()) benchmarkButton.SetText("Benchmark") @@ -267,15 +889,29 @@ func main() { tokensPerSecond := float64(response.EvalCount) / (float64(response.EvalDuration) / 1e9) totalTokensPerSecond += tokensPerSecond + evalCount = response.EvalCount + evalDuration = float64(response.EvalDuration) / 1e9 } + EvalCount := evalCount + EvalDuration := evalDuration + avgTokensPerSecond := totalTokensPerSecond / float64(iterations) benchmarkResult = &BenchmarkResult{ ModelName: modelName, Timestamp: time.Now().Unix(), + Duration: time.Since(start).Seconds(), + EvalCount: EvalCount, + EvalDuration: int64(EvalDuration), TokensPerSecond: avgTokensPerSecond, Iterations: iterations, + SysInfo: sysinfo, + GPUInfo: gpuinfo, + OllamaVersion: ollamaVersion, + ClientType: "ollamark-gui", + ClientVersion: clientVersion, + IP: getIPAddress(), } resultLabel.SetText(fmt.Sprintf("Benchmark completed for %s\nAverage Tokens per second: %.2f\nBenchmarked with %d iterations", modelName, avgTokensPerSecond, iterations)) @@ -293,42 +929,144 @@ func main() { progressBar.Hide() gif.Hide() progressBar.Refresh() // Refresh after hiding the ProgressBar - benchmarkButton.SetText("Benchmark Again") + benchmarkButton.SetText("Benchmark") benchmarkButton.Enable() submitButton.Show() submitButton.Enable() }() } - submitButton = widget.NewButton("Submit Benchmark", nil) + submitButton = widget.NewButton("Share Benchmark", nil) + linkButton = widget.NewButton("View on Ollamark.com", nil) + linkButton.Hide() + submitButton.OnTapped = func() { if benchmarkResult != nil { - apiEndpoint := os.Getenv("SUBMISSION_API_ENDPOINT") - apiKey := os.Getenv("API_KEY") + subEndpoint := os.Getenv("OLLAMARK_API") + secretKey := os.Getenv("KEY") + publicKey, err := LoadPublicKey() + if err != nil { + resultLabel.SetText("Error loading public key: " + err.Error()) + return + } + // Generate AES key + aesKey, err := generateAESKey() + if err != nil { + resultLabel.SetText("Error generating AES key: " + err.Error()) + return + } + + var submissionID = generateUUID() + + // Generate JWT token + jwtToken, err := generateJWT(submissionID) + if err != nil { + resultLabel.SetText("Error generating JWT token: " + err.Error()) + return + } + + // Request proof-of-work challenge + challenge, err := requestProofOfWorkChallenge(subEndpoint) + if err != nil { + resultLabel.SetText("Error requesting proof-of-work challenge: " + err.Error()) + return + } + + // Solve proof-of-work challenge + powNonce, err := solveProofOfWork(challenge) + if err != nil { + resultLabel.SetText("Error solving proof-of-work challenge: " + err.Error()) + return + } + + // Include proof-of-work solution in the benchmark result + benchmarkResult.ProofOfWork = ProofOfWorkSolution{ + Challenge: challenge.Challenge, + Nonce: powNonce, + Timestamp: challenge.Timestamp, + Difficulty: challenge.Difficulty, + } + + // Encrypt benchmark result with AES key jsonData, _ := json.Marshal(benchmarkResult) - req, err := http.NewRequest("POST", apiEndpoint, bytes.NewBuffer(jsonData)) + nonce, encryptedData, err := encryptAESGCM(aesKey, jsonData) + if err != nil { + resultLabel.SetText("Error encrypting data with AES: " + err.Error()) + return + } + + // Encrypt AES key with RSA public key + encryptedAESKey, err := encryptRSA(publicKey, aesKey) + if err != nil { + resultLabel.SetText("Error encrypting AES key: " + err.Error()) + return + } + + // Prepare payload + payload := map[string]interface{}{ + "data": base64.StdEncoding.EncodeToString(encryptedData), + "nonce": base64.StdEncoding.EncodeToString(nonce), + "encrypted_key": base64.StdEncoding.EncodeToString(encryptedAESKey), + } + + payloadBytes, _ := json.Marshal(payload) + + // Sign the UUID + signature := signUUID(submissionID, secretKey) + + // Create and send the request + req, err := http.NewRequest("POST", subEndpoint+"/api/submit-benchmark", bytes.NewBuffer(payloadBytes)) if err != nil { - resultLabel.SetText("Error creating request: " + err.Error()) + resultLabel.SetText("Error submitting benchmark! Try again!") return } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+apiKey) + req.Header.Set("Authorization", "Bearer "+jwtToken) + req.Header.Set("X-Submission-ID", submissionID) + req.Header.Set("X-Signature", signature) client := &http.Client{} resp, err := client.Do(req) if err != nil { resultLabel.SetText("Error submitting benchmark: " + err.Error()) - } else { - resp.Body.Close() - resultLabel.SetText("Benchmark submitted successfully!") + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + resultLabel.SetText("Error submitting benchmark: " + string(body)) + return } + + resultLabel.SetText("Benchmark submitted successfully!") + submitButton.Hide() + // set linkButton link + linkButton.OnTapped = func() { + submissionURL, err := url.Parse(fmt.Sprintf("https://ollamark.com/marks/%s", submissionID)) + if err != nil { + fmt.Printf("Failed to parse URL: %v\n", err) + return + } + fyne.CurrentApp().OpenURL(submissionURL) + } + linkButton.Show() } } + submitButton.Hide() + linkButton.Hide() + + // border/group around systext and gputext + sysInfoGroup := container.NewVBox(ollamaVersionText, sysText, gpuText) + sysInfoGroupLabel := widget.NewLabel("System Information") + sysInfoGroupLabel.TextStyle = fyne.TextStyle{Bold: true} + sysInfoGroup = container.NewBorder(sysInfoGroupLabel, nil, nil, nil, sysInfoGroup) content := container.NewVBox( logo, + sysInfoGroup, titleLabel, apiEntry, title2Label, @@ -336,23 +1074,27 @@ func main() { iterationsLabel, iterationsSlider, gif, - widget.NewSeparator(), + // widget.NewSeparator(), tokensPerSecondText, tpsText, resultLabel, progressBar, - widget.NewSeparator(), + // widget.NewSeparator(), benchmarkButton, submitButton, + linkButton, ) - w.SetContent(content) + // Wrap the content with a padded container + paddedContent := container.NewPadded(container.NewPadded(content)) + + w.SetContent(paddedContent) w.ShowAndRun() } -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { +func contains(models []ModelInfo, modelName string) bool { + for _, model := range models { + if model.Name == modelName { return true } } @@ -360,16 +1102,59 @@ func contains(slice []string, item string) bool { } func runBenchmarkCLI(modelName string, submit bool, ollamaAPI string, iterations int) { - // Get Ollama API URL from environment variable ollamaAPIURL := ollamaAPI var totalTokensPerSecond float64 + var evalCount int + var evalDuration float64 // modelName needs to match a model name in MODELS - if !contains(MODELS, modelName) { - fmt.Println("Model not supported. Please use a supported model from the list:", MODELS) + if !contains(globalModels, modelName) { + fmt.Println("Model not supported. Please use a supported model from the list:", globalModels) + return + } + + sysinfo, err := getSysInfo() + if err != nil { + // fmt.Println("Error:", err) + return + } + fmt.Printf("CPU: %+v\n", sysinfo.CPUName) + fmt.Printf("Memory: %+v\n", sysinfo.Memory) + fmt.Printf("OS: %+v\n", sysinfo.OS) + fmt.Printf("Kernel: %+v\n", sysinfo.Kernel) + + gpuinfo, err := getGPUInfo() + if err != nil { + // fmt.Println("Error:", err) + return + } + fmt.Printf("GPU Name: %+v\n", gpuinfo.Name) + fmt.Printf("Driver Version: %+v\n", gpuinfo.DriverVersion) + fmt.Printf("GPU Memory: %+v\n", gpuinfo.Memory) + + modelRequest := ModelRequest{ + Name: modelName, + } + jsonData, _ := json.Marshal(modelRequest) + fullURL := ollamaAPI + "/api/pull" + fmt.Println("Pulling model " + modelName + ", Please wait...") + resp, err := http.Post(fullURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + fmt.Println("Error:", err) return } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + fmt.Println("Error pulling model:", string(body)) + return + } + + fmt.Println("Model pulled successfully") + fmt.Println("Benchmarking...") + start := time.Now() for i := 0; i < iterations; i++ { requestBody := OllamaRequest{ @@ -378,15 +1163,13 @@ func runBenchmarkCLI(modelName string, submit bool, ollamaAPI string, iterations } jsonData, _ := json.Marshal(requestBody) - resp, err := http.Post(ollamaAPIURL, "application/json", bytes.NewBuffer(jsonData)) + resp, err := http.Post(ollamaAPIURL+"/api/generate", "application/json", bytes.NewBuffer(jsonData)) if err != nil { fmt.Println("Error:", err) return } defer resp.Body.Close() - // start := time.Now() - var response OllamaResponse var responseText string decoder := json.NewDecoder(resp.Body) @@ -427,44 +1210,149 @@ func runBenchmarkCLI(modelName string, submit bool, ollamaAPI string, iterations tokensPerSecond := float64(response.EvalCount) / (float64(response.EvalDuration) / 1e9) totalTokensPerSecond += tokensPerSecond + evalCount = response.EvalCount + evalDuration = float64(response.EvalDuration) / 1e9 } + EvalCount := evalCount + EvalDuration := evalDuration avgTokensPerSecond := totalTokensPerSecond / float64(iterations) fmt.Printf("\nBenchmark completed for %s\n", modelName) fmt.Printf("Average Tokens per second: %.2f\n", avgTokensPerSecond) + sysinfo, _ = getSysInfo() + gpuinfo, _ = getGPUInfo() + benchmarkResult := &BenchmarkResult{ ModelName: modelName, Timestamp: time.Now().Unix(), + Duration: time.Since(start).Seconds(), + EvalCount: EvalCount, + EvalDuration: int64(EvalDuration), TokensPerSecond: avgTokensPerSecond, Iterations: iterations, + SysInfo: sysinfo, + GPUInfo: gpuinfo, + OllamaVersion: getOllamaVersion(), + ClientType: "ollamark-cli", + ClientVersion: clientVersion, + IP: getIPAddress(), } if submit { - // Get Submission API URL from environment variable - submissionAPIURL := os.Getenv("SUBMISSION_API_ENDPOINT") - apiKey := os.Getenv("API_KEY") - - jsonData, _ := json.Marshal(benchmarkResult) - req, err := http.NewRequest("POST", submissionAPIURL, bytes.NewBuffer(jsonData)) - if err != nil { - fmt.Println("Error creating request:", err) - return - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+apiKey) - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - fmt.Println("Error submitting benchmark:", err) - } else { - resp.Body.Close() - fmt.Println("Benchmark submitted successfully!") - } + submitBenchmark(benchmarkResult) } else { fmt.Println("Benchmark results not submitted.") } } + +func generateJWT(nonce string) (string, error) { + secretKey := os.Getenv("KEY") + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "iat": time.Now().Unix(), + "exp": time.Now().Add(time.Minute * 1).Unix(), // Token expires in 1 minute + "nonce": nonce, + }) + + tokenString, err := token.SignedString([]byte(secretKey)) + if err != nil { + return "", err + } + + return tokenString, nil +} + +func submitBenchmark(benchmarkResult *BenchmarkResult) error { + apiEndpoint := os.Getenv("OLLAMARK_API") + secretKey := os.Getenv("KEY") + publicKey, err := LoadPublicKey() + if err != nil { + return fmt.Errorf("error loading public key: %v", err) + } + + // Generate AES key + aesKey, err := generateAESKey() + if err != nil { + return fmt.Errorf("error generating AES key: %v", err) + } + + var submissionID = generateUUID() + + // Generate JWT token + jwtToken, err := generateJWT(submissionID) + if err != nil { + return fmt.Errorf("error generating JWT token: %v", err) + } + + // Request proof-of-work challenge + challenge, err := requestProofOfWorkChallenge(apiEndpoint) + if err != nil { + return fmt.Errorf("error requesting proof-of-work challenge: %v", err) + } + + // Solve proof-of-work challenge + powNonce, err := solveProofOfWork(challenge) + if err != nil { + return fmt.Errorf("error solving proof-of-work challenge: %v", err) + } + + // Include proof-of-work solution in the benchmark result + benchmarkResult.ProofOfWork = ProofOfWorkSolution{ + Challenge: challenge.Challenge, + Nonce: powNonce, + Timestamp: challenge.Timestamp, + Difficulty: challenge.Difficulty, + } + + // Encrypt benchmark result with AES key + jsonData, _ := json.Marshal(benchmarkResult) + nonce, encryptedData, err := encryptAESGCM(aesKey, jsonData) + if err != nil { + return fmt.Errorf("error encrypting data with AES: %v", err) + } + + // Encrypt AES key with RSA public key + encryptedAESKey, err := encryptRSA(publicKey, aesKey) + if err != nil { + return fmt.Errorf("error encrypting AES key: %v", err) + } + + // Prepare payload + payload := map[string]interface{}{ + "data": base64.StdEncoding.EncodeToString(encryptedData), + "nonce": base64.StdEncoding.EncodeToString(nonce), + "encrypted_key": base64.StdEncoding.EncodeToString(encryptedAESKey), + } + + payloadBytes, _ := json.Marshal(payload) + + // Sign the UUID + signature := signUUID(submissionID, secretKey) + + // Create and send the request + req, err := http.NewRequest("POST", apiEndpoint+"/api/submit-benchmark", bytes.NewBuffer(payloadBytes)) + if err != nil { + return fmt.Errorf("error submitting benchmark! %v", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+jwtToken) + req.Header.Set("X-Submission-ID", submissionID) + req.Header.Set("X-Signature", signature) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("error submitting benchmark: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("server responded with status %d: %s", resp.StatusCode, body) + } + + fmt.Printf("Benchmark submitted successfully! View it at: https://ollamark.com/marks/%s\n", submissionID) + return nil +} diff --git a/server/.env.example b/server/.env.example new file mode 100644 index 0000000..9ee0aea --- /dev/null +++ b/server/.env.example @@ -0,0 +1,4 @@ +PRIVATE_KEY= +KEY= +MONGODB="mongodb://localhost:27017" +REDIS="localhost:6379" \ No newline at end of file diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..c5f5f6b --- /dev/null +++ b/server/server.go @@ -0,0 +1,685 @@ +// By Carsen Klock 2024 under the MIT license +// https://github.com/context-labs/ollamark +// https://ollamark.com +// Ollamark Server + +package main + +import ( + "context" + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "log" + "net/http" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/dgrijalva/jwt-go" + tollbooth "github.com/didip/tollbooth/v6" + "github.com/didip/tollbooth/v6/limiter" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +type BenchmarkResult struct { + ModelName string `json:"model_name"` + Timestamp int64 `json:"timestamp"` + Duration float64 `json:"duration"` + TokensPerSecond float64 `json:"tokens_per_second"` + EvalCount int `json:"eval_count"` + EvalDuration int64 `json:"eval_duration"` + Iterations int `json:"iterations"` + SysInfo *SysInfo `json:"sys_info"` + GPUInfo *GPUInfo `json:"gpu_info"` + OllamaVersion string `json:"ollama_version"` + ClientType string `json:"client_type"` + ClientVersion string `json:"client_version"` + SubmissionID string `json:"submission_id"` + IP string `json:"ip"` + ProofOfWork ProofOfWorkSolution `json:"proof_of_work"` +} + +type SysInfo struct { + OS string `json:"os"` + Arch string `json:"arch"` + Version string `json:"version"` + Kernel string `json:"kernel"` + CPU string `json:"cpu"` + CPUName string `json:"cpu_name"` + Memory string `json:"memory"` +} + +type GPUInfo struct { + Name string `json:"name"` + Vendor string `json:"vendor"` + Memory string `json:"memory"` + DriverVersion string `json:"driver_version"` + Count int `json:"count"` +} + +type ModelInfo struct { + Name string + Parameters string + Quantization string +} + +// Models supported +var MODELS = []ModelInfo{ + {Name: "llama3", Parameters: "8B", Quantization: "Q4_0"}, + {Name: "phi3", Parameters: "3B", Quantization: "Q4_K_M"}, + {Name: "phi3:14b", Parameters: "14B", Quantization: "Q4_0"}, + {Name: "aya", Parameters: "8B", Quantization: "Q4_0"}, + {Name: "aya:35b", Parameters: "35B", Quantization: "Q4_0"}, + {Name: "gemma", Parameters: "7B", Quantization: "Q4_0"}, + {Name: "gemma:2b", Parameters: "2B", Quantization: "Q4_0"}, + {Name: "falcon2", Parameters: "11B", Quantization: "Q4_0"}, + {Name: "mistral", Parameters: "7B", Quantization: "Q4_0"}, + {Name: "mixtral:8x22b", Parameters: "176B", Quantization: "Q4_0"}, + {Name: "mixtral:8x7b", Parameters: "56B", Quantization: "Q4_0"}, + {Name: "command-r", Parameters: "35B", Quantization: "Q4_0"}, + {Name: "command-r-plus", Parameters: "104B", Quantization: "Q4_0"}, + {Name: "dolphin-llama3", Parameters: "8B", Quantization: "Q4_0"}, + {Name: "dolphin-llama3:70b", Parameters: "70B", Quantization: "Q4_0"}, + {Name: "dolphin-mixtral:8x22b", Parameters: "176B", Quantization: "Q4_0"}, + {Name: "dolphin-mixtral:8x7b", Parameters: "56B", Quantization: "Q4_0"}, + {Name: "llama3-chatqa", Parameters: "8B", Quantization: "Q4_0"}, + {Name: "llama3:70b", Parameters: "70B", Quantization: "Q4_0"}, + {Name: "llama3-gradient:8b", Parameters: "8B", Quantization: "Q4_0"}, + {Name: "llama3-gradient:70b", Parameters: "70B", Quantization: "Q4_0"}, + {Name: "qwen", Parameters: "7B", Quantization: "Q4_0"}, + {Name: "qwen2", Parameters: "7B", Quantization: "Q4_0"}, + {Name: "qwen2:0.5b", Parameters: "0.5B", Quantization: "Q4_0"}, + {Name: "qwen2:1.5b", Parameters: "1.5B", Quantization: "Q4_0"}, + {Name: "llama2", Parameters: "7B", Quantization: "Q4_0"}, +} + +var cache sync.Map + +type CacheItem struct { + Data []BenchmarkResult + Count int64 + Timestamp time.Time +} + +func connectDB() (*mongo.Client, error) { + mongodblink := os.Getenv("MONGODB") + clientOptions := options.Client().ApplyURI(mongodblink) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + client, err := mongo.Connect(ctx, clientOptions) + if err != nil { + return nil, err + } + + return client, nil +} + +func insertBenchmark(client *mongo.Client, benchmark BenchmarkResult) error { + collection := client.Database("ollamark_db").Collection("benchmarks") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := collection.InsertOne(ctx, benchmark) + if err != nil { + return err + } + return nil +} + +func LoadPrivateKey(privateKeyData string) (*rsa.PrivateKey, error) { + block, _ := pem.Decode([]byte(privateKeyData)) + if block == nil { + return nil, fmt.Errorf("failed to parse PEM block containing the private key") + } + + privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + + return privateKey.(*rsa.PrivateKey), nil +} + +func DecryptData(privateKey *rsa.PrivateKey, data []byte) ([]byte, error) { + return privateKey.Decrypt(nil, data, &rsa.OAEPOptions{Hash: crypto.SHA256}) +} + +func decryptAESGCM(key, nonce, ciphertext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} + +func verifySignature(submissionID, signature, secretKey string) bool { + mac := hmac.New(sha256.New, []byte(secretKey)) + mac.Write([]byte(submissionID)) + expectedMAC := mac.Sum(nil) + signatureBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return false + } + return hmac.Equal(signatureBytes, expectedMAC) +} + +func checkSubmissionID(client *mongo.Client, submissionID string) (bool, error) { + collection := client.Database("ollamark_db").Collection("benchmarks") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + count, err := collection.CountDocuments(ctx, bson.M{"submissionid": submissionID}) + if err != nil { + return false, err + } + + return count == 0, nil +} + +// Function to validate JWT token +func validateJWT(tokenString string) (jwt.MapClaims, error) { + secretKey := os.Getenv("KEY") + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(secretKey), nil + }) + if err != nil { + log.Printf("JWT parsing error: %v", err) // Log any JWT parsing errors + return nil, err + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + return claims, nil + } else { + log.Printf("Invalid JWT token") // Log if the token is invalid + return nil, fmt.Errorf("invalid token") + } +} + +// Middleware to validate JWT token +func authMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + tokenString := c.GetHeader("Authorization") + if tokenString == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing Authorization header"}) + fmt.Printf("Missing Authorization header: %v", tokenString) + c.Abort() + return + } + + claims, err := validateJWT(strings.TrimPrefix(tokenString, "Bearer ")) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + fmt.Printf("Invalid token: %v", err) + c.Abort() + return + } + + // monogo client + client, err := connectDB() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect to database"}) + c.Abort() + return + } + + // Check if the nonce has been used before to prevent replay attacks + nonce := claims["nonce"].(string) + isUnique, err := checkSubmissionID(client, nonce) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check submission"}) + fmt.Printf("Failed to check submission ID: %v", err) + return + } + + if !isUnique { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Replay attack detected"}) + return + } + + c.Set("claims", claims) + c.Next() + } +} + +func contains(models []ModelInfo, modelName string) bool { + for _, model := range models { + if model.Name == modelName { + return true + } + } + return false +} + +var ipRequests = make(map[string]int) +var ipLastRequest = make(map[string]time.Time) +var requestLimit = 1 +var timeWindow = 1 * time.Second + +// checkIP checks if an IP address is spamming and rate limits it +func checkIP(ip string) bool { + now := time.Now() + if lastRequest, exists := ipLastRequest[ip]; exists && now.Sub(lastRequest) > timeWindow { + ipRequests[ip] = 0 + } + + ipRequests[ip]++ + ipLastRequest[ip] = now + + return ipRequests[ip] <= requestLimit +} + +// ADMIN ONLY: ban ip from submit benchmark +func banIP(ip string) { + // if ip is in db then remove all its benchmark submissions + client, err := connectDB() + if err != nil { + panic(err) + } + defer client.Disconnect(context.Background()) + + collection := client.Database("ollamark_db").Collection("benchmarks") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + collection.DeleteMany(ctx, bson.M{"ip": ip}) +} + +// ADMIN ONLY: remove benchmark submission +func removeBenchmark(client *mongo.Client, submissionID string) { + collection := client.Database("ollamark_db").Collection("benchmarks") + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + collection.DeleteOne(ctx, bson.M{"submissionid": submissionID}) +} +func fetchBenchmarks(client *mongo.Client, filter bson.M, sortBy string, sortOrder int, page, limit int) ([]BenchmarkResult, int64, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cacheKey := fmt.Sprintf("benchmarks:%s:%d:%d:%d:%s", sortBy, sortOrder, page, limit, filter) + if item, found := cache.Load(cacheKey); found { + cacheItem := item.(CacheItem) + if time.Since(cacheItem.Timestamp) < 5*time.Second { + return cacheItem.Data, cacheItem.Count, nil + } + } + + collection := client.Database("ollamark_db").Collection("benchmarks") + + pipeline := []bson.M{ + {"$match": filter}, + {"$sort": bson.M{sortBy: sortOrder}}, + {"$skip": int64((page - 1) * limit)}, + {"$limit": int64(limit)}, + } + + cursor, err := collection.Aggregate(ctx, pipeline) + if err != nil { + return nil, 0, err + } + defer cursor.Close(ctx) + + var benchmarks []BenchmarkResult + if err := cursor.All(ctx, &benchmarks); err != nil { + return nil, 0, err + } + + total, err := collection.CountDocuments(ctx, filter) + if err != nil { + return nil, 0, err + } + + cache.Store(cacheKey, CacheItem{Data: benchmarks, Count: total, Timestamp: time.Now()}) + + return benchmarks, total, nil +} + +// ProofOfWorkChallenge represents a proof-of-work challenge +type ProofOfWorkChallenge struct { + Challenge string `json:"challenge"` + Difficulty int `json:"difficulty"` + Timestamp int64 `json:"timestamp"` +} + +// ProofOfWorkSolution represents a solution to a proof-of-work challenge +type ProofOfWorkSolution struct { + Challenge string `json:"challenge"` + Nonce string `json:"nonce"` + Timestamp int64 `json:"timestamp"` + Difficulty int `json:"difficulty"` +} + +// GenerateProofOfWorkChallenge generates a new proof-of-work challenge +func GenerateProofOfWorkChallenge() ProofOfWorkChallenge { + difficulty := GetDynamicDifficulty() + // log.Printf("Generated PoW challenge with difficulty: %d", difficulty) + challenge := make([]byte, 32) + rand.Read(challenge) + return ProofOfWorkChallenge{ + Challenge: hex.EncodeToString(challenge), + Difficulty: difficulty, + Timestamp: time.Now().Unix(), + } +} + +// VerifyProofOfWork checks if the provided solution is valid +func VerifyProofOfWork(challenge string, nonce string, difficulty int, timestamp int64) bool { + // Check if the challenge is expired (e.g., valid for 1 minute) + if time.Now().Unix()-timestamp > 60 { + return false + } + data := challenge + nonce + hash := sha256.Sum256([]byte(data)) + hashStr := hex.EncodeToString(hash[:]) + prefix := strings.Repeat("0", difficulty) + return strings.HasPrefix(hashStr, prefix) +} + +var submissionCount int +var submissionCountMutex sync.Mutex + +// IncrementSubmissionCount increments the submission count +func IncrementSubmissionCount() { + submissionCountMutex.Lock() + defer submissionCountMutex.Unlock() + submissionCount++ +} + +// ResetSubmissionCount resets the submission count +func ResetSubmissionCount() { + submissionCountMutex.Lock() + defer submissionCountMutex.Unlock() + submissionCount = 0 +} + +// GetSubmissionCount returns the current submission count +func GetSubmissionCount() int { + submissionCountMutex.Lock() + defer submissionCountMutex.Unlock() + return submissionCount +} + +// Periodically reset the submission count (e.g., every minute) +func StartSubmissionCountReset() { + ticker := time.NewTicker(1 * time.Minute) + go func() { + for { + <-ticker.C + ResetSubmissionCount() + } + }() +} + +// GetDynamicDifficulty calculates the difficulty based on the current load +func GetDynamicDifficulty() int { + count := GetSubmissionCount() + if count > 100 { + return 6 // High load, increase difficulty + } else if count > 50 { + return 5 // Medium load, moderate difficulty + } + return 4 // Low load, default difficulty +} + +func main() { + // gin.SetMode(gin.ReleaseMode) // Uncomment this line to disable debug mode + + // Load environment variables from .env file + err := godotenv.Load() + if err != nil { + fmt.Println("Error loading .env file:", err) + } + + privateKeyData := os.Getenv("PRIVATE_KEY") + privateKey, err := LoadPrivateKey(privateKeyData) + if err != nil { + panic(err) + } + + secretKey := os.Getenv("KEY") + + client, err := connectDB() + if err != nil { + panic(err) + } + defer client.Disconnect(context.Background()) + + // admin commands? + + r := gin.Default() + r.Use(cors.Default()) // Enable CORS for all routes + + // Rate limiter configuration: max 10 requests per 5s per IP + limiter := tollbooth.NewLimiter(10, &limiter.ExpirableOptions{DefaultExpirationTTL: 5 * time.Second}) + + StartSubmissionCountReset() + + // Middleware to apply the rate limiter + r.Use(func(c *gin.Context) { + httpError := tollbooth.LimitByRequest(limiter, c.Writer, c.Request) + if httpError != nil { + c.JSON(httpError.StatusCode, gin.H{"error": httpError.Message}) + c.Abort() + return + } + c.Next() + }) + + r.GET("/api/model-list", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"models": MODELS}) + }) + + r.GET("/api/benchmark/:submissionid", func(c *gin.Context) { + submissionID := c.Param("submissionid") + collection := client.Database("ollamark_db").Collection("benchmarks") + + var benchmark BenchmarkResult + err := collection.FindOne(context.Background(), bson.M{"submissionid": submissionID}).Decode(&benchmark) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Benchmark not found"}) + return + } + + c.JSON(http.StatusOK, benchmark) + }) + + r.GET("/api/pow-challenge", func(c *gin.Context) { + challenge := GenerateProofOfWorkChallenge() + c.JSON(http.StatusOK, challenge) + }) + + r.GET("/api/benchmarks", func(c *gin.Context) { + sortBy := c.DefaultQuery("sort_by", "timestamp") + order := c.DefaultQuery("order", "desc") + modelFilter := c.DefaultQuery("model", "") + ollamaVersionFilter := c.DefaultQuery("ollama_version", "") + osFilter := c.DefaultQuery("os", "") + cpuFilter := c.DefaultQuery("cpu", "") + gpuFilter := c.DefaultQuery("gpu", "") + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10")) + + var sortOrder int + if order == "asc" { + sortOrder = 1 + } else { + sortOrder = -1 + } + + if limit == 0 { + // Set a large limit value when limit is 0 + limit = 1000000 // Adjust this value according to your needs + } + + filter := bson.M{} + if modelFilter != "" { + filter["modelname"] = modelFilter + } + if osFilter != "" { + filter["sysinfo.os"] = bson.M{"$regex": osFilter, "$options": "i"} + } + if cpuFilter != "" { + filter["sysinfo.cpuname"] = bson.M{"$regex": cpuFilter, "$options": "i"} + } + if gpuFilter != "" { + filter["gpuinfo.name"] = bson.M{"$regex": gpuFilter, "$options": "i"} + } + if ollamaVersionFilter != "" { + filter["ollamaversion"] = ollamaVersionFilter + } + + benchmarks, total, err := fetchBenchmarks(client, filter, sortBy, sortOrder, page, limit) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"benchmarks": benchmarks, "total": total}) + }) + + r.POST("/api/submit-benchmark", authMiddleware(), func(c *gin.Context) { + encryptedData, err := io.ReadAll(c.Request.Body) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"}) + fmt.Printf("Invalid request payload: %v", err) + return + } + + submissionID := c.GetHeader("X-Submission-ID") + signature := c.GetHeader("X-Signature") + + if !verifySignature(submissionID, signature, secretKey) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"}) + fmt.Printf("Invalid signature: %v", err) + return + } + + // Check for replay attacks by storing and checking used submission IDs + isUnique, err := checkSubmissionID(client, submissionID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check submission"}) + fmt.Printf("Failed to check submission ID: %v", err) + return + } + + if !isUnique { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Not a unique submission"}) + return + } + + var payload map[string]string + if err := json.Unmarshal(encryptedData, &payload); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload format"}) + fmt.Printf("Invalid payload format: %v", err) + return + } + + encryptedAESKey, _ := base64.StdEncoding.DecodeString(payload["encrypted_key"]) + nonce, _ := base64.StdEncoding.DecodeString(payload["nonce"]) + ciphertext, _ := base64.StdEncoding.DecodeString(payload["data"]) + + // Decrypt AES key with RSA private key + aesKey, err := DecryptData(privateKey, encryptedAESKey) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Decryption failed"}) + fmt.Printf("Decryption failed: %v", err) + return + } + + // Decrypt data with AES key + decryptedData, err := decryptAESGCM(aesKey, nonce, ciphertext) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Decryption failed"}) + fmt.Printf("Decryption failed: %v", err) + return + } + + var benchmarkResult BenchmarkResult + if err := json.Unmarshal(decryptedData, &benchmarkResult); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid benchmark data"}) + fmt.Printf("Invalid benchmark data: %v", err) + return + } + + // Basic verification of benchmark data + if benchmarkResult.EvalCount <= 0 || benchmarkResult.TokensPerSecond <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid benchmark metrics"}) + return + } + + // Validate the modelName against the predefined list + if !contains(MODELS, benchmarkResult.ModelName) { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid model name"}) + return + } + + // Verify proof-of-work + if !VerifyProofOfWork(benchmarkResult.ProofOfWork.Challenge, benchmarkResult.ProofOfWork.Nonce, benchmarkResult.ProofOfWork.Difficulty, benchmarkResult.ProofOfWork.Timestamp) { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid proof-of-work solution"}) + return + } + + checkedIP := checkIP(benchmarkResult.IP) + if !checkedIP { + c.JSON(http.StatusUnauthorized, gin.H{"error": "IP address is rate limited"}) + return + } + + log.Println("Benchmark was received successfully:", benchmarkResult) + log.Printf("SysInfo: %+v\n", *benchmarkResult.SysInfo) + log.Printf("GPUInfo: %+v\n", *benchmarkResult.GPUInfo) + benchmarkResult.SubmissionID = submissionID + + // Insert benchmarks into the MongoDB + err = insertBenchmark(client, benchmarkResult) + if err != nil { + fmt.Printf("Failed to insert benchmark: %v", err) + return + } + + IncrementSubmissionCount() + + c.JSON(http.StatusOK, gin.H{"message": "Benchmark submitted successfully"}) + }) + + port := ":3333" + log.Printf("Ollamark Server is running on port %s\n", port) + if err := r.Run(port); err != nil { + log.Printf("Failed to start Ollamark server: %v\n", err) + } +} diff --git a/web/craco.config.js b/web/craco.config.js new file mode 100644 index 0000000..6a24459 --- /dev/null +++ b/web/craco.config.js @@ -0,0 +1,5 @@ +module.exports = { + devServer: { + allowedHosts: 'all', // Allows all hosts for development + }, +}; diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..b1dfc08 --- /dev/null +++ b/web/package.json @@ -0,0 +1,52 @@ +{ + "name": "ollamark.com", + "version": "1.0.0", + "description": "Ollamark Web Client", + "author": "Carsen Klock", + "license": "MIT", + "homepage": "https://ollamark.com", + "private": true, + "proxy": "http://localhost:3333", + "dependencies": { + "@craco/craco": "^7.1.0", + "@radix-ui/react-icons": "^1.3.0", + "axios": "^1.2.1", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-icons": "^5.2.1", + "react-router-dom": "^6.23.1", + "react-scripts": "5.0.1", + "tailwind-merge": "^2.3.0", + "tailwindcss-animate": "^1.0.7", + "web-vitals": "^2.1.4" + }, + "scripts": { + "start": "craco start", + "build": "craco build", + "test": "craco test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "tailwindcss": "^3.4.4" + } +} diff --git a/web/public/index.css b/web/public/index.css new file mode 100644 index 0000000..2d88cf5 --- /dev/null +++ b/web/public/index.css @@ -0,0 +1,21 @@ +@import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + margin: 0; + font-family: 'Poppins', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: #181818; + color: #fff; + } + + code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; + background-color: #181818; + color: #006dad; + } + \ No newline at end of file diff --git a/web/public/index.html b/web/public/index.html new file mode 100644 index 0000000..02b00e8 --- /dev/null +++ b/web/public/index.html @@ -0,0 +1,13 @@ + + +
+ + +{benchmark.tokens_per_second.toFixed(2)}
+Ollamark Score (Tokens Per Second)
+{benchmark.submission_id}
+Benchmarks
+{benchmark.iterations}
+Timestamp
+{new Date(benchmark.timestamp * 1000).toLocaleString()}
+Ollama Version
+{benchmark.ollama_version}
+
+ {benchmark.sys_info.os.includes("darwin") ?
Arch
+{benchmark.sys_info.arch}
+Kernel Version
+{benchmark.sys_info.kernel}
+Client Type
+{benchmark.client_type}
+Client Version
+{benchmark.client_version}
+CPU
+{benchmark.sys_info.cpu_name}
+RAM
+{benchmark.sys_info.memory}
+GPU
+{benchmark.gpu_info.name}
+GPU Driver Version
+{benchmark.gpu_info.driver_version}
+ID | +Model | +OS | +CPU | +GPU | +Ollama Version | +Tokens Per Second | +Iterations | +Timestamp | +
---|---|---|---|---|---|---|---|---|
+ {benchmark.submission_id ? benchmark.submission_id.slice(0, 8) : 'N/A'} + | +{benchmark.model_name} | +{benchmark.sys_info.os === 'windows' ? <> |
+ {benchmark.sys_info.cpu_name} | +{benchmark.gpu_info.name} | +{benchmark.ollama_version} | +{benchmark.tokens_per_second.toFixed(2)} | +{benchmark.iterations} | +{new Date(benchmark.timestamp * 1000).toLocaleString()} | +
No Ollamarks found... | +