Compare commits
	
		
			1 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 11b844e1bb | 
| @@ -1,9 +1,7 @@ | |||||||
|  | build | ||||||
|  | llama/build | ||||||
|  | .venv | ||||||
| .vscode | .vscode | ||||||
| ollama | ollama | ||||||
| app | app | ||||||
| macapp | web | ||||||
| dist |  | ||||||
| llm/llama.cpp |  | ||||||
| .env |  | ||||||
| .cache |  | ||||||
| test_data |  | ||||||
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1 +0,0 @@ | |||||||
| llm/ext_server/* linguist-vendored |  | ||||||
							
								
								
									
										60
									
								
								.github/ISSUE_TEMPLATE/10_bug_report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,60 +0,0 @@ | |||||||
| name: Bug report |  | ||||||
| labels: [bug] |  | ||||||
| description: Something isn't working right. |  | ||||||
| body: |  | ||||||
|   - type: textarea |  | ||||||
|     id: description |  | ||||||
|     attributes: |  | ||||||
|       label: What is the issue? |  | ||||||
|       description: What happened? What did you expect to happen? |  | ||||||
|     validations: |  | ||||||
|       required: true |  | ||||||
|   - type: dropdown |  | ||||||
|     id: os |  | ||||||
|     attributes: |  | ||||||
|       label: OS |  | ||||||
|       description: Which operating system are you using? |  | ||||||
|       multiple: true |  | ||||||
|       options: |  | ||||||
|         - Linux |  | ||||||
|         - macOS |  | ||||||
|         - Windows |  | ||||||
|         - Docker |  | ||||||
|         - WSL2 |  | ||||||
|     validations: |  | ||||||
|       required: false |  | ||||||
|   - type: dropdown |  | ||||||
|     id: gpu |  | ||||||
|     attributes: |  | ||||||
|       label: GPU |  | ||||||
|       description: Which GPU are you using? |  | ||||||
|       multiple: true |  | ||||||
|       options: |  | ||||||
|         - Nvidia |  | ||||||
|         - AMD |  | ||||||
|         - Intel |  | ||||||
|         - Apple |  | ||||||
|         - Other |  | ||||||
|     validations: |  | ||||||
|       required: false |  | ||||||
|   - type: dropdown |  | ||||||
|     id: cpu |  | ||||||
|     attributes: |  | ||||||
|       label: CPU |  | ||||||
|       description: Which CPU are you using? |  | ||||||
|       multiple: true |  | ||||||
|       options: |  | ||||||
|         - Intel |  | ||||||
|         - AMD |  | ||||||
|         - Apple |  | ||||||
|         - Other |  | ||||||
|     validations: |  | ||||||
|       required: false |  | ||||||
|   - type: input |  | ||||||
|     id: version |  | ||||||
|     attributes: |  | ||||||
|       label: Ollama version |  | ||||||
|       description: What version of Ollama are you using? (`ollama --version`) |  | ||||||
|       placeholder: e.g., 0.1.32 |  | ||||||
|     validations: |  | ||||||
|       required: false |  | ||||||
							
								
								
									
										6
									
								
								.github/ISSUE_TEMPLATE/20_feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,6 +0,0 @@ | |||||||
| --- |  | ||||||
| name: Feature request |  | ||||||
| about: Request a new feature |  | ||||||
| labels: feature request |  | ||||||
| --- |  | ||||||
|  |  | ||||||
							
								
								
									
										5
									
								
								.github/ISSUE_TEMPLATE/30_model_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,5 +0,0 @@ | |||||||
| --- |  | ||||||
| name: Model request |  | ||||||
| about: Request support for a new model to be added to Ollama |  | ||||||
| labels: model request |  | ||||||
| --- |  | ||||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,8 +0,0 @@ | |||||||
| blank_issues_enabled: true |  | ||||||
| contact_links: |  | ||||||
|   - name: Help |  | ||||||
|     url: https://discord.com/invite/ollama |  | ||||||
|     about: Please join our Discord server for help using Ollama |  | ||||||
|   - name: Troubleshooting |  | ||||||
|     url: https://github.com/ollama/ollama/blob/main/docs/faq.md#faq |  | ||||||
|     about: See the FAQ for common issues and solutions |  | ||||||
							
								
								
									
										24
									
								
								.github/workflows/latest.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,24 +0,0 @@ | |||||||
| name: latest |  | ||||||
|  |  | ||||||
| on: |  | ||||||
|   release: |  | ||||||
|     types: [released] |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   update-latest: |  | ||||||
|     environment: release |  | ||||||
|     runs-on: linux |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - name: Login to Docker Hub |  | ||||||
|         uses: docker/login-action@v3 |  | ||||||
|         with: |  | ||||||
|           username: ${{ vars.DOCKER_USER }} |  | ||||||
|           password: ${{ secrets.DOCKER_ACCESS_TOKEN }} |  | ||||||
|       - name: Tag images as latest |  | ||||||
|         env: |  | ||||||
|           PUSH: "1" |  | ||||||
|         shell: bash |  | ||||||
|         run: | |  | ||||||
|           export "VERSION=${GITHUB_REF_NAME#v}" |  | ||||||
|           ./scripts/tag_latest.sh |  | ||||||
							
								
								
									
										474
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,474 +0,0 @@ | |||||||
| name: release |  | ||||||
|  |  | ||||||
| on: |  | ||||||
|   push: |  | ||||||
|     tags: |  | ||||||
|       - 'v*' |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   # Full build of the Mac assets |  | ||||||
|   build-darwin: |  | ||||||
|     runs-on: macos-12 |  | ||||||
|     environment: release |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - name: Set Version |  | ||||||
|         shell: bash |  | ||||||
|         run: | |  | ||||||
|           echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV |  | ||||||
|           echo "RELEASE_VERSION=$(echo ${GITHUB_REF_NAME} | cut -f1 -d-)" >> $GITHUB_ENV |  | ||||||
|       - name: key |  | ||||||
|         env: |  | ||||||
|           MACOS_SIGNING_KEY: ${{ secrets.MACOS_SIGNING_KEY }} |  | ||||||
|           MACOS_SIGNING_KEY_PASSWORD: ${{ secrets.MACOS_SIGNING_KEY_PASSWORD }} |  | ||||||
|         run: | |  | ||||||
|           echo $MACOS_SIGNING_KEY | base64 --decode > certificate.p12 |  | ||||||
|           security create-keychain -p password build.keychain |  | ||||||
|           security default-keychain -s build.keychain |  | ||||||
|           security unlock-keychain -p password build.keychain |  | ||||||
|           security import certificate.p12 -k build.keychain -P $MACOS_SIGNING_KEY_PASSWORD -T /usr/bin/codesign |  | ||||||
|           security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k password build.keychain |  | ||||||
|           security set-keychain-settings -lut 3600 build.keychain |  | ||||||
|       - uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - name: Build Darwin |  | ||||||
|         env: |  | ||||||
|           APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }} |  | ||||||
|           APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} |  | ||||||
|           APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} |  | ||||||
|           APPLE_ID: ${{ vars.APPLE_ID }} |  | ||||||
|           SDKROOT: /Applications/Xcode_13.4.1.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk |  | ||||||
|           DEVELOPER_DIR: /Applications/Xcode_13.4.1.app/Contents/Developer |  | ||||||
|         run: | |  | ||||||
|           ./scripts/build_darwin.sh |  | ||||||
|  |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: dist-darwin |  | ||||||
|           path: | |  | ||||||
|             dist/*arwin* |  | ||||||
|             !dist/*-cov |  | ||||||
|  |  | ||||||
|   # Windows builds take a long time to both install the dependencies and build, so parallelize |  | ||||||
|   # CPU generation step |  | ||||||
|   generate-windows-cpu: |  | ||||||
|     environment: release |  | ||||||
|     runs-on: windows |  | ||||||
|     env: |  | ||||||
|       KEY_CONTAINER: ${{ vars.KEY_CONTAINER }} |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - name: Set Version |  | ||||||
|         shell: bash |  | ||||||
|         run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV |  | ||||||
|       - uses: 'google-github-actions/auth@v2' |  | ||||||
|         with: |  | ||||||
|           project_id: 'ollama' |  | ||||||
|           credentials_json: '${{ secrets.GOOGLE_SIGNING_CREDENTIALS }}' |  | ||||||
|       - run: echo "${{ vars.OLLAMA_CERT }}" > ollama_inc.crt |  | ||||||
|       - name: install Windows SDK 8.1 to get signtool |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading SDK" |  | ||||||
|           Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=323507" -OutFile "${env:RUNNER_TEMP}\sdksetup.exe" |  | ||||||
|           Start-Process "${env:RUNNER_TEMP}\sdksetup.exe" -ArgumentList @("/q") -NoNewWindow -Wait |  | ||||||
|           write-host "Win SDK 8.1 installed" |  | ||||||
|           gci -path 'C:\Program Files (x86)\Windows Kits\' -r -fi 'signtool.exe' |  | ||||||
|       - name: install signing plugin |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading plugin" |  | ||||||
|           Invoke-WebRequest -Uri "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.0/kmscng-1.0-windows-amd64.zip" -OutFile "${env:RUNNER_TEMP}\plugin.zip" |  | ||||||
|           Expand-Archive -Path "${env:RUNNER_TEMP}\plugin.zip" -DestinationPath ${env:RUNNER_TEMP}\plugin\ |  | ||||||
|           write-host "Installing plugin" |  | ||||||
|           & "${env:RUNNER_TEMP}\plugin\*\kmscng.msi" /quiet |  | ||||||
|           write-host "plugin installed" |  | ||||||
|       - uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - run: go get ./... |  | ||||||
|       - run: | |  | ||||||
|           $gopath=(get-command go).source | split-path -parent |  | ||||||
|           & "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" |  | ||||||
|           cd $env:GITHUB_WORKSPACE |  | ||||||
|           $env:CMAKE_SYSTEM_VERSION="10.0.22621.0" |  | ||||||
|           $env:PATH="$gopath;$env:PATH" |  | ||||||
|           go generate -x ./... |  | ||||||
|         name: go generate |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: generate-windows-cpu |  | ||||||
|           path: | |  | ||||||
|             llm/build/**/bin/* |  | ||||||
|             llm/build/**/*.a |  | ||||||
|             dist/windows-amd64/** |  | ||||||
|  |  | ||||||
|   # ROCm generation step |  | ||||||
|   generate-windows-rocm: |  | ||||||
|     environment: release |  | ||||||
|     runs-on: windows |  | ||||||
|     env: |  | ||||||
|       KEY_CONTAINER: ${{ vars.KEY_CONTAINER }} |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - name: Set Version |  | ||||||
|         shell: bash |  | ||||||
|         run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV |  | ||||||
|       - uses: 'google-github-actions/auth@v2' |  | ||||||
|         with: |  | ||||||
|           project_id: 'ollama' |  | ||||||
|           credentials_json: '${{ secrets.GOOGLE_SIGNING_CREDENTIALS }}' |  | ||||||
|       - run: echo "${{ vars.OLLAMA_CERT }}" > ollama_inc.crt |  | ||||||
|       - name: install Windows SDK 8.1 to get signtool |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading SDK" |  | ||||||
|           Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=323507" -OutFile "${env:RUNNER_TEMP}\sdksetup.exe" |  | ||||||
|           Start-Process "${env:RUNNER_TEMP}\sdksetup.exe" -ArgumentList @("/q") -NoNewWindow -Wait |  | ||||||
|           write-host "Win SDK 8.1 installed" |  | ||||||
|           gci -path 'C:\Program Files (x86)\Windows Kits\' -r -fi 'signtool.exe' |  | ||||||
|       - name: install signing plugin |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading plugin" |  | ||||||
|           Invoke-WebRequest -Uri "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.0/kmscng-1.0-windows-amd64.zip" -OutFile "${env:RUNNER_TEMP}\plugin.zip" |  | ||||||
|           Expand-Archive -Path "${env:RUNNER_TEMP}\plugin.zip" -DestinationPath ${env:RUNNER_TEMP}\plugin\ |  | ||||||
|           write-host "Installing plugin" |  | ||||||
|           & "${env:RUNNER_TEMP}\plugin\*\kmscng.msi" /quiet |  | ||||||
|           write-host "plugin installed" |  | ||||||
|       - uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - name: 'Install ROCm' |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading AMD HIP Installer" |  | ||||||
|           Invoke-WebRequest -Uri "https://download.amd.com/developer/eula/rocm-hub/AMD-Software-PRO-Edition-23.Q4-WinSvr2022-For-HIP.exe" -OutFile "${env:RUNNER_TEMP}\rocm-install.exe" |  | ||||||
|           write-host "Installing AMD HIP" |  | ||||||
|           Start-Process "${env:RUNNER_TEMP}\rocm-install.exe" -ArgumentList '-install' -NoNewWindow -Wait |  | ||||||
|           write-host "Completed AMD HIP" |  | ||||||
|       - name: 'Verify ROCm' |  | ||||||
|         run: | |  | ||||||
|           & 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' --version |  | ||||||
|       - run: go get ./... |  | ||||||
|       - run: | |  | ||||||
|           $gopath=(get-command go).source | split-path -parent |  | ||||||
|           & "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" |  | ||||||
|           cd $env:GITHUB_WORKSPACE |  | ||||||
|           $env:CMAKE_SYSTEM_VERSION="10.0.22621.0" |  | ||||||
|           $env:PATH="$gopath;$env:PATH" |  | ||||||
|           $env:OLLAMA_SKIP_CPU_GENERATE="1" |  | ||||||
|           $env:HIP_PATH=$(Resolve-Path 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' | split-path | split-path) |  | ||||||
|           go generate -x ./... |  | ||||||
|         name: go generate |  | ||||||
|       - name: 'gather rocm dependencies' |  | ||||||
|         run: | |  | ||||||
|           $HIP_PATH=$(Resolve-Path 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' | split-path | split-path) |  | ||||||
|           md "dist\deps\bin\rocblas\library" |  | ||||||
|           cp "${HIP_PATH}\bin\hipblas.dll" "dist\deps\bin\" |  | ||||||
|           cp "${HIP_PATH}\bin\rocblas.dll" "dist\deps\bin\" |  | ||||||
|           cp "${HIP_PATH}\bin\rocblas\library\*" "dist\deps\bin\rocblas\library\" |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: generate-windows-rocm |  | ||||||
|           path: | |  | ||||||
|             llm/build/**/bin/* |  | ||||||
|             dist/windows-amd64/** |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: windows-rocm-deps |  | ||||||
|           path: dist/deps/* |  | ||||||
|  |  | ||||||
|   # CUDA generation step |  | ||||||
|   generate-windows-cuda: |  | ||||||
|     environment: release |  | ||||||
|     runs-on: windows |  | ||||||
|     env: |  | ||||||
|       KEY_CONTAINER: ${{ vars.KEY_CONTAINER }} |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - name: Set Version |  | ||||||
|         shell: bash |  | ||||||
|         run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV |  | ||||||
|       - uses: 'google-github-actions/auth@v2' |  | ||||||
|         with: |  | ||||||
|           project_id: 'ollama' |  | ||||||
|           credentials_json: '${{ secrets.GOOGLE_SIGNING_CREDENTIALS }}' |  | ||||||
|       - run: echo "${{ vars.OLLAMA_CERT }}" > ollama_inc.crt |  | ||||||
|       - name: install Windows SDK 8.1 to get signtool |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading SDK" |  | ||||||
|           Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=323507" -OutFile "${env:RUNNER_TEMP}\sdksetup.exe" |  | ||||||
|           Start-Process "${env:RUNNER_TEMP}\sdksetup.exe" -ArgumentList @("/q") -NoNewWindow -Wait |  | ||||||
|           write-host "Win SDK 8.1 installed" |  | ||||||
|           gci -path 'C:\Program Files (x86)\Windows Kits\' -r -fi 'signtool.exe' |  | ||||||
|       - name: install signing plugin |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading plugin" |  | ||||||
|           Invoke-WebRequest -Uri "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.0/kmscng-1.0-windows-amd64.zip" -OutFile "${env:RUNNER_TEMP}\plugin.zip" |  | ||||||
|           Expand-Archive -Path "${env:RUNNER_TEMP}\plugin.zip" -DestinationPath ${env:RUNNER_TEMP}\plugin\ |  | ||||||
|           write-host "Installing plugin" |  | ||||||
|           & "${env:RUNNER_TEMP}\plugin\*\kmscng.msi" /quiet |  | ||||||
|           write-host "plugin installed" |  | ||||||
|       - uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - name: 'Install CUDA' |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading CUDA Installer" |  | ||||||
|           Invoke-WebRequest -Uri "https://developer.download.nvidia.com/compute/cuda/11.3.1/local_installers/cuda_11.3.1_465.89_win10.exe" -OutFile "${env:RUNNER_TEMP}\cuda-install.exe" |  | ||||||
|           write-host "Installing CUDA" |  | ||||||
|           Start-Process "${env:RUNNER_TEMP}\cuda-install.exe" -ArgumentList '-s' -NoNewWindow -Wait |  | ||||||
|           write-host "Completed CUDA" |  | ||||||
|           $cudaPath=((resolve-path "c:\Program Files\NVIDIA*\CUDA\v*\bin\nvcc.exe")[0].path | split-path | split-path) |  | ||||||
|           $cudaVer=($cudaPath | split-path -leaf ) -replace 'v(\d+).(\d+)', '$1_$2'  |  | ||||||
|           echo "$cudaPath\bin" >> $env:GITHUB_PATH |  | ||||||
|           echo "CUDA_PATH=$cudaPath" >> $env:GITHUB_ENV |  | ||||||
|           echo "CUDA_PATH_V${cudaVer}=$cudaPath" >> $env:GITHUB_ENV |  | ||||||
|           echo "CUDA_PATH_VX_Y=CUDA_PATH_V${cudaVer}" >> $env:GITHUB_ENV |  | ||||||
|       - name: 'Verify CUDA' |  | ||||||
|         run: nvcc -V |  | ||||||
|       - run: go get ./... |  | ||||||
|       - name: go generate |  | ||||||
|         run: | |  | ||||||
|           $gopath=(get-command go).source | split-path -parent |  | ||||||
|           $cudabin=(get-command nvcc).source | split-path |  | ||||||
|           & "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" |  | ||||||
|           cd $env:GITHUB_WORKSPACE |  | ||||||
|           $env:CMAKE_SYSTEM_VERSION="10.0.22621.0" |  | ||||||
|           $env:PATH="$gopath;$cudabin;$env:PATH" |  | ||||||
|           $env:OLLAMA_SKIP_CPU_GENERATE="1" |  | ||||||
|           go generate -x ./... |  | ||||||
|       - name: 'gather cuda dependencies' |  | ||||||
|         run: | |  | ||||||
|           $NVIDIA_DIR=(resolve-path 'C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\*\bin\')[0] |  | ||||||
|           md "dist\deps" |  | ||||||
|           cp "${NVIDIA_DIR}\cudart64_*.dll" "dist\deps\" |  | ||||||
|           cp "${NVIDIA_DIR}\cublas64_*.dll" "dist\deps\" |  | ||||||
|           cp "${NVIDIA_DIR}\cublasLt64_*.dll" "dist\deps\" |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: generate-windows-cuda |  | ||||||
|           path: | |  | ||||||
|             llm/build/**/bin/* |  | ||||||
|             dist/windows-amd64/** |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: windows-cuda-deps |  | ||||||
|           path: dist/deps/* |  | ||||||
|  |  | ||||||
|   # Import the prior generation steps and build the final windows assets |  | ||||||
|   build-windows: |  | ||||||
|     environment: release |  | ||||||
|     runs-on: windows |  | ||||||
|     needs: |  | ||||||
|       - generate-windows-cuda |  | ||||||
|       - generate-windows-rocm |  | ||||||
|       - generate-windows-cpu |  | ||||||
|     env: |  | ||||||
|       KEY_CONTAINER: ${{ vars.KEY_CONTAINER }} |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           submodules: recursive |  | ||||||
|       - name: Set Version |  | ||||||
|         shell: bash |  | ||||||
|         run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV |  | ||||||
|       - uses: 'google-github-actions/auth@v2' |  | ||||||
|         with: |  | ||||||
|           project_id: 'ollama' |  | ||||||
|           credentials_json: '${{ secrets.GOOGLE_SIGNING_CREDENTIALS }}' |  | ||||||
|       - run: echo "${{ vars.OLLAMA_CERT }}" > ollama_inc.crt |  | ||||||
|       - name: install Windows SDK 8.1 to get signtool |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading SDK" |  | ||||||
|           Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=323507" -OutFile "${env:RUNNER_TEMP}\sdksetup.exe" |  | ||||||
|           Start-Process "${env:RUNNER_TEMP}\sdksetup.exe" -ArgumentList @("/q") -NoNewWindow -Wait |  | ||||||
|           write-host "Win SDK 8.1 installed" |  | ||||||
|           gci -path 'C:\Program Files (x86)\Windows Kits\' -r -fi 'signtool.exe' |  | ||||||
|       - name: install signing plugin |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading plugin" |  | ||||||
|           Invoke-WebRequest -Uri "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.0/kmscng-1.0-windows-amd64.zip" -OutFile "${env:RUNNER_TEMP}\plugin.zip" |  | ||||||
|           Expand-Archive -Path "${env:RUNNER_TEMP}\plugin.zip" -DestinationPath ${env:RUNNER_TEMP}\plugin\ |  | ||||||
|           write-host "Installing plugin" |  | ||||||
|           & "${env:RUNNER_TEMP}\plugin\*\kmscng.msi" /quiet |  | ||||||
|           write-host "plugin installed" |  | ||||||
|       - uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - run: go get |  | ||||||
|       - uses: actions/download-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: generate-windows-cpu |  | ||||||
|       - uses: actions/download-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: generate-windows-cuda |  | ||||||
|       - uses: actions/download-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: windows-cuda-deps |  | ||||||
|       - uses: actions/download-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: windows-rocm-deps |  | ||||||
|       - uses: actions/download-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: generate-windows-rocm |  | ||||||
|       - run: dir llm/build |  | ||||||
|       - run: | |  | ||||||
|           $gopath=(get-command go).source | split-path -parent |  | ||||||
|           & "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" |  | ||||||
|           cd $env:GITHUB_WORKSPACE |  | ||||||
|           $env:CMAKE_SYSTEM_VERSION="10.0.22621.0" |  | ||||||
|           $env:PATH="$gopath;$env:PATH" |  | ||||||
|           $env:OLLAMA_SKIP_GENERATE="1" |  | ||||||
|           & .\scripts\build_windows.ps1 |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: dist-windows |  | ||||||
|           path: | |  | ||||||
|             dist/OllamaSetup.exe |  | ||||||
|             dist/ollama-windows-*.zip |  | ||||||
|  |  | ||||||
|   # Linux x86 assets built using the container based build |  | ||||||
|   build-linux-amd64: |  | ||||||
|     environment: release |  | ||||||
|     runs-on: linux |  | ||||||
|     env: |  | ||||||
|       OLLAMA_SKIP_MANIFEST_CREATE: '1' |  | ||||||
|       BUILD_ARCH: amd64 |  | ||||||
|       PUSH: '1' |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           submodules: recursive |  | ||||||
|       - name: Set Version |  | ||||||
|         shell: bash |  | ||||||
|         run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV |  | ||||||
|       - name: Login to Docker Hub |  | ||||||
|         uses: docker/login-action@v3 |  | ||||||
|         with: |  | ||||||
|           username: ${{ vars.DOCKER_USER }} |  | ||||||
|           password: ${{ secrets.DOCKER_ACCESS_TOKEN }} |  | ||||||
|       - run: | |  | ||||||
|           ./scripts/build_linux.sh |  | ||||||
|           ./scripts/build_docker.sh |  | ||||||
|           mv dist/deps/* dist/ |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: dist-linux-amd64 |  | ||||||
|           path: | |  | ||||||
|             dist/*linux* |  | ||||||
|             !dist/*-cov |  | ||||||
|  |  | ||||||
|   # Linux ARM assets built using the container based build |  | ||||||
|   # (at present, docker isn't pre-installed on arm ubunutu images) |  | ||||||
|   build-linux-arm64: |  | ||||||
|     environment: release |  | ||||||
|     runs-on: linux-arm64 |  | ||||||
|     env: |  | ||||||
|       OLLAMA_SKIP_MANIFEST_CREATE: '1' |  | ||||||
|       BUILD_ARCH: arm64 |  | ||||||
|       PUSH: '1' |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           submodules: recursive |  | ||||||
|       - name: Set Version |  | ||||||
|         shell: bash |  | ||||||
|         run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV |  | ||||||
|       - name: 'Install Docker' |  | ||||||
|         run: | |  | ||||||
|           # Add Docker's official GPG key: |  | ||||||
|           env |  | ||||||
|           uname -a |  | ||||||
|           sudo apt-get update |  | ||||||
|           sudo apt-get install -y ca-certificates curl |  | ||||||
|           sudo install -m 0755 -d /etc/apt/keyrings |  | ||||||
|           sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc |  | ||||||
|           sudo chmod a+r /etc/apt/keyrings/docker.asc |  | ||||||
|  |  | ||||||
|           # Add the repository to Apt sources: |  | ||||||
|           echo \ |  | ||||||
|             "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ |  | ||||||
|             $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ |  | ||||||
|             sudo tee /etc/apt/sources.list.d/docker.list > /dev/null |  | ||||||
|           sudo apt-get update |  | ||||||
|           sudo apt-get install -y docker-ce docker-ce-cli containerd.io |  | ||||||
|           sudo usermod -aG docker $USER |  | ||||||
|           sudo apt-get install acl |  | ||||||
|           sudo setfacl --modify user:$USER:rw /var/run/docker.sock |  | ||||||
|       - name: Login to Docker Hub |  | ||||||
|         uses: docker/login-action@v3 |  | ||||||
|         with: |  | ||||||
|           username: ${{ vars.DOCKER_USER }} |  | ||||||
|           password: ${{ secrets.DOCKER_ACCESS_TOKEN }} |  | ||||||
|       - run: | |  | ||||||
|           ./scripts/build_linux.sh |  | ||||||
|           ./scripts/build_docker.sh |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: dist-linux-arm64 |  | ||||||
|           path: | |  | ||||||
|             dist/*linux* |  | ||||||
|             !dist/*-cov |  | ||||||
|  |  | ||||||
|   # Aggregate all the assets and ship a release |  | ||||||
|   release: |  | ||||||
|     needs: |  | ||||||
|       - build-darwin |  | ||||||
|       - build-windows |  | ||||||
|       - build-linux-amd64 |  | ||||||
|       - build-linux-arm64 |  | ||||||
|     runs-on: linux |  | ||||||
|     environment: release |  | ||||||
|     permissions: |  | ||||||
|       contents: write |  | ||||||
|     env: |  | ||||||
|       OLLAMA_SKIP_IMAGE_BUILD: '1' |  | ||||||
|       PUSH: '1' |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - name: Set Version |  | ||||||
|         shell: bash |  | ||||||
|         run: | |  | ||||||
|           echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV |  | ||||||
|           echo "RELEASE_VERSION=$(echo ${GITHUB_REF_NAME} | cut -f1 -d-)" >> $GITHUB_ENV |  | ||||||
|       - name: Login to Docker Hub |  | ||||||
|         uses: docker/login-action@v3 |  | ||||||
|         with: |  | ||||||
|           username: ${{ vars.DOCKER_USER }} |  | ||||||
|           password: ${{ secrets.DOCKER_ACCESS_TOKEN }} |  | ||||||
|       - run: ./scripts/build_docker.sh |  | ||||||
|       - name: Retrieve built artifact |  | ||||||
|         uses: actions/download-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           path: dist |  | ||||||
|           pattern: dist-* |  | ||||||
|           merge-multiple: true |  | ||||||
|       - run: | |  | ||||||
|           ls -lh dist/ |  | ||||||
|           (cd dist; sha256sum * > sha256sum.txt) |  | ||||||
|           cat dist/sha256sum.txt |  | ||||||
|       - uses: ncipollo/release-action@v1 |  | ||||||
|         with: |  | ||||||
|           name: ${{ env.RELEASE_VERSION }} |  | ||||||
|           allowUpdates: true |  | ||||||
|           artifacts: 'dist/*' |  | ||||||
|           draft: true |  | ||||||
|           prerelease: true |  | ||||||
|           omitBodyDuringUpdate: true |  | ||||||
|           generateReleaseNotes: true |  | ||||||
|           omitDraftDuringUpdate: true |  | ||||||
|           omitPrereleaseDuringUpdate: true |  | ||||||
|           replacesArtifacts: true |  | ||||||
							
								
								
									
										321
									
								
								.github/workflows/test.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,321 +0,0 @@ | |||||||
| name: test |  | ||||||
|  |  | ||||||
| concurrency: |  | ||||||
|   # For PRs, later CI runs preempt previous ones. e.g. a force push on a PR |  | ||||||
|   # cancels running CI jobs and starts all new ones. |  | ||||||
|   # |  | ||||||
|   # For non-PR pushes, concurrency.group needs to be unique for every distinct |  | ||||||
|   # CI run we want to have happen. Use run_id, which in practice means all |  | ||||||
|   # non-PR CI runs will be allowed to run without preempting each other. |  | ||||||
|   group: ${{ github.workflow }}-$${{ github.pull_request.number || github.run_id }} |  | ||||||
|   cancel-in-progress: true |  | ||||||
|  |  | ||||||
| on: |  | ||||||
|   pull_request: |  | ||||||
|     paths: |  | ||||||
|       - '**/*' |  | ||||||
|       - '!docs/**' |  | ||||||
|       - '!README.md' |  | ||||||
|  |  | ||||||
| jobs: |  | ||||||
|   changes: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     outputs: |  | ||||||
|       GENERATE: ${{ steps.changes.outputs.GENERATE }} |  | ||||||
|       GENERATE_CUDA: ${{ steps.changes.outputs.GENERATE_CUDA }} |  | ||||||
|       GENERATE_ROCM: ${{ steps.changes.outputs.GENERATE_ROCM }} |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           fetch-depth: 0 |  | ||||||
|       - id: changes |  | ||||||
|         run: | |  | ||||||
|           changed() { |  | ||||||
|             git diff-tree -r --no-commit-id --name-only \ |  | ||||||
|               $(git merge-base ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}) \ |  | ||||||
|               ${{ github.event.pull_request.head.sha }} \ |  | ||||||
|               | xargs python3 -c "import sys; from pathlib import Path; print(any(Path(x).match(glob) for x in sys.argv[1:] for glob in '$*'.split(' ')))" |  | ||||||
|           } |  | ||||||
|  |  | ||||||
|           { |  | ||||||
|             echo GENERATE=$(changed 'llm/llama.cpp' 'llm/patches/**' 'llm/ext_server/**' 'llm/generate/**') |  | ||||||
|             echo GENERATE_CUDA=$(changed 'llm/llama.cpp' 'llm/patches/**' 'llm/ext_server/**' 'llm/generate/**') |  | ||||||
|             echo GENERATE_ROCM=$(changed 'llm/llama.cpp' 'llm/patches/**' 'llm/ext_server/**' 'llm/generate/**') |  | ||||||
|           } >>$GITHUB_OUTPUT |  | ||||||
|  |  | ||||||
|   generate: |  | ||||||
|     needs: [changes] |  | ||||||
|     if: ${{ needs.changes.outputs.GENERATE == 'True' }} |  | ||||||
|     strategy: |  | ||||||
|       matrix: |  | ||||||
|         os: [ubuntu-latest, macos-latest, windows-2019] |  | ||||||
|         arch: [amd64, arm64] |  | ||||||
|         exclude: |  | ||||||
|           - os: ubuntu-latest |  | ||||||
|             arch: arm64 |  | ||||||
|           - os: windows-2019 |  | ||||||
|             arch: arm64 |  | ||||||
|     runs-on: ${{ matrix.os }} |  | ||||||
|     env: |  | ||||||
|       GOARCH: ${{ matrix.arch }} |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - run: go get ./... |  | ||||||
|       - run: | |  | ||||||
|           $gopath=(get-command go).source | split-path -parent |  | ||||||
|           $gccpath=(get-command gcc).source | split-path -parent |  | ||||||
|           & "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" |  | ||||||
|           cd $env:GITHUB_WORKSPACE |  | ||||||
|           $env:CMAKE_SYSTEM_VERSION="10.0.22621.0" |  | ||||||
|           $env:PATH="$gopath;$gccpath;$env:PATH" |  | ||||||
|           echo $env:PATH |  | ||||||
|           go generate -x ./... |  | ||||||
|         if: ${{ startsWith(matrix.os, 'windows-') }} |  | ||||||
|         name: 'Windows Go Generate' |  | ||||||
|       - run: go generate -x ./... |  | ||||||
|         if: ${{ ! startsWith(matrix.os, 'windows-') }} |  | ||||||
|         name: 'Unix Go Generate' |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: ${{ matrix.os }}-${{ matrix.arch }}-libraries |  | ||||||
|           path: | |  | ||||||
|             llm/build/**/bin/* |  | ||||||
|             llm/build/**/*.a |  | ||||||
|   generate-cuda: |  | ||||||
|     needs: [changes] |  | ||||||
|     if: ${{ needs.changes.outputs.GENERATE_CUDA == 'True' }} |  | ||||||
|     strategy: |  | ||||||
|       matrix: |  | ||||||
|         cuda-version: |  | ||||||
|           - '11.8.0' |  | ||||||
|     runs-on: linux |  | ||||||
|     container: nvidia/cuda:${{ matrix.cuda-version }}-devel-ubuntu20.04 |  | ||||||
|     steps: |  | ||||||
|       - run: | |  | ||||||
|           apt-get update && apt-get install -y git build-essential curl |  | ||||||
|           curl -fsSL https://github.com/Kitware/CMake/releases/download/v3.28.1/cmake-3.28.1-linux-x86_64.tar.gz \ |  | ||||||
|             | tar -zx -C /usr --strip-components 1 |  | ||||||
|         env: |  | ||||||
|           DEBIAN_FRONTEND: noninteractive |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - uses: actions/setup-go@v4 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - run: go get ./... |  | ||||||
|       - run: | |  | ||||||
|           git config --global --add safe.directory /__w/ollama/ollama |  | ||||||
|           go generate -x ./... |  | ||||||
|         env: |  | ||||||
|           OLLAMA_SKIP_CPU_GENERATE: '1' |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: cuda-${{ matrix.cuda-version }}-libraries |  | ||||||
|           path: | |  | ||||||
|             llm/build/**/bin/* |  | ||||||
|             dist/windows-amd64/** |  | ||||||
|   generate-rocm: |  | ||||||
|     needs: [changes] |  | ||||||
|     if: ${{ needs.changes.outputs.GENERATE_ROCM == 'True' }} |  | ||||||
|     strategy: |  | ||||||
|       matrix: |  | ||||||
|         rocm-version: |  | ||||||
|           - '6.0.2' |  | ||||||
|     runs-on: linux |  | ||||||
|     container: rocm/dev-ubuntu-20.04:${{ matrix.rocm-version }} |  | ||||||
|     steps: |  | ||||||
|       - run: | |  | ||||||
|           apt-get update && apt-get install -y git build-essential curl rocm-libs |  | ||||||
|           curl -fsSL https://github.com/Kitware/CMake/releases/download/v3.28.1/cmake-3.28.1-linux-x86_64.tar.gz \ |  | ||||||
|             | tar -zx -C /usr --strip-components 1 |  | ||||||
|         env: |  | ||||||
|           DEBIAN_FRONTEND: noninteractive |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - uses: actions/setup-go@v4 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - run: go get ./... |  | ||||||
|       - run: | |  | ||||||
|           git config --global --add safe.directory /__w/ollama/ollama |  | ||||||
|           go generate -x ./... |  | ||||||
|         env: |  | ||||||
|           OLLAMA_SKIP_CPU_GENERATE: '1' |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: rocm-${{ matrix.rocm-version }}-libraries |  | ||||||
|           path: | |  | ||||||
|             llm/build/**/bin/* |  | ||||||
|             dist/windows-amd64/** |  | ||||||
|  |  | ||||||
|   # ROCm generation step |  | ||||||
|   generate-windows-rocm: |  | ||||||
|     needs: [changes] |  | ||||||
|     if: ${{ needs.changes.outputs.GENERATE_ROCM == 'True' }} |  | ||||||
|     runs-on: windows |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - name: 'Install ROCm' |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading AMD HIP Installer" |  | ||||||
|           Invoke-WebRequest -Uri "https://download.amd.com/developer/eula/rocm-hub/AMD-Software-PRO-Edition-23.Q4-WinSvr2022-For-HIP.exe" -OutFile "${env:RUNNER_TEMP}\rocm-install.exe" |  | ||||||
|           write-host "Installing AMD HIP" |  | ||||||
|           Start-Process "${env:RUNNER_TEMP}\rocm-install.exe" -ArgumentList '-install' -NoNewWindow -Wait |  | ||||||
|           write-host "Completed AMD HIP" |  | ||||||
|       - name: 'Verify ROCm' |  | ||||||
|         run: | |  | ||||||
|           & 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' --version |  | ||||||
|       - run: go get ./... |  | ||||||
|       - run: | |  | ||||||
|           $gopath=(get-command go).source | split-path -parent |  | ||||||
|           & "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" |  | ||||||
|           cd $env:GITHUB_WORKSPACE |  | ||||||
|           $env:CMAKE_SYSTEM_VERSION="10.0.22621.0" |  | ||||||
|           $env:PATH="$gopath;$env:PATH" |  | ||||||
|           $env:OLLAMA_SKIP_CPU_GENERATE="1" |  | ||||||
|           $env:HIP_PATH=$(Resolve-Path 'C:\Program Files\AMD\ROCm\*\bin\clang.exe' | split-path | split-path) |  | ||||||
|           go generate -x ./... |  | ||||||
|         name: go generate |  | ||||||
|         env: |  | ||||||
|           OLLAMA_SKIP_CPU_GENERATE: '1' |  | ||||||
|       # TODO - do we need any artifacts? |  | ||||||
|  |  | ||||||
|   # CUDA generation step |  | ||||||
|   generate-windows-cuda: |  | ||||||
|     needs: [changes] |  | ||||||
|     if: ${{ needs.changes.outputs.GENERATE_CUDA == 'True' }} |  | ||||||
|     runs-on: windows |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|       - uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - name: 'Install CUDA' |  | ||||||
|         run: | |  | ||||||
|           $ErrorActionPreference = "Stop" |  | ||||||
|           write-host "downloading CUDA Installer" |  | ||||||
|           Invoke-WebRequest -Uri "https://developer.download.nvidia.com/compute/cuda/11.3.1/local_installers/cuda_11.3.1_465.89_win10.exe" -OutFile "${env:RUNNER_TEMP}\cuda-install.exe" |  | ||||||
|           write-host "Installing CUDA" |  | ||||||
|           Start-Process "${env:RUNNER_TEMP}\cuda-install.exe" -ArgumentList '-s' -NoNewWindow -Wait |  | ||||||
|           write-host "Completed CUDA" |  | ||||||
|           $cudaPath=((resolve-path "c:\Program Files\NVIDIA*\CUDA\v*\bin\nvcc.exe")[0].path | split-path | split-path) |  | ||||||
|           $cudaVer=($cudaPath | split-path -leaf ) -replace 'v(\d+).(\d+)', '$1_$2'  |  | ||||||
|           echo "$cudaPath\bin" >> $env:GITHUB_PATH |  | ||||||
|           echo "CUDA_PATH=$cudaPath" >> $env:GITHUB_ENV |  | ||||||
|           echo "CUDA_PATH_V${cudaVer}=$cudaPath" >> $env:GITHUB_ENV |  | ||||||
|           echo "CUDA_PATH_VX_Y=CUDA_PATH_V${cudaVer}" >> $env:GITHUB_ENV |  | ||||||
|       - name: 'Verify CUDA' |  | ||||||
|         run: nvcc -V |  | ||||||
|       - run: go get ./... |  | ||||||
|       - name: go generate |  | ||||||
|         run: | |  | ||||||
|           $gopath=(get-command go).source | split-path -parent |  | ||||||
|           $cudabin=(get-command nvcc).source | split-path |  | ||||||
|           & "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\Launch-VsDevShell.ps1" |  | ||||||
|           cd $env:GITHUB_WORKSPACE |  | ||||||
|           $env:CMAKE_SYSTEM_VERSION="10.0.22621.0" |  | ||||||
|           $env:PATH="$gopath;$cudabin;$env:PATH" |  | ||||||
|           $env:OLLAMA_SKIP_CPU_GENERATE="1" |  | ||||||
|           go generate -x ./... |  | ||||||
|         env: |  | ||||||
|           OLLAMA_SKIP_CPU_GENERATE: '1' |  | ||||||
|       # TODO - do we need any artifacts? |  | ||||||
|  |  | ||||||
|   lint: |  | ||||||
|     strategy: |  | ||||||
|       matrix: |  | ||||||
|         os: [ubuntu-latest, macos-latest, windows-2019] |  | ||||||
|         arch: [amd64, arm64] |  | ||||||
|         exclude: |  | ||||||
|           - os: ubuntu-latest |  | ||||||
|             arch: arm64 |  | ||||||
|           - os: windows-2019 |  | ||||||
|             arch: arm64 |  | ||||||
|           - os: macos-latest |  | ||||||
|             arch: amd64 |  | ||||||
|     runs-on: ${{ matrix.os }} |  | ||||||
|     env: |  | ||||||
|       GOARCH: ${{ matrix.arch }} |  | ||||||
|       CGO_ENABLED: '1' |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           submodules: recursive |  | ||||||
|       - uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: false |  | ||||||
|       - run: | |  | ||||||
|           case ${{ matrix.arch }} in |  | ||||||
|             amd64) echo ARCH=x86_64 ;; |  | ||||||
|             arm64) echo ARCH=arm64 ;; |  | ||||||
|           esac >>$GITHUB_ENV |  | ||||||
|         shell: bash |  | ||||||
|       - run: | |  | ||||||
|           mkdir -p llm/build/linux/$ARCH/stub/bin |  | ||||||
|           touch llm/build/linux/$ARCH/stub/bin/ollama_llama_server |  | ||||||
|         if: ${{ startsWith(matrix.os, 'ubuntu-') }} |  | ||||||
|       - run: | |  | ||||||
|           mkdir -p llm/build/darwin/$ARCH/stub/bin |  | ||||||
|           touch llm/build/darwin/$ARCH/stub/bin/ollama_llama_server |  | ||||||
|         if: ${{ startsWith(matrix.os, 'macos-') }} |  | ||||||
|       - uses: golangci/golangci-lint-action@v4 |  | ||||||
|         with: |  | ||||||
|           args: --timeout 8m0s -v |  | ||||||
|   test: |  | ||||||
|     strategy: |  | ||||||
|       matrix: |  | ||||||
|         os: [ubuntu-latest, macos-latest, windows-2019] |  | ||||||
|         arch: [amd64] |  | ||||||
|         exclude: |  | ||||||
|           - os: ubuntu-latest |  | ||||||
|             arch: arm64 |  | ||||||
|           - os: windows-2019 |  | ||||||
|             arch: arm64 |  | ||||||
|     runs-on: ${{ matrix.os }} |  | ||||||
|     env: |  | ||||||
|       GOARCH: ${{ matrix.arch }} |  | ||||||
|       CGO_ENABLED: '1' |  | ||||||
|       OLLAMA_CPU_TARGET: 'static' |  | ||||||
|       OLLAMA_SKIP_CPU_GENERATE: '1' |  | ||||||
|       OLLAMA_SKIP_METAL_GENERATE: '1' |  | ||||||
|     steps: |  | ||||||
|       - uses: actions/checkout@v4 |  | ||||||
|         with: |  | ||||||
|           submodules: recursive |  | ||||||
|       - uses: actions/setup-go@v5 |  | ||||||
|         with: |  | ||||||
|           go-version-file: go.mod |  | ||||||
|           cache: true |  | ||||||
|       - run: | |  | ||||||
|           case ${{ matrix.arch }} in |  | ||||||
|             amd64) echo ARCH=x86_64 ;; |  | ||||||
|             arm64) echo ARCH=arm64 ;; |  | ||||||
|           esac >>$GITHUB_ENV |  | ||||||
|         shell: bash |  | ||||||
|       - run: | |  | ||||||
|           mkdir -p llm/build/linux/$ARCH/stub/bin |  | ||||||
|           touch llm/build/linux/$ARCH/stub/bin/ollama_llama_server |  | ||||||
|         if: ${{ startsWith(matrix.os, 'ubuntu-') }} |  | ||||||
|       - run: | |  | ||||||
|           mkdir -p llm/build/darwin/$ARCH/stub/bin |  | ||||||
|           touch llm/build/darwin/$ARCH/stub/bin/ollama_llama_server |  | ||||||
|         if: ${{ startsWith(matrix.os, 'macos-') }} |  | ||||||
|         shell: bash |  | ||||||
|       - run: go generate ./... |  | ||||||
|       - run: go build |  | ||||||
|       - run: go test -v ./... |  | ||||||
|       - uses: actions/upload-artifact@v4 |  | ||||||
|         with: |  | ||||||
|           name: ${{ matrix.os }}-binaries |  | ||||||
|           path: ollama |  | ||||||
							
								
								
									
										8
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -5,11 +5,3 @@ | |||||||
| .swp | .swp | ||||||
| dist | dist | ||||||
| ollama | ollama | ||||||
| ggml-metal.metal |  | ||||||
| .cache |  | ||||||
| *.exe |  | ||||||
| .idea |  | ||||||
| test_data |  | ||||||
| *.crt |  | ||||||
| llm/build |  | ||||||
| __debug_bin* |  | ||||||
							
								
								
									
										4
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1,4 +0,0 @@ | |||||||
| [submodule "llama.cpp"] |  | ||||||
| 	path = llm/llama.cpp |  | ||||||
| 	url = https://github.com/ggerganov/llama.cpp.git |  | ||||||
| 	shallow = true |  | ||||||
| @@ -1,17 +0,0 @@ | |||||||
| run: |  | ||||||
|   timeout: 5m |  | ||||||
| linters: |  | ||||||
|   enable: |  | ||||||
|     - asasalint |  | ||||||
|     - bidichk |  | ||||||
|     - bodyclose |  | ||||||
|     - containedctx |  | ||||||
|     - contextcheck |  | ||||||
|     - exportloopref |  | ||||||
|     - gocheckcompilerdirectives |  | ||||||
|     # FIXME: for some reason this errors on windows |  | ||||||
|     # - gofmt |  | ||||||
|     # - goimports |  | ||||||
|     - misspell |  | ||||||
|     - nilerr |  | ||||||
|     - unused |  | ||||||
							
								
								
									
										147
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						| @@ -1,144 +1,15 @@ | |||||||
| ARG GOLANG_VERSION=1.22.1 | FROM golang:1.20 | ||||||
| ARG CMAKE_VERSION=3.22.1 | WORKDIR /go/src/github.com/jmorganca/ollama | ||||||
| # this CUDA_VERSION corresponds with the one specified in docs/gpu.md |  | ||||||
| ARG CUDA_VERSION=11.3.1 |  | ||||||
| ARG ROCM_VERSION=6.0.2 |  | ||||||
|  |  | ||||||
| # Copy the minimal context we need to run the generate scripts |  | ||||||
| FROM scratch AS llm-code |  | ||||||
| COPY .git .git |  | ||||||
| COPY .gitmodules .gitmodules |  | ||||||
| COPY llm llm |  | ||||||
|  |  | ||||||
| FROM --platform=linux/amd64 nvidia/cuda:$CUDA_VERSION-devel-centos7 AS cuda-build-amd64 |  | ||||||
| ARG CMAKE_VERSION |  | ||||||
| COPY ./scripts/rh_linux_deps.sh / |  | ||||||
| RUN CMAKE_VERSION=${CMAKE_VERSION} sh /rh_linux_deps.sh |  | ||||||
| ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH |  | ||||||
| COPY --from=llm-code / /go/src/github.com/ollama/ollama/ |  | ||||||
| WORKDIR /go/src/github.com/ollama/ollama/llm/generate |  | ||||||
| ARG CGO_CFLAGS |  | ||||||
| RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_SKIP_CPU_GENERATE=1 sh gen_linux.sh |  | ||||||
|  |  | ||||||
| FROM --platform=linux/arm64 nvidia/cuda:$CUDA_VERSION-devel-rockylinux8 AS cuda-build-arm64 |  | ||||||
| ARG CMAKE_VERSION |  | ||||||
| COPY ./scripts/rh_linux_deps.sh / |  | ||||||
| RUN CMAKE_VERSION=${CMAKE_VERSION} sh /rh_linux_deps.sh |  | ||||||
| ENV PATH /opt/rh/gcc-toolset-10/root/usr/bin:$PATH |  | ||||||
| COPY --from=llm-code / /go/src/github.com/ollama/ollama/ |  | ||||||
| WORKDIR /go/src/github.com/ollama/ollama/llm/generate |  | ||||||
| ARG CGO_CFLAGS |  | ||||||
| RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_SKIP_CPU_GENERATE=1 sh gen_linux.sh |  | ||||||
|  |  | ||||||
| FROM --platform=linux/amd64 rocm/dev-centos-7:${ROCM_VERSION}-complete AS rocm-build-amd64 |  | ||||||
| ARG CMAKE_VERSION |  | ||||||
| COPY ./scripts/rh_linux_deps.sh / |  | ||||||
| RUN CMAKE_VERSION=${CMAKE_VERSION} sh /rh_linux_deps.sh |  | ||||||
| ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH |  | ||||||
| ENV LIBRARY_PATH /opt/amdgpu/lib64 |  | ||||||
| COPY --from=llm-code / /go/src/github.com/ollama/ollama/ |  | ||||||
| WORKDIR /go/src/github.com/ollama/ollama/llm/generate |  | ||||||
| ARG CGO_CFLAGS |  | ||||||
| ARG AMDGPU_TARGETS |  | ||||||
| RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_SKIP_CPU_GENERATE=1 sh gen_linux.sh |  | ||||||
| RUN mkdir /tmp/scratch && \ |  | ||||||
|     for dep in $(zcat /go/src/github.com/ollama/ollama/llm/build/linux/x86_64/rocm*/bin/deps.txt.gz) ; do \ |  | ||||||
|         cp ${dep} /tmp/scratch/ || exit 1 ; \ |  | ||||||
|     done && \ |  | ||||||
|     (cd /opt/rocm/lib && tar cf - rocblas/library) | (cd /tmp/scratch/ && tar xf - ) && \ |  | ||||||
|     mkdir -p /go/src/github.com/ollama/ollama/dist/deps/ && \ |  | ||||||
|     (cd /tmp/scratch/ && tar czvf /go/src/github.com/ollama/ollama/dist/deps/ollama-linux-amd64-rocm.tgz . ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| FROM --platform=linux/amd64 centos:7 AS cpu-builder-amd64 |  | ||||||
| ARG CMAKE_VERSION |  | ||||||
| ARG GOLANG_VERSION |  | ||||||
| COPY ./scripts/rh_linux_deps.sh / |  | ||||||
| RUN CMAKE_VERSION=${CMAKE_VERSION} GOLANG_VERSION=${GOLANG_VERSION} sh /rh_linux_deps.sh |  | ||||||
| ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH |  | ||||||
| COPY --from=llm-code / /go/src/github.com/ollama/ollama/ |  | ||||||
| ARG OLLAMA_CUSTOM_CPU_DEFS |  | ||||||
| ARG CGO_CFLAGS |  | ||||||
| WORKDIR /go/src/github.com/ollama/ollama/llm/generate |  | ||||||
|  |  | ||||||
| FROM --platform=linux/amd64 cpu-builder-amd64 AS static-build-amd64 |  | ||||||
| RUN OLLAMA_CPU_TARGET="static" sh gen_linux.sh |  | ||||||
| FROM --platform=linux/amd64 cpu-builder-amd64 AS cpu-build-amd64 |  | ||||||
| RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_CPU_TARGET="cpu" sh gen_linux.sh |  | ||||||
| FROM --platform=linux/amd64 cpu-builder-amd64 AS cpu_avx-build-amd64 |  | ||||||
| RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_CPU_TARGET="cpu_avx" sh gen_linux.sh |  | ||||||
| FROM --platform=linux/amd64 cpu-builder-amd64 AS cpu_avx2-build-amd64 |  | ||||||
| RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_CPU_TARGET="cpu_avx2" sh gen_linux.sh |  | ||||||
|  |  | ||||||
| FROM --platform=linux/arm64 centos:7 AS cpu-builder-arm64 |  | ||||||
| ARG CMAKE_VERSION |  | ||||||
| ARG GOLANG_VERSION |  | ||||||
| COPY ./scripts/rh_linux_deps.sh / |  | ||||||
| RUN CMAKE_VERSION=${CMAKE_VERSION} GOLANG_VERSION=${GOLANG_VERSION} sh /rh_linux_deps.sh |  | ||||||
| ENV PATH /opt/rh/devtoolset-10/root/usr/bin:$PATH |  | ||||||
| COPY --from=llm-code / /go/src/github.com/ollama/ollama/ |  | ||||||
| ARG OLLAMA_CUSTOM_CPU_DEFS |  | ||||||
| ARG CGO_CFLAGS |  | ||||||
| WORKDIR /go/src/github.com/ollama/ollama/llm/generate |  | ||||||
|  |  | ||||||
| FROM --platform=linux/arm64 cpu-builder-arm64 AS static-build-arm64 |  | ||||||
| RUN OLLAMA_CPU_TARGET="static" sh gen_linux.sh |  | ||||||
| FROM --platform=linux/arm64 cpu-builder-arm64 AS cpu-build-arm64 |  | ||||||
| RUN OLLAMA_SKIP_STATIC_GENERATE=1 OLLAMA_CPU_TARGET="cpu" sh gen_linux.sh |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # Intermediate stage used for ./scripts/build_linux.sh |  | ||||||
| FROM --platform=linux/amd64 cpu-build-amd64 AS build-amd64 |  | ||||||
| ENV CGO_ENABLED 1 |  | ||||||
| WORKDIR /go/src/github.com/ollama/ollama |  | ||||||
| COPY . . | COPY . . | ||||||
| COPY --from=static-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/ | RUN CGO_ENABLED=1 go build -ldflags '-linkmode external -extldflags "-static"' . | ||||||
| COPY --from=cpu_avx-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/ |  | ||||||
| COPY --from=cpu_avx2-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/ |  | ||||||
| COPY --from=cuda-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/ |  | ||||||
| COPY --from=rocm-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/ |  | ||||||
| COPY --from=rocm-build-amd64 /go/src/github.com/ollama/ollama/dist/deps/ ./dist/deps/ |  | ||||||
| ARG GOFLAGS |  | ||||||
| ARG CGO_CFLAGS |  | ||||||
| RUN go build -trimpath . |  | ||||||
|  |  | ||||||
| # Intermediate stage used for ./scripts/build_linux.sh | FROM alpine | ||||||
| FROM --platform=linux/arm64 cpu-build-arm64 AS build-arm64 | COPY --from=0 /go/src/github.com/jmorganca/ollama/ollama /bin/ollama | ||||||
| ENV CGO_ENABLED 1 |  | ||||||
| ARG GOLANG_VERSION |  | ||||||
| WORKDIR /go/src/github.com/ollama/ollama |  | ||||||
| COPY . . |  | ||||||
| COPY --from=static-build-arm64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/ |  | ||||||
| COPY --from=cuda-build-arm64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/ |  | ||||||
| ARG GOFLAGS |  | ||||||
| ARG CGO_CFLAGS |  | ||||||
| RUN go build -trimpath . |  | ||||||
|  |  | ||||||
| # Runtime stages |  | ||||||
| FROM --platform=linux/amd64 ubuntu:22.04 as runtime-amd64 |  | ||||||
| RUN apt-get update && apt-get install -y ca-certificates |  | ||||||
| COPY --from=build-amd64 /go/src/github.com/ollama/ollama/ollama /bin/ollama |  | ||||||
| FROM --platform=linux/arm64 ubuntu:22.04 as runtime-arm64 |  | ||||||
| RUN apt-get update && apt-get install -y ca-certificates |  | ||||||
| COPY --from=build-arm64 /go/src/github.com/ollama/ollama/ollama /bin/ollama |  | ||||||
|  |  | ||||||
| # Radeon images are much larger so we keep it distinct from the CPU/CUDA image |  | ||||||
| FROM --platform=linux/amd64 rocm/dev-centos-7:${ROCM_VERSION}-complete as runtime-rocm |  | ||||||
| RUN update-pciids |  | ||||||
| COPY --from=build-amd64 /go/src/github.com/ollama/ollama/ollama /bin/ollama |  | ||||||
| EXPOSE 11434 | EXPOSE 11434 | ||||||
| ENV OLLAMA_HOST 0.0.0.0 | ARG USER=ollama | ||||||
|  | ARG GROUP=ollama | ||||||
|  | RUN addgroup -g 1000 $GROUP && adduser -u 1000 -DG $GROUP $USER | ||||||
|  | USER $USER:$GROUP | ||||||
| ENTRYPOINT ["/bin/ollama"] | ENTRYPOINT ["/bin/ollama"] | ||||||
| CMD ["serve"] |  | ||||||
|  |  | ||||||
| FROM runtime-$TARGETARCH |  | ||||||
| EXPOSE 11434 |  | ||||||
| ENV OLLAMA_HOST 0.0.0.0 | ENV OLLAMA_HOST 0.0.0.0 | ||||||
| ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin |  | ||||||
| ENV LD_LIBRARY_PATH=/usr/local/nvidia/lib:/usr/local/nvidia/lib64 |  | ||||||
| ENV NVIDIA_DRIVER_CAPABILITIES=compute,utility |  | ||||||
| ENV NVIDIA_VISIBLE_DEVICES=all |  | ||||||
|  |  | ||||||
| ENTRYPOINT ["/bin/ollama"] |  | ||||||
| CMD ["serve"] | CMD ["serve"] | ||||||
|   | |||||||
							
								
								
									
										369
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -1,116 +1,74 @@ | |||||||
| <div align="center"> | <div align="center"> | ||||||
|  <img alt="ollama" height="200px" src="https://github.com/ollama/ollama/assets/3325447/0d0b44e2-8f4a-4e99-9b52-a5c1c741c8f7"> |   <picture> | ||||||
|  |     <source media="(prefers-color-scheme: dark)" height="200px" srcset="https://github.com/jmorganca/ollama/assets/3325447/56ea1849-1284-4645-8970-956de6e51c3c"> | ||||||
|  |     <img alt="logo" height="200px" src="https://github.com/jmorganca/ollama/assets/3325447/0d0b44e2-8f4a-4e99-9b52-a5c1c741c8f7"> | ||||||
|  |   </picture> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| # Ollama | # Ollama | ||||||
|  |  | ||||||
| [](https://discord.gg/ollama) | [](https://discord.gg/ollama) | ||||||
|  |  | ||||||
| Get up and running with large language models locally. | > Note: Ollama is in early preview. Please report any issues you find. | ||||||
|  |  | ||||||
| ### macOS | Run, create, and share large language models (LLMs). | ||||||
|  |  | ||||||
| [Download](https://ollama.com/download/Ollama-darwin.zip) | ## Download | ||||||
|  |  | ||||||
| ### Windows preview | - [Download](https://ollama.ai/download) for macOS on Apple Silicon (Intel coming soon) | ||||||
|  | - Download for Windows and Linux (coming soon) | ||||||
| [Download](https://ollama.com/download/OllamaSetup.exe) | - Build [from source](#building) | ||||||
|  |  | ||||||
| ### Linux |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| curl -fsSL https://ollama.com/install.sh | sh |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| [Manual install instructions](https://github.com/ollama/ollama/blob/main/docs/linux.md) |  | ||||||
|  |  | ||||||
| ### Docker |  | ||||||
|  |  | ||||||
| The official [Ollama Docker image](https://hub.docker.com/r/ollama/ollama) `ollama/ollama` is available on Docker Hub. |  | ||||||
|  |  | ||||||
| ### Libraries |  | ||||||
|  |  | ||||||
| - [ollama-python](https://github.com/ollama/ollama-python) |  | ||||||
| - [ollama-js](https://github.com/ollama/ollama-js) |  | ||||||
|  |  | ||||||
| ## Quickstart | ## Quickstart | ||||||
|  |  | ||||||
| To run and chat with [Llama 3](https://ollama.com/library/llama3): | To run and chat with [Llama 2](https://ai.meta.com/llama), the new model by Meta: | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| ollama run llama3 | ollama run llama2 | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## Model library | ## Model library | ||||||
|  |  | ||||||
| Ollama supports a list of models available on [ollama.com/library](https://ollama.com/library 'ollama model library') | `ollama` includes a library of open-source models: | ||||||
|  |  | ||||||
| Here are some example models that can be downloaded: |  | ||||||
|  |  | ||||||
| | Model                    | Parameters | Size  | Download                    | | | Model                    | Parameters | Size  | Download                    | | ||||||
| | ------------------ | ---------- | ----- | ------------------------------ | | | ------------------------ | ---------- | ----- | --------------------------- | | ||||||
| | Llama 3            | 8B         | 4.7GB | `ollama run llama3`            | | | Llama2                   | 7B         | 3.8GB | `ollama pull llama2`        | | ||||||
| | Llama 3            | 70B        | 40GB  | `ollama run llama3:70b`        | | | Llama2 13B               | 13B        | 7.3GB | `ollama pull llama2:13b`    | | ||||||
| | Phi 3 Mini         | 3.8B       | 2.3GB | `ollama run phi3`              | | | Orca Mini                | 3B         | 1.9GB | `ollama pull orca`          | | ||||||
| | Phi 3 Medium       | 14B        | 7.9GB | `ollama run phi3:medium`       | | | Vicuna                   | 7B         | 3.8GB | `ollama pull vicuna`        | | ||||||
| | Gemma              | 2B         | 1.4GB | `ollama run gemma:2b`          | | | Nous-Hermes              | 13B        | 7.3GB | `ollama pull nous-hermes`   | | ||||||
| | Gemma              | 7B         | 4.8GB | `ollama run gemma:7b`          | | | Wizard Vicuna Uncensored | 13B        | 7.3GB | `ollama pull wizard-vicuna` | | ||||||
| | Mistral            | 7B         | 4.1GB | `ollama run mistral`           | |  | ||||||
| | Moondream 2        | 1.4B       | 829MB | `ollama run moondream`         | |  | ||||||
| | Neural Chat        | 7B         | 4.1GB | `ollama run neural-chat`       | |  | ||||||
| | Starling           | 7B         | 4.1GB | `ollama run starling-lm`       | |  | ||||||
| | Code Llama         | 7B         | 3.8GB | `ollama run codellama`         | |  | ||||||
| | Llama 2 Uncensored | 7B         | 3.8GB | `ollama run llama2-uncensored` | |  | ||||||
| | LLaVA              | 7B         | 4.5GB | `ollama run llava`             | |  | ||||||
| | Solar              | 10.7B      | 6.1GB | `ollama run solar`             | |  | ||||||
|  |  | ||||||
| > Note: You should have at least 8 GB of RAM available to run the 7B models, 16 GB to run the 13B models, and 32 GB to run the 33B models. | > Note: You should have at least 8 GB of RAM to run the 3B models, 16 GB to run the 7B models, and 32 GB to run the 13B models. | ||||||
|  |  | ||||||
| ## Customize a model | ## Examples | ||||||
|  |  | ||||||
| ### Import from GGUF | ### Run a model | ||||||
|  |  | ||||||
| Ollama supports importing GGUF models in the Modelfile: |  | ||||||
|  |  | ||||||
| 1. Create a file named `Modelfile`, with a `FROM` instruction with the local filepath to the model you want to import. |  | ||||||
|  |  | ||||||
|    ``` |  | ||||||
|    FROM ./vicuna-33b.Q4_0.gguf |  | ||||||
|    ``` |  | ||||||
|  |  | ||||||
| 2. Create the model in Ollama |  | ||||||
|  |  | ||||||
|    ``` |  | ||||||
|    ollama create example -f Modelfile |  | ||||||
|    ``` |  | ||||||
|  |  | ||||||
| 3. Run the model |  | ||||||
|  |  | ||||||
|    ``` |  | ||||||
|    ollama run example |  | ||||||
|    ``` |  | ||||||
|  |  | ||||||
| ### Import from PyTorch or Safetensors |  | ||||||
|  |  | ||||||
| See the [guide](docs/import.md) on importing models for more information. |  | ||||||
|  |  | ||||||
| ### Customize a prompt |  | ||||||
|  |  | ||||||
| Models from the Ollama library can be customized with a prompt. For example, to customize the `llama3` model: |  | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| ollama pull llama3 | ollama run llama2 | ||||||
|  | >>> hi | ||||||
|  | Hello! How can I help you today? | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Create a custom model | ||||||
|  |  | ||||||
|  | Pull a base model: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | ollama pull llama2 | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Create a `Modelfile`: | Create a `Modelfile`: | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| FROM llama3 | FROM llama2 | ||||||
|  |  | ||||||
| # set the temperature to 1 [higher is more creative, lower is more coherent] | # set the temperature to 1 [higher is more creative, lower is more coherent] | ||||||
| PARAMETER temperature 1 | PARAMETER temperature 1 | ||||||
|  |  | ||||||
| # set the system message | # set the system prompt | ||||||
| SYSTEM """ | SYSTEM """ | ||||||
| You are Mario from Super Mario Bros. Answer as Mario, the assistant, only. | You are Mario from Super Mario Bros. Answer as Mario, the assistant, only. | ||||||
| """ | """ | ||||||
| @@ -125,260 +83,55 @@ ollama run mario | |||||||
| Hello! It's your friend Mario. | Hello! It's your friend Mario. | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| For more examples, see the [examples](examples) directory. For more information on working with a Modelfile, see the [Modelfile](docs/modelfile.md) documentation. | For more examples, see the [examples](./examples) directory. | ||||||
|  |  | ||||||
| ## CLI Reference | ### Pull a model from the registry | ||||||
|  |  | ||||||
| ### Create a model |  | ||||||
|  |  | ||||||
| `ollama create` is used to create a model from a Modelfile. |  | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| ollama create mymodel -f ./Modelfile | ollama pull orca | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Pull a model | ### Listing local models | ||||||
|  |  | ||||||
| ``` |  | ||||||
| ollama pull llama3 |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| > This command can also be used to update a local model. Only the diff will be pulled. |  | ||||||
|  |  | ||||||
| ### Remove a model |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| ollama rm llama3 |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Copy a model |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| ollama cp llama3 my-model |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Multiline input |  | ||||||
|  |  | ||||||
| For multiline input, you can wrap text with `"""`: |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| >>> """Hello, |  | ||||||
| ... world! |  | ||||||
| ... """ |  | ||||||
| I'm a basic program that prints the famous "Hello, world!" message to the console. |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Multimodal models |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| >>> What's in this image? /Users/jmorgan/Desktop/smile.png |  | ||||||
| The image features a yellow smiley face, which is likely the central focus of the picture. |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### Pass the prompt as an argument |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| $ ollama run llama3 "Summarize this file: $(cat README.md)" |  | ||||||
|  Ollama is a lightweight, extensible framework for building and running language models on the local machine. It provides a simple API for creating, running, and managing models, as well as a library of pre-built models that can be easily used in a variety of applications. |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| ### List models on your computer |  | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| ollama list | ollama list | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Start Ollama | ## Model packages | ||||||
|  |  | ||||||
| `ollama serve` is used when you want to start ollama without running the desktop application. | ### Overview | ||||||
|  |  | ||||||
|  | Ollama bundles model weights, configuration, and data into a single package, defined by a [Modelfile](./docs/modelfile.md). | ||||||
|  |  | ||||||
|  | <picture> | ||||||
|  |   <source media="(prefers-color-scheme: dark)" height="480" srcset="https://github.com/jmorganca/ollama/assets/251292/2fd96b5f-191b-45c1-9668-941cfad4eb70"> | ||||||
|  |   <img alt="logo" height="480" src="https://github.com/jmorganca/ollama/assets/251292/2fd96b5f-191b-45c1-9668-941cfad4eb70"> | ||||||
|  | </picture> | ||||||
|  |  | ||||||
| ## Building | ## Building | ||||||
|  |  | ||||||
| See the [developer guide](https://github.com/ollama/ollama/blob/main/docs/development.md) |  | ||||||
|  |  | ||||||
| ### Running local builds |  | ||||||
|  |  | ||||||
| Next, start the server: |  | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| ./ollama serve | go build . | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Finally, in a separate shell, run a model: | To run it start the server: | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| ./ollama run llama3 | ./ollama serve & | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | Finally, run a model! | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | ./ollama run llama2 | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ## REST API | ## REST API | ||||||
|  |  | ||||||
| Ollama has a REST API for running and managing models. | ### `POST /api/generate` | ||||||
|  |  | ||||||
| ### Generate a response | Generate text from a model. | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| curl http://localhost:11434/api/generate -d '{ | curl -X POST http://localhost:11434/api/generate -d '{"model": "llama2", "prompt":"Why is the sky blue?"}' | ||||||
|   "model": "llama3", |  | ||||||
|   "prompt":"Why is the sky blue?" |  | ||||||
| }' |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Chat with a model |  | ||||||
|  |  | ||||||
| ``` |  | ||||||
| curl http://localhost:11434/api/chat -d '{ |  | ||||||
|   "model": "llama3", |  | ||||||
|   "messages": [ |  | ||||||
|     { "role": "user", "content": "why is the sky blue?" } |  | ||||||
|   ] |  | ||||||
| }' |  | ||||||
| ``` |  | ||||||
|  |  | ||||||
| See the [API documentation](./docs/api.md) for all endpoints. |  | ||||||
|  |  | ||||||
| ## Community Integrations |  | ||||||
|  |  | ||||||
| ### Web & Desktop |  | ||||||
|  |  | ||||||
| - [Open WebUI](https://github.com/open-webui/open-webui) |  | ||||||
| - [Enchanted (macOS native)](https://github.com/AugustDev/enchanted) |  | ||||||
| - [Hollama](https://github.com/fmaclen/hollama) |  | ||||||
| - [Lollms-Webui](https://github.com/ParisNeo/lollms-webui) |  | ||||||
| - [LibreChat](https://github.com/danny-avila/LibreChat) |  | ||||||
| - [Bionic GPT](https://github.com/bionic-gpt/bionic-gpt) |  | ||||||
| - [HTML UI](https://github.com/rtcfirefly/ollama-ui) |  | ||||||
| - [Saddle](https://github.com/jikkuatwork/saddle) |  | ||||||
| - [Chatbot UI](https://github.com/ivanfioravanti/chatbot-ollama) |  | ||||||
| - [Chatbot UI v2](https://github.com/mckaywrigley/chatbot-ui) |  | ||||||
| - [Typescript UI](https://github.com/ollama-interface/Ollama-Gui?tab=readme-ov-file) |  | ||||||
| - [Minimalistic React UI for Ollama Models](https://github.com/richawo/minimal-llm-ui) |  | ||||||
| - [Ollamac](https://github.com/kevinhermawan/Ollamac) |  | ||||||
| - [big-AGI](https://github.com/enricoros/big-AGI/blob/main/docs/config-local-ollama.md) |  | ||||||
| - [Cheshire Cat assistant framework](https://github.com/cheshire-cat-ai/core) |  | ||||||
| - [Amica](https://github.com/semperai/amica) |  | ||||||
| - [chatd](https://github.com/BruceMacD/chatd) |  | ||||||
| - [Ollama-SwiftUI](https://github.com/kghandour/Ollama-SwiftUI) |  | ||||||
| - [Dify.AI](https://github.com/langgenius/dify) |  | ||||||
| - [MindMac](https://mindmac.app) |  | ||||||
| - [NextJS Web Interface for Ollama](https://github.com/jakobhoeg/nextjs-ollama-llm-ui) |  | ||||||
| - [Msty](https://msty.app) |  | ||||||
| - [Chatbox](https://github.com/Bin-Huang/Chatbox) |  | ||||||
| - [WinForm Ollama Copilot](https://github.com/tgraupmann/WinForm_Ollama_Copilot) |  | ||||||
| - [NextChat](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) with [Get Started Doc](https://docs.nextchat.dev/models/ollama) |  | ||||||
| - [Alpaca WebUI](https://github.com/mmo80/alpaca-webui) |  | ||||||
| - [OllamaGUI](https://github.com/enoch1118/ollamaGUI) |  | ||||||
| - [OpenAOE](https://github.com/InternLM/OpenAOE) |  | ||||||
| - [Odin Runes](https://github.com/leonid20000/OdinRunes) |  | ||||||
| - [LLM-X](https://github.com/mrdjohnson/llm-x) (Progressive Web App) |  | ||||||
| - [AnythingLLM (Docker + MacOs/Windows/Linux native app)](https://github.com/Mintplex-Labs/anything-llm) |  | ||||||
| - [Ollama Basic Chat: Uses HyperDiv Reactive UI](https://github.com/rapidarchitect/ollama_basic_chat) |  | ||||||
| - [Ollama-chats RPG](https://github.com/drazdra/ollama-chats) |  | ||||||
| - [QA-Pilot](https://github.com/reid41/QA-Pilot) (Chat with Code Repository) |  | ||||||
| - [ChatOllama](https://github.com/sugarforever/chat-ollama) (Open Source Chatbot based on Ollama with Knowledge Bases) |  | ||||||
| - [CRAG Ollama Chat](https://github.com/Nagi-ovo/CRAG-Ollama-Chat) (Simple Web Search with Corrective RAG) |  | ||||||
| - [RAGFlow](https://github.com/infiniflow/ragflow) (Open-source Retrieval-Augmented Generation engine based on deep document understanding) |  | ||||||
| - [StreamDeploy](https://github.com/StreamDeploy-DevRel/streamdeploy-llm-app-scaffold) (LLM Application Scaffold) |  | ||||||
| - [chat](https://github.com/swuecho/chat) (chat web app for teams) |  | ||||||
| - [Lobe Chat](https://github.com/lobehub/lobe-chat) with [Integrating Doc](https://lobehub.com/docs/self-hosting/examples/ollama) |  | ||||||
| - [Ollama RAG Chatbot](https://github.com/datvodinh/rag-chatbot.git) (Local Chat with multiple PDFs using Ollama and RAG) |  | ||||||
| - [BrainSoup](https://www.nurgo-software.com/products/brainsoup) (Flexible native client with RAG & multi-agent automation) |  | ||||||
| - [macai](https://github.com/Renset/macai) (macOS client for Ollama, ChatGPT, and other compatible API back-ends) |  | ||||||
| - [Olpaka](https://github.com/Otacon/olpaka) (User-friendly Flutter Web App for Ollama) |  | ||||||
| - [OllamaSpring](https://github.com/CrazyNeil/OllamaSpring) (Ollama Client for macOS) |  | ||||||
|  |  | ||||||
| ### Terminal |  | ||||||
|  |  | ||||||
| - [oterm](https://github.com/ggozad/oterm) |  | ||||||
| - [Ellama Emacs client](https://github.com/s-kostyaev/ellama) |  | ||||||
| - [Emacs client](https://github.com/zweifisch/ollama) |  | ||||||
| - [gen.nvim](https://github.com/David-Kunz/gen.nvim) |  | ||||||
| - [ollama.nvim](https://github.com/nomnivore/ollama.nvim) |  | ||||||
| - [ollero.nvim](https://github.com/marco-souza/ollero.nvim) |  | ||||||
| - [ollama-chat.nvim](https://github.com/gerazov/ollama-chat.nvim) |  | ||||||
| - [ogpt.nvim](https://github.com/huynle/ogpt.nvim) |  | ||||||
| - [gptel Emacs client](https://github.com/karthink/gptel) |  | ||||||
| - [Oatmeal](https://github.com/dustinblackman/oatmeal) |  | ||||||
| - [cmdh](https://github.com/pgibler/cmdh) |  | ||||||
| - [ooo](https://github.com/npahlfer/ooo) |  | ||||||
| - [shell-pilot](https://github.com/reid41/shell-pilot) |  | ||||||
| - [tenere](https://github.com/pythops/tenere) |  | ||||||
| - [llm-ollama](https://github.com/taketwo/llm-ollama) for [Datasette's LLM CLI](https://llm.datasette.io/en/stable/). |  | ||||||
| - [typechat-cli](https://github.com/anaisbetts/typechat-cli) |  | ||||||
| - [ShellOracle](https://github.com/djcopley/ShellOracle) |  | ||||||
| - [tlm](https://github.com/yusufcanb/tlm) |  | ||||||
| - [podman-ollama](https://github.com/ericcurtin/podman-ollama) |  | ||||||
|  |  | ||||||
| ### Database |  | ||||||
|  |  | ||||||
| - [MindsDB](https://github.com/mindsdb/mindsdb/blob/staging/mindsdb/integrations/handlers/ollama_handler/README.md) (Connects Ollama models with nearly 200 data platforms and apps) |  | ||||||
| - [chromem-go](https://github.com/philippgille/chromem-go/blob/v0.5.0/embed_ollama.go) with [example](https://github.com/philippgille/chromem-go/tree/v0.5.0/examples/rag-wikipedia-ollama) |  | ||||||
|  |  | ||||||
| ### Package managers |  | ||||||
|  |  | ||||||
| - [Pacman](https://archlinux.org/packages/extra/x86_64/ollama/) |  | ||||||
| - [Helm Chart](https://artifacthub.io/packages/helm/ollama-helm/ollama) |  | ||||||
| - [Guix channel](https://codeberg.org/tusharhero/ollama-guix) |  | ||||||
|  |  | ||||||
| ### Libraries |  | ||||||
|  |  | ||||||
| - [LangChain](https://python.langchain.com/docs/integrations/llms/ollama) and [LangChain.js](https://js.langchain.com/docs/modules/model_io/models/llms/integrations/ollama) with [example](https://js.langchain.com/docs/use_cases/question_answering/local_retrieval_qa) |  | ||||||
| - [LangChainGo](https://github.com/tmc/langchaingo/) with [example](https://github.com/tmc/langchaingo/tree/main/examples/ollama-completion-example) |  | ||||||
| - [LangChain4j](https://github.com/langchain4j/langchain4j) with [example](https://github.com/langchain4j/langchain4j-examples/tree/main/ollama-examples/src/main/java) |  | ||||||
| - [LlamaIndex](https://gpt-index.readthedocs.io/en/stable/examples/llm/ollama.html) |  | ||||||
| - [LiteLLM](https://github.com/BerriAI/litellm) |  | ||||||
| - [OllamaSharp for .NET](https://github.com/awaescher/OllamaSharp) |  | ||||||
| - [Ollama for Ruby](https://github.com/gbaptista/ollama-ai) |  | ||||||
| - [Ollama-rs for Rust](https://github.com/pepperoni21/ollama-rs) |  | ||||||
| - [Ollama4j for Java](https://github.com/amithkoujalgi/ollama4j) |  | ||||||
| - [ModelFusion Typescript Library](https://modelfusion.dev/integration/model-provider/ollama) |  | ||||||
| - [OllamaKit for Swift](https://github.com/kevinhermawan/OllamaKit) |  | ||||||
| - [Ollama for Dart](https://github.com/breitburg/dart-ollama) |  | ||||||
| - [Ollama for Laravel](https://github.com/cloudstudio/ollama-laravel) |  | ||||||
| - [LangChainDart](https://github.com/davidmigloz/langchain_dart) |  | ||||||
| - [Semantic Kernel - Python](https://github.com/microsoft/semantic-kernel/tree/main/python/semantic_kernel/connectors/ai/ollama) |  | ||||||
| - [Haystack](https://github.com/deepset-ai/haystack-integrations/blob/main/integrations/ollama.md) |  | ||||||
| - [Elixir LangChain](https://github.com/brainlid/langchain) |  | ||||||
| - [Ollama for R - rollama](https://github.com/JBGruber/rollama) |  | ||||||
| - [Ollama for R - ollama-r](https://github.com/hauselin/ollama-r) |  | ||||||
| - [Ollama-ex for Elixir](https://github.com/lebrunel/ollama-ex) |  | ||||||
| - [Ollama Connector for SAP ABAP](https://github.com/b-tocs/abap_btocs_ollama) |  | ||||||
| - [Testcontainers](https://testcontainers.com/modules/ollama/) |  | ||||||
| - [Portkey](https://portkey.ai/docs/welcome/integration-guides/ollama) |  | ||||||
| - [PromptingTools.jl](https://github.com/svilupp/PromptingTools.jl) with an [example](https://svilupp.github.io/PromptingTools.jl/dev/examples/working_with_ollama) |  | ||||||
| - [LlamaScript](https://github.com/Project-Llama/llamascript) |  | ||||||
| ### Mobile |  | ||||||
|  |  | ||||||
| - [Enchanted](https://github.com/AugustDev/enchanted) |  | ||||||
| - [Maid](https://github.com/Mobile-Artificial-Intelligence/maid) |  | ||||||
|  |  | ||||||
| ### Extensions & Plugins |  | ||||||
|  |  | ||||||
| - [Raycast extension](https://github.com/MassimilianoPasquini97/raycast_ollama) |  | ||||||
| - [Discollama](https://github.com/mxyng/discollama) (Discord bot inside the Ollama discord channel) |  | ||||||
| - [Continue](https://github.com/continuedev/continue) |  | ||||||
| - [Obsidian Ollama plugin](https://github.com/hinterdupfinger/obsidian-ollama) |  | ||||||
| - [Logseq Ollama plugin](https://github.com/omagdy7/ollama-logseq) |  | ||||||
| - [NotesOllama](https://github.com/andersrex/notesollama) (Apple Notes Ollama plugin) |  | ||||||
| - [Dagger Chatbot](https://github.com/samalba/dagger-chatbot) |  | ||||||
| - [Discord AI Bot](https://github.com/mekb-turtle/discord-ai-bot) |  | ||||||
| - [Ollama Telegram Bot](https://github.com/ruecat/ollama-telegram) |  | ||||||
| - [Hass Ollama Conversation](https://github.com/ej52/hass-ollama-conversation) |  | ||||||
| - [Rivet plugin](https://github.com/abrenneke/rivet-plugin-ollama) |  | ||||||
| - [Obsidian BMO Chatbot plugin](https://github.com/longy2k/obsidian-bmo-chatbot) |  | ||||||
| - [Cliobot](https://github.com/herval/cliobot) (Telegram bot with Ollama support) |  | ||||||
| - [Copilot for Obsidian plugin](https://github.com/logancyang/obsidian-copilot) |  | ||||||
| - [Obsidian Local GPT plugin](https://github.com/pfrankov/obsidian-local-gpt) |  | ||||||
| - [Open Interpreter](https://docs.openinterpreter.com/language-model-setup/local-models/ollama) |  | ||||||
| - [Llama Coder](https://github.com/ex3ndr/llama-coder) (Copilot alternative using Ollama) |  | ||||||
| - [Ollama Copilot](https://github.com/bernardo-bruning/ollama-copilot) (Proxy that allows you to use ollama as a copilot like Github copilot) |  | ||||||
| - [twinny](https://github.com/rjmacarthy/twinny) (Copilot and Copilot chat alternative using Ollama) |  | ||||||
| - [Wingman-AI](https://github.com/RussellCanfield/wingman-ai) (Copilot code and chat alternative using Ollama and HuggingFace) |  | ||||||
| - [Page Assist](https://github.com/n4ze3m/page-assist) (Chrome Extension) |  | ||||||
| - [AI Telegram Bot](https://github.com/tusharhero/aitelegrambot) (Telegram bot using Ollama in backend) |  | ||||||
| - [AI ST Completion](https://github.com/yaroslavyaroslav/OpenAI-sublime-text) (Sublime Text 4 AI assistant plugin with Ollama support) |  | ||||||
| - [Discord-Ollama Chat Bot](https://github.com/kevinthedang/discord-ollama) (Generalized TypeScript Discord Bot w/ Tuning Documentation) |  | ||||||
| - [Discord AI chat/moderation bot](https://github.com/rapmd73/Companion) Chat/moderation bot written in python. Uses Ollama to create personalities. |  | ||||||
|  |  | ||||||
| ### Supported backends  |  | ||||||
| - [llama.cpp](https://github.com/ggerganov/llama.cpp) project founded by Georgi Gerganov. |  | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										258
									
								
								api/client.go
									
									
									
									
									
								
							
							
						
						| @@ -1,16 +1,3 @@ | |||||||
| // Package api implements the client-side API for code wishing to interact |  | ||||||
| // with the ollama service. The methods of the [Client] type correspond to |  | ||||||
| // the ollama REST API as described in [the API documentation]. |  | ||||||
| // The ollama command-line client itself uses this package to interact with |  | ||||||
| // the backend service. |  | ||||||
| // |  | ||||||
| // # Examples |  | ||||||
| // |  | ||||||
| // Several examples of using this package are available [in the GitHub |  | ||||||
| // repository]. |  | ||||||
| // |  | ||||||
| // [the API documentation]: https://github.com/ollama/ollama/blob/main/docs/api.md |  | ||||||
| // [in the GitHub repository]: https://github.com/ollama/ollama/tree/main/examples |  | ||||||
| package api | package api | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| @@ -20,27 +7,18 @@ import ( | |||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net" |  | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"os" |  | ||||||
| 	"runtime" |  | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/format" |  | ||||||
| 	"github.com/ollama/ollama/version" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Client encapsulates client state for interacting with the ollama |  | ||||||
| // service. Use [ClientFromEnvironment] to create new Clients. |  | ||||||
| type Client struct { | type Client struct { | ||||||
| 	base *url.URL | 	base    url.URL | ||||||
| 	http *http.Client | 	HTTP    http.Client | ||||||
|  | 	Headers http.Header | ||||||
| } | } | ||||||
|  |  | ||||||
| func checkError(resp *http.Response, body []byte) error { | func checkError(resp *http.Response, body []byte) error { | ||||||
| 	if resp.StatusCode < http.StatusBadRequest { | 	if resp.StatusCode >= 200 && resp.StatusCode < 400 { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -55,80 +33,15 @@ func checkError(resp *http.Response, body []byte) error { | |||||||
| 	return apiError | 	return apiError | ||||||
| } | } | ||||||
|  |  | ||||||
| // ClientFromEnvironment creates a new [Client] using configuration from the | func NewClient(hosts ...string) *Client { | ||||||
| // environment variable OLLAMA_HOST, which points to the network host and | 	host := "127.0.0.1:11434" | ||||||
| // port on which the ollama service is listenting. The format of this variable | 	if len(hosts) > 0 { | ||||||
| // is: | 		host = hosts[0] | ||||||
| // |  | ||||||
| //	<scheme>://<host>:<port> |  | ||||||
| // |  | ||||||
| // If the variable is not specified, a default ollama host and port will be |  | ||||||
| // used. |  | ||||||
| func ClientFromEnvironment() (*Client, error) { |  | ||||||
| 	ollamaHost, err := GetOllamaHost() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &Client{ | 	return &Client{ | ||||||
| 		base: &url.URL{ | 		base: url.URL{Scheme: "http", Host: host}, | ||||||
| 			Scheme: ollamaHost.Scheme, | 		HTTP: http.Client{}, | ||||||
| 			Host:   net.JoinHostPort(ollamaHost.Host, ollamaHost.Port), |  | ||||||
| 		}, |  | ||||||
| 		http: http.DefaultClient, |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type OllamaHost struct { |  | ||||||
| 	Scheme string |  | ||||||
| 	Host   string |  | ||||||
| 	Port   string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func GetOllamaHost() (OllamaHost, error) { |  | ||||||
| 	defaultPort := "11434" |  | ||||||
|  |  | ||||||
| 	hostVar := os.Getenv("OLLAMA_HOST") |  | ||||||
| 	hostVar = strings.TrimSpace(strings.Trim(strings.TrimSpace(hostVar), "\"'")) |  | ||||||
|  |  | ||||||
| 	scheme, hostport, ok := strings.Cut(hostVar, "://") |  | ||||||
| 	switch { |  | ||||||
| 	case !ok: |  | ||||||
| 		scheme, hostport = "http", hostVar |  | ||||||
| 	case scheme == "http": |  | ||||||
| 		defaultPort = "80" |  | ||||||
| 	case scheme == "https": |  | ||||||
| 		defaultPort = "443" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// trim trailing slashes |  | ||||||
| 	hostport = strings.TrimRight(hostport, "/") |  | ||||||
|  |  | ||||||
| 	host, port, err := net.SplitHostPort(hostport) |  | ||||||
| 	if err != nil { |  | ||||||
| 		host, port = "127.0.0.1", defaultPort |  | ||||||
| 		if ip := net.ParseIP(strings.Trim(hostport, "[]")); ip != nil { |  | ||||||
| 			host = ip.String() |  | ||||||
| 		} else if hostport != "" { |  | ||||||
| 			host = hostport |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if portNum, err := strconv.ParseInt(port, 10, 32); err != nil || portNum > 65535 || portNum < 0 { |  | ||||||
| 		return OllamaHost{}, ErrInvalidHostPort |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return OllamaHost{ |  | ||||||
| 		Scheme: scheme, |  | ||||||
| 		Host:   host, |  | ||||||
| 		Port:   port, |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func NewClient(base *url.URL, http *http.Client) *Client { |  | ||||||
| 	return &Client{ |  | ||||||
| 		base: base, |  | ||||||
| 		http: http, |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -136,33 +49,29 @@ func (c *Client) do(ctx context.Context, method, path string, reqData, respData | |||||||
| 	var reqBody io.Reader | 	var reqBody io.Reader | ||||||
| 	var data []byte | 	var data []byte | ||||||
| 	var err error | 	var err error | ||||||
|  | 	if reqData != nil { | ||||||
| 	switch reqData := reqData.(type) { |  | ||||||
| 	case io.Reader: |  | ||||||
| 		// reqData is already an io.Reader |  | ||||||
| 		reqBody = reqData |  | ||||||
| 	case nil: |  | ||||||
| 		// noop |  | ||||||
| 	default: |  | ||||||
| 		data, err = json.Marshal(reqData) | 		data, err = json.Marshal(reqData) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		reqBody = bytes.NewReader(data) | 		reqBody = bytes.NewReader(data) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	requestURL := c.base.JoinPath(path) | 	url := c.base.JoinPath(path).String() | ||||||
| 	request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), reqBody) |  | ||||||
|  | 	req, err := http.NewRequestWithContext(ctx, method, url, reqBody) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	request.Header.Set("Content-Type", "application/json") | 	req.Header.Set("Content-Type", "application/json") | ||||||
| 	request.Header.Set("Accept", "application/json") | 	req.Header.Set("Accept", "application/json") | ||||||
| 	request.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version())) |  | ||||||
|  |  | ||||||
| 	respObj, err := c.http.Do(request) | 	for k, v := range c.Headers { | ||||||
|  | 		req.Header[k] = v | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	respObj, err := c.HTTP.Do(req) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -185,8 +94,6 @@ func (c *Client) do(ctx context.Context, method, path string, reqData, respData | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| const maxBufferSize = 512 * format.KiloByte |  | ||||||
|  |  | ||||||
| func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error { | func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error { | ||||||
| 	var buf *bytes.Buffer | 	var buf *bytes.Buffer | ||||||
| 	if data != nil { | 	if data != nil { | ||||||
| @@ -198,26 +105,21 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f | |||||||
| 		buf = bytes.NewBuffer(bts) | 		buf = bytes.NewBuffer(bts) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	requestURL := c.base.JoinPath(path) | 	request, err := http.NewRequestWithContext(ctx, method, c.base.JoinPath(path).String(), buf) | ||||||
| 	request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), buf) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	request.Header.Set("Content-Type", "application/json") | 	request.Header.Set("Content-Type", "application/json") | ||||||
| 	request.Header.Set("Accept", "application/x-ndjson") | 	request.Header.Set("Accept", "application/json") | ||||||
| 	request.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version())) |  | ||||||
|  |  | ||||||
| 	response, err := c.http.Do(request) | 	response, err := http.DefaultClient.Do(request) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	defer response.Body.Close() | 	defer response.Body.Close() | ||||||
|  |  | ||||||
| 	scanner := bufio.NewScanner(response.Body) | 	scanner := bufio.NewScanner(response.Body) | ||||||
| 	// increase the buffer size to avoid running out of space |  | ||||||
| 	scanBuf := make([]byte, 0, maxBufferSize) |  | ||||||
| 	scanner.Buffer(scanBuf, maxBufferSize) |  | ||||||
| 	for scanner.Scan() { | 	for scanner.Scan() { | ||||||
| 		var errorResponse struct { | 		var errorResponse struct { | ||||||
| 			Error string `json:"error,omitempty"` | 			Error string `json:"error,omitempty"` | ||||||
| @@ -229,10 +131,10 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if errorResponse.Error != "" { | 		if errorResponse.Error != "" { | ||||||
| 			return fmt.Errorf(errorResponse.Error) | 			return fmt.Errorf("stream: %s", errorResponse.Error) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if response.StatusCode >= http.StatusBadRequest { | 		if response.StatusCode >= 400 { | ||||||
| 			return StatusError{ | 			return StatusError{ | ||||||
| 				StatusCode:   response.StatusCode, | 				StatusCode:   response.StatusCode, | ||||||
| 				Status:       response.Status, | 				Status:       response.Status, | ||||||
| @@ -248,14 +150,8 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // GenerateResponseFunc is a function that [Client.Generate] invokes every time |  | ||||||
| // a response is received from the service. If this function returns an error, |  | ||||||
| // [Client.Generate] will stop generating and return this error. |  | ||||||
| type GenerateResponseFunc func(GenerateResponse) error | type GenerateResponseFunc func(GenerateResponse) error | ||||||
|  |  | ||||||
| // Generate generates a response for a given prompt. The req parameter should |  | ||||||
| // be populated with prompt details. fn is called for each response (there may |  | ||||||
| // be multiple responses, e.g. in case streaming is enabled). |  | ||||||
| func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn GenerateResponseFunc) error { | func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn GenerateResponseFunc) error { | ||||||
| 	return c.stream(ctx, http.MethodPost, "/api/generate", req, func(bts []byte) error { | 	return c.stream(ctx, http.MethodPost, "/api/generate", req, func(bts []byte) error { | ||||||
| 		var resp GenerateResponse | 		var resp GenerateResponse | ||||||
| @@ -267,34 +163,8 @@ func (c *Client) Generate(ctx context.Context, req *GenerateRequest, fn Generate | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| // ChatResponseFunc is a function that [Client.Chat] invokes every time |  | ||||||
| // a response is received from the service. If this function returns an error, |  | ||||||
| // [Client.Chat] will stop generating and return this error. |  | ||||||
| type ChatResponseFunc func(ChatResponse) error |  | ||||||
|  |  | ||||||
| // Chat generates the next message in a chat. [ChatRequest] may contain a |  | ||||||
| // sequence of messages which can be used to maintain chat history with a model. |  | ||||||
| // fn is called for each response (there may be multiple responses, e.g. if case |  | ||||||
| // streaming is enabled). |  | ||||||
| func (c *Client) Chat(ctx context.Context, req *ChatRequest, fn ChatResponseFunc) error { |  | ||||||
| 	return c.stream(ctx, http.MethodPost, "/api/chat", req, func(bts []byte) error { |  | ||||||
| 		var resp ChatResponse |  | ||||||
| 		if err := json.Unmarshal(bts, &resp); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return fn(resp) |  | ||||||
| 	}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // PullProgressFunc is a function that [Client.Pull] invokes every time there |  | ||||||
| // is progress with a "pull" request sent to the service. If this function |  | ||||||
| // returns an error, [Client.Pull] will stop the process and return this error. |  | ||||||
| type PullProgressFunc func(ProgressResponse) error | type PullProgressFunc func(ProgressResponse) error | ||||||
|  |  | ||||||
| // Pull downloads a model from the ollama library. fn is called each time |  | ||||||
| // progress is made on the request and can be used to display a progress bar, |  | ||||||
| // etc. |  | ||||||
| func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc) error { | func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc) error { | ||||||
| 	return c.stream(ctx, http.MethodPost, "/api/pull", req, func(bts []byte) error { | 	return c.stream(ctx, http.MethodPost, "/api/pull", req, func(bts []byte) error { | ||||||
| 		var resp ProgressResponse | 		var resp ProgressResponse | ||||||
| @@ -306,14 +176,8 @@ func (c *Client) Pull(ctx context.Context, req *PullRequest, fn PullProgressFunc | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| // PushProgressFunc is a function that [Client.Push] invokes when progress is |  | ||||||
| // made. |  | ||||||
| // It's similar to other progress function types like [PullProgressFunc]. |  | ||||||
| type PushProgressFunc func(ProgressResponse) error | type PushProgressFunc func(ProgressResponse) error | ||||||
|  |  | ||||||
| // Push uploads a model to the model library; requires registering for ollama.ai |  | ||||||
| // and adding a public key first. fn is called each time progress is made on |  | ||||||
| // the request and can be used to display a progress bar, etc. |  | ||||||
| func (c *Client) Push(ctx context.Context, req *PushRequest, fn PushProgressFunc) error { | func (c *Client) Push(ctx context.Context, req *PushRequest, fn PushProgressFunc) error { | ||||||
| 	return c.stream(ctx, http.MethodPost, "/api/push", req, func(bts []byte) error { | 	return c.stream(ctx, http.MethodPost, "/api/push", req, func(bts []byte) error { | ||||||
| 		var resp ProgressResponse | 		var resp ProgressResponse | ||||||
| @@ -325,18 +189,11 @@ func (c *Client) Push(ctx context.Context, req *PushRequest, fn PushProgressFunc | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| // CreateProgressFunc is a function that [Client.Create] invokes when progress | type CreateProgressFunc func(CreateProgress) error | ||||||
| // is made. |  | ||||||
| // It's similar to other progress function types like [PullProgressFunc]. |  | ||||||
| type CreateProgressFunc func(ProgressResponse) error |  | ||||||
|  |  | ||||||
| // Create creates a model from a [Modelfile]. fn is a progress function that |  | ||||||
| // behaves similarly to other methods (see [Client.Pull]). |  | ||||||
| // |  | ||||||
| // [Modelfile]: https://github.com/ollama/ollama/blob/main/docs/modelfile.md |  | ||||||
| func (c *Client) Create(ctx context.Context, req *CreateRequest, fn CreateProgressFunc) error { | func (c *Client) Create(ctx context.Context, req *CreateRequest, fn CreateProgressFunc) error { | ||||||
| 	return c.stream(ctx, http.MethodPost, "/api/create", req, func(bts []byte) error { | 	return c.stream(ctx, http.MethodPost, "/api/create", req, func(bts []byte) error { | ||||||
| 		var resp ProgressResponse | 		var resp CreateProgress | ||||||
| 		if err := json.Unmarshal(bts, &resp); err != nil { | 		if err := json.Unmarshal(bts, &resp); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| @@ -345,7 +202,6 @@ func (c *Client) Create(ctx context.Context, req *CreateRequest, fn CreateProgre | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| // List lists models that are available locally. |  | ||||||
| func (c *Client) List(ctx context.Context) (*ListResponse, error) { | func (c *Client) List(ctx context.Context) (*ListResponse, error) { | ||||||
| 	var lr ListResponse | 	var lr ListResponse | ||||||
| 	if err := c.do(ctx, http.MethodGet, "/api/tags", nil, &lr); err != nil { | 	if err := c.do(ctx, http.MethodGet, "/api/tags", nil, &lr); err != nil { | ||||||
| @@ -354,17 +210,6 @@ func (c *Client) List(ctx context.Context) (*ListResponse, error) { | |||||||
| 	return &lr, nil | 	return &lr, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // List running models. |  | ||||||
| func (c *Client) ListRunning(ctx context.Context) (*ListResponse, error) { |  | ||||||
| 	var lr ListResponse |  | ||||||
| 	if err := c.do(ctx, http.MethodGet, "/api/ps", nil, &lr); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return &lr, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Copy copies a model - creating a model with another name from an existing |  | ||||||
| // model. |  | ||||||
| func (c *Client) Copy(ctx context.Context, req *CopyRequest) error { | func (c *Client) Copy(ctx context.Context, req *CopyRequest) error { | ||||||
| 	if err := c.do(ctx, http.MethodPost, "/api/copy", req, nil); err != nil { | 	if err := c.do(ctx, http.MethodPost, "/api/copy", req, nil); err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -372,56 +217,9 @@ func (c *Client) Copy(ctx context.Context, req *CopyRequest) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Delete deletes a model and its data. |  | ||||||
| func (c *Client) Delete(ctx context.Context, req *DeleteRequest) error { | func (c *Client) Delete(ctx context.Context, req *DeleteRequest) error { | ||||||
| 	if err := c.do(ctx, http.MethodDelete, "/api/delete", req, nil); err != nil { | 	if err := c.do(ctx, http.MethodDelete, "/api/delete", req, nil); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // Show obtains model information, including details, modelfile, license etc. |  | ||||||
| func (c *Client) Show(ctx context.Context, req *ShowRequest) (*ShowResponse, error) { |  | ||||||
| 	var resp ShowResponse |  | ||||||
| 	if err := c.do(ctx, http.MethodPost, "/api/show", req, &resp); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return &resp, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Hearbeat checks if the server has started and is responsive; if yes, it |  | ||||||
| // returns nil, otherwise an error. |  | ||||||
| func (c *Client) Heartbeat(ctx context.Context) error { |  | ||||||
| 	if err := c.do(ctx, http.MethodHead, "/", nil, nil); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Embeddings generates embeddings from a model. |  | ||||||
| func (c *Client) Embeddings(ctx context.Context, req *EmbeddingRequest) (*EmbeddingResponse, error) { |  | ||||||
| 	var resp EmbeddingResponse |  | ||||||
| 	if err := c.do(ctx, http.MethodPost, "/api/embeddings", req, &resp); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	return &resp, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // CreateBlob creates a blob from a file on the server. digest is the |  | ||||||
| // expected SHA256 digest of the file, and r represents the file. |  | ||||||
| func (c *Client) CreateBlob(ctx context.Context, digest string, r io.Reader) error { |  | ||||||
| 	return c.do(ctx, http.MethodPost, fmt.Sprintf("/api/blobs/%s", digest), r, nil) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Version returns the Ollama server version as a string. |  | ||||||
| func (c *Client) Version(ctx context.Context) (string, error) { |  | ||||||
| 	var version struct { |  | ||||||
| 		Version string `json:"version"` |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := c.do(ctx, http.MethodGet, "/api/version", nil, &version); err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return version.Version, nil |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,85 +0,0 @@ | |||||||
| package api |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"net" |  | ||||||
| 	"testing" |  | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestClientFromEnvironment(t *testing.T) { |  | ||||||
| 	type testCase struct { |  | ||||||
| 		value  string |  | ||||||
| 		expect string |  | ||||||
| 		err    error |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	testCases := map[string]*testCase{ |  | ||||||
| 		"empty":                      {value: "", expect: "http://127.0.0.1:11434"}, |  | ||||||
| 		"only address":               {value: "1.2.3.4", expect: "http://1.2.3.4:11434"}, |  | ||||||
| 		"only port":                  {value: ":1234", expect: "http://:1234"}, |  | ||||||
| 		"address and port":           {value: "1.2.3.4:1234", expect: "http://1.2.3.4:1234"}, |  | ||||||
| 		"scheme http and address":    {value: "http://1.2.3.4", expect: "http://1.2.3.4:80"}, |  | ||||||
| 		"scheme https and address":   {value: "https://1.2.3.4", expect: "https://1.2.3.4:443"}, |  | ||||||
| 		"scheme, address, and port":  {value: "https://1.2.3.4:1234", expect: "https://1.2.3.4:1234"}, |  | ||||||
| 		"hostname":                   {value: "example.com", expect: "http://example.com:11434"}, |  | ||||||
| 		"hostname and port":          {value: "example.com:1234", expect: "http://example.com:1234"}, |  | ||||||
| 		"scheme http and hostname":   {value: "http://example.com", expect: "http://example.com:80"}, |  | ||||||
| 		"scheme https and hostname":  {value: "https://example.com", expect: "https://example.com:443"}, |  | ||||||
| 		"scheme, hostname, and port": {value: "https://example.com:1234", expect: "https://example.com:1234"}, |  | ||||||
| 		"trailing slash":             {value: "example.com/", expect: "http://example.com:11434"}, |  | ||||||
| 		"trailing slash port":        {value: "example.com:1234/", expect: "http://example.com:1234"}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for k, v := range testCases { |  | ||||||
| 		t.Run(k, func(t *testing.T) { |  | ||||||
| 			t.Setenv("OLLAMA_HOST", v.value) |  | ||||||
|  |  | ||||||
| 			client, err := ClientFromEnvironment() |  | ||||||
| 			if err != v.err { |  | ||||||
| 				t.Fatalf("expected %s, got %s", v.err, err) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if client.base.String() != v.expect { |  | ||||||
| 				t.Fatalf("expected %s, got %s", v.expect, client.base.String()) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	hostTestCases := map[string]*testCase{ |  | ||||||
| 		"empty":               {value: "", expect: "127.0.0.1:11434"}, |  | ||||||
| 		"only address":        {value: "1.2.3.4", expect: "1.2.3.4:11434"}, |  | ||||||
| 		"only port":           {value: ":1234", expect: ":1234"}, |  | ||||||
| 		"address and port":    {value: "1.2.3.4:1234", expect: "1.2.3.4:1234"}, |  | ||||||
| 		"hostname":            {value: "example.com", expect: "example.com:11434"}, |  | ||||||
| 		"hostname and port":   {value: "example.com:1234", expect: "example.com:1234"}, |  | ||||||
| 		"zero port":           {value: ":0", expect: ":0"}, |  | ||||||
| 		"too large port":      {value: ":66000", err: ErrInvalidHostPort}, |  | ||||||
| 		"too small port":      {value: ":-1", err: ErrInvalidHostPort}, |  | ||||||
| 		"ipv6 localhost":      {value: "[::1]", expect: "[::1]:11434"}, |  | ||||||
| 		"ipv6 world open":     {value: "[::]", expect: "[::]:11434"}, |  | ||||||
| 		"ipv6 no brackets":    {value: "::1", expect: "[::1]:11434"}, |  | ||||||
| 		"ipv6 + port":         {value: "[::1]:1337", expect: "[::1]:1337"}, |  | ||||||
| 		"extra space":         {value: " 1.2.3.4 ", expect: "1.2.3.4:11434"}, |  | ||||||
| 		"extra quotes":        {value: "\"1.2.3.4\"", expect: "1.2.3.4:11434"}, |  | ||||||
| 		"extra space+quotes":  {value: " \" 1.2.3.4 \" ", expect: "1.2.3.4:11434"}, |  | ||||||
| 		"extra single quotes": {value: "'1.2.3.4'", expect: "1.2.3.4:11434"}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for k, v := range hostTestCases { |  | ||||||
| 		t.Run(k, func(t *testing.T) { |  | ||||||
| 			t.Setenv("OLLAMA_HOST", v.value) |  | ||||||
|  |  | ||||||
| 			oh, err := GetOllamaHost() |  | ||||||
| 			if err != v.err { |  | ||||||
| 				t.Fatalf("expected %s, got %s", v.err, err) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if err == nil { |  | ||||||
| 				host := net.JoinHostPort(oh.Host, oh.Port) |  | ||||||
| 				assert.Equal(t, v.expect, host, fmt.Sprintf("%s: expected %s, got %s", k, v.expect, host)) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										614
									
								
								api/types.go
									
									
									
									
									
								
							
							
						
						| @@ -1,19 +1,12 @@ | |||||||
| package api | package api | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"log/slog" |  | ||||||
| 	"math" |  | ||||||
| 	"os" | 	"os" | ||||||
| 	"reflect" | 	"runtime" | ||||||
| 	"strconv" |  | ||||||
| 	"strings" |  | ||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // StatusError is an error with and HTTP status code. |  | ||||||
| type StatusError struct { | type StatusError struct { | ||||||
| 	StatusCode   int | 	StatusCode   int | ||||||
| 	Status       string | 	Status       string | ||||||
| @@ -34,133 +27,109 @@ func (e StatusError) Error() string { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // ImageData represents the raw binary data of an image file. |  | ||||||
| type ImageData []byte |  | ||||||
|  |  | ||||||
| // GenerateRequest describes a request sent by [Client.Generate]. While you |  | ||||||
| // have to specify the Model and Prompt fields, all the other fields have |  | ||||||
| // reasonable defaults for basic uses. |  | ||||||
| type GenerateRequest struct { | type GenerateRequest struct { | ||||||
| 	// Model is the model name; it should be a name familiar to Ollama from |  | ||||||
| 	// the library at https://ollama.com/library |  | ||||||
| 	Model   string `json:"model"` | 	Model   string `json:"model"` | ||||||
|  |  | ||||||
| 	// Prompt is the textual prompt to send to the model. |  | ||||||
| 	Prompt  string `json:"prompt"` | 	Prompt  string `json:"prompt"` | ||||||
|  |  | ||||||
| 	// System overrides the model's default system message/prompt. |  | ||||||
| 	System string `json:"system"` |  | ||||||
|  |  | ||||||
| 	// Template overrides the model's default prompt template. |  | ||||||
| 	Template string `json:"template"` |  | ||||||
|  |  | ||||||
| 	// Context is the context parameter returned from a previous call to |  | ||||||
| 	// Generate call. It can be used to keep a short conversational memory. |  | ||||||
| 	Context []int  `json:"context,omitempty"` | 	Context []int  `json:"context,omitempty"` | ||||||
|  |  | ||||||
| 	// Stream specifies whether the response is streaming; it is true by default. | 	Options `json:"options"` | ||||||
| 	Stream *bool `json:"stream,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Raw set to true means that no formatting will be applied to the prompt. |  | ||||||
| 	Raw bool `json:"raw,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Format specifies the format to return a response in. |  | ||||||
| 	Format string `json:"format"` |  | ||||||
|  |  | ||||||
| 	// KeepAlive controls how long the model will stay loaded in memory following |  | ||||||
| 	// this request. |  | ||||||
| 	KeepAlive *Duration `json:"keep_alive,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Images is an optional list of base64-encoded images accompanying this |  | ||||||
| 	// request, for multimodal models. |  | ||||||
| 	Images []ImageData `json:"images,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Options lists model-specific options. For example, temperature can be |  | ||||||
| 	// set through this field, if the model supports it. |  | ||||||
| 	Options map[string]interface{} `json:"options"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // ChatRequest describes a request sent by [Client.Chat]. | type CreateRequest struct { | ||||||
| type ChatRequest struct { | 	Name string `json:"name"` | ||||||
| 	// Model is the model name, as in [GenerateRequest]. | 	Path string `json:"path"` | ||||||
| 	Model string `json:"model"` |  | ||||||
|  |  | ||||||
| 	// Messages is the messages of the chat - can be used to keep a chat memory. |  | ||||||
| 	Messages []Message `json:"messages"` |  | ||||||
|  |  | ||||||
| 	// Stream enable streaming of returned response; true by default. |  | ||||||
| 	Stream *bool `json:"stream,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Format is the format to return the response in (e.g. "json"). |  | ||||||
| 	Format string `json:"format"` |  | ||||||
|  |  | ||||||
| 	// KeepAlive controls how long the model will stay loaded into memory |  | ||||||
| 	// followin the request. |  | ||||||
| 	KeepAlive *Duration `json:"keep_alive,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Options lists model-specific options. |  | ||||||
| 	Options map[string]interface{} `json:"options"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Message is a single message in a chat sequence. The message contains the | type CreateProgress struct { | ||||||
| // role ("system", "user", or "assistant"), the content and an optional list | 	Status string `json:"status"` | ||||||
| // of images. |  | ||||||
| type Message struct { |  | ||||||
| 	Role    string      `json:"role"` |  | ||||||
| 	Content string      `json:"content"` |  | ||||||
| 	Images  []ImageData `json:"images,omitempty"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // ChatResponse is the response returned by [Client.Chat]. Its fields are | type DeleteRequest struct { | ||||||
| // similar to [GenerateResponse]. | 	Name string `json:"name"` | ||||||
| type ChatResponse struct { | } | ||||||
|  |  | ||||||
|  | type CopyRequest struct { | ||||||
|  | 	Source      string `json:"source"` | ||||||
|  | 	Destination string `json:"destination"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type PullRequest struct { | ||||||
|  | 	Name     string `json:"name"` | ||||||
|  | 	Insecure bool   `json:"insecure,omitempty"` | ||||||
|  | 	Username string `json:"username"` | ||||||
|  | 	Password string `json:"password"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ProgressResponse struct { | ||||||
|  | 	Status    string `json:"status"` | ||||||
|  | 	Digest    string `json:"digest,omitempty"` | ||||||
|  | 	Total     int    `json:"total,omitempty"` | ||||||
|  | 	Completed int    `json:"completed,omitempty"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type PushRequest struct { | ||||||
|  | 	Name     string `json:"name"` | ||||||
|  | 	Insecure bool   `json:"insecure,omitempty"` | ||||||
|  | 	Username string `json:"username"` | ||||||
|  | 	Password string `json:"password"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ListResponse struct { | ||||||
|  | 	Models []ListResponseModel `json:"models"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ListResponseModel struct { | ||||||
|  | 	Name       string    `json:"name"` | ||||||
|  | 	ModifiedAt time.Time `json:"modified_at"` | ||||||
|  | 	Size       int       `json:"size"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GenerateResponse struct { | ||||||
| 	Model     string    `json:"model"` | 	Model     string    `json:"model"` | ||||||
| 	CreatedAt time.Time `json:"created_at"` | 	CreatedAt time.Time `json:"created_at"` | ||||||
| 	Message    Message   `json:"message"` | 	Response  string    `json:"response,omitempty"` | ||||||
| 	DoneReason string    `json:"done_reason,omitempty"` |  | ||||||
|  |  | ||||||
| 	Done    bool  `json:"done"` | 	Done    bool  `json:"done"` | ||||||
|  | 	Context []int `json:"context,omitempty"` | ||||||
|  |  | ||||||
| 	Metrics |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type Metrics struct { |  | ||||||
| 	TotalDuration      time.Duration `json:"total_duration,omitempty"` | 	TotalDuration      time.Duration `json:"total_duration,omitempty"` | ||||||
| 	LoadDuration       time.Duration `json:"load_duration,omitempty"` |  | ||||||
| 	PromptEvalCount    int           `json:"prompt_eval_count,omitempty"` | 	PromptEvalCount    int           `json:"prompt_eval_count,omitempty"` | ||||||
| 	PromptEvalDuration time.Duration `json:"prompt_eval_duration,omitempty"` | 	PromptEvalDuration time.Duration `json:"prompt_eval_duration,omitempty"` | ||||||
| 	EvalCount          int           `json:"eval_count,omitempty"` | 	EvalCount          int           `json:"eval_count,omitempty"` | ||||||
| 	EvalDuration       time.Duration `json:"eval_duration,omitempty"` | 	EvalDuration       time.Duration `json:"eval_duration,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Options specified in [GenerateRequest], if you add a new option here add it | func (r *GenerateResponse) Summary() { | ||||||
| // to the API docs also. | 	if r.TotalDuration > 0 { | ||||||
| type Options struct { | 		fmt.Fprintf(os.Stderr, "total duration:       %v\n", r.TotalDuration) | ||||||
| 	Runner | 	} | ||||||
|  |  | ||||||
| 	// Predict options used at runtime | 	if r.PromptEvalCount > 0 { | ||||||
| 	NumKeep          int      `json:"num_keep,omitempty"` | 		fmt.Fprintf(os.Stderr, "prompt eval count:    %d token(s)\n", r.PromptEvalCount) | ||||||
| 	Seed             int      `json:"seed,omitempty"` | 	} | ||||||
| 	NumPredict       int      `json:"num_predict,omitempty"` |  | ||||||
| 	TopK             int      `json:"top_k,omitempty"` | 	if r.PromptEvalDuration > 0 { | ||||||
| 	TopP             float32  `json:"top_p,omitempty"` | 		fmt.Fprintf(os.Stderr, "prompt eval duration: %s\n", r.PromptEvalDuration) | ||||||
| 	TFSZ             float32  `json:"tfs_z,omitempty"` | 		fmt.Fprintf(os.Stderr, "prompt eval rate:     %.2f tokens/s\n", float64(r.PromptEvalCount)/r.PromptEvalDuration.Seconds()) | ||||||
| 	TypicalP         float32  `json:"typical_p,omitempty"` | 	} | ||||||
| 	RepeatLastN      int      `json:"repeat_last_n,omitempty"` |  | ||||||
| 	Temperature      float32  `json:"temperature,omitempty"` | 	if r.EvalCount > 0 { | ||||||
| 	RepeatPenalty    float32  `json:"repeat_penalty,omitempty"` | 		fmt.Fprintf(os.Stderr, "eval count:           %d token(s)\n", r.EvalCount) | ||||||
| 	PresencePenalty  float32  `json:"presence_penalty,omitempty"` | 	} | ||||||
| 	FrequencyPenalty float32  `json:"frequency_penalty,omitempty"` |  | ||||||
| 	Mirostat         int      `json:"mirostat,omitempty"` | 	if r.EvalDuration > 0 { | ||||||
| 	MirostatTau      float32  `json:"mirostat_tau,omitempty"` | 		fmt.Fprintf(os.Stderr, "eval duration:        %s\n", r.EvalDuration) | ||||||
| 	MirostatEta      float32  `json:"mirostat_eta,omitempty"` | 		fmt.Fprintf(os.Stderr, "eval rate:            %.2f tokens/s\n", float64(r.EvalCount)/r.EvalDuration.Seconds()) | ||||||
| 	PenalizeNewline  bool     `json:"penalize_newline,omitempty"` | 	} | ||||||
| 	Stop             []string `json:"stop,omitempty"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Runner options which must be set when the model is loaded into memory | type Options struct { | ||||||
| type Runner struct { | 	Seed int `json:"seed,omitempty"` | ||||||
|  |  | ||||||
|  | 	// Backend options | ||||||
| 	UseNUMA bool `json:"numa,omitempty"` | 	UseNUMA bool `json:"numa,omitempty"` | ||||||
|  |  | ||||||
|  | 	// Model options | ||||||
| 	NumCtx        int  `json:"num_ctx,omitempty"` | 	NumCtx        int  `json:"num_ctx,omitempty"` | ||||||
| 	NumBatch      int  `json:"num_batch,omitempty"` | 	NumBatch      int  `json:"num_batch,omitempty"` | ||||||
| 	NumGPU        int  `json:"num_gpu,omitempty"` | 	NumGPU        int  `json:"num_gpu,omitempty"` | ||||||
| @@ -171,417 +140,52 @@ type Runner struct { | |||||||
| 	VocabOnly     bool `json:"vocab_only,omitempty"` | 	VocabOnly     bool `json:"vocab_only,omitempty"` | ||||||
| 	UseMMap       bool `json:"use_mmap,omitempty"` | 	UseMMap       bool `json:"use_mmap,omitempty"` | ||||||
| 	UseMLock      bool `json:"use_mlock,omitempty"` | 	UseMLock      bool `json:"use_mlock,omitempty"` | ||||||
|  | 	EmbeddingOnly bool `json:"embedding_only,omitempty"` | ||||||
|  |  | ||||||
|  | 	// Predict options | ||||||
|  | 	RepeatLastN      int     `json:"repeat_last_n,omitempty"` | ||||||
|  | 	RepeatPenalty    float32 `json:"repeat_penalty,omitempty"` | ||||||
|  | 	FrequencyPenalty float32 `json:"frequency_penalty,omitempty"` | ||||||
|  | 	PresencePenalty  float32 `json:"presence_penalty,omitempty"` | ||||||
|  | 	Temperature      float32 `json:"temperature,omitempty"` | ||||||
|  | 	TopK             int     `json:"top_k,omitempty"` | ||||||
|  | 	TopP             float32 `json:"top_p,omitempty"` | ||||||
|  | 	TFSZ             float32 `json:"tfs_z,omitempty"` | ||||||
|  | 	TypicalP         float32 `json:"typical_p,omitempty"` | ||||||
|  | 	Mirostat         int     `json:"mirostat,omitempty"` | ||||||
|  | 	MirostatTau      float32 `json:"mirostat_tau,omitempty"` | ||||||
|  | 	MirostatEta      float32 `json:"mirostat_eta,omitempty"` | ||||||
|  |  | ||||||
| 	NumThread int `json:"num_thread,omitempty"` | 	NumThread int `json:"num_thread,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // EmbeddingRequest is the request passed to [Client.Embeddings]. |  | ||||||
| type EmbeddingRequest struct { |  | ||||||
| 	// Model is the model name. |  | ||||||
| 	Model string `json:"model"` |  | ||||||
|  |  | ||||||
| 	// Prompt is the textual prompt to embed. |  | ||||||
| 	Prompt string `json:"prompt"` |  | ||||||
|  |  | ||||||
| 	// KeepAlive controls how long the model will stay loaded in memory following |  | ||||||
| 	// this request. |  | ||||||
| 	KeepAlive *Duration `json:"keep_alive,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Options lists model-specific options. |  | ||||||
| 	Options map[string]interface{} `json:"options"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // EmbeddingResponse is the response from [Client.Embeddings]. |  | ||||||
| type EmbeddingResponse struct { |  | ||||||
| 	Embedding []float64 `json:"embedding"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // CreateRequest is the request passed to [Client.Create]. |  | ||||||
| type CreateRequest struct { |  | ||||||
| 	Model     string `json:"model"` |  | ||||||
| 	Path      string `json:"path"` |  | ||||||
| 	Modelfile string `json:"modelfile"` |  | ||||||
| 	Stream    *bool  `json:"stream,omitempty"` |  | ||||||
| 	Quantize  string `json:"quantize,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Name is deprecated, see Model |  | ||||||
| 	Name string `json:"name"` |  | ||||||
|  |  | ||||||
| 	// Quantization is deprecated, see Quantize |  | ||||||
| 	Quantization string `json:"quantization,omitempty"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // DeleteRequest is the request passed to [Client.Delete]. |  | ||||||
| type DeleteRequest struct { |  | ||||||
| 	Model string `json:"model"` |  | ||||||
|  |  | ||||||
| 	// Name is deprecated, see Model |  | ||||||
| 	Name string `json:"name"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ShowRequest is the request passed to [Client.Show]. |  | ||||||
| type ShowRequest struct { |  | ||||||
| 	Model    string `json:"model"` |  | ||||||
| 	System   string `json:"system"` |  | ||||||
| 	Template string `json:"template"` |  | ||||||
|  |  | ||||||
| 	Options map[string]interface{} `json:"options"` |  | ||||||
|  |  | ||||||
| 	// Name is deprecated, see Model |  | ||||||
| 	Name string `json:"name"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ShowResponse is the response returned from [Client.Show]. |  | ||||||
| type ShowResponse struct { |  | ||||||
| 	License    string       `json:"license,omitempty"` |  | ||||||
| 	Modelfile  string       `json:"modelfile,omitempty"` |  | ||||||
| 	Parameters string       `json:"parameters,omitempty"` |  | ||||||
| 	Template   string       `json:"template,omitempty"` |  | ||||||
| 	System     string       `json:"system,omitempty"` |  | ||||||
| 	Details    ModelDetails `json:"details,omitempty"` |  | ||||||
| 	Messages   []Message    `json:"messages,omitempty"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // CopyRequest is the request passed to [Client.Copy]. |  | ||||||
| type CopyRequest struct { |  | ||||||
| 	Source      string `json:"source"` |  | ||||||
| 	Destination string `json:"destination"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // PullRequest is the request passed to [Client.Pull]. |  | ||||||
| type PullRequest struct { |  | ||||||
| 	Model    string `json:"model"` |  | ||||||
| 	Insecure bool   `json:"insecure,omitempty"` |  | ||||||
| 	Username string `json:"username"` |  | ||||||
| 	Password string `json:"password"` |  | ||||||
| 	Stream   *bool  `json:"stream,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Name is deprecated, see Model |  | ||||||
| 	Name string `json:"name"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ProgressResponse is the response passed to progress functions like |  | ||||||
| // [PullProgressFunc] and [PushProgressFunc]. |  | ||||||
| type ProgressResponse struct { |  | ||||||
| 	Status    string `json:"status"` |  | ||||||
| 	Digest    string `json:"digest,omitempty"` |  | ||||||
| 	Total     int64  `json:"total,omitempty"` |  | ||||||
| 	Completed int64  `json:"completed,omitempty"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // PushRequest is the request passed to [Client.Push]. |  | ||||||
| type PushRequest struct { |  | ||||||
| 	Model    string `json:"model"` |  | ||||||
| 	Insecure bool   `json:"insecure,omitempty"` |  | ||||||
| 	Username string `json:"username"` |  | ||||||
| 	Password string `json:"password"` |  | ||||||
| 	Stream   *bool  `json:"stream,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Name is deprecated, see Model |  | ||||||
| 	Name string `json:"name"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ListResponse is the response from [Client.List]. |  | ||||||
| type ListResponse struct { |  | ||||||
| 	Models []ModelResponse `json:"models"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ModelResponse is a single model description in [ListResponse]. |  | ||||||
| type ModelResponse struct { |  | ||||||
| 	Name       string       `json:"name"` |  | ||||||
| 	Model      string       `json:"model"` |  | ||||||
| 	ModifiedAt time.Time    `json:"modified_at,omitempty"` |  | ||||||
| 	Size       int64        `json:"size"` |  | ||||||
| 	Digest     string       `json:"digest"` |  | ||||||
| 	Details    ModelDetails `json:"details,omitempty"` |  | ||||||
| 	ExpiresAt  time.Time    `json:"expires_at,omitempty"` |  | ||||||
| 	SizeVRAM   int64        `json:"size_vram,omitempty"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type TokenResponse struct { |  | ||||||
| 	Token string `json:"token"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GenerateResponse is the response passed into [GenerateResponseFunc]. |  | ||||||
| type GenerateResponse struct { |  | ||||||
| 	// Model is the model name that generated the response. |  | ||||||
| 	Model string `json:"model"` |  | ||||||
|  |  | ||||||
| 	//CreatedAt is the timestamp of the response. |  | ||||||
| 	CreatedAt time.Time `json:"created_at"` |  | ||||||
|  |  | ||||||
| 	// Response is the textual response itself. |  | ||||||
| 	Response string `json:"response"` |  | ||||||
|  |  | ||||||
| 	// Done specifies if the response is complete. |  | ||||||
| 	Done bool `json:"done"` |  | ||||||
|  |  | ||||||
| 	// DoneReason is the reason the model stopped generating text. |  | ||||||
| 	DoneReason string `json:"done_reason,omitempty"` |  | ||||||
|  |  | ||||||
| 	// Context is an encoding of the conversation used in this response; this |  | ||||||
| 	// can be sent in the next request to keep a conversational memory. |  | ||||||
| 	Context []int `json:"context,omitempty"` |  | ||||||
|  |  | ||||||
| 	Metrics |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ModelDetails provides details about a model. |  | ||||||
| type ModelDetails struct { |  | ||||||
| 	ParentModel       string   `json:"parent_model"` |  | ||||||
| 	Format            string   `json:"format"` |  | ||||||
| 	Family            string   `json:"family"` |  | ||||||
| 	Families          []string `json:"families"` |  | ||||||
| 	ParameterSize     string   `json:"parameter_size"` |  | ||||||
| 	QuantizationLevel string   `json:"quantization_level"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *Metrics) Summary() { |  | ||||||
| 	if m.TotalDuration > 0 { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "total duration:       %v\n", m.TotalDuration) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if m.LoadDuration > 0 { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "load duration:        %v\n", m.LoadDuration) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if m.PromptEvalCount > 0 { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "prompt eval count:    %d token(s)\n", m.PromptEvalCount) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if m.PromptEvalDuration > 0 { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "prompt eval duration: %s\n", m.PromptEvalDuration) |  | ||||||
| 		fmt.Fprintf(os.Stderr, "prompt eval rate:     %.2f tokens/s\n", float64(m.PromptEvalCount)/m.PromptEvalDuration.Seconds()) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if m.EvalCount > 0 { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "eval count:           %d token(s)\n", m.EvalCount) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if m.EvalDuration > 0 { |  | ||||||
| 		fmt.Fprintf(os.Stderr, "eval duration:        %s\n", m.EvalDuration) |  | ||||||
| 		fmt.Fprintf(os.Stderr, "eval rate:            %.2f tokens/s\n", float64(m.EvalCount)/m.EvalDuration.Seconds()) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var ErrInvalidHostPort = errors.New("invalid port specified in OLLAMA_HOST") |  | ||||||
|  |  | ||||||
| func (opts *Options) FromMap(m map[string]interface{}) error { |  | ||||||
| 	valueOpts := reflect.ValueOf(opts).Elem() // names of the fields in the options struct |  | ||||||
| 	typeOpts := reflect.TypeOf(opts).Elem()   // types of the fields in the options struct |  | ||||||
|  |  | ||||||
| 	// build map of json struct tags to their types |  | ||||||
| 	jsonOpts := make(map[string]reflect.StructField) |  | ||||||
| 	for _, field := range reflect.VisibleFields(typeOpts) { |  | ||||||
| 		jsonTag := strings.Split(field.Tag.Get("json"), ",")[0] |  | ||||||
| 		if jsonTag != "" { |  | ||||||
| 			jsonOpts[jsonTag] = field |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for key, val := range m { |  | ||||||
| 		opt, ok := jsonOpts[key] |  | ||||||
| 		if !ok { |  | ||||||
| 			slog.Warn("invalid option provided", "option", opt.Name) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		field := valueOpts.FieldByName(opt.Name) |  | ||||||
| 		if field.IsValid() && field.CanSet() { |  | ||||||
| 			if val == nil { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			switch field.Kind() { |  | ||||||
| 			case reflect.Int: |  | ||||||
| 				switch t := val.(type) { |  | ||||||
| 				case int64: |  | ||||||
| 					field.SetInt(t) |  | ||||||
| 				case float64: |  | ||||||
| 					// when JSON unmarshals numbers, it uses float64, not int |  | ||||||
| 					field.SetInt(int64(t)) |  | ||||||
| 				default: |  | ||||||
| 					return fmt.Errorf("option %q must be of type integer", key) |  | ||||||
| 				} |  | ||||||
| 			case reflect.Bool: |  | ||||||
| 				val, ok := val.(bool) |  | ||||||
| 				if !ok { |  | ||||||
| 					return fmt.Errorf("option %q must be of type boolean", key) |  | ||||||
| 				} |  | ||||||
| 				field.SetBool(val) |  | ||||||
| 			case reflect.Float32: |  | ||||||
| 				// JSON unmarshals to float64 |  | ||||||
| 				val, ok := val.(float64) |  | ||||||
| 				if !ok { |  | ||||||
| 					return fmt.Errorf("option %q must be of type float32", key) |  | ||||||
| 				} |  | ||||||
| 				field.SetFloat(val) |  | ||||||
| 			case reflect.String: |  | ||||||
| 				val, ok := val.(string) |  | ||||||
| 				if !ok { |  | ||||||
| 					return fmt.Errorf("option %q must be of type string", key) |  | ||||||
| 				} |  | ||||||
| 				field.SetString(val) |  | ||||||
| 			case reflect.Slice: |  | ||||||
| 				// JSON unmarshals to []interface{}, not []string |  | ||||||
| 				val, ok := val.([]interface{}) |  | ||||||
| 				if !ok { |  | ||||||
| 					return fmt.Errorf("option %q must be of type array", key) |  | ||||||
| 				} |  | ||||||
| 				// convert []interface{} to []string |  | ||||||
| 				slice := make([]string, len(val)) |  | ||||||
| 				for i, item := range val { |  | ||||||
| 					str, ok := item.(string) |  | ||||||
| 					if !ok { |  | ||||||
| 						return fmt.Errorf("option %q must be of an array of strings", key) |  | ||||||
| 					} |  | ||||||
| 					slice[i] = str |  | ||||||
| 				} |  | ||||||
| 				field.Set(reflect.ValueOf(slice)) |  | ||||||
| 			default: |  | ||||||
| 				return fmt.Errorf("unknown type loading config params: %v", field.Kind()) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // DefaultOptions is the default set of options for [GenerateRequest]; these |  | ||||||
| // values are used unless the user specifies other values explicitly. |  | ||||||
| func DefaultOptions() Options { | func DefaultOptions() Options { | ||||||
| 	return Options{ | 	return Options{ | ||||||
| 		// options set on request to runner | 		Seed: -1, | ||||||
| 		NumPredict: -1, |  | ||||||
|  |  | ||||||
| 		// set a minimal num_keep to avoid issues on context shifts | 		UseNUMA: false, | ||||||
| 		NumKeep:          4, |  | ||||||
|  | 		NumCtx:   2048, | ||||||
|  | 		NumBatch: 512, | ||||||
|  | 		NumGPU:   1, | ||||||
|  | 		LowVRAM:  false, | ||||||
|  | 		F16KV:    true, | ||||||
|  | 		UseMMap:  true, | ||||||
|  | 		UseMLock: false, | ||||||
|  |  | ||||||
|  | 		RepeatLastN:      512, | ||||||
|  | 		RepeatPenalty:    1.1, | ||||||
|  | 		FrequencyPenalty: 0.0, | ||||||
|  | 		PresencePenalty:  0.0, | ||||||
| 		Temperature:      0.8, | 		Temperature:      0.8, | ||||||
| 		TopK:             40, | 		TopK:             40, | ||||||
| 		TopP:             0.9, | 		TopP:             0.9, | ||||||
| 		TFSZ:             1.0, | 		TFSZ:             1.0, | ||||||
| 		TypicalP:         1.0, | 		TypicalP:         1.0, | ||||||
| 		RepeatLastN:      64, |  | ||||||
| 		RepeatPenalty:    1.1, |  | ||||||
| 		PresencePenalty:  0.0, |  | ||||||
| 		FrequencyPenalty: 0.0, |  | ||||||
| 		Mirostat:         0, | 		Mirostat:         0, | ||||||
| 		MirostatTau:      5.0, | 		MirostatTau:      5.0, | ||||||
| 		MirostatEta:      0.1, | 		MirostatEta:      0.1, | ||||||
| 		PenalizeNewline:  true, |  | ||||||
| 		Seed:             -1, |  | ||||||
|  |  | ||||||
| 		Runner: Runner{ | 		NumThread: runtime.NumCPU(), | ||||||
| 			// options set when the model is loaded |  | ||||||
| 			NumCtx:    2048, |  | ||||||
| 			NumBatch:  512, |  | ||||||
| 			NumGPU:    -1, // -1 here indicates that NumGPU should be set dynamically |  | ||||||
| 			NumThread: 0,  // let the runtime decide |  | ||||||
| 			LowVRAM:   false, |  | ||||||
| 			F16KV:     true, |  | ||||||
| 			UseMLock:  false, |  | ||||||
| 			UseMMap:   true, |  | ||||||
| 			UseNUMA:   false, |  | ||||||
| 		}, |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| type Duration struct { |  | ||||||
| 	time.Duration |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (d Duration) MarshalJSON() ([]byte, error) { |  | ||||||
| 	if d.Duration < 0 { |  | ||||||
| 		return []byte("-1"), nil |  | ||||||
| 	} |  | ||||||
| 	return []byte("\"" + d.Duration.String() + "\""), nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (d *Duration) UnmarshalJSON(b []byte) (err error) { |  | ||||||
| 	var v any |  | ||||||
| 	if err := json.Unmarshal(b, &v); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	d.Duration = 5 * time.Minute |  | ||||||
|  |  | ||||||
| 	switch t := v.(type) { |  | ||||||
| 	case float64: |  | ||||||
| 		if t < 0 { |  | ||||||
| 			d.Duration = time.Duration(math.MaxInt64) |  | ||||||
| 		} else { |  | ||||||
| 			d.Duration = time.Duration(int(t) * int(time.Second)) |  | ||||||
| 		} |  | ||||||
| 	case string: |  | ||||||
| 		d.Duration, err = time.ParseDuration(t) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		if d.Duration < 0 { |  | ||||||
| 			d.Duration = time.Duration(math.MaxInt64) |  | ||||||
| 		} |  | ||||||
| 	default: |  | ||||||
| 		return fmt.Errorf("Unsupported type: '%s'", reflect.TypeOf(v)) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // FormatParams converts specified parameter options to their correct types |  | ||||||
| func FormatParams(params map[string][]string) (map[string]interface{}, error) { |  | ||||||
| 	opts := Options{} |  | ||||||
| 	valueOpts := reflect.ValueOf(&opts).Elem() // names of the fields in the options struct |  | ||||||
| 	typeOpts := reflect.TypeOf(opts)           // types of the fields in the options struct |  | ||||||
|  |  | ||||||
| 	// build map of json struct tags to their types |  | ||||||
| 	jsonOpts := make(map[string]reflect.StructField) |  | ||||||
| 	for _, field := range reflect.VisibleFields(typeOpts) { |  | ||||||
| 		jsonTag := strings.Split(field.Tag.Get("json"), ",")[0] |  | ||||||
| 		if jsonTag != "" { |  | ||||||
| 			jsonOpts[jsonTag] = field |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	out := make(map[string]interface{}) |  | ||||||
| 	// iterate params and set values based on json struct tags |  | ||||||
| 	for key, vals := range params { |  | ||||||
| 		if opt, ok := jsonOpts[key]; !ok { |  | ||||||
| 			return nil, fmt.Errorf("unknown parameter '%s'", key) |  | ||||||
| 		} else { |  | ||||||
| 			field := valueOpts.FieldByName(opt.Name) |  | ||||||
| 			if field.IsValid() && field.CanSet() { |  | ||||||
| 				switch field.Kind() { |  | ||||||
| 				case reflect.Float32: |  | ||||||
| 					floatVal, err := strconv.ParseFloat(vals[0], 32) |  | ||||||
| 					if err != nil { |  | ||||||
| 						return nil, fmt.Errorf("invalid float value %s", vals) |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					out[key] = float32(floatVal) |  | ||||||
| 				case reflect.Int: |  | ||||||
| 					intVal, err := strconv.ParseInt(vals[0], 10, 64) |  | ||||||
| 					if err != nil { |  | ||||||
| 						return nil, fmt.Errorf("invalid int value %s", vals) |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					out[key] = intVal |  | ||||||
| 				case reflect.Bool: |  | ||||||
| 					boolVal, err := strconv.ParseBool(vals[0]) |  | ||||||
| 					if err != nil { |  | ||||||
| 						return nil, fmt.Errorf("invalid bool value %s", vals) |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					out[key] = boolVal |  | ||||||
| 				case reflect.String: |  | ||||||
| 					out[key] = vals[0] |  | ||||||
| 				case reflect.Slice: |  | ||||||
| 					// TODO: only string slices are supported right now |  | ||||||
| 					out[key] = vals |  | ||||||
| 				default: |  | ||||||
| 					return nil, fmt.Errorf("unknown type %s for %s", field.Kind(), key) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return out, nil |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -1,107 +0,0 @@ | |||||||
| package api |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"math" |  | ||||||
| 	"testing" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
| 	"github.com/stretchr/testify/require" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestKeepAliveParsingFromJSON(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name string |  | ||||||
| 		req  string |  | ||||||
| 		exp  *Duration |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			name: "Positive Integer", |  | ||||||
| 			req:  `{ "keep_alive": 42 }`, |  | ||||||
| 			exp:  &Duration{42 * time.Second}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Positive Float", |  | ||||||
| 			req:  `{ "keep_alive": 42.5 }`, |  | ||||||
| 			exp:  &Duration{42 * time.Second}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Positive Integer String", |  | ||||||
| 			req:  `{ "keep_alive": "42m" }`, |  | ||||||
| 			exp:  &Duration{42 * time.Minute}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Negative Integer", |  | ||||||
| 			req:  `{ "keep_alive": -1 }`, |  | ||||||
| 			exp:  &Duration{math.MaxInt64}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Negative Float", |  | ||||||
| 			req:  `{ "keep_alive": -3.14 }`, |  | ||||||
| 			exp:  &Duration{math.MaxInt64}, |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			name: "Negative Integer String", |  | ||||||
| 			req:  `{ "keep_alive": "-1m" }`, |  | ||||||
| 			exp:  &Duration{math.MaxInt64}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, test := range tests { |  | ||||||
| 		t.Run(test.name, func(t *testing.T) { |  | ||||||
| 			var dec ChatRequest |  | ||||||
| 			err := json.Unmarshal([]byte(test.req), &dec) |  | ||||||
| 			require.NoError(t, err) |  | ||||||
|  |  | ||||||
| 			assert.Equal(t, test.exp, dec.KeepAlive) |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestDurationMarshalUnmarshal(t *testing.T) { |  | ||||||
| 	tests := []struct { |  | ||||||
| 		name     string |  | ||||||
| 		input    time.Duration |  | ||||||
| 		expected time.Duration |  | ||||||
| 	}{ |  | ||||||
| 		{ |  | ||||||
| 			"negative duration", |  | ||||||
| 			time.Duration(-1), |  | ||||||
| 			time.Duration(math.MaxInt64), |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			"positive duration", |  | ||||||
| 			time.Duration(42 * time.Second), |  | ||||||
| 			time.Duration(42 * time.Second), |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			"another positive duration", |  | ||||||
| 			time.Duration(42 * time.Minute), |  | ||||||
| 			time.Duration(42 * time.Minute), |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			"zero duration", |  | ||||||
| 			time.Duration(0), |  | ||||||
| 			time.Duration(0), |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			"max duration", |  | ||||||
| 			time.Duration(math.MaxInt64), |  | ||||||
| 			time.Duration(math.MaxInt64), |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, test := range tests { |  | ||||||
| 		t.Run(test.name, func(t *testing.T) { |  | ||||||
| 			b, err := json.Marshal(Duration{test.input}) |  | ||||||
| 			require.NoError(t, err) |  | ||||||
|  |  | ||||||
| 			var d Duration |  | ||||||
| 			err = json.Unmarshal(b, &d) |  | ||||||
| 			require.NoError(t, err) |  | ||||||
|  |  | ||||||
| 			assert.Equal(t, test.expected, d.Duration, "input %v, marshalled %v, got %v", test.input, string(b), d.Duration) |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										93
									
								
								app/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -1 +1,92 @@ | |||||||
| ollama.syso | # Logs | ||||||
|  | logs | ||||||
|  | *.log | ||||||
|  | npm-debug.log* | ||||||
|  | yarn-debug.log* | ||||||
|  | yarn-error.log* | ||||||
|  | lerna-debug.log* | ||||||
|  |  | ||||||
|  | # Diagnostic reports (https://nodejs.org/api/report.html) | ||||||
|  | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | ||||||
|  |  | ||||||
|  | # Runtime data | ||||||
|  | pids | ||||||
|  | *.pid | ||||||
|  | *.seed | ||||||
|  | *.pid.lock | ||||||
|  | .DS_Store | ||||||
|  |  | ||||||
|  | # Directory for instrumented libs generated by jscoverage/JSCover | ||||||
|  | lib-cov | ||||||
|  |  | ||||||
|  | # Coverage directory used by tools like istanbul | ||||||
|  | coverage | ||||||
|  | *.lcov | ||||||
|  |  | ||||||
|  | # nyc test coverage | ||||||
|  | .nyc_output | ||||||
|  |  | ||||||
|  | # node-waf configuration | ||||||
|  | .lock-wscript | ||||||
|  |  | ||||||
|  | # Compiled binary addons (https://nodejs.org/api/addons.html) | ||||||
|  | build/Release | ||||||
|  |  | ||||||
|  | # Dependency directories | ||||||
|  | node_modules/ | ||||||
|  | jspm_packages/ | ||||||
|  |  | ||||||
|  | # TypeScript v1 declaration files | ||||||
|  | typings/ | ||||||
|  |  | ||||||
|  | # TypeScript cache | ||||||
|  | *.tsbuildinfo | ||||||
|  |  | ||||||
|  | # Optional npm cache directory | ||||||
|  | .npm | ||||||
|  |  | ||||||
|  | # Optional eslint cache | ||||||
|  | .eslintcache | ||||||
|  |  | ||||||
|  | # Optional REPL history | ||||||
|  | .node_repl_history | ||||||
|  |  | ||||||
|  | # Output of 'npm pack' | ||||||
|  | *.tgz | ||||||
|  |  | ||||||
|  | # Yarn Integrity file | ||||||
|  | .yarn-integrity | ||||||
|  |  | ||||||
|  | # dotenv environment variables file | ||||||
|  | .env | ||||||
|  | .env.test | ||||||
|  |  | ||||||
|  | # parcel-bundler cache (https://parceljs.org/) | ||||||
|  | .cache | ||||||
|  |  | ||||||
|  | # next.js build output | ||||||
|  | .next | ||||||
|  |  | ||||||
|  | # nuxt.js build output | ||||||
|  | .nuxt | ||||||
|  |  | ||||||
|  | # vuepress build output | ||||||
|  | .vuepress/dist | ||||||
|  |  | ||||||
|  | # Serverless directories | ||||||
|  | .serverless/ | ||||||
|  |  | ||||||
|  | # FuseBox cache | ||||||
|  | .fusebox/ | ||||||
|  |  | ||||||
|  | # DynamoDB Local files | ||||||
|  | .dynamodb/ | ||||||
|  |  | ||||||
|  | # Webpack | ||||||
|  | .webpack/ | ||||||
|  |  | ||||||
|  | # Vite | ||||||
|  | .vite/ | ||||||
|  |  | ||||||
|  | # Electron-Forge | ||||||
|  | out/ | ||||||
|   | |||||||
| @@ -1,22 +1,27 @@ | |||||||
| # Ollama App | # Desktop | ||||||
|  |  | ||||||
| ## Linux | _Note: the Ollama desktop app is a work in progress and is not ready yet for general use._ | ||||||
|  |  | ||||||
| TODO | This app builds upon Ollama to provide a desktop experience for running models. | ||||||
|  |  | ||||||
| ## MacOS | ## Developing | ||||||
|  |  | ||||||
| TODO | First, build the `ollama` binary: | ||||||
|  |  | ||||||
| ## Windows |  | ||||||
|  |  | ||||||
| If you want to build the installer, youll need to install |  | ||||||
| - https://jrsoftware.org/isinfo.php |  | ||||||
|  |  | ||||||
|  |  | ||||||
| In the top directory of this repo, run the following powershell script |  | ||||||
| to build the ollama CLI, ollama app, and ollama installer. |  | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1 | make -C .. | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | Then run the desktop app with `npm start`: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | npm install | ||||||
|  | npm start | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ## Coming soon | ||||||
|  |  | ||||||
|  | - Browse the latest available models on Hugging Face and other sources | ||||||
|  | - Keep track of previous conversations with models | ||||||
|  | - Switch quickly between models | ||||||
|  | - Connect to remote Ollama servers to run models | ||||||
|   | |||||||
| Before Width: | Height: | Size: 7.3 KiB | 
| @@ -1,17 +0,0 @@ | |||||||
| package assets |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"embed" |  | ||||||
| 	"io/fs" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| //go:embed *.ico |  | ||||||
| var icons embed.FS |  | ||||||
|  |  | ||||||
| func ListIcons() ([]string, error) { |  | ||||||
| 	return fs.Glob(icons, "*") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func GetIcon(filename string) ([]byte, error) { |  | ||||||
| 	return icons.ReadFile(filename) |  | ||||||
| } |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								app/assets/ollama_icon_16x16Template.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 403 B | 
| Before Width: | Height: | Size: 741 B After Width: | Height: | Size: 741 B | 
							
								
								
									
										
											BIN
										
									
								
								app/assets/ollama_outline_icon_16x16Template.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 445 B | 
| Before Width: | Height: | Size: 891 B After Width: | Height: | Size: 891 B | 
| Before Width: | Height: | Size: 76 KiB | 
| Before Width: | Height: | Size: 89 KiB | 
| Before Width: | Height: | Size: 91 KiB | 
| @@ -18,15 +18,12 @@ const config: ForgeConfig = { | |||||||
|     asar: true, |     asar: true, | ||||||
|     icon: './assets/icon.icns', |     icon: './assets/icon.icns', | ||||||
|     extraResource: [ |     extraResource: [ | ||||||
|       '../dist/ollama', |       '../ollama', | ||||||
|       path.join(__dirname, './assets/iconTemplate.png'), |       path.join(__dirname, './assets/ollama_icon_16x16Template.png'), | ||||||
|       path.join(__dirname, './assets/iconTemplate@2x.png'), |       path.join(__dirname, './assets/ollama_icon_16x16Template@2x.png'), | ||||||
|       path.join(__dirname, './assets/iconUpdateTemplate.png'), |       path.join(__dirname, './assets/ollama_outline_icon_16x16Template.png'), | ||||||
|       path.join(__dirname, './assets/iconUpdateTemplate@2x.png'), |       path.join(__dirname, './assets/ollama_outline_icon_16x16Template@2x.png'), | ||||||
|       path.join(__dirname, './assets/iconDarkTemplate.png'), |       ...(process.platform === 'darwin' ? ['../llama/ggml-metal.metal'] : []), | ||||||
|       path.join(__dirname, './assets/iconDarkTemplate@2x.png'), |  | ||||||
|       path.join(__dirname, './assets/iconDarkUpdateTemplate.png'), |  | ||||||
|       path.join(__dirname, './assets/iconDarkUpdateTemplate@2x.png'), |  | ||||||
|     ], |     ], | ||||||
|     ...(process.env.SIGN |     ...(process.env.SIGN | ||||||
|       ? { |       ? { | ||||||
| @@ -41,12 +38,19 @@ const config: ForgeConfig = { | |||||||
|           }, |           }, | ||||||
|         } |         } | ||||||
|       : {}), |       : {}), | ||||||
|     osxUniversal: { |  | ||||||
|       x64ArchFiles: '**/ollama', |  | ||||||
|     }, |  | ||||||
|   }, |   }, | ||||||
|   rebuildConfig: {}, |   rebuildConfig: {}, | ||||||
|   makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin'])], |   makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin'])], | ||||||
|  |   publishers: [ | ||||||
|  |     new PublisherGithub({ | ||||||
|  |       repository: { | ||||||
|  |         name: 'ollama', | ||||||
|  |         owner: 'jmorganca', | ||||||
|  |       }, | ||||||
|  |       draft: false, | ||||||
|  |       prerelease: true, | ||||||
|  |     }), | ||||||
|  |   ], | ||||||
|   hooks: { |   hooks: { | ||||||
|     readPackageJson: async (_, packageJson) => { |     readPackageJson: async (_, packageJson) => { | ||||||
|       return { ...packageJson, version: process.env.VERSION || packageJson.version } |       return { ...packageJson, version: process.env.VERSION || packageJson.version } | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| //go:build !windows |  | ||||||
|  |  | ||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import "fmt" |  | ||||||
|  |  | ||||||
| func GetStarted() error { |  | ||||||
| 	return fmt.Errorf("GetStarted not implemented") |  | ||||||
| } |  | ||||||
| @@ -1,44 +0,0 @@ | |||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os" |  | ||||||
| 	"os/exec" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"syscall" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func GetStarted() error { |  | ||||||
| 	const CREATE_NEW_CONSOLE = 0x00000010 |  | ||||||
| 	var err error |  | ||||||
| 	bannerScript := filepath.Join(AppDir, "ollama_welcome.ps1") |  | ||||||
| 	args := []string{ |  | ||||||
| 		// TODO once we're signed, the execution policy bypass should be removed |  | ||||||
| 		"powershell", "-noexit", "-ExecutionPolicy", "Bypass", "-nologo", "-file", bannerScript, |  | ||||||
| 	} |  | ||||||
| 	args[0], err = exec.LookPath(args[0]) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Make sure the script actually exists |  | ||||||
| 	_, err = os.Stat(bannerScript) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("getting started banner script error %s", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	slog.Info(fmt.Sprintf("opening getting started terminal with %v", args)) |  | ||||||
| 	attrs := &os.ProcAttr{ |  | ||||||
| 		Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, |  | ||||||
| 		Sys:   &syscall.SysProcAttr{CreationFlags: CREATE_NEW_CONSOLE, HideWindow: false}, |  | ||||||
| 	} |  | ||||||
| 	proc, err := os.StartProcess(args[0], args, attrs) |  | ||||||
|  |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("unable to start getting started shell %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	slog.Debug(fmt.Sprintf("getting started terminal PID: %d", proc.Pid)) |  | ||||||
| 	return proc.Release() |  | ||||||
| } |  | ||||||
| @@ -1,92 +0,0 @@ | |||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"log" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os" |  | ||||||
| 	"os/signal" |  | ||||||
| 	"syscall" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/app/store" |  | ||||||
| 	"github.com/ollama/ollama/app/tray" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func Run() { |  | ||||||
| 	InitLogging() |  | ||||||
|  |  | ||||||
| 	ctx, cancel := context.WithCancel(context.Background()) |  | ||||||
| 	var done chan int |  | ||||||
|  |  | ||||||
| 	t, err := tray.NewTray() |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Fatalf("Failed to start: %s", err) |  | ||||||
| 	} |  | ||||||
| 	callbacks := t.GetCallbacks() |  | ||||||
|  |  | ||||||
| 	signals := make(chan os.Signal, 1) |  | ||||||
| 	signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) |  | ||||||
|  |  | ||||||
| 	go func() { |  | ||||||
| 		slog.Debug("starting callback loop") |  | ||||||
| 		for { |  | ||||||
| 			select { |  | ||||||
| 			case <-callbacks.Quit: |  | ||||||
| 				slog.Debug("quit called") |  | ||||||
| 				t.Quit() |  | ||||||
| 			case <-signals: |  | ||||||
| 				slog.Debug("shutting down due to signal") |  | ||||||
| 				t.Quit() |  | ||||||
| 			case <-callbacks.Update: |  | ||||||
| 				err := DoUpgrade(cancel, done) |  | ||||||
| 				if err != nil { |  | ||||||
| 					slog.Warn(fmt.Sprintf("upgrade attempt failed: %s", err)) |  | ||||||
| 				} |  | ||||||
| 			case <-callbacks.ShowLogs: |  | ||||||
| 				ShowLogs() |  | ||||||
| 			case <-callbacks.DoFirstUse: |  | ||||||
| 				err := GetStarted() |  | ||||||
| 				if err != nil { |  | ||||||
| 					slog.Warn(fmt.Sprintf("Failed to launch getting started shell: %s", err)) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	// Are we first use? |  | ||||||
| 	if !store.GetFirstTimeRun() { |  | ||||||
| 		slog.Debug("First time run") |  | ||||||
| 		err = t.DisplayFirstUseNotification() |  | ||||||
| 		if err != nil { |  | ||||||
| 			slog.Debug(fmt.Sprintf("XXX failed to display first use notification %v", err)) |  | ||||||
| 		} |  | ||||||
| 		store.SetFirstTimeRun(true) |  | ||||||
| 	} else { |  | ||||||
| 		slog.Debug("Not first time, skipping first run notification") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if IsServerRunning(ctx) { |  | ||||||
| 		slog.Info("Detected another instance of ollama running, exiting") |  | ||||||
| 		os.Exit(1) |  | ||||||
| 	} else { |  | ||||||
| 		done, err = SpawnServer(ctx, CLIName) |  | ||||||
| 		if err != nil { |  | ||||||
| 			// TODO - should we retry in a backoff loop? |  | ||||||
| 			// TODO - should we pop up a warning and maybe add a menu item to view application logs? |  | ||||||
| 			slog.Error(fmt.Sprintf("Failed to spawn ollama server %s", err)) |  | ||||||
| 			done = make(chan int, 1) |  | ||||||
| 			done <- 1 |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	StartBackgroundUpdaterChecker(ctx, t.UpdateAvailable) |  | ||||||
|  |  | ||||||
| 	t.Run() |  | ||||||
| 	cancel() |  | ||||||
| 	slog.Info("Waiting for ollama server to shutdown...") |  | ||||||
| 	if done != nil { |  | ||||||
| 		<-done |  | ||||||
| 	} |  | ||||||
| 	slog.Info("Ollama app exiting") |  | ||||||
| } |  | ||||||
| @@ -1,48 +0,0 @@ | |||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/envconfig" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func InitLogging() { |  | ||||||
| 	level := slog.LevelInfo |  | ||||||
|  |  | ||||||
| 	if envconfig.Debug { |  | ||||||
| 		level = slog.LevelDebug |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var logFile *os.File |  | ||||||
| 	var err error |  | ||||||
| 	// Detect if we're a GUI app on windows, and if not, send logs to console |  | ||||||
| 	if os.Stderr.Fd() != 0 { |  | ||||||
| 		// Console app detected |  | ||||||
| 		logFile = os.Stderr |  | ||||||
| 		// TODO - write one-line to the app.log file saying we're running in console mode to help avoid confusion |  | ||||||
| 	} else { |  | ||||||
| 		logFile, err = os.OpenFile(AppLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755) |  | ||||||
| 		if err != nil { |  | ||||||
| 			slog.Error(fmt.Sprintf("failed to create server log %v", err)) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	handler := slog.NewTextHandler(logFile, &slog.HandlerOptions{ |  | ||||||
| 		Level:     level, |  | ||||||
| 		AddSource: true, |  | ||||||
| 		ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr { |  | ||||||
| 			if attr.Key == slog.SourceKey { |  | ||||||
| 				source := attr.Value.Any().(*slog.Source) |  | ||||||
| 				source.File = filepath.Base(source.File) |  | ||||||
| 			} |  | ||||||
| 			return attr |  | ||||||
| 		}, |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	slog.SetDefault(slog.New(handler)) |  | ||||||
|  |  | ||||||
| 	slog.Info("ollama app started") |  | ||||||
| } |  | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| //go:build !windows |  | ||||||
|  |  | ||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import "log/slog" |  | ||||||
|  |  | ||||||
| func ShowLogs() { |  | ||||||
| 	slog.Warn("ShowLogs not yet implemented") |  | ||||||
| } |  | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os/exec" |  | ||||||
| 	"syscall" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func ShowLogs() { |  | ||||||
| 	cmd_path := "c:\\Windows\\system32\\cmd.exe" |  | ||||||
| 	slog.Debug(fmt.Sprintf("viewing logs with start %s", AppDataDir)) |  | ||||||
| 	cmd := exec.Command(cmd_path, "/c", "start", AppDataDir) |  | ||||||
| 	cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false, CreationFlags: 0x08000000} |  | ||||||
| 	err := cmd.Start() |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Error(fmt.Sprintf("Failed to open log dir: %s", err)) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,79 +0,0 @@ | |||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"runtime" |  | ||||||
| 	"strings" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	AppName    = "ollama app" |  | ||||||
| 	CLIName    = "ollama" |  | ||||||
| 	AppDir     = "/opt/Ollama" |  | ||||||
| 	AppDataDir = "/opt/Ollama" |  | ||||||
| 	// TODO - should there be a distinct log dir? |  | ||||||
| 	UpdateStageDir = "/tmp" |  | ||||||
| 	AppLogFile     = "/tmp/ollama_app.log" |  | ||||||
| 	ServerLogFile  = "/tmp/ollama.log" |  | ||||||
| 	UpgradeLogFile = "/tmp/ollama_update.log" |  | ||||||
| 	Installer      = "OllamaSetup.exe" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func init() { |  | ||||||
| 	if runtime.GOOS == "windows" { |  | ||||||
| 		AppName += ".exe" |  | ||||||
| 		CLIName += ".exe" |  | ||||||
| 		// Logs, configs, downloads go to LOCALAPPDATA |  | ||||||
| 		localAppData := os.Getenv("LOCALAPPDATA") |  | ||||||
| 		AppDataDir = filepath.Join(localAppData, "Ollama") |  | ||||||
| 		UpdateStageDir = filepath.Join(AppDataDir, "updates") |  | ||||||
| 		AppLogFile = filepath.Join(AppDataDir, "app.log") |  | ||||||
| 		ServerLogFile = filepath.Join(AppDataDir, "server.log") |  | ||||||
| 		UpgradeLogFile = filepath.Join(AppDataDir, "upgrade.log") |  | ||||||
|  |  | ||||||
| 		// Executables are stored in APPDATA |  | ||||||
| 		AppDir = filepath.Join(localAppData, "Programs", "Ollama") |  | ||||||
|  |  | ||||||
| 		// Make sure we have PATH set correctly for any spawned children |  | ||||||
| 		paths := strings.Split(os.Getenv("PATH"), ";") |  | ||||||
| 		// Start with whatever we find in the PATH/LD_LIBRARY_PATH |  | ||||||
| 		found := false |  | ||||||
| 		for _, path := range paths { |  | ||||||
| 			d, err := filepath.Abs(path) |  | ||||||
| 			if err != nil { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			if strings.EqualFold(AppDir, d) { |  | ||||||
| 				found = true |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		if !found { |  | ||||||
| 			paths = append(paths, AppDir) |  | ||||||
|  |  | ||||||
| 			pathVal := strings.Join(paths, ";") |  | ||||||
| 			slog.Debug("setting PATH=" + pathVal) |  | ||||||
| 			err := os.Setenv("PATH", pathVal) |  | ||||||
| 			if err != nil { |  | ||||||
| 				slog.Error(fmt.Sprintf("failed to update PATH: %s", err)) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Make sure our logging dir exists |  | ||||||
| 		_, err := os.Stat(AppDataDir) |  | ||||||
| 		if errors.Is(err, os.ErrNotExist) { |  | ||||||
| 			if err := os.MkdirAll(AppDataDir, 0o755); err != nil { |  | ||||||
| 				slog.Error(fmt.Sprintf("create ollama dir %s: %v", AppDataDir, err)) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	} else if runtime.GOOS == "darwin" { |  | ||||||
| 		// TODO |  | ||||||
| 		AppName += ".app" |  | ||||||
| 		// } else if runtime.GOOS == "linux" { |  | ||||||
| 		// TODO |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,181 +0,0 @@ | |||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os" |  | ||||||
| 	"os/exec" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/api" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func getCLIFullPath(command string) string { |  | ||||||
| 	cmdPath := "" |  | ||||||
| 	appExe, err := os.Executable() |  | ||||||
| 	if err == nil { |  | ||||||
| 		cmdPath = filepath.Join(filepath.Dir(appExe), command) |  | ||||||
| 		_, err := os.Stat(cmdPath) |  | ||||||
| 		if err == nil { |  | ||||||
| 			return cmdPath |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	cmdPath, err = exec.LookPath(command) |  | ||||||
| 	if err == nil { |  | ||||||
| 		_, err := os.Stat(cmdPath) |  | ||||||
| 		if err == nil { |  | ||||||
| 			return cmdPath |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	pwd, err := os.Getwd() |  | ||||||
| 	if err == nil { |  | ||||||
| 		cmdPath = filepath.Join(pwd, command) |  | ||||||
| 		_, err = os.Stat(cmdPath) |  | ||||||
| 		if err == nil { |  | ||||||
| 			return cmdPath |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return command |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func start(ctx context.Context, command string) (*exec.Cmd, error) { |  | ||||||
| 	cmd := getCmd(ctx, getCLIFullPath(command)) |  | ||||||
| 	stdout, err := cmd.StdoutPipe() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to spawn server stdout pipe: %w", err) |  | ||||||
| 	} |  | ||||||
| 	stderr, err := cmd.StderrPipe() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to spawn server stderr pipe: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// TODO - rotation |  | ||||||
| 	logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0755) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to create server log: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	logDir := filepath.Dir(ServerLogFile) |  | ||||||
| 	_, err = os.Stat(logDir) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if !errors.Is(err, os.ErrNotExist) { |  | ||||||
| 			return nil, fmt.Errorf("stat ollama server log dir %s: %v", logDir, err) |  | ||||||
|  |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if err := os.MkdirAll(logDir, 0o755); err != nil { |  | ||||||
| 			return nil, fmt.Errorf("create ollama server log dir %s: %v", logDir, err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	go func() { |  | ||||||
| 		defer logFile.Close() |  | ||||||
| 		io.Copy(logFile, stdout) //nolint:errcheck |  | ||||||
| 	}() |  | ||||||
| 	go func() { |  | ||||||
| 		defer logFile.Close() |  | ||||||
| 		io.Copy(logFile, stderr) //nolint:errcheck |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	// Re-wire context done behavior to attempt a graceful shutdown of the server |  | ||||||
| 	cmd.Cancel = func() error { |  | ||||||
| 		if cmd.Process != nil { |  | ||||||
| 			err := terminate(cmd) |  | ||||||
| 			if err != nil { |  | ||||||
| 				slog.Warn("error trying to gracefully terminate server", "err", err) |  | ||||||
| 				return cmd.Process.Kill() |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			tick := time.NewTicker(10 * time.Millisecond) |  | ||||||
| 			defer tick.Stop() |  | ||||||
|  |  | ||||||
| 			for { |  | ||||||
| 				select { |  | ||||||
| 				case <-tick.C: |  | ||||||
| 					exited, err := isProcessExited(cmd.Process.Pid) |  | ||||||
| 					if err != nil { |  | ||||||
| 						return err |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					if exited { |  | ||||||
| 						return nil |  | ||||||
| 					} |  | ||||||
| 				case <-time.After(5 * time.Second): |  | ||||||
| 					slog.Warn("graceful server shutdown timeout, killing", "pid", cmd.Process.Pid) |  | ||||||
| 					return cmd.Process.Kill() |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// run the command and wait for it to finish |  | ||||||
| 	if err := cmd.Start(); err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to start server %w", err) |  | ||||||
| 	} |  | ||||||
| 	if cmd.Process != nil { |  | ||||||
| 		slog.Info(fmt.Sprintf("started ollama server with pid %d", cmd.Process.Pid)) |  | ||||||
| 	} |  | ||||||
| 	slog.Info(fmt.Sprintf("ollama server logs %s", ServerLogFile)) |  | ||||||
|  |  | ||||||
| 	return cmd, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func SpawnServer(ctx context.Context, command string) (chan int, error) { |  | ||||||
| 	done := make(chan int) |  | ||||||
|  |  | ||||||
| 	go func() { |  | ||||||
| 		// Keep the server running unless we're shuttind down the app |  | ||||||
| 		crashCount := 0 |  | ||||||
| 		for { |  | ||||||
| 			slog.Info("starting server...") |  | ||||||
| 			cmd, err := start(ctx, command) |  | ||||||
| 			if err != nil { |  | ||||||
| 				crashCount++ |  | ||||||
| 				slog.Error(fmt.Sprintf("failed to start server %s", err)) |  | ||||||
| 				time.Sleep(500 * time.Millisecond * time.Duration(crashCount)) |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			cmd.Wait() //nolint:errcheck |  | ||||||
| 			var code int |  | ||||||
| 			if cmd.ProcessState != nil { |  | ||||||
| 				code = cmd.ProcessState.ExitCode() |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			select { |  | ||||||
| 			case <-ctx.Done(): |  | ||||||
| 				slog.Info(fmt.Sprintf("server shutdown with exit code %d", code)) |  | ||||||
| 				done <- code |  | ||||||
| 				return |  | ||||||
| 			default: |  | ||||||
| 				crashCount++ |  | ||||||
| 				slog.Warn(fmt.Sprintf("server crash %d - exit code %d - respawning", crashCount, code)) |  | ||||||
| 				time.Sleep(500 * time.Millisecond * time.Duration(crashCount)) |  | ||||||
| 				break |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
|  |  | ||||||
| 	return done, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func IsServerRunning(ctx context.Context) bool { |  | ||||||
| 	client, err := api.ClientFromEnvironment() |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Info("unable to connect to server") |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	err = client.Heartbeat(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Debug(fmt.Sprintf("heartbeat from server: %s", err)) |  | ||||||
| 		slog.Info("unable to connect to server") |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	return true |  | ||||||
| } |  | ||||||
| @@ -1,38 +0,0 @@ | |||||||
| //go:build !windows |  | ||||||
|  |  | ||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"os/exec" |  | ||||||
| 	"syscall" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func getCmd(ctx context.Context, cmd string) *exec.Cmd { |  | ||||||
| 	return exec.CommandContext(ctx, cmd, "serve") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func terminate(cmd *exec.Cmd) error { |  | ||||||
| 	return cmd.Process.Signal(os.Interrupt) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func isProcessExited(pid int) (bool, error) { |  | ||||||
| 	proc, err := os.FindProcess(pid) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, fmt.Errorf("failed to find process: %v", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	err = proc.Signal(syscall.Signal(0)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) { |  | ||||||
| 			return true, nil |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return false, fmt.Errorf("error signaling process: %v", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return false, nil |  | ||||||
| } |  | ||||||
| @@ -1,89 +0,0 @@ | |||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os/exec" |  | ||||||
| 	"syscall" |  | ||||||
|  |  | ||||||
| 	"golang.org/x/sys/windows" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func getCmd(ctx context.Context, exePath string) *exec.Cmd { |  | ||||||
| 	cmd := exec.CommandContext(ctx, exePath, "serve") |  | ||||||
| 	cmd.SysProcAttr = &syscall.SysProcAttr{ |  | ||||||
| 		HideWindow:    true, |  | ||||||
| 		CreationFlags: windows.CREATE_NEW_PROCESS_GROUP, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return cmd |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func terminate(cmd *exec.Cmd) error { |  | ||||||
| 	dll, err := windows.LoadDLL("kernel32.dll") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	defer dll.Release() // nolint: errcheck |  | ||||||
|  |  | ||||||
| 	pid := cmd.Process.Pid |  | ||||||
|  |  | ||||||
| 	f, err := dll.FindProc("AttachConsole") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	r1, _, err := f.Call(uintptr(pid)) |  | ||||||
| 	if r1 == 0 && err != syscall.ERROR_ACCESS_DENIED { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	f, err = dll.FindProc("SetConsoleCtrlHandler") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	r1, _, err = f.Call(0, 1) |  | ||||||
| 	if r1 == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	f, err = dll.FindProc("GenerateConsoleCtrlEvent") |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	r1, _, err = f.Call(windows.CTRL_BREAK_EVENT, uintptr(pid)) |  | ||||||
| 	if r1 == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	r1, _, err = f.Call(windows.CTRL_C_EVENT, uintptr(pid)) |  | ||||||
| 	if r1 == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| const STILL_ACTIVE = 259 |  | ||||||
|  |  | ||||||
| func isProcessExited(pid int) (bool, error) { |  | ||||||
| 	hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, fmt.Errorf("failed to open process: %v", err) |  | ||||||
| 	} |  | ||||||
| 	defer windows.CloseHandle(hProcess) // nolint: errcheck |  | ||||||
|  |  | ||||||
| 	var exitCode uint32 |  | ||||||
| 	err = windows.GetExitCodeProcess(hProcess, &exitCode) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, fmt.Errorf("failed to get exit code: %v", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if exitCode == STILL_ACTIVE { |  | ||||||
| 		return false, nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return true, nil |  | ||||||
| } |  | ||||||
| @@ -1,228 +0,0 @@ | |||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"crypto/rand" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"mime" |  | ||||||
| 	"net/http" |  | ||||||
| 	"net/url" |  | ||||||
| 	"os" |  | ||||||
| 	"path" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"runtime" |  | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/auth" |  | ||||||
| 	"github.com/ollama/ollama/version" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	UpdateCheckURLBase  = "https://ollama.com/api/update" |  | ||||||
| 	UpdateDownloaded    = false |  | ||||||
| 	UpdateCheckInterval = 60 * 60 * time.Second |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // TODO - maybe move up to the API package? |  | ||||||
| type UpdateResponse struct { |  | ||||||
| 	UpdateURL     string `json:"url"` |  | ||||||
| 	UpdateVersion string `json:"version"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func IsNewReleaseAvailable(ctx context.Context) (bool, UpdateResponse) { |  | ||||||
| 	var updateResp UpdateResponse |  | ||||||
|  |  | ||||||
| 	requestURL, err := url.Parse(UpdateCheckURLBase) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, updateResp |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	query := requestURL.Query() |  | ||||||
| 	query.Add("os", runtime.GOOS) |  | ||||||
| 	query.Add("arch", runtime.GOARCH) |  | ||||||
| 	query.Add("version", version.Version) |  | ||||||
| 	query.Add("ts", fmt.Sprintf("%d", time.Now().Unix())) |  | ||||||
|  |  | ||||||
| 	nonce, err := auth.NewNonce(rand.Reader, 16) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, updateResp |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	query.Add("nonce", nonce) |  | ||||||
| 	requestURL.RawQuery = query.Encode() |  | ||||||
|  |  | ||||||
| 	data := []byte(fmt.Sprintf("%s,%s", http.MethodGet, requestURL.RequestURI())) |  | ||||||
| 	signature, err := auth.Sign(ctx, data) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, updateResp |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil) |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Warn(fmt.Sprintf("failed to check for update: %s", err)) |  | ||||||
| 		return false, updateResp |  | ||||||
| 	} |  | ||||||
| 	req.Header.Set("Authorization", signature) |  | ||||||
| 	req.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version())) |  | ||||||
|  |  | ||||||
| 	slog.Debug("checking for available update", "requestURL", requestURL) |  | ||||||
| 	resp, err := http.DefaultClient.Do(req) |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Warn(fmt.Sprintf("failed to check for update: %s", err)) |  | ||||||
| 		return false, updateResp |  | ||||||
| 	} |  | ||||||
| 	defer resp.Body.Close() |  | ||||||
|  |  | ||||||
| 	if resp.StatusCode == 204 { |  | ||||||
| 		slog.Debug("check update response 204 (current version is up to date)") |  | ||||||
| 		return false, updateResp |  | ||||||
| 	} |  | ||||||
| 	body, err := io.ReadAll(resp.Body) |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Warn(fmt.Sprintf("failed to read body response: %s", err)) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if resp.StatusCode != 200 { |  | ||||||
| 		slog.Info(fmt.Sprintf("check update error %d - %.96s", resp.StatusCode, string(body))) |  | ||||||
| 		return false, updateResp |  | ||||||
| 	} |  | ||||||
| 	err = json.Unmarshal(body, &updateResp) |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Warn(fmt.Sprintf("malformed response checking for update: %s", err)) |  | ||||||
| 		return false, updateResp |  | ||||||
| 	} |  | ||||||
| 	// Extract the version string from the URL in the github release artifact path |  | ||||||
| 	updateResp.UpdateVersion = path.Base(path.Dir(updateResp.UpdateURL)) |  | ||||||
|  |  | ||||||
| 	slog.Info("New update available at " + updateResp.UpdateURL) |  | ||||||
| 	return true, updateResp |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func DownloadNewRelease(ctx context.Context, updateResp UpdateResponse) error { |  | ||||||
| 	// Do a head first to check etag info |  | ||||||
| 	req, err := http.NewRequestWithContext(ctx, http.MethodHead, updateResp.UpdateURL, nil) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	resp, err := http.DefaultClient.Do(req) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("error checking update: %w", err) |  | ||||||
| 	} |  | ||||||
| 	if resp.StatusCode != 200 { |  | ||||||
| 		return fmt.Errorf("unexpected status attempting to download update %d", resp.StatusCode) |  | ||||||
| 	} |  | ||||||
| 	resp.Body.Close() |  | ||||||
| 	etag := strings.Trim(resp.Header.Get("etag"), "\"") |  | ||||||
| 	if etag == "" { |  | ||||||
| 		slog.Debug("no etag detected, falling back to filename based dedup") |  | ||||||
| 		etag = "_" |  | ||||||
| 	} |  | ||||||
| 	filename := Installer |  | ||||||
| 	_, params, err := mime.ParseMediaType(resp.Header.Get("content-disposition")) |  | ||||||
| 	if err == nil { |  | ||||||
| 		filename = params["filename"] |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	stageFilename := filepath.Join(UpdateStageDir, etag, filename) |  | ||||||
|  |  | ||||||
| 	// Check to see if we already have it downloaded |  | ||||||
| 	_, err = os.Stat(stageFilename) |  | ||||||
| 	if err == nil { |  | ||||||
| 		slog.Info("update already downloaded") |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	cleanupOldDownloads() |  | ||||||
|  |  | ||||||
| 	req.Method = http.MethodGet |  | ||||||
| 	resp, err = http.DefaultClient.Do(req) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("error checking update: %w", err) |  | ||||||
| 	} |  | ||||||
| 	defer resp.Body.Close() |  | ||||||
| 	etag = strings.Trim(resp.Header.Get("etag"), "\"") |  | ||||||
| 	if etag == "" { |  | ||||||
| 		slog.Debug("no etag detected, falling back to filename based dedup") // TODO probably can get rid of this redundant log |  | ||||||
| 		etag = "_" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	stageFilename = filepath.Join(UpdateStageDir, etag, filename) |  | ||||||
|  |  | ||||||
| 	_, err = os.Stat(filepath.Dir(stageFilename)) |  | ||||||
| 	if errors.Is(err, os.ErrNotExist) { |  | ||||||
| 		if err := os.MkdirAll(filepath.Dir(stageFilename), 0o755); err != nil { |  | ||||||
| 			return fmt.Errorf("create ollama dir %s: %v", filepath.Dir(stageFilename), err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	payload, err := io.ReadAll(resp.Body) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to read body response: %w", err) |  | ||||||
| 	} |  | ||||||
| 	fp, err := os.OpenFile(stageFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("write payload %s: %w", stageFilename, err) |  | ||||||
| 	} |  | ||||||
| 	defer fp.Close() |  | ||||||
| 	if n, err := fp.Write(payload); err != nil || n != len(payload) { |  | ||||||
| 		return fmt.Errorf("write payload %s: %d vs %d -- %w", stageFilename, n, len(payload), err) |  | ||||||
| 	} |  | ||||||
| 	slog.Info("new update downloaded " + stageFilename) |  | ||||||
|  |  | ||||||
| 	UpdateDownloaded = true |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func cleanupOldDownloads() { |  | ||||||
| 	files, err := os.ReadDir(UpdateStageDir) |  | ||||||
| 	if err != nil && errors.Is(err, os.ErrNotExist) { |  | ||||||
| 		// Expected behavior on first run |  | ||||||
| 		return |  | ||||||
| 	} else if err != nil { |  | ||||||
| 		slog.Warn(fmt.Sprintf("failed to list stage dir: %s", err)) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	for _, file := range files { |  | ||||||
| 		fullname := filepath.Join(UpdateStageDir, file.Name()) |  | ||||||
| 		slog.Debug("cleaning up old download: " + fullname) |  | ||||||
| 		err = os.RemoveAll(fullname) |  | ||||||
| 		if err != nil { |  | ||||||
| 			slog.Warn(fmt.Sprintf("failed to cleanup stale update download %s", err)) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func StartBackgroundUpdaterChecker(ctx context.Context, cb func(string) error) { |  | ||||||
| 	go func() { |  | ||||||
| 		// Don't blast an update message immediately after startup |  | ||||||
| 		// time.Sleep(30 * time.Second) |  | ||||||
| 		time.Sleep(3 * time.Second) |  | ||||||
|  |  | ||||||
| 		for { |  | ||||||
| 			available, resp := IsNewReleaseAvailable(ctx) |  | ||||||
| 			if available { |  | ||||||
| 				err := DownloadNewRelease(ctx, resp) |  | ||||||
| 				if err != nil { |  | ||||||
| 					slog.Error(fmt.Sprintf("failed to download new release: %s", err)) |  | ||||||
| 				} |  | ||||||
| 				err = cb(resp.UpdateVersion) |  | ||||||
| 				if err != nil { |  | ||||||
| 					slog.Warn(fmt.Sprintf("failed to register update available with tray: %s", err)) |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			select { |  | ||||||
| 			case <-ctx.Done(): |  | ||||||
| 				slog.Debug("stopping background update checker") |  | ||||||
| 				return |  | ||||||
| 			default: |  | ||||||
| 				time.Sleep(UpdateCheckInterval) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}() |  | ||||||
| } |  | ||||||
| @@ -1,12 +0,0 @@ | |||||||
| //go:build !windows |  | ||||||
|  |  | ||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func DoUpgrade(cancel context.CancelFunc, done chan int) error { |  | ||||||
| 	return fmt.Errorf("DoUpgrade not yet implemented") |  | ||||||
| } |  | ||||||
| @@ -1,77 +0,0 @@ | |||||||
| package lifecycle |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os" |  | ||||||
| 	"os/exec" |  | ||||||
| 	"path/filepath" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func DoUpgrade(cancel context.CancelFunc, done chan int) error { |  | ||||||
| 	files, err := filepath.Glob(filepath.Join(UpdateStageDir, "*", "*.exe")) // TODO generalize for multiplatform |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("failed to lookup downloads: %s", err) |  | ||||||
| 	} |  | ||||||
| 	if len(files) == 0 { |  | ||||||
| 		return fmt.Errorf("no update downloads found") |  | ||||||
| 	} else if len(files) > 1 { |  | ||||||
| 		// Shouldn't happen |  | ||||||
| 		slog.Warn(fmt.Sprintf("multiple downloads found, using first one %v", files)) |  | ||||||
| 	} |  | ||||||
| 	installerExe := files[0] |  | ||||||
|  |  | ||||||
| 	slog.Info("starting upgrade with " + installerExe) |  | ||||||
| 	slog.Info("upgrade log file " + UpgradeLogFile) |  | ||||||
|  |  | ||||||
| 	// When running in debug mode, we'll be "verbose" and let the installer pop up and prompt |  | ||||||
| 	installArgs := []string{ |  | ||||||
| 		"/CLOSEAPPLICATIONS",                    // Quit the tray app if it's still running |  | ||||||
| 		"/LOG=" + filepath.Base(UpgradeLogFile), // Only relative seems reliable, so set pwd |  | ||||||
| 		"/FORCECLOSEAPPLICATIONS",               // Force close the tray app - might be needed |  | ||||||
| 	} |  | ||||||
| 	// make the upgrade as quiet as possible (no GUI, no prompts) |  | ||||||
| 	installArgs = append(installArgs, |  | ||||||
| 		"/SP", // Skip the "This will install... Do you wish to continue" prompt |  | ||||||
| 		"/SUPPRESSMSGBOXES", |  | ||||||
| 		"/SILENT", |  | ||||||
| 		"/VERYSILENT", |  | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	// Safeguard in case we have requests in flight that need to drain... |  | ||||||
| 	slog.Info("Waiting for server to shutdown") |  | ||||||
| 	cancel() |  | ||||||
| 	if done != nil { |  | ||||||
| 		<-done |  | ||||||
| 	} else { |  | ||||||
| 		// Shouldn't happen |  | ||||||
| 		slog.Warn("done chan was nil, not actually waiting") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	slog.Debug(fmt.Sprintf("starting installer: %s %v", installerExe, installArgs)) |  | ||||||
| 	os.Chdir(filepath.Dir(UpgradeLogFile)) //nolint:errcheck |  | ||||||
| 	cmd := exec.Command(installerExe, installArgs...) |  | ||||||
|  |  | ||||||
| 	if err := cmd.Start(); err != nil { |  | ||||||
| 		return fmt.Errorf("unable to start ollama app %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if cmd.Process != nil { |  | ||||||
| 		err = cmd.Process.Release() |  | ||||||
| 		if err != nil { |  | ||||||
| 			slog.Error(fmt.Sprintf("failed to release server process: %s", err)) |  | ||||||
| 		} |  | ||||||
| 	} else { |  | ||||||
| 		// TODO - some details about why it didn't start, or is this a pedantic error case? |  | ||||||
| 		return fmt.Errorf("installer process did not start") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// TODO should we linger for a moment and check to make sure it's actually running by checking the pid? |  | ||||||
|  |  | ||||||
| 	slog.Info("Installer started in background, exiting") |  | ||||||
|  |  | ||||||
| 	os.Exit(0) |  | ||||||
| 	// Not reached |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
							
								
								
									
										12
									
								
								app/main.go
									
									
									
									
									
								
							
							
						
						| @@ -1,12 +0,0 @@ | |||||||
| package main |  | ||||||
|  |  | ||||||
| // Compile with the following to get rid of the cmd pop up on windows |  | ||||||
| // go build -ldflags="-H windowsgui" . |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"github.com/ollama/ollama/app/lifecycle" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func main() { |  | ||||||
| 	lifecycle.Run() |  | ||||||
| } |  | ||||||
							
								
								
									
										156
									
								
								app/ollama.iss
									
									
									
									
									
								
							
							
						
						| @@ -1,156 +0,0 @@ | |||||||
| ; Inno Setup Installer for Ollama |  | ||||||
| ; |  | ||||||
| ; To build the installer use the build script invoked from the top of the source tree |  | ||||||
| ;  |  | ||||||
| ; powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps |  | ||||||
|  |  | ||||||
|  |  | ||||||
| #define MyAppName "Ollama" |  | ||||||
| #if GetEnv("PKG_VERSION") != "" |  | ||||||
|   #define MyAppVersion GetEnv("PKG_VERSION") |  | ||||||
| #else |  | ||||||
|   #define MyAppVersion "0.0.0" |  | ||||||
| #endif |  | ||||||
| #define MyAppPublisher "Ollama" |  | ||||||
| #define MyAppURL "https://ollama.com/" |  | ||||||
| #define MyAppExeName "ollama app.exe" |  | ||||||
| #define MyIcon ".\assets\app.ico" |  | ||||||
|  |  | ||||||
| [Setup] |  | ||||||
| ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. |  | ||||||
| ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) |  | ||||||
| AppId={{44E83376-CE68-45EB-8FC1-393500EB558C} |  | ||||||
| AppName={#MyAppName} |  | ||||||
| AppVersion={#MyAppVersion} |  | ||||||
| VersionInfoVersion={#MyAppVersion} |  | ||||||
| ;AppVerName={#MyAppName} {#MyAppVersion} |  | ||||||
| AppPublisher={#MyAppPublisher} |  | ||||||
| AppPublisherURL={#MyAppURL} |  | ||||||
| AppSupportURL={#MyAppURL} |  | ||||||
| AppUpdatesURL={#MyAppURL} |  | ||||||
| ArchitecturesAllowed=x64 arm64 |  | ||||||
| ArchitecturesInstallIn64BitMode=x64 arm64 |  | ||||||
| DefaultDirName={localappdata}\Programs\{#MyAppName} |  | ||||||
| DefaultGroupName={#MyAppName} |  | ||||||
| DisableProgramGroupPage=yes |  | ||||||
| PrivilegesRequired=lowest |  | ||||||
| OutputBaseFilename="OllamaSetup" |  | ||||||
| SetupIconFile={#MyIcon} |  | ||||||
| UninstallDisplayIcon={uninstallexe} |  | ||||||
| Compression=lzma2 |  | ||||||
| SolidCompression=no |  | ||||||
| WizardStyle=modern |  | ||||||
| ChangesEnvironment=yes |  | ||||||
| OutputDir=..\dist\ |  | ||||||
|  |  | ||||||
| ; Disable logging once everything's battle tested |  | ||||||
| ; Filename will be %TEMP%\Setup Log*.txt |  | ||||||
| SetupLogging=yes |  | ||||||
| CloseApplications=yes |  | ||||||
| RestartApplications=no |  | ||||||
|  |  | ||||||
| ; https://jrsoftware.org/ishelp/index.php?topic=setup_wizardimagefile |  | ||||||
| WizardSmallImageFile=.\assets\setup.bmp |  | ||||||
|  |  | ||||||
| ; TODO verifty actual min windows version... |  | ||||||
| ; OG Win 10 |  | ||||||
| MinVersion=10.0.10240 |  | ||||||
|  |  | ||||||
| ; First release that supports WinRT UI Composition for win32 apps |  | ||||||
| ; MinVersion=10.0.17134 |  | ||||||
| ; First release with XAML Islands - possible UI path forward |  | ||||||
| ; MinVersion=10.0.18362 |  | ||||||
|  |  | ||||||
| ; quiet... |  | ||||||
| DisableDirPage=yes |  | ||||||
| DisableFinishedPage=yes |  | ||||||
| DisableReadyMemo=yes |  | ||||||
| DisableReadyPage=yes |  | ||||||
| DisableStartupPrompt=yes |  | ||||||
| DisableWelcomePage=yes |  | ||||||
|  |  | ||||||
| ; TODO - percentage can't be set less than 100, so how to make it shorter? |  | ||||||
| ; WizardSizePercent=100,80 |  | ||||||
|  |  | ||||||
| #if GetEnv("KEY_CONTAINER") |  | ||||||
| SignTool=MySignTool |  | ||||||
| SignedUninstaller=yes |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| SetupMutex=OllamaSetupMutex |  | ||||||
|  |  | ||||||
| [Languages] |  | ||||||
| Name: "english"; MessagesFile: "compiler:Default.isl" |  | ||||||
|  |  | ||||||
| [LangOptions] |  | ||||||
| DialogFontSize=12 |  | ||||||
|  |  | ||||||
| [Files] |  | ||||||
| Source: ".\app.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}" ; Flags: ignoreversion 64bit |  | ||||||
| Source: "..\ollama.exe"; DestDir: "{app}"; Flags: ignoreversion 64bit |  | ||||||
| Source: "..\dist\windows-{#ARCH}\*.dll"; DestDir: "{app}"; Flags: ignoreversion 64bit |  | ||||||
| Source: "..\dist\windows-{#ARCH}\ollama_runners\*"; DestDir: "{app}\ollama_runners"; Flags: ignoreversion 64bit recursesubdirs |  | ||||||
| Source: "..\dist\ollama_welcome.ps1"; DestDir: "{app}"; Flags: ignoreversion |  | ||||||
| Source: ".\assets\app.ico"; DestDir: "{app}"; Flags: ignoreversion |  | ||||||
| #if DirExists("..\dist\windows-amd64\rocm") |  | ||||||
|   Source: "..\dist\windows-amd64\rocm\*"; DestDir: "{app}\rocm\"; Flags: ignoreversion recursesubdirs |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
|  |  | ||||||
| [Icons] |  | ||||||
| Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico" |  | ||||||
| Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico" |  | ||||||
| Name: "{userprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico" |  | ||||||
|  |  | ||||||
| [Run] |  | ||||||
| Filename: "{cmd}"; Parameters: "/C set PATH={app};%PATH% & ""{app}\{#MyAppExeName}"""; Flags: postinstall nowait runhidden |  | ||||||
|  |  | ||||||
| [UninstallRun] |  | ||||||
| ; Filename: "{cmd}"; Parameters: "/C ""taskkill /im ''{#MyAppExeName}'' /f /t"; Flags: runhidden |  | ||||||
| ; Filename: "{cmd}"; Parameters: "/C ""taskkill /im ollama.exe /f /t"; Flags: runhidden |  | ||||||
| Filename: "taskkill"; Parameters: "/im ""{#MyAppExeName}"" /f /t"; Flags: runhidden |  | ||||||
| Filename: "taskkill"; Parameters: "/im ""ollama.exe"" /f /t"; Flags: runhidden |  | ||||||
| ; HACK!  need to give the server and app enough time to exit |  | ||||||
| ; TODO - convert this to a Pascal code script so it waits until they're no longer running, then completes |  | ||||||
| Filename: "{cmd}"; Parameters: "/c timeout 5"; Flags: runhidden |  | ||||||
|  |  | ||||||
| [UninstallDelete] |  | ||||||
| Type: filesandordirs; Name: "{%TEMP}\ollama*" |  | ||||||
| Type: filesandordirs; Name: "{%LOCALAPPDATA}\Ollama" |  | ||||||
| Type: filesandordirs; Name: "{%LOCALAPPDATA}\Programs\Ollama" |  | ||||||
| Type: filesandordirs; Name: "{%USERPROFILE}\.ollama\models" |  | ||||||
| Type: filesandordirs; Name: "{%USERPROFILE}\.ollama\history" |  | ||||||
| ; NOTE: if the user has a custom OLLAMA_MODELS it will be preserved |  | ||||||
|  |  | ||||||
| [Messages] |  | ||||||
| WizardReady=Ollama Windows Preview |  | ||||||
| ReadyLabel1=%nLet's get you up and running with your own large language models. |  | ||||||
| SetupAppRunningError=Another Ollama installer is running.%n%nPlease cancel or finish the other installer, then click OK to continue with this install, or Cancel to exit. |  | ||||||
|  |  | ||||||
|  |  | ||||||
| ;FinishedHeadingLabel=Run your first model |  | ||||||
| ;FinishedLabel=%nRun this command in a PowerShell or cmd terminal.%n%n%n    ollama run llama3 |  | ||||||
| ;ClickFinish=%n |  | ||||||
|  |  | ||||||
| [Registry] |  | ||||||
| Root: HKCU; Subkey: "Environment"; \ |  | ||||||
|     ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \ |  | ||||||
|     Check: NeedsAddPath('{app}') |  | ||||||
|  |  | ||||||
| [Code] |  | ||||||
|  |  | ||||||
| function NeedsAddPath(Param: string): boolean; |  | ||||||
| var |  | ||||||
|   OrigPath: string; |  | ||||||
| begin |  | ||||||
|   if not RegQueryStringValue(HKEY_CURRENT_USER, |  | ||||||
|     'Environment', |  | ||||||
|     'Path', OrigPath) |  | ||||||
|   then begin |  | ||||||
|     Result := True; |  | ||||||
|     exit; |  | ||||||
|   end; |  | ||||||
|   { look for the path with leading and trailing semicolon } |  | ||||||
|   { Pos() returns 0 if not found } |  | ||||||
|   Result := Pos(';' + ExpandConstant(Param) + ';', ';' + OrigPath + ';') = 0; |  | ||||||
| end; |  | ||||||
| @@ -1,29 +0,0 @@ | |||||||
| #include <winver.h> |  | ||||||
|  |  | ||||||
| VS_VERSION_INFO VERSIONINFO |  | ||||||
|  FILEFLAGSMASK 0x3fL |  | ||||||
| #ifdef _DEBUG |  | ||||||
|  FILEFLAGS 0x1L |  | ||||||
| #else |  | ||||||
|  FILEFLAGS 0x0L |  | ||||||
| #endif |  | ||||||
|  FILEOS 0x40004L |  | ||||||
|  FILETYPE 0x1L |  | ||||||
|  FILESUBTYPE 0x0L |  | ||||||
| BEGIN |  | ||||||
|     BLOCK "StringFileInfo" |  | ||||||
|     BEGIN |  | ||||||
|         BLOCK "040904b0" |  | ||||||
|         BEGIN |  | ||||||
|             VALUE "FileDescription", "Ollama" |  | ||||||
|             VALUE "InternalName", "Ollama" |  | ||||||
|             VALUE "OriginalFilename", "ollama app.exe" |  | ||||||
|             VALUE "ProductName", "Ollama" |  | ||||||
|         END |  | ||||||
|     END |  | ||||||
|  |  | ||||||
|     BLOCK "VarFileInfo" |  | ||||||
|     BEGIN |  | ||||||
|         VALUE "Translation", 0x409, 1200 |  | ||||||
|     END |  | ||||||
| END |  | ||||||
| @@ -1,8 +0,0 @@ | |||||||
| # TODO - consider ANSI colors and maybe ASCII art... |  | ||||||
| write-host "" |  | ||||||
| write-host "Welcome to Ollama!" |  | ||||||
| write-host "" |  | ||||||
| write-host "Run your first model:" |  | ||||||
| write-host "" |  | ||||||
| write-host "`tollama run llama2" |  | ||||||
| write-host "" |  | ||||||
							
								
								
									
										995
									
								
								macapp/package-lock.json → app/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -6,10 +6,10 @@ | |||||||
|   "main": ".webpack/main", |   "main": ".webpack/main", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "electron-forge start", |     "start": "electron-forge start", | ||||||
|     "package": "electron-forge package --arch universal", |     "package": "electron-forge package", | ||||||
|     "package:sign": "SIGN=1 electron-forge package --arch universal", |     "package:sign": "SIGN=1 electron-forge package", | ||||||
|     "make": "electron-forge make --arch universal", |     "make": "electron-forge make", | ||||||
|     "make:sign": "SIGN=1 electron-forge make --arch universal", |     "make:sign": "SIGN=1 electron-forge make", | ||||||
|     "publish": "SIGN=1 electron-forge publish", |     "publish": "SIGN=1 electron-forge publish", | ||||||
|     "lint": "eslint --ext .ts,.tsx .", |     "lint": "eslint --ext .ts,.tsx .", | ||||||
|     "format": "prettier --check . --ignore-path .gitignore", |     "format": "prettier --check . --ignore-path .gitignore", | ||||||
| @@ -32,7 +32,6 @@ | |||||||
|     "@electron-forge/plugin-auto-unpack-natives": "^6.2.1", |     "@electron-forge/plugin-auto-unpack-natives": "^6.2.1", | ||||||
|     "@electron-forge/plugin-webpack": "^6.2.1", |     "@electron-forge/plugin-webpack": "^6.2.1", | ||||||
|     "@electron-forge/publisher-github": "^6.2.1", |     "@electron-forge/publisher-github": "^6.2.1", | ||||||
|     "@electron/universal": "^1.4.1", |  | ||||||
|     "@svgr/webpack": "^8.0.1", |     "@svgr/webpack": "^8.0.1", | ||||||
|     "@types/chmodr": "^1.0.0", |     "@types/chmodr": "^1.0.0", | ||||||
|     "@types/node": "^20.4.0", |     "@types/node": "^20.4.0", | ||||||
| @@ -46,7 +45,7 @@ | |||||||
|     "chmodr": "^1.2.0", |     "chmodr": "^1.2.0", | ||||||
|     "copy-webpack-plugin": "^11.0.0", |     "copy-webpack-plugin": "^11.0.0", | ||||||
|     "css-loader": "^6.8.1", |     "css-loader": "^6.8.1", | ||||||
|     "electron": "25.9.2", |     "electron": "25.2.0", | ||||||
|     "eslint": "^8.43.0", |     "eslint": "^8.43.0", | ||||||
|     "eslint-plugin-import": "^2.27.5", |     "eslint-plugin-import": "^2.27.5", | ||||||
|     "fork-ts-checker-webpack-plugin": "^7.3.0", |     "fork-ts-checker-webpack-plugin": "^7.3.0", | ||||||
| @@ -2,7 +2,7 @@ import { useState } from 'react' | |||||||
| import copy from 'copy-to-clipboard' | import copy from 'copy-to-clipboard' | ||||||
| import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/24/outline' | import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/24/outline' | ||||||
| import Store from 'electron-store' | import Store from 'electron-store' | ||||||
| import { getCurrentWindow, app } from '@electron/remote' | import { getCurrentWindow } from '@electron/remote' | ||||||
| 
 | 
 | ||||||
| import { install } from './install' | import { install } from './install' | ||||||
| import OllamaIcon from './ollama.svg' | import OllamaIcon from './ollama.svg' | ||||||
| @@ -19,7 +19,7 @@ export default function () { | |||||||
|   const [step, setStep] = useState<Step>(Step.WELCOME) |   const [step, setStep] = useState<Step>(Step.WELCOME) | ||||||
|   const [commandCopied, setCommandCopied] = useState<boolean>(false) |   const [commandCopied, setCommandCopied] = useState<boolean>(false) | ||||||
| 
 | 
 | ||||||
|   const command = 'ollama run llama3' |   const command = 'ollama run llama2' | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <div className='drag'> |     <div className='drag'> | ||||||
| @@ -51,15 +51,10 @@ export default function () { | |||||||
|               <div className='mx-auto'> |               <div className='mx-auto'> | ||||||
|                 <button |                 <button | ||||||
|                   onClick={async () => { |                   onClick={async () => { | ||||||
|                     try { |  | ||||||
|                     await install() |                     await install() | ||||||
|                       setStep(Step.FINISH) |  | ||||||
|                     } catch (e) { |  | ||||||
|                       console.error('could not install: ', e) |  | ||||||
|                     } finally { |  | ||||||
|                     getCurrentWindow().show() |                     getCurrentWindow().show() | ||||||
|                     getCurrentWindow().focus() |                     getCurrentWindow().focus() | ||||||
|                     } |                     setStep(Step.FINISH) | ||||||
|                   }} |                   }} | ||||||
|                   className='no-drag rounded-dm mx-auto w-[60%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110' |                   className='no-drag rounded-dm mx-auto w-[60%] rounded-md bg-black px-4 py-2 text-sm text-white hover:brightness-110' | ||||||
|                 > |                 > | ||||||
| @@ -1,21 +1,17 @@ | |||||||
| import { spawn, ChildProcess } from 'child_process' | import { spawn } from 'child_process' | ||||||
| import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow, MenuItemConstructorOptions, nativeTheme } from 'electron' | import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow, nativeTheme } from 'electron' | ||||||
| import Store from 'electron-store' | import Store from 'electron-store' | ||||||
| import winston from 'winston' | import winston from 'winston' | ||||||
| import 'winston-daily-rotate-file' | import 'winston-daily-rotate-file' | ||||||
| import * as path from 'path' | import * as path from 'path' | ||||||
| 
 | 
 | ||||||
| import { v4 as uuidv4 } from 'uuid' | import { analytics, id } from './telemetry' | ||||||
| import { installed } from './install' | import { installed } from './install' | ||||||
| 
 | 
 | ||||||
| require('@electron/remote/main').initialize() | require('@electron/remote/main').initialize() | ||||||
| 
 | 
 | ||||||
| if (require('electron-squirrel-startup')) { |  | ||||||
|   app.quit() |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| const store = new Store() | const store = new Store() | ||||||
| 
 | let tray: Tray | null = null | ||||||
| let welcomeWindow: BrowserWindow | null = null | let welcomeWindow: BrowserWindow | null = null | ||||||
| 
 | 
 | ||||||
| declare const MAIN_WINDOW_WEBPACK_ENTRY: string | declare const MAIN_WINDOW_WEBPACK_ENTRY: string | ||||||
| @@ -32,30 +28,10 @@ const logger = winston.createLogger({ | |||||||
|   format: winston.format.printf(info => info.message), |   format: winston.format.printf(info => info.message), | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| app.on('ready', () => { | const SingleInstanceLock = app.requestSingleInstanceLock() | ||||||
|   const gotTheLock = app.requestSingleInstanceLock() | if (!SingleInstanceLock) { | ||||||
|   if (!gotTheLock) { |   app.quit() | ||||||
|     app.exit(0) | } | ||||||
|     return |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   app.on('second-instance', () => { |  | ||||||
|     if (app.hasSingleInstanceLock()) { |  | ||||||
|       app.releaseSingleInstanceLock() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (proc) { |  | ||||||
|       proc.off('exit', restart) |  | ||||||
|       proc.kill() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     app.exit(0) |  | ||||||
|   }) |  | ||||||
| 
 |  | ||||||
|   app.focus({ steal: true }) |  | ||||||
| 
 |  | ||||||
|   init() |  | ||||||
| }) |  | ||||||
| 
 | 
 | ||||||
| function firstRunWindow() { | function firstRunWindow() { | ||||||
|   // Create the browser window.
 |   // Create the browser window.
 | ||||||
| @@ -71,74 +47,65 @@ function firstRunWindow() { | |||||||
|       nodeIntegration: true, |       nodeIntegration: true, | ||||||
|       contextIsolation: false, |       contextIsolation: false, | ||||||
|     }, |     }, | ||||||
|  |     alwaysOnTop: true, | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|   require('@electron/remote/main').enable(welcomeWindow.webContents) |   require('@electron/remote/main').enable(welcomeWindow.webContents) | ||||||
| 
 | 
 | ||||||
|  |   // and load the index.html of the app.
 | ||||||
|   welcomeWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY) |   welcomeWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY) | ||||||
|  | 
 | ||||||
|   welcomeWindow.on('ready-to-show', () => welcomeWindow.show()) |   welcomeWindow.on('ready-to-show', () => welcomeWindow.show()) | ||||||
|   welcomeWindow.on('closed', () => { | 
 | ||||||
|  |   // for debugging
 | ||||||
|  |   // welcomeWindow.webContents.openDevTools()
 | ||||||
|  | 
 | ||||||
|   if (process.platform === 'darwin') { |   if (process.platform === 'darwin') { | ||||||
|     app.dock.hide() |     app.dock.hide() | ||||||
|   } |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function createSystemtray() { | ||||||
|  |   let iconPath = nativeTheme.shouldUseDarkColors | ||||||
|  |     ? path.join(__dirname, '..', '..', 'assets', 'ollama_icon_16x16Template.png') | ||||||
|  |     : path.join(__dirname, '..', '..', 'assets', 'ollama_outline_icon_16x16Template.png') | ||||||
|  | 
 | ||||||
|  |   if (app.isPackaged) { | ||||||
|  |     iconPath = nativeTheme.shouldUseDarkColors | ||||||
|  |       ? path.join(process.resourcesPath, 'ollama_icon_16x16Template.png') | ||||||
|  |       : path.join(process.resourcesPath, 'ollama_outline_icon_16x16Template.png') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   tray = new Tray(iconPath) | ||||||
|  | 
 | ||||||
|  |   nativeTheme.on('updated', function theThemeHasChanged() { | ||||||
|  |     if (nativeTheme.shouldUseDarkColors) { | ||||||
|  |       app.isPackaged | ||||||
|  |         ? tray.setImage(path.join(process.resourcesPath, 'ollama_icon_16x16Template.png')) | ||||||
|  |         : tray.setImage(path.join(__dirname, '..', '..', 'assets', 'ollama_icon_16x16Template.png')) | ||||||
|  |     } else { | ||||||
|  |       app.isPackaged | ||||||
|  |         ? tray.setImage(path.join(process.resourcesPath, 'ollama_outline_icon_16x16Template.png')) | ||||||
|  |         : tray.setImage(path.join(__dirname, '..', '..', 'assets', 'ollama_outline_icon_16x16Template.png')) | ||||||
|  |     } | ||||||
|   }) |   }) | ||||||
|  | 
 | ||||||
|  |   const contextMenu = Menu.buildFromTemplate([{ role: 'quit', label: 'Quit Ollama', accelerator: 'Command+Q' }]) | ||||||
|  | 
 | ||||||
|  |   tray.setContextMenu(contextMenu) | ||||||
|  |   tray.setToolTip('Ollama') | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| let tray: Tray | null = null | if (require('electron-squirrel-startup')) { | ||||||
| let updateAvailable = false |   app.quit() | ||||||
| const assetPath = app.isPackaged ? process.resourcesPath : path.join(__dirname, '..', '..', 'assets') |  | ||||||
| 
 |  | ||||||
| function trayIconPath() { |  | ||||||
|   return nativeTheme.shouldUseDarkColors |  | ||||||
|     ? updateAvailable |  | ||||||
|       ? path.join(assetPath, 'iconDarkUpdateTemplate.png') |  | ||||||
|       : path.join(assetPath, 'iconDarkTemplate.png') |  | ||||||
|     : updateAvailable |  | ||||||
|     ? path.join(assetPath, 'iconUpdateTemplate.png') |  | ||||||
|     : path.join(assetPath, 'iconTemplate.png') |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function updateTrayIcon() { |  | ||||||
|   if (tray) { |  | ||||||
|     tray.setImage(trayIconPath()) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function updateTray() { |  | ||||||
|   const updateItems: MenuItemConstructorOptions[] = [ |  | ||||||
|     { label: 'An update is available', enabled: false }, |  | ||||||
|     { |  | ||||||
|       label: 'Restart to update', |  | ||||||
|       click: () => autoUpdater.quitAndInstall(), |  | ||||||
|     }, |  | ||||||
|     { type: 'separator' }, |  | ||||||
|   ] |  | ||||||
| 
 |  | ||||||
|   const menu = Menu.buildFromTemplate([ |  | ||||||
|     ...(updateAvailable ? updateItems : []), |  | ||||||
|     { role: 'quit', label: 'Quit Ollama', accelerator: 'Command+Q' }, |  | ||||||
|   ]) |  | ||||||
| 
 |  | ||||||
|   if (!tray) { |  | ||||||
|     tray = new Tray(trayIconPath()) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   tray.setToolTip(updateAvailable ? 'An update is available' : 'Ollama') |  | ||||||
|   tray.setContextMenu(menu) |  | ||||||
|   tray.setImage(trayIconPath()) |  | ||||||
| 
 |  | ||||||
|   nativeTheme.off('updated', updateTrayIcon) |  | ||||||
|   nativeTheme.on('updated', updateTrayIcon) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| let proc: ChildProcess = null |  | ||||||
| 
 |  | ||||||
| function server() { | function server() { | ||||||
|   const binary = app.isPackaged |   const binary = app.isPackaged | ||||||
|     ? path.join(process.resourcesPath, 'ollama') |     ? path.join(process.resourcesPath, 'ollama') | ||||||
|     : path.resolve(process.cwd(), '..', 'ollama') |     : path.resolve(process.cwd(), '..', 'ollama') | ||||||
| 
 | 
 | ||||||
|   proc = spawn(binary, ['serve']) |   const proc = spawn(binary, ['serve']) | ||||||
| 
 | 
 | ||||||
|   proc.stdout.on('data', data => { |   proc.stdout.on('data', data => { | ||||||
|     logger.info(data.toString().trim()) |     logger.info(data.toString().trim()) | ||||||
| @@ -148,75 +115,23 @@ function server() { | |||||||
|     logger.error(data.toString().trim()) |     logger.error(data.toString().trim()) | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  |   function restart() { | ||||||
|  |     setTimeout(server, 3000) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   proc.on('exit', restart) |   proc.on('exit', restart) | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| function restart() { |   app.on('before-quit', () => { | ||||||
|   setTimeout(server, 1000) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| app.on('before-quit', () => { |  | ||||||
|   if (proc) { |  | ||||||
|     proc.off('exit', restart) |     proc.off('exit', restart) | ||||||
|     proc.kill('SIGINT') // send SIGINT signal to the server, which also stops any loaded llms
 |     proc.kill() | ||||||
|   } |   }) | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| const updateURL = `https://ollama.com/api/update?os=${process.platform}&arch=${ |  | ||||||
|   process.arch |  | ||||||
| }&version=${app.getVersion()}&id=${id()}` |  | ||||||
| 
 |  | ||||||
| let latest = '' |  | ||||||
| async function isNewReleaseAvailable() { |  | ||||||
|   try { |  | ||||||
|     const response = await fetch(updateURL) |  | ||||||
| 
 |  | ||||||
|     if (!response.ok) { |  | ||||||
|       return false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (response.status === 204) { |  | ||||||
|       return false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const data = await response.json() |  | ||||||
| 
 |  | ||||||
|     const url = data?.url |  | ||||||
|     if (!url) { |  | ||||||
|       return false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (latest === url) { |  | ||||||
|       return false |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     latest = url |  | ||||||
| 
 |  | ||||||
|     return true |  | ||||||
|   } catch (error) { |  | ||||||
|     logger.error(`update check failed - ${error}`) |  | ||||||
|     return false |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| async function checkUpdate() { | if (process.platform === 'darwin') { | ||||||
|   const available = await isNewReleaseAvailable() |   app.dock.hide() | ||||||
|   if (available) { |  | ||||||
|     logger.info('checking for update') |  | ||||||
|     autoUpdater.checkForUpdates() |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function init() { | app.on('ready', () => { | ||||||
|   if (app.isPackaged) { |  | ||||||
|     checkUpdate() |  | ||||||
|     setInterval(() => { |  | ||||||
|       checkUpdate() |  | ||||||
|     }, 60 * 60 * 1000) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   updateTray() |  | ||||||
| 
 |  | ||||||
|   if (process.platform === 'darwin') { |   if (process.platform === 'darwin') { | ||||||
|     if (app.isPackaged) { |     if (app.isPackaged) { | ||||||
|       if (!app.isInApplicationsFolder()) { |       if (!app.isInApplicationsFolder()) { | ||||||
| @@ -252,13 +167,10 @@ function init() { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   createSystemtray() | ||||||
|   server() |   server() | ||||||
| 
 | 
 | ||||||
|   if (store.get('first-time-run') && installed()) { |   if (store.get('first-time-run') && installed()) { | ||||||
|     if (process.platform === 'darwin') { |  | ||||||
|       app.dock.hide() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin }) |     app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin }) | ||||||
|     return |     return | ||||||
|   } |   } | ||||||
| @@ -266,7 +178,7 @@ function init() { | |||||||
|   // This is the first run or the CLI is no longer installed
 |   // This is the first run or the CLI is no longer installed
 | ||||||
|   app.setLoginItemSettings({ openAtLogin: true }) |   app.setLoginItemSettings({ openAtLogin: true }) | ||||||
|   firstRunWindow() |   firstRunWindow() | ||||||
| } | }) | ||||||
| 
 | 
 | ||||||
| // Quit when all windows are closed, except on macOS. There, it's common
 | // Quit when all windows are closed, except on macOS. There, it's common
 | ||||||
| // for applications and their menu bar to stay active until the user quits
 | // for applications and their menu bar to stay active until the user quits
 | ||||||
| @@ -277,26 +189,45 @@ app.on('window-all-closed', () => { | |||||||
|   } |   } | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| function id(): string { | // In this file you can include the rest of your app's specific main process
 | ||||||
|   const id = store.get('id') as string | // code. You can also put them in separate files and import them here.
 | ||||||
|  | autoUpdater.setFeedURL({ | ||||||
|  |   url: `https://ollama.ai/api/update?os=${process.platform}&arch=${process.arch}&version=${app.getVersion()}`, | ||||||
|  | }) | ||||||
| 
 | 
 | ||||||
|   if (id) { | async function heartbeat() { | ||||||
|     return id |   analytics.track({ | ||||||
|   } |     anonymousId: id(), | ||||||
| 
 |     event: 'heartbeat', | ||||||
|   const uuid = uuidv4() |     properties: { | ||||||
|   store.set('id', uuid) |       version: app.getVersion(), | ||||||
|   return uuid |     }, | ||||||
|  |   }) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| autoUpdater.setFeedURL({ url: updateURL }) | if (app.isPackaged) { | ||||||
|  |   heartbeat() | ||||||
|  |   autoUpdater.checkForUpdates() | ||||||
|  |   setInterval(() => { | ||||||
|  |     heartbeat() | ||||||
|  |     autoUpdater.checkForUpdates() | ||||||
|  |   }, 60 * 60 * 1000) | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| autoUpdater.on('error', e => { | autoUpdater.on('error', e => { | ||||||
|   logger.error(`update check failed - ${e.message}`) |   logger.error(`update check failed - ${e.message}`) | ||||||
|   console.error(`update check failed - ${e.message}`) |  | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| autoUpdater.on('update-downloaded', () => { | autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => { | ||||||
|   updateAvailable = true |   dialog | ||||||
|   updateTray() |     .showMessageBox({ | ||||||
|  |       type: 'info', | ||||||
|  |       buttons: ['Restart Now', 'Later'], | ||||||
|  |       title: 'New update available', | ||||||
|  |       message: process.platform === 'win32' ? releaseNotes : releaseName, | ||||||
|  |       detail: 'A new version of Ollama is available. Restart to apply the update.', | ||||||
|  |     }) | ||||||
|  |     .then(returnValue => { | ||||||
|  |       if (returnValue.response === 0) autoUpdater.quitAndInstall() | ||||||
|  |     }) | ||||||
| }) | }) | ||||||
| @@ -15,7 +15,12 @@ export function installed() { | |||||||
| export async function install() { | export async function install() { | ||||||
|   const command = `do shell script "mkdir -p ${path.dirname( |   const command = `do shell script "mkdir -p ${path.dirname( | ||||||
|     symlinkPath |     symlinkPath | ||||||
|   )} && ln -F -s \\"${ollama}\\" \\"${symlinkPath}\\"" with administrator privileges` |   )} && ln -F -s ${ollama} ${symlinkPath}" with administrator privileges` | ||||||
| 
 | 
 | ||||||
|  |   try { | ||||||
|     await exec(`osascript -e '${command}'`) |     await exec(`osascript -e '${command}'`) | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error(`cli: failed to install cli: ${error.message}`) | ||||||
|  |     return | ||||||
|  |   } | ||||||
| } | } | ||||||
| Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB | 
							
								
								
									
										19
									
								
								app/src/telemetry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,19 @@ | |||||||
|  | import { Analytics } from '@segment/analytics-node' | ||||||
|  | import { v4 as uuidv4 } from 'uuid' | ||||||
|  | import Store from 'electron-store' | ||||||
|  |  | ||||||
|  | const store = new Store() | ||||||
|  |  | ||||||
|  | export const analytics = new Analytics({ writeKey: process.env.TELEMETRY_WRITE_KEY || '<empty>' }) | ||||||
|  |  | ||||||
|  | export function id(): string { | ||||||
|  |   const id = store.get('id') as string | ||||||
|  |  | ||||||
|  |   if (id) { | ||||||
|  |     return id | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const uuid = uuidv4() | ||||||
|  |   store.set('id', uuid) | ||||||
|  |   return uuid | ||||||
|  | } | ||||||
| @@ -1,98 +0,0 @@ | |||||||
| package store |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"sync" |  | ||||||
|  |  | ||||||
| 	"github.com/google/uuid" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type Store struct { |  | ||||||
| 	ID           string `json:"id"` |  | ||||||
| 	FirstTimeRun bool   `json:"first-time-run"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	lock  sync.Mutex |  | ||||||
| 	store Store |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func GetID() string { |  | ||||||
| 	lock.Lock() |  | ||||||
| 	defer lock.Unlock() |  | ||||||
| 	if store.ID == "" { |  | ||||||
| 		initStore() |  | ||||||
| 	} |  | ||||||
| 	return store.ID |  | ||||||
|  |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func GetFirstTimeRun() bool { |  | ||||||
| 	lock.Lock() |  | ||||||
| 	defer lock.Unlock() |  | ||||||
| 	if store.ID == "" { |  | ||||||
| 		initStore() |  | ||||||
| 	} |  | ||||||
| 	return store.FirstTimeRun |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func SetFirstTimeRun(val bool) { |  | ||||||
| 	lock.Lock() |  | ||||||
| 	defer lock.Unlock() |  | ||||||
| 	if store.FirstTimeRun == val { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	store.FirstTimeRun = val |  | ||||||
| 	writeStore(getStorePath()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // lock must be held |  | ||||||
| func initStore() { |  | ||||||
| 	storeFile, err := os.Open(getStorePath()) |  | ||||||
| 	if err == nil { |  | ||||||
| 		defer storeFile.Close() |  | ||||||
| 		err = json.NewDecoder(storeFile).Decode(&store) |  | ||||||
| 		if err == nil { |  | ||||||
| 			slog.Debug(fmt.Sprintf("loaded existing store %s - ID: %s", getStorePath(), store.ID)) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} else if !errors.Is(err, os.ErrNotExist) { |  | ||||||
| 		slog.Debug(fmt.Sprintf("unexpected error searching for store: %s", err)) |  | ||||||
| 	} |  | ||||||
| 	slog.Debug("initializing new store") |  | ||||||
| 	store.ID = uuid.New().String() |  | ||||||
| 	writeStore(getStorePath()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func writeStore(storeFilename string) { |  | ||||||
| 	ollamaDir := filepath.Dir(storeFilename) |  | ||||||
| 	_, err := os.Stat(ollamaDir) |  | ||||||
| 	if errors.Is(err, os.ErrNotExist) { |  | ||||||
| 		if err := os.MkdirAll(ollamaDir, 0o755); err != nil { |  | ||||||
| 			slog.Error(fmt.Sprintf("create ollama dir %s: %v", ollamaDir, err)) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	payload, err := json.Marshal(store) |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Error(fmt.Sprintf("failed to marshal store: %s", err)) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	fp, err := os.OpenFile(storeFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Error(fmt.Sprintf("write store payload %s: %v", storeFilename, err)) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	defer fp.Close() |  | ||||||
| 	if n, err := fp.Write(payload); err != nil || n != len(payload) { |  | ||||||
| 		slog.Error(fmt.Sprintf("write store payload %s: %d vs %d -- %v", storeFilename, n, len(payload), err)) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	slog.Debug("Store contents: " + string(payload)) |  | ||||||
| 	slog.Info(fmt.Sprintf("wrote store: %s", storeFilename)) |  | ||||||
| } |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| package store |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func getStorePath() string { |  | ||||||
| 	// TODO - system wide location? |  | ||||||
|  |  | ||||||
| 	home := os.Getenv("HOME") |  | ||||||
| 	return filepath.Join(home, "Library", "Application Support", "Ollama", "config.json") |  | ||||||
| } |  | ||||||
| @@ -1,16 +0,0 @@ | |||||||
| package store |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func getStorePath() string { |  | ||||||
| 	if os.Geteuid() == 0 { |  | ||||||
| 		// TODO where should we store this on linux for system-wide operation? |  | ||||||
| 		return "/etc/ollama/config.json" |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	home := os.Getenv("HOME") |  | ||||||
| 	return filepath.Join(home, ".ollama", "config.json") |  | ||||||
| } |  | ||||||
| @@ -1,11 +0,0 @@ | |||||||
| package store |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func getStorePath() string { |  | ||||||
| 	localAppData := os.Getenv("LOCALAPPDATA") |  | ||||||
| 	return filepath.Join(localAppData, "Ollama", "config.json") |  | ||||||
| } |  | ||||||
| @@ -1,24 +0,0 @@ | |||||||
| package commontray |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	Title   = "Ollama" |  | ||||||
| 	ToolTip = "Ollama" |  | ||||||
|  |  | ||||||
| 	UpdateIconName = "tray_upgrade" |  | ||||||
| 	IconName       = "tray" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type Callbacks struct { |  | ||||||
| 	Quit       chan struct{} |  | ||||||
| 	Update     chan struct{} |  | ||||||
| 	DoFirstUse chan struct{} |  | ||||||
| 	ShowLogs   chan struct{} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type OllamaTray interface { |  | ||||||
| 	GetCallbacks() Callbacks |  | ||||||
| 	Run() |  | ||||||
| 	UpdateAvailable(ver string) error |  | ||||||
| 	DisplayFirstUseNotification() error |  | ||||||
| 	Quit() |  | ||||||
| } |  | ||||||
| @@ -1,28 +0,0 @@ | |||||||
| package tray |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"runtime" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/app/assets" |  | ||||||
| 	"github.com/ollama/ollama/app/tray/commontray" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func NewTray() (commontray.OllamaTray, error) { |  | ||||||
| 	extension := ".png" |  | ||||||
| 	if runtime.GOOS == "windows" { |  | ||||||
| 		extension = ".ico" |  | ||||||
| 	} |  | ||||||
| 	iconName := commontray.UpdateIconName + extension |  | ||||||
| 	updateIcon, err := assets.GetIcon(iconName) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to load icon %s: %w", iconName, err) |  | ||||||
| 	} |  | ||||||
| 	iconName = commontray.IconName + extension |  | ||||||
| 	icon, err := assets.GetIcon(iconName) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("failed to load icon %s: %w", iconName, err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return InitPlatformTray(icon, updateIcon) |  | ||||||
| } |  | ||||||
| @@ -1,13 +0,0 @@ | |||||||
| //go:build !windows |  | ||||||
|  |  | ||||||
| package tray |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/app/tray/commontray" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func InitPlatformTray(icon, updateIcon []byte) (commontray.OllamaTray, error) { |  | ||||||
| 	return nil, fmt.Errorf("NOT IMPLEMENTED YET") |  | ||||||
| } |  | ||||||
| @@ -1,10 +0,0 @@ | |||||||
| package tray |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"github.com/ollama/ollama/app/tray/commontray" |  | ||||||
| 	"github.com/ollama/ollama/app/tray/wintray" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func InitPlatformTray(icon, updateIcon []byte) (commontray.OllamaTray, error) { |  | ||||||
| 	return wintray.InitTray(icon, updateIcon) |  | ||||||
| } |  | ||||||
| @@ -1,184 +0,0 @@ | |||||||
| //go:build windows |  | ||||||
|  |  | ||||||
| package wintray |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"sync" |  | ||||||
| 	"unsafe" |  | ||||||
|  |  | ||||||
| 	"golang.org/x/sys/windows" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	quitOnce sync.Once |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func (t *winTray) Run() { |  | ||||||
| 	nativeLoop() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func nativeLoop() { |  | ||||||
| 	// Main message pump. |  | ||||||
| 	slog.Debug("starting event handling loop") |  | ||||||
| 	m := &struct { |  | ||||||
| 		WindowHandle windows.Handle |  | ||||||
| 		Message      uint32 |  | ||||||
| 		Wparam       uintptr |  | ||||||
| 		Lparam       uintptr |  | ||||||
| 		Time         uint32 |  | ||||||
| 		Pt           point |  | ||||||
| 		LPrivate     uint32 |  | ||||||
| 	}{} |  | ||||||
| 	for { |  | ||||||
| 		ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0) |  | ||||||
|  |  | ||||||
| 		// If the function retrieves a message other than WM_QUIT, the return value is nonzero. |  | ||||||
| 		// If the function retrieves the WM_QUIT message, the return value is zero. |  | ||||||
| 		// If there is an error, the return value is -1 |  | ||||||
| 		// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx |  | ||||||
| 		switch int32(ret) { |  | ||||||
| 		case -1: |  | ||||||
| 			slog.Error(fmt.Sprintf("get message failure: %v", err)) |  | ||||||
| 			return |  | ||||||
| 		case 0: |  | ||||||
| 			return |  | ||||||
| 		default: |  | ||||||
| 			pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck |  | ||||||
| 			pDispatchMessage.Call(uintptr(unsafe.Pointer(m)))  //nolint:errcheck |  | ||||||
|  |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // WindowProc callback function that processes messages sent to a window. |  | ||||||
| // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx |  | ||||||
| func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) { |  | ||||||
| 	const ( |  | ||||||
| 		WM_RBUTTONUP   = 0x0205 |  | ||||||
| 		WM_LBUTTONUP   = 0x0202 |  | ||||||
| 		WM_COMMAND     = 0x0111 |  | ||||||
| 		WM_ENDSESSION  = 0x0016 |  | ||||||
| 		WM_CLOSE       = 0x0010 |  | ||||||
| 		WM_DESTROY     = 0x0002 |  | ||||||
| 		WM_MOUSEMOVE   = 0x0200 |  | ||||||
| 		WM_LBUTTONDOWN = 0x0201 |  | ||||||
| 	) |  | ||||||
| 	switch message { |  | ||||||
| 	case WM_COMMAND: |  | ||||||
| 		menuItemId := int32(wParam) |  | ||||||
| 		// https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus |  | ||||||
| 		switch menuItemId { |  | ||||||
| 		case quitMenuID: |  | ||||||
| 			select { |  | ||||||
| 			case t.callbacks.Quit <- struct{}{}: |  | ||||||
| 			// should not happen but in case not listening |  | ||||||
| 			default: |  | ||||||
| 				slog.Error("no listener on Quit") |  | ||||||
| 			} |  | ||||||
| 		case updateMenuID: |  | ||||||
| 			select { |  | ||||||
| 			case t.callbacks.Update <- struct{}{}: |  | ||||||
| 			// should not happen but in case not listening |  | ||||||
| 			default: |  | ||||||
| 				slog.Error("no listener on Update") |  | ||||||
| 			} |  | ||||||
| 		case diagLogsMenuID: |  | ||||||
| 			select { |  | ||||||
| 			case t.callbacks.ShowLogs <- struct{}{}: |  | ||||||
| 			// should not happen but in case not listening |  | ||||||
| 			default: |  | ||||||
| 				slog.Error("no listener on ShowLogs") |  | ||||||
| 			} |  | ||||||
| 		default: |  | ||||||
| 			slog.Debug(fmt.Sprintf("Unexpected menu item id: %d", menuItemId)) |  | ||||||
| 		} |  | ||||||
| 	case WM_CLOSE: |  | ||||||
| 		boolRet, _, err := pDestroyWindow.Call(uintptr(t.window)) |  | ||||||
| 		if boolRet == 0 { |  | ||||||
| 			slog.Error(fmt.Sprintf("failed to destroy window: %s", err)) |  | ||||||
| 		} |  | ||||||
| 		err = t.wcex.unregister() |  | ||||||
| 		if err != nil { |  | ||||||
| 			slog.Error(fmt.Sprintf("failed to uregister windo %s", err)) |  | ||||||
| 		} |  | ||||||
| 	case WM_DESTROY: |  | ||||||
| 		// same as WM_ENDSESSION, but throws 0 exit code after all |  | ||||||
| 		defer pPostQuitMessage.Call(uintptr(int32(0))) //nolint:errcheck |  | ||||||
| 		fallthrough |  | ||||||
| 	case WM_ENDSESSION: |  | ||||||
| 		t.muNID.Lock() |  | ||||||
| 		if t.nid != nil { |  | ||||||
| 			err := t.nid.delete() |  | ||||||
| 			if err != nil { |  | ||||||
| 				slog.Error(fmt.Sprintf("failed to delete nid: %s", err)) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		t.muNID.Unlock() |  | ||||||
| 	case t.wmSystrayMessage: |  | ||||||
| 		switch lParam { |  | ||||||
| 		case WM_MOUSEMOVE, WM_LBUTTONDOWN: |  | ||||||
| 			// Ignore these... |  | ||||||
| 		case WM_RBUTTONUP, WM_LBUTTONUP: |  | ||||||
| 			err := t.showMenu() |  | ||||||
| 			if err != nil { |  | ||||||
| 				slog.Error(fmt.Sprintf("failed to show menu: %s", err)) |  | ||||||
| 			} |  | ||||||
| 		case 0x405: // TODO - how is this magic value derived for the notification left click |  | ||||||
| 			if t.pendingUpdate { |  | ||||||
| 				select { |  | ||||||
| 				case t.callbacks.Update <- struct{}{}: |  | ||||||
| 				// should not happen but in case not listening |  | ||||||
| 				default: |  | ||||||
| 					slog.Error("no listener on Update") |  | ||||||
| 				} |  | ||||||
| 			} else { |  | ||||||
| 				select { |  | ||||||
| 				case t.callbacks.DoFirstUse <- struct{}{}: |  | ||||||
| 				// should not happen but in case not listening |  | ||||||
| 				default: |  | ||||||
| 					slog.Error("no listener on DoFirstUse") |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		case 0x404: // Middle click or close notification |  | ||||||
| 			// slog.Debug("doing nothing on close of first time notification") |  | ||||||
| 		default: |  | ||||||
| 			// 0x402 also seems common - what is it? |  | ||||||
| 			slog.Debug(fmt.Sprintf("unmanaged app message, lParm: 0x%x", lParam)) |  | ||||||
| 		} |  | ||||||
| 	case t.wmTaskbarCreated: // on explorer.exe restarts |  | ||||||
| 		t.muNID.Lock() |  | ||||||
| 		err := t.nid.add() |  | ||||||
| 		if err != nil { |  | ||||||
| 			slog.Error(fmt.Sprintf("failed to refresh the taskbar on explorer restart: %s", err)) |  | ||||||
| 		} |  | ||||||
| 		t.muNID.Unlock() |  | ||||||
| 	default: |  | ||||||
| 		// Calls the default window procedure to provide default processing for any window messages that an application does not process. |  | ||||||
| 		// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx |  | ||||||
| 		lResult, _, _ = pDefWindowProc.Call( |  | ||||||
| 			uintptr(hWnd), |  | ||||||
| 			uintptr(message), |  | ||||||
| 			uintptr(wParam), |  | ||||||
| 			uintptr(lParam), |  | ||||||
| 		) |  | ||||||
| 	} |  | ||||||
| 	return |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (t *winTray) Quit() { |  | ||||||
| 	quitOnce.Do(quit) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func quit() { |  | ||||||
| 	boolRet, _, err := pPostMessage.Call( |  | ||||||
| 		uintptr(wt.window), |  | ||||||
| 		WM_CLOSE, |  | ||||||
| 		0, |  | ||||||
| 		0, |  | ||||||
| 	) |  | ||||||
| 	if boolRet == 0 { |  | ||||||
| 		slog.Error(fmt.Sprintf("failed to post close message on shutdown %s", err)) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -1,71 +0,0 @@ | |||||||
| //go:build windows |  | ||||||
|  |  | ||||||
| package wintray |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"unsafe" |  | ||||||
|  |  | ||||||
| 	"golang.org/x/sys/windows" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	updatAvailableMenuID = 1 |  | ||||||
| 	updateMenuID         = updatAvailableMenuID + 1 |  | ||||||
| 	separatorMenuID      = updateMenuID + 1 |  | ||||||
| 	diagLogsMenuID       = separatorMenuID + 1 |  | ||||||
| 	diagSeparatorMenuID  = diagLogsMenuID + 1 |  | ||||||
| 	quitMenuID           = diagSeparatorMenuID + 1 |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func (t *winTray) initMenus() error { |  | ||||||
| 	if err := t.addOrUpdateMenuItem(diagLogsMenuID, 0, diagLogsMenuTitle, false); err != nil { |  | ||||||
| 		return fmt.Errorf("unable to create menu entries %w\n", err) |  | ||||||
| 	} |  | ||||||
| 	if err := t.addSeparatorMenuItem(diagSeparatorMenuID, 0); err != nil { |  | ||||||
| 		return fmt.Errorf("unable to create menu entries %w", err) |  | ||||||
| 	} |  | ||||||
| 	if err := t.addOrUpdateMenuItem(quitMenuID, 0, quitMenuTitle, false); err != nil { |  | ||||||
| 		return fmt.Errorf("unable to create menu entries %w\n", err) |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (t *winTray) UpdateAvailable(ver string) error { |  | ||||||
| 	if !t.updateNotified { |  | ||||||
| 		slog.Debug("updating menu and sending notification for new update") |  | ||||||
| 		if err := t.addOrUpdateMenuItem(updatAvailableMenuID, 0, updateAvailableMenuTitle, true); err != nil { |  | ||||||
| 			return fmt.Errorf("unable to create menu entries %w", err) |  | ||||||
| 		} |  | ||||||
| 		if err := t.addOrUpdateMenuItem(updateMenuID, 0, updateMenutTitle, false); err != nil { |  | ||||||
| 			return fmt.Errorf("unable to create menu entries %w", err) |  | ||||||
| 		} |  | ||||||
| 		if err := t.addSeparatorMenuItem(separatorMenuID, 0); err != nil { |  | ||||||
| 			return fmt.Errorf("unable to create menu entries %w", err) |  | ||||||
| 		} |  | ||||||
| 		iconFilePath, err := iconBytesToFilePath(wt.updateIcon) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("unable to write icon data to temp file: %w", err) |  | ||||||
| 		} |  | ||||||
| 		if err := wt.setIcon(iconFilePath); err != nil { |  | ||||||
| 			return fmt.Errorf("unable to set icon: %w", err) |  | ||||||
| 		} |  | ||||||
| 		t.updateNotified = true |  | ||||||
|  |  | ||||||
| 		t.pendingUpdate = true |  | ||||||
| 		// Now pop up the notification |  | ||||||
| 		t.muNID.Lock() |  | ||||||
| 		defer t.muNID.Unlock() |  | ||||||
| 		copy(t.nid.InfoTitle[:], windows.StringToUTF16(updateTitle)) |  | ||||||
| 		copy(t.nid.Info[:], windows.StringToUTF16(fmt.Sprintf(updateMessage, ver))) |  | ||||||
| 		t.nid.Flags |= NIF_INFO |  | ||||||
| 		t.nid.Timeout = 10 |  | ||||||
| 		t.nid.Size = uint32(unsafe.Sizeof(*wt.nid)) |  | ||||||
| 		err = t.nid.modify() |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @@ -1,15 +0,0 @@ | |||||||
| //go:build windows |  | ||||||
|  |  | ||||||
| package wintray |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	firstTimeTitle   = "Ollama is running" |  | ||||||
| 	firstTimeMessage = "Click here to get started" |  | ||||||
| 	updateTitle      = "Update available" |  | ||||||
| 	updateMessage    = "Ollama version %s is ready to install" |  | ||||||
|  |  | ||||||
| 	quitMenuTitle            = "Quit Ollama" |  | ||||||
| 	updateAvailableMenuTitle = "An update is available" |  | ||||||
| 	updateMenutTitle         = "Restart to update" |  | ||||||
| 	diagLogsMenuTitle        = "View logs" |  | ||||||
| ) |  | ||||||
| @@ -1,66 +0,0 @@ | |||||||
| //go:build windows |  | ||||||
|  |  | ||||||
| package wintray |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"unsafe" |  | ||||||
|  |  | ||||||
| 	"golang.org/x/sys/windows" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // Contains information that the system needs to display notifications in the notification area. |  | ||||||
| // Used by Shell_NotifyIcon. |  | ||||||
| // https://msdn.microsoft.com/en-us/library/windows/desktop/bb773352(v=vs.85).aspx |  | ||||||
| // https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159 |  | ||||||
| type notifyIconData struct { |  | ||||||
| 	Size                       uint32 |  | ||||||
| 	Wnd                        windows.Handle |  | ||||||
| 	ID, Flags, CallbackMessage uint32 |  | ||||||
| 	Icon                       windows.Handle |  | ||||||
| 	Tip                        [128]uint16 |  | ||||||
| 	State, StateMask           uint32 |  | ||||||
| 	Info                       [256]uint16 |  | ||||||
| 	// Timeout, Version           uint32 |  | ||||||
| 	Timeout uint32 |  | ||||||
|  |  | ||||||
| 	InfoTitle   [64]uint16 |  | ||||||
| 	InfoFlags   uint32 |  | ||||||
| 	GuidItem    windows.GUID |  | ||||||
| 	BalloonIcon windows.Handle |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (nid *notifyIconData) add() error { |  | ||||||
| 	const NIM_ADD = 0x00000000 |  | ||||||
| 	res, _, err := pShellNotifyIcon.Call( |  | ||||||
| 		uintptr(NIM_ADD), |  | ||||||
| 		uintptr(unsafe.Pointer(nid)), |  | ||||||
| 	) |  | ||||||
| 	if res == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (nid *notifyIconData) modify() error { |  | ||||||
| 	const NIM_MODIFY = 0x00000001 |  | ||||||
| 	res, _, err := pShellNotifyIcon.Call( |  | ||||||
| 		uintptr(NIM_MODIFY), |  | ||||||
| 		uintptr(unsafe.Pointer(nid)), |  | ||||||
| 	) |  | ||||||
| 	if res == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (nid *notifyIconData) delete() error { |  | ||||||
| 	const NIM_DELETE = 0x00000002 |  | ||||||
| 	res, _, err := pShellNotifyIcon.Call( |  | ||||||
| 		uintptr(NIM_DELETE), |  | ||||||
| 		uintptr(unsafe.Pointer(nid)), |  | ||||||
| 	) |  | ||||||
| 	if res == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @@ -1,485 +0,0 @@ | |||||||
| //go:build windows |  | ||||||
|  |  | ||||||
| package wintray |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"crypto/md5" |  | ||||||
| 	"encoding/hex" |  | ||||||
| 	"fmt" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"sort" |  | ||||||
| 	"sync" |  | ||||||
| 	"unsafe" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/app/tray/commontray" |  | ||||||
| 	"golang.org/x/sys/windows" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // Helpful sources: https://github.com/golang/exp/blob/master/shiny/driver/internal/win32 |  | ||||||
|  |  | ||||||
| // Contains information about loaded resources |  | ||||||
| type winTray struct { |  | ||||||
| 	instance, |  | ||||||
| 	icon, |  | ||||||
| 	cursor, |  | ||||||
| 	window windows.Handle |  | ||||||
|  |  | ||||||
| 	loadedImages   map[string]windows.Handle |  | ||||||
| 	muLoadedImages sync.RWMutex |  | ||||||
|  |  | ||||||
| 	// menus keeps track of the submenus keyed by the menu item ID, plus 0 |  | ||||||
| 	// which corresponds to the main popup menu. |  | ||||||
| 	menus    map[uint32]windows.Handle |  | ||||||
| 	muMenus  sync.RWMutex |  | ||||||
| 	menuOf   map[uint32]windows.Handle |  | ||||||
| 	muMenuOf sync.RWMutex |  | ||||||
| 	// menuItemIcons maintains the bitmap of each menu item (if applies). It's |  | ||||||
| 	// needed to show the icon correctly when showing a previously hidden menu |  | ||||||
| 	// item again. |  | ||||||
| 	// menuItemIcons   map[uint32]windows.Handle |  | ||||||
| 	// muMenuItemIcons sync.RWMutex |  | ||||||
| 	visibleItems   map[uint32][]uint32 |  | ||||||
| 	muVisibleItems sync.RWMutex |  | ||||||
|  |  | ||||||
| 	nid   *notifyIconData |  | ||||||
| 	muNID sync.RWMutex |  | ||||||
| 	wcex  *wndClassEx |  | ||||||
|  |  | ||||||
| 	wmSystrayMessage, |  | ||||||
| 	wmTaskbarCreated uint32 |  | ||||||
|  |  | ||||||
| 	pendingUpdate  bool |  | ||||||
| 	updateNotified bool // Only pop up the notification once - TODO consider daily nag? |  | ||||||
| 	// Callbacks |  | ||||||
| 	callbacks  commontray.Callbacks |  | ||||||
| 	normalIcon []byte |  | ||||||
| 	updateIcon []byte |  | ||||||
| } |  | ||||||
|  |  | ||||||
| var wt winTray |  | ||||||
|  |  | ||||||
| func (t *winTray) GetCallbacks() commontray.Callbacks { |  | ||||||
| 	return t.callbacks |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func InitTray(icon, updateIcon []byte) (*winTray, error) { |  | ||||||
| 	wt.callbacks.Quit = make(chan struct{}) |  | ||||||
| 	wt.callbacks.Update = make(chan struct{}) |  | ||||||
| 	wt.callbacks.ShowLogs = make(chan struct{}) |  | ||||||
| 	wt.callbacks.DoFirstUse = make(chan struct{}) |  | ||||||
| 	wt.normalIcon = icon |  | ||||||
| 	wt.updateIcon = updateIcon |  | ||||||
| 	if err := wt.initInstance(); err != nil { |  | ||||||
| 		return nil, fmt.Errorf("Unable to init instance: %w\n", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := wt.createMenu(); err != nil { |  | ||||||
| 		return nil, fmt.Errorf("Unable to create menu: %w\n", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	iconFilePath, err := iconBytesToFilePath(wt.normalIcon) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("Unable to write icon data to temp file: %w", err) |  | ||||||
| 	} |  | ||||||
| 	if err := wt.setIcon(iconFilePath); err != nil { |  | ||||||
| 		return nil, fmt.Errorf("Unable to set icon: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return &wt, wt.initMenus() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (t *winTray) initInstance() error { |  | ||||||
| 	const ( |  | ||||||
| 		className  = "OllamaClass" |  | ||||||
| 		windowName = "" |  | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	t.wmSystrayMessage = WM_USER + 1 |  | ||||||
| 	t.visibleItems = make(map[uint32][]uint32) |  | ||||||
| 	t.menus = make(map[uint32]windows.Handle) |  | ||||||
| 	t.menuOf = make(map[uint32]windows.Handle) |  | ||||||
|  |  | ||||||
| 	t.loadedImages = make(map[string]windows.Handle) |  | ||||||
|  |  | ||||||
| 	taskbarEventNamePtr, _ := windows.UTF16PtrFromString("TaskbarCreated") |  | ||||||
| 	// https://msdn.microsoft.com/en-us/library/windows/desktop/ms644947 |  | ||||||
| 	res, _, err := pRegisterWindowMessage.Call( |  | ||||||
| 		uintptr(unsafe.Pointer(taskbarEventNamePtr)), |  | ||||||
| 	) |  | ||||||
| 	if res == 0 { // success 0xc000-0xfff |  | ||||||
| 		return fmt.Errorf("failed to register window: %w", err) |  | ||||||
| 	} |  | ||||||
| 	t.wmTaskbarCreated = uint32(res) |  | ||||||
|  |  | ||||||
| 	instanceHandle, _, err := pGetModuleHandle.Call(0) |  | ||||||
| 	if instanceHandle == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	t.instance = windows.Handle(instanceHandle) |  | ||||||
|  |  | ||||||
| 	// https://msdn.microsoft.com/en-us/library/windows/desktop/ms648072(v=vs.85).aspx |  | ||||||
| 	iconHandle, _, err := pLoadIcon.Call(0, uintptr(IDI_APPLICATION)) |  | ||||||
| 	if iconHandle == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	t.icon = windows.Handle(iconHandle) |  | ||||||
|  |  | ||||||
| 	// https://msdn.microsoft.com/en-us/library/windows/desktop/ms648391(v=vs.85).aspx |  | ||||||
| 	cursorHandle, _, err := pLoadCursor.Call(0, uintptr(IDC_ARROW)) |  | ||||||
| 	if cursorHandle == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	t.cursor = windows.Handle(cursorHandle) |  | ||||||
|  |  | ||||||
| 	classNamePtr, err := windows.UTF16PtrFromString(className) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	windowNamePtr, err := windows.UTF16PtrFromString(windowName) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	t.wcex = &wndClassEx{ |  | ||||||
| 		Style:      CS_HREDRAW | CS_VREDRAW, |  | ||||||
| 		WndProc:    windows.NewCallback(t.wndProc), |  | ||||||
| 		Instance:   t.instance, |  | ||||||
| 		Icon:       t.icon, |  | ||||||
| 		Cursor:     t.cursor, |  | ||||||
| 		Background: windows.Handle(6), // (COLOR_WINDOW + 1) |  | ||||||
| 		ClassName:  classNamePtr, |  | ||||||
| 		IconSm:     t.icon, |  | ||||||
| 	} |  | ||||||
| 	if err := t.wcex.register(); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	windowHandle, _, err := pCreateWindowEx.Call( |  | ||||||
| 		uintptr(0), |  | ||||||
| 		uintptr(unsafe.Pointer(classNamePtr)), |  | ||||||
| 		uintptr(unsafe.Pointer(windowNamePtr)), |  | ||||||
| 		uintptr(WS_OVERLAPPEDWINDOW), |  | ||||||
| 		uintptr(CW_USEDEFAULT), |  | ||||||
| 		uintptr(CW_USEDEFAULT), |  | ||||||
| 		uintptr(CW_USEDEFAULT), |  | ||||||
| 		uintptr(CW_USEDEFAULT), |  | ||||||
| 		uintptr(0), |  | ||||||
| 		uintptr(0), |  | ||||||
| 		uintptr(t.instance), |  | ||||||
| 		uintptr(0), |  | ||||||
| 	) |  | ||||||
| 	if windowHandle == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	t.window = windows.Handle(windowHandle) |  | ||||||
|  |  | ||||||
| 	pShowWindow.Call(uintptr(t.window), uintptr(SW_HIDE)) //nolint:errcheck |  | ||||||
|  |  | ||||||
| 	boolRet, _, err := pUpdateWindow.Call(uintptr(t.window)) |  | ||||||
| 	if boolRet == 0 { |  | ||||||
| 		slog.Error(fmt.Sprintf("failed to update window: %s", err)) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	t.muNID.Lock() |  | ||||||
| 	defer t.muNID.Unlock() |  | ||||||
| 	t.nid = ¬ifyIconData{ |  | ||||||
| 		Wnd:             windows.Handle(t.window), |  | ||||||
| 		ID:              100, |  | ||||||
| 		Flags:           NIF_MESSAGE, |  | ||||||
| 		CallbackMessage: t.wmSystrayMessage, |  | ||||||
| 	} |  | ||||||
| 	t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) |  | ||||||
|  |  | ||||||
| 	return t.nid.add() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (t *winTray) createMenu() error { |  | ||||||
|  |  | ||||||
| 	menuHandle, _, err := pCreatePopupMenu.Call() |  | ||||||
| 	if menuHandle == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	t.menus[0] = windows.Handle(menuHandle) |  | ||||||
|  |  | ||||||
| 	// https://msdn.microsoft.com/en-us/library/windows/desktop/ms647575(v=vs.85).aspx |  | ||||||
| 	mi := struct { |  | ||||||
| 		Size, Mask, Style, Max uint32 |  | ||||||
| 		Background             windows.Handle |  | ||||||
| 		ContextHelpID          uint32 |  | ||||||
| 		MenuData               uintptr |  | ||||||
| 	}{ |  | ||||||
| 		Mask: MIM_APPLYTOSUBMENUS, |  | ||||||
| 	} |  | ||||||
| 	mi.Size = uint32(unsafe.Sizeof(mi)) |  | ||||||
|  |  | ||||||
| 	res, _, err := pSetMenuInfo.Call( |  | ||||||
| 		uintptr(t.menus[0]), |  | ||||||
| 		uintptr(unsafe.Pointer(&mi)), |  | ||||||
| 	) |  | ||||||
| 	if res == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Contains information about a menu item. |  | ||||||
| // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647578(v=vs.85).aspx |  | ||||||
| type menuItemInfo struct { |  | ||||||
| 	Size, Mask, Type, State     uint32 |  | ||||||
| 	ID                          uint32 |  | ||||||
| 	SubMenu, Checked, Unchecked windows.Handle |  | ||||||
| 	ItemData                    uintptr |  | ||||||
| 	TypeData                    *uint16 |  | ||||||
| 	Cch                         uint32 |  | ||||||
| 	BMPItem                     windows.Handle |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (t *winTray) addOrUpdateMenuItem(menuItemId uint32, parentId uint32, title string, disabled bool) error { |  | ||||||
| 	titlePtr, err := windows.UTF16PtrFromString(title) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	mi := menuItemInfo{ |  | ||||||
| 		Mask:     MIIM_FTYPE | MIIM_STRING | MIIM_ID | MIIM_STATE, |  | ||||||
| 		Type:     MFT_STRING, |  | ||||||
| 		ID:       uint32(menuItemId), |  | ||||||
| 		TypeData: titlePtr, |  | ||||||
| 		Cch:      uint32(len(title)), |  | ||||||
| 	} |  | ||||||
| 	mi.Size = uint32(unsafe.Sizeof(mi)) |  | ||||||
| 	if disabled { |  | ||||||
| 		mi.State |= MFS_DISABLED |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var res uintptr |  | ||||||
| 	t.muMenus.RLock() |  | ||||||
| 	menu := t.menus[parentId] |  | ||||||
| 	t.muMenus.RUnlock() |  | ||||||
| 	if t.getVisibleItemIndex(parentId, menuItemId) != -1 { |  | ||||||
| 		// We set the menu item info based on the menuID |  | ||||||
| 		boolRet, _, err := pSetMenuItemInfo.Call( |  | ||||||
| 			uintptr(menu), |  | ||||||
| 			uintptr(menuItemId), |  | ||||||
| 			0, |  | ||||||
| 			uintptr(unsafe.Pointer(&mi)), |  | ||||||
| 		) |  | ||||||
| 		if boolRet == 0 { |  | ||||||
| 			return fmt.Errorf("failed to set menu item: %w", err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if res == 0 { |  | ||||||
| 		// Menu item does not already exist, create it |  | ||||||
| 		t.muMenus.RLock() |  | ||||||
| 		submenu, exists := t.menus[menuItemId] |  | ||||||
| 		t.muMenus.RUnlock() |  | ||||||
| 		if exists { |  | ||||||
| 			mi.Mask |= MIIM_SUBMENU |  | ||||||
| 			mi.SubMenu = submenu |  | ||||||
| 		} |  | ||||||
| 		t.addToVisibleItems(parentId, menuItemId) |  | ||||||
| 		position := t.getVisibleItemIndex(parentId, menuItemId) |  | ||||||
| 		res, _, err = pInsertMenuItem.Call( |  | ||||||
| 			uintptr(menu), |  | ||||||
| 			uintptr(position), |  | ||||||
| 			1, |  | ||||||
| 			uintptr(unsafe.Pointer(&mi)), |  | ||||||
| 		) |  | ||||||
| 		if res == 0 { |  | ||||||
| 			t.delFromVisibleItems(parentId, menuItemId) |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 		t.muMenuOf.Lock() |  | ||||||
| 		t.menuOf[menuItemId] = menu |  | ||||||
| 		t.muMenuOf.Unlock() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (t *winTray) addSeparatorMenuItem(menuItemId, parentId uint32) error { |  | ||||||
|  |  | ||||||
| 	mi := menuItemInfo{ |  | ||||||
| 		Mask: MIIM_FTYPE | MIIM_ID | MIIM_STATE, |  | ||||||
| 		Type: MFT_SEPARATOR, |  | ||||||
| 		ID:   uint32(menuItemId), |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	mi.Size = uint32(unsafe.Sizeof(mi)) |  | ||||||
|  |  | ||||||
| 	t.addToVisibleItems(parentId, menuItemId) |  | ||||||
| 	position := t.getVisibleItemIndex(parentId, menuItemId) |  | ||||||
| 	t.muMenus.RLock() |  | ||||||
| 	menu := uintptr(t.menus[parentId]) |  | ||||||
| 	t.muMenus.RUnlock() |  | ||||||
| 	res, _, err := pInsertMenuItem.Call( |  | ||||||
| 		menu, |  | ||||||
| 		uintptr(position), |  | ||||||
| 		1, |  | ||||||
| 		uintptr(unsafe.Pointer(&mi)), |  | ||||||
| 	) |  | ||||||
| 	if res == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // func (t *winTray) hideMenuItem(menuItemId, parentId uint32) error { |  | ||||||
| // 	const ERROR_SUCCESS syscall.Errno = 0 |  | ||||||
|  |  | ||||||
| // 	t.muMenus.RLock() |  | ||||||
| // 	menu := uintptr(t.menus[parentId]) |  | ||||||
| // 	t.muMenus.RUnlock() |  | ||||||
| // 	res, _, err := pRemoveMenu.Call( |  | ||||||
| // 		menu, |  | ||||||
| // 		uintptr(menuItemId), |  | ||||||
| // 		MF_BYCOMMAND, |  | ||||||
| // 	) |  | ||||||
| // 	if res == 0 && err.(syscall.Errno) != ERROR_SUCCESS { |  | ||||||
| // 		return err |  | ||||||
| // 	} |  | ||||||
| // 	t.delFromVisibleItems(parentId, menuItemId) |  | ||||||
|  |  | ||||||
| // 	return nil |  | ||||||
| // } |  | ||||||
|  |  | ||||||
| func (t *winTray) showMenu() error { |  | ||||||
| 	p := point{} |  | ||||||
| 	boolRet, _, err := pGetCursorPos.Call(uintptr(unsafe.Pointer(&p))) |  | ||||||
| 	if boolRet == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	boolRet, _, err = pSetForegroundWindow.Call(uintptr(t.window)) |  | ||||||
| 	if boolRet == 0 { |  | ||||||
| 		slog.Warn(fmt.Sprintf("failed to bring menu to foreground: %s", err)) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	boolRet, _, err = pTrackPopupMenu.Call( |  | ||||||
| 		uintptr(t.menus[0]), |  | ||||||
| 		TPM_BOTTOMALIGN|TPM_LEFTALIGN, |  | ||||||
| 		uintptr(p.X), |  | ||||||
| 		uintptr(p.Y), |  | ||||||
| 		0, |  | ||||||
| 		uintptr(t.window), |  | ||||||
| 		0, |  | ||||||
| 	) |  | ||||||
| 	if boolRet == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (t *winTray) delFromVisibleItems(parent, val uint32) { |  | ||||||
| 	t.muVisibleItems.Lock() |  | ||||||
| 	defer t.muVisibleItems.Unlock() |  | ||||||
| 	visibleItems := t.visibleItems[parent] |  | ||||||
| 	for i, itemval := range visibleItems { |  | ||||||
| 		if val == itemval { |  | ||||||
| 			t.visibleItems[parent] = append(visibleItems[:i], visibleItems[i+1:]...) |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (t *winTray) addToVisibleItems(parent, val uint32) { |  | ||||||
| 	t.muVisibleItems.Lock() |  | ||||||
| 	defer t.muVisibleItems.Unlock() |  | ||||||
| 	if visibleItems, exists := t.visibleItems[parent]; !exists { |  | ||||||
| 		t.visibleItems[parent] = []uint32{val} |  | ||||||
| 	} else { |  | ||||||
| 		newvisible := append(visibleItems, val) |  | ||||||
| 		sort.Slice(newvisible, func(i, j int) bool { return newvisible[i] < newvisible[j] }) |  | ||||||
| 		t.visibleItems[parent] = newvisible |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (t *winTray) getVisibleItemIndex(parent, val uint32) int { |  | ||||||
| 	t.muVisibleItems.RLock() |  | ||||||
| 	defer t.muVisibleItems.RUnlock() |  | ||||||
| 	for i, itemval := range t.visibleItems[parent] { |  | ||||||
| 		if val == itemval { |  | ||||||
| 			return i |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return -1 |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func iconBytesToFilePath(iconBytes []byte) (string, error) { |  | ||||||
| 	bh := md5.Sum(iconBytes) |  | ||||||
| 	dataHash := hex.EncodeToString(bh[:]) |  | ||||||
| 	iconFilePath := filepath.Join(os.TempDir(), "ollama_temp_icon_"+dataHash) |  | ||||||
|  |  | ||||||
| 	if _, err := os.Stat(iconFilePath); os.IsNotExist(err) { |  | ||||||
| 		if err := os.WriteFile(iconFilePath, iconBytes, 0644); err != nil { |  | ||||||
| 			return "", err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return iconFilePath, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Loads an image from file and shows it in tray. |  | ||||||
| // Shell_NotifyIcon: https://msdn.microsoft.com/en-us/library/windows/desktop/bb762159(v=vs.85).aspx |  | ||||||
| func (t *winTray) setIcon(src string) error { |  | ||||||
|  |  | ||||||
| 	h, err := t.loadIconFrom(src) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	t.muNID.Lock() |  | ||||||
| 	defer t.muNID.Unlock() |  | ||||||
| 	t.nid.Icon = h |  | ||||||
| 	t.nid.Flags |= NIF_ICON |  | ||||||
| 	t.nid.Size = uint32(unsafe.Sizeof(*t.nid)) |  | ||||||
|  |  | ||||||
| 	return t.nid.modify() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Loads an image from file to be shown in tray or menu item. |  | ||||||
| // LoadImage: https://msdn.microsoft.com/en-us/library/windows/desktop/ms648045(v=vs.85).aspx |  | ||||||
| func (t *winTray) loadIconFrom(src string) (windows.Handle, error) { |  | ||||||
|  |  | ||||||
| 	// Save and reuse handles of loaded images |  | ||||||
| 	t.muLoadedImages.RLock() |  | ||||||
| 	h, ok := t.loadedImages[src] |  | ||||||
| 	t.muLoadedImages.RUnlock() |  | ||||||
| 	if !ok { |  | ||||||
| 		srcPtr, err := windows.UTF16PtrFromString(src) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return 0, err |  | ||||||
| 		} |  | ||||||
| 		res, _, err := pLoadImage.Call( |  | ||||||
| 			0, |  | ||||||
| 			uintptr(unsafe.Pointer(srcPtr)), |  | ||||||
| 			IMAGE_ICON, |  | ||||||
| 			0, |  | ||||||
| 			0, |  | ||||||
| 			LR_LOADFROMFILE|LR_DEFAULTSIZE, |  | ||||||
| 		) |  | ||||||
| 		if res == 0 { |  | ||||||
| 			return 0, err |  | ||||||
| 		} |  | ||||||
| 		h = windows.Handle(res) |  | ||||||
| 		t.muLoadedImages.Lock() |  | ||||||
| 		t.loadedImages[src] = h |  | ||||||
| 		t.muLoadedImages.Unlock() |  | ||||||
| 	} |  | ||||||
| 	return h, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (t *winTray) DisplayFirstUseNotification() error { |  | ||||||
| 	t.muNID.Lock() |  | ||||||
| 	defer t.muNID.Unlock() |  | ||||||
| 	copy(t.nid.InfoTitle[:], windows.StringToUTF16(firstTimeTitle)) |  | ||||||
| 	copy(t.nid.Info[:], windows.StringToUTF16(firstTimeMessage)) |  | ||||||
| 	t.nid.Flags |= NIF_INFO |  | ||||||
| 	t.nid.Size = uint32(unsafe.Sizeof(*wt.nid)) |  | ||||||
|  |  | ||||||
| 	return t.nid.modify() |  | ||||||
| } |  | ||||||
| @@ -1,89 +0,0 @@ | |||||||
| //go:build windows |  | ||||||
|  |  | ||||||
| package wintray |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"runtime" |  | ||||||
|  |  | ||||||
| 	"golang.org/x/sys/windows" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	k32 = windows.NewLazySystemDLL("Kernel32.dll") |  | ||||||
| 	u32 = windows.NewLazySystemDLL("User32.dll") |  | ||||||
| 	s32 = windows.NewLazySystemDLL("Shell32.dll") |  | ||||||
|  |  | ||||||
| 	pCreatePopupMenu       = u32.NewProc("CreatePopupMenu") |  | ||||||
| 	pCreateWindowEx        = u32.NewProc("CreateWindowExW") |  | ||||||
| 	pDefWindowProc         = u32.NewProc("DefWindowProcW") |  | ||||||
| 	pDestroyWindow         = u32.NewProc("DestroyWindow") |  | ||||||
| 	pDispatchMessage       = u32.NewProc("DispatchMessageW") |  | ||||||
| 	pGetCursorPos          = u32.NewProc("GetCursorPos") |  | ||||||
| 	pGetMessage            = u32.NewProc("GetMessageW") |  | ||||||
| 	pGetModuleHandle       = k32.NewProc("GetModuleHandleW") |  | ||||||
| 	pInsertMenuItem        = u32.NewProc("InsertMenuItemW") |  | ||||||
| 	pLoadCursor            = u32.NewProc("LoadCursorW") |  | ||||||
| 	pLoadIcon              = u32.NewProc("LoadIconW") |  | ||||||
| 	pLoadImage             = u32.NewProc("LoadImageW") |  | ||||||
| 	pPostMessage           = u32.NewProc("PostMessageW") |  | ||||||
| 	pPostQuitMessage       = u32.NewProc("PostQuitMessage") |  | ||||||
| 	pRegisterClass         = u32.NewProc("RegisterClassExW") |  | ||||||
| 	pRegisterWindowMessage = u32.NewProc("RegisterWindowMessageW") |  | ||||||
| 	pSetForegroundWindow   = u32.NewProc("SetForegroundWindow") |  | ||||||
| 	pSetMenuInfo           = u32.NewProc("SetMenuInfo") |  | ||||||
| 	pSetMenuItemInfo       = u32.NewProc("SetMenuItemInfoW") |  | ||||||
| 	pShellNotifyIcon       = s32.NewProc("Shell_NotifyIconW") |  | ||||||
| 	pShowWindow            = u32.NewProc("ShowWindow") |  | ||||||
| 	pTrackPopupMenu        = u32.NewProc("TrackPopupMenu") |  | ||||||
| 	pTranslateMessage      = u32.NewProc("TranslateMessage") |  | ||||||
| 	pUnregisterClass       = u32.NewProc("UnregisterClassW") |  | ||||||
| 	pUpdateWindow          = u32.NewProc("UpdateWindow") |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	CS_HREDRAW          = 0x0002 |  | ||||||
| 	CS_VREDRAW          = 0x0001 |  | ||||||
| 	CW_USEDEFAULT       = 0x80000000 |  | ||||||
| 	IDC_ARROW           = 32512 // Standard arrow |  | ||||||
| 	IDI_APPLICATION     = 32512 |  | ||||||
| 	IMAGE_ICON          = 1          // Loads an icon |  | ||||||
| 	LR_DEFAULTSIZE      = 0x00000040 // Loads default-size icon for windows(SM_CXICON x SM_CYICON) if cx, cy are set to zero |  | ||||||
| 	LR_LOADFROMFILE     = 0x00000010 // Loads the stand-alone image from the file |  | ||||||
| 	MF_BYCOMMAND        = 0x00000000 |  | ||||||
| 	MFS_DISABLED        = 0x00000003 |  | ||||||
| 	MFT_SEPARATOR       = 0x00000800 |  | ||||||
| 	MFT_STRING          = 0x00000000 |  | ||||||
| 	MIIM_BITMAP         = 0x00000080 |  | ||||||
| 	MIIM_FTYPE          = 0x00000100 |  | ||||||
| 	MIIM_ID             = 0x00000002 |  | ||||||
| 	MIIM_STATE          = 0x00000001 |  | ||||||
| 	MIIM_STRING         = 0x00000040 |  | ||||||
| 	MIIM_SUBMENU        = 0x00000004 |  | ||||||
| 	MIM_APPLYTOSUBMENUS = 0x80000000 |  | ||||||
| 	NIF_ICON            = 0x00000002 |  | ||||||
| 	NIF_INFO            = 0x00000010 |  | ||||||
| 	NIF_MESSAGE         = 0x00000001 |  | ||||||
| 	SW_HIDE             = 0 |  | ||||||
| 	TPM_BOTTOMALIGN     = 0x0020 |  | ||||||
| 	TPM_LEFTALIGN       = 0x0000 |  | ||||||
| 	WM_CLOSE            = 0x0010 |  | ||||||
| 	WM_USER             = 0x0400 |  | ||||||
| 	WS_CAPTION          = 0x00C00000 |  | ||||||
| 	WS_MAXIMIZEBOX      = 0x00010000 |  | ||||||
| 	WS_MINIMIZEBOX      = 0x00020000 |  | ||||||
| 	WS_OVERLAPPED       = 0x00000000 |  | ||||||
| 	WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX |  | ||||||
| 	WS_SYSMENU          = 0x00080000 |  | ||||||
| 	WS_THICKFRAME       = 0x00040000 |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // Not sure if this is actually needed on windows |  | ||||||
| func init() { |  | ||||||
| 	runtime.LockOSThread() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // The POINT structure defines the x- and y- coordinates of a point. |  | ||||||
| // https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx |  | ||||||
| type point struct { |  | ||||||
| 	X, Y int32 |  | ||||||
| } |  | ||||||
| @@ -1,45 +0,0 @@ | |||||||
| //go:build windows |  | ||||||
|  |  | ||||||
| package wintray |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"unsafe" |  | ||||||
|  |  | ||||||
| 	"golang.org/x/sys/windows" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // Contains window class information. |  | ||||||
| // It is used with the RegisterClassEx and GetClassInfoEx functions. |  | ||||||
| // https://msdn.microsoft.com/en-us/library/ms633577.aspx |  | ||||||
| type wndClassEx struct { |  | ||||||
| 	Size, Style                        uint32 |  | ||||||
| 	WndProc                            uintptr |  | ||||||
| 	ClsExtra, WndExtra                 int32 |  | ||||||
| 	Instance, Icon, Cursor, Background windows.Handle |  | ||||||
| 	MenuName, ClassName                *uint16 |  | ||||||
| 	IconSm                             windows.Handle |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Registers a window class for subsequent use in calls to the CreateWindow or CreateWindowEx function. |  | ||||||
| // https://msdn.microsoft.com/en-us/library/ms633587.aspx |  | ||||||
| func (w *wndClassEx) register() error { |  | ||||||
| 	w.Size = uint32(unsafe.Sizeof(*w)) |  | ||||||
| 	res, _, err := pRegisterClass.Call(uintptr(unsafe.Pointer(w))) |  | ||||||
| 	if res == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Unregisters a window class, freeing the memory required for the class. |  | ||||||
| // https://msdn.microsoft.com/en-us/library/ms644899.aspx |  | ||||||
| func (w *wndClassEx) unregister() error { |  | ||||||
| 	res, _, err := pUnregisterClass.Call( |  | ||||||
| 		uintptr(unsafe.Pointer(w.ClassName)), |  | ||||||
| 		uintptr(w.Instance), |  | ||||||
| 	) |  | ||||||
| 	if res == 0 { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
							
								
								
									
										91
									
								
								auth/auth.go
									
									
									
									
									
								
							
							
						
						| @@ -1,91 +0,0 @@ | |||||||
| package auth |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"context" |  | ||||||
| 	"crypto/rand" |  | ||||||
| 	"encoding/base64" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"golang.org/x/crypto/ssh" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const defaultPrivateKey = "id_ed25519" |  | ||||||
|  |  | ||||||
| func keyPath() (string, error) { |  | ||||||
| 	home, err := os.UserHomeDir() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return filepath.Join(home, ".ollama", defaultPrivateKey), nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func GetPublicKey() (string, error) { |  | ||||||
| 	keyPath, err := keyPath() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	privateKeyFile, err := os.ReadFile(keyPath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Info(fmt.Sprintf("Failed to load private key: %v", err)) |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	privateKey, err := ssh.ParsePrivateKey(privateKeyFile) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	publicKey := ssh.MarshalAuthorizedKey(privateKey.PublicKey()) |  | ||||||
|  |  | ||||||
| 	return strings.TrimSpace(string(publicKey)), nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func NewNonce(r io.Reader, length int) (string, error) { |  | ||||||
| 	nonce := make([]byte, length) |  | ||||||
| 	if _, err := io.ReadFull(r, nonce); err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return base64.RawURLEncoding.EncodeToString(nonce), nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func Sign(ctx context.Context, bts []byte) (string, error) { |  | ||||||
| 	keyPath, err := keyPath() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	privateKeyFile, err := os.ReadFile(keyPath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		slog.Info(fmt.Sprintf("Failed to load private key: %v", err)) |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	privateKey, err := ssh.ParsePrivateKey(privateKeyFile) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// get the pubkey, but remove the type |  | ||||||
| 	publicKey := ssh.MarshalAuthorizedKey(privateKey.PublicKey()) |  | ||||||
| 	parts := bytes.Split(publicKey, []byte(" ")) |  | ||||||
| 	if len(parts) < 2 { |  | ||||||
| 		return "", fmt.Errorf("malformed public key") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	signedData, err := privateKey.Sign(rand.Reader, bts) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return "", err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// signature is <pubkey>:<signature> |  | ||||||
| 	return fmt.Sprintf("%s:%s", bytes.TrimSpace(parts[1]), base64.StdEncoding.EncodeToString(signedData.Blob)), nil |  | ||||||
| } |  | ||||||
							
								
								
									
										1221
									
								
								cmd/cmd.go
									
									
									
									
									
								
							
							
						
						| @@ -1,691 +0,0 @@ | |||||||
| package cmd |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"net/http" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"regexp" |  | ||||||
| 	"sort" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/spf13/cobra" |  | ||||||
| 	"golang.org/x/exp/slices" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/api" |  | ||||||
| 	"github.com/ollama/ollama/envconfig" |  | ||||||
| 	"github.com/ollama/ollama/progress" |  | ||||||
| 	"github.com/ollama/ollama/readline" |  | ||||||
| 	"github.com/ollama/ollama/types/errtypes" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type MultilineState int |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	MultilineNone MultilineState = iota |  | ||||||
| 	MultilinePrompt |  | ||||||
| 	MultilineSystem |  | ||||||
| 	MultilineTemplate |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func loadModel(cmd *cobra.Command, opts *runOptions) error { |  | ||||||
| 	client, err := api.ClientFromEnvironment() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	p := progress.NewProgress(os.Stderr) |  | ||||||
| 	defer p.StopAndClear() |  | ||||||
|  |  | ||||||
| 	spinner := progress.NewSpinner("") |  | ||||||
| 	p.Add("", spinner) |  | ||||||
|  |  | ||||||
| 	showReq := api.ShowRequest{Name: opts.Model} |  | ||||||
| 	showResp, err := client.Show(cmd.Context(), &showReq) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	opts.MultiModal = slices.Contains(showResp.Details.Families, "clip") |  | ||||||
| 	opts.ParentModel = showResp.Details.ParentModel |  | ||||||
|  |  | ||||||
| 	if len(showResp.Messages) > 0 { |  | ||||||
| 		opts.Messages = append(opts.Messages, showResp.Messages...) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	chatReq := &api.ChatRequest{ |  | ||||||
| 		Model:    opts.Model, |  | ||||||
| 		Messages: []api.Message{}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if opts.KeepAlive != nil { |  | ||||||
| 		chatReq.KeepAlive = opts.KeepAlive |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	err = client.Chat(cmd.Context(), chatReq, func(resp api.ChatResponse) error { |  | ||||||
| 		p.StopAndClear() |  | ||||||
| 		if len(opts.Messages) > 0 { |  | ||||||
| 			for _, msg := range opts.Messages { |  | ||||||
| 				switch msg.Role { |  | ||||||
| 				case "user": |  | ||||||
| 					fmt.Printf(">>> %s\n", msg.Content) |  | ||||||
| 				case "assistant": |  | ||||||
| 					state := &displayResponseState{} |  | ||||||
| 					displayResponse(msg.Content, opts.WordWrap, state) |  | ||||||
| 					fmt.Println() |  | ||||||
| 					fmt.Println() |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		return nil |  | ||||||
| 	}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func generateInteractive(cmd *cobra.Command, opts runOptions) error { |  | ||||||
| 	opts.Messages = make([]api.Message, 0) |  | ||||||
|  |  | ||||||
| 	err := loadModel(cmd, &opts) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	usage := func() { |  | ||||||
| 		fmt.Fprintln(os.Stderr, "Available Commands:") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set            Set session variables") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /show           Show model information") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /load <model>   Load a session or model") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /save <model>   Save your current session") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /clear          Clear session context") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /bye            Exit") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /?, /help       Help for a command") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /? shortcuts    Help for keyboard shortcuts") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "Use \"\"\" to begin a multi-line message.") |  | ||||||
|  |  | ||||||
| 		if opts.MultiModal { |  | ||||||
| 			fmt.Fprintf(os.Stderr, "Use %s to include .jpg or .png images.\n", filepath.FromSlash("/path/to/file")) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		fmt.Fprintln(os.Stderr, "") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	usageSet := func() { |  | ||||||
| 		fmt.Fprintln(os.Stderr, "Available Commands:") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter ...     Set a parameter") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set system <string>   Set system message") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set template <string> Set prompt template") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set history           Enable history") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set nohistory         Disable history") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set wordwrap          Enable wordwrap") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set nowordwrap        Disable wordwrap") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set format json       Enable JSON mode") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set noformat          Disable formatting") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set verbose           Show LLM stats") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set quiet             Disable LLM stats") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	usageShortcuts := func() { |  | ||||||
| 		fmt.Fprintln(os.Stderr, "Available keyboard shortcuts:") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  Ctrl + a            Move to the beginning of the line (Home)") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  Ctrl + e            Move to the end of the line (End)") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "   Alt + b            Move back (left) one word") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "   Alt + f            Move forward (right) one word") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  Ctrl + k            Delete the sentence after the cursor") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  Ctrl + u            Delete the sentence before the cursor") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  Ctrl + w            Delete the word before the cursor") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  Ctrl + l            Clear the screen") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  Ctrl + c            Stop the model from responding") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  Ctrl + d            Exit ollama (/bye)") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	usageShow := func() { |  | ||||||
| 		fmt.Fprintln(os.Stderr, "Available Commands:") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /show info         Show details for this model") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /show license      Show model license") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /show modelfile    Show Modelfile for this model") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /show parameters   Show parameters for this model") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /show system       Show system message") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /show template     Show prompt template") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// only list out the most common parameters |  | ||||||
| 	usageParameters := func() { |  | ||||||
| 		fmt.Fprintln(os.Stderr, "Available Parameters:") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter seed <int>             Random number seed") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter num_predict <int>      Max number of tokens to predict") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter top_k <int>            Pick from top k num of tokens") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter top_p <float>          Pick token based on sum of probabilities") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter num_ctx <int>          Set the context size") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter temperature <float>    Set creativity level") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter repeat_penalty <float> How strongly to penalize repetitions") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter repeat_last_n <int>    Set how far back to look for repetitions") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter num_gpu <int>          The number of layers to send to the GPU") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "  /set parameter stop <string> <string> ...   Set the stop parameters") |  | ||||||
| 		fmt.Fprintln(os.Stderr, "") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	scanner, err := readline.New(readline.Prompt{ |  | ||||||
| 		Prompt:         ">>> ", |  | ||||||
| 		AltPrompt:      "... ", |  | ||||||
| 		Placeholder:    "Send a message (/? for help)", |  | ||||||
| 		AltPlaceholder: `Use """ to end multi-line input`, |  | ||||||
| 	}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if envconfig.NoHistory { |  | ||||||
| 		scanner.HistoryDisable() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	fmt.Print(readline.StartBracketedPaste) |  | ||||||
| 	defer fmt.Printf(readline.EndBracketedPaste) |  | ||||||
|  |  | ||||||
| 	var sb strings.Builder |  | ||||||
| 	var multiline MultilineState |  | ||||||
|  |  | ||||||
| 	for { |  | ||||||
| 		line, err := scanner.Readline() |  | ||||||
| 		switch { |  | ||||||
| 		case errors.Is(err, io.EOF): |  | ||||||
| 			fmt.Println() |  | ||||||
| 			return nil |  | ||||||
| 		case errors.Is(err, readline.ErrInterrupt): |  | ||||||
| 			if line == "" { |  | ||||||
| 				fmt.Println("\nUse Ctrl + d or /bye to exit.") |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			scanner.Prompt.UseAlt = false |  | ||||||
| 			sb.Reset() |  | ||||||
|  |  | ||||||
| 			continue |  | ||||||
| 		case err != nil: |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		switch { |  | ||||||
| 		case multiline != MultilineNone: |  | ||||||
| 			// check if there's a multiline terminating string |  | ||||||
| 			before, ok := strings.CutSuffix(line, `"""`) |  | ||||||
| 			sb.WriteString(before) |  | ||||||
| 			if !ok { |  | ||||||
| 				fmt.Fprintln(&sb) |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			switch multiline { |  | ||||||
| 			case MultilineSystem: |  | ||||||
| 				opts.System = sb.String() |  | ||||||
| 				opts.Messages = append(opts.Messages, api.Message{Role: "system", Content: opts.System}) |  | ||||||
| 				fmt.Println("Set system message.") |  | ||||||
| 				sb.Reset() |  | ||||||
| 			case MultilineTemplate: |  | ||||||
| 				opts.Template = sb.String() |  | ||||||
| 				fmt.Println("Set prompt template.") |  | ||||||
| 				sb.Reset() |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			multiline = MultilineNone |  | ||||||
| 			scanner.Prompt.UseAlt = false |  | ||||||
| 		case strings.HasPrefix(line, `"""`): |  | ||||||
| 			line := strings.TrimPrefix(line, `"""`) |  | ||||||
| 			line, ok := strings.CutSuffix(line, `"""`) |  | ||||||
| 			sb.WriteString(line) |  | ||||||
| 			if !ok { |  | ||||||
| 				// no multiline terminating string; need more input |  | ||||||
| 				fmt.Fprintln(&sb) |  | ||||||
| 				multiline = MultilinePrompt |  | ||||||
| 				scanner.Prompt.UseAlt = true |  | ||||||
| 			} |  | ||||||
| 		case scanner.Pasting: |  | ||||||
| 			fmt.Fprintln(&sb, line) |  | ||||||
| 			continue |  | ||||||
| 		case strings.HasPrefix(line, "/list"): |  | ||||||
| 			args := strings.Fields(line) |  | ||||||
| 			if err := ListHandler(cmd, args[1:]); err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 		case strings.HasPrefix(line, "/load"): |  | ||||||
| 			args := strings.Fields(line) |  | ||||||
| 			if len(args) != 2 { |  | ||||||
| 				fmt.Println("Usage:\n  /load <modelname>") |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			opts.Model = args[1] |  | ||||||
| 			opts.Messages = []api.Message{} |  | ||||||
| 			fmt.Printf("Loading model '%s'\n", opts.Model) |  | ||||||
| 			if err := loadModel(cmd, &opts); err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			continue |  | ||||||
| 		case strings.HasPrefix(line, "/save"): |  | ||||||
| 			args := strings.Fields(line) |  | ||||||
| 			if len(args) != 2 { |  | ||||||
| 				fmt.Println("Usage:\n  /save <modelname>") |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			client, err := api.ClientFromEnvironment() |  | ||||||
| 			if err != nil { |  | ||||||
| 				fmt.Println("error: couldn't connect to ollama server") |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			req := &api.CreateRequest{ |  | ||||||
| 				Name:      args[1], |  | ||||||
| 				Modelfile: buildModelfile(opts), |  | ||||||
| 			} |  | ||||||
| 			fn := func(resp api.ProgressResponse) error { return nil } |  | ||||||
| 			err = client.Create(cmd.Context(), req, fn) |  | ||||||
| 			if err != nil { |  | ||||||
| 				if strings.Contains(err.Error(), errtypes.InvalidModelNameErrMsg) { |  | ||||||
| 					fmt.Printf("error: The model name '%s' is invalid\n", args[1]) |  | ||||||
| 					continue |  | ||||||
| 				} |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			fmt.Printf("Created new model '%s'\n", args[1]) |  | ||||||
| 			continue |  | ||||||
| 		case strings.HasPrefix(line, "/clear"): |  | ||||||
| 			opts.Messages = []api.Message{} |  | ||||||
| 			if opts.System != "" { |  | ||||||
| 				newMessage := api.Message{Role: "system", Content: opts.System} |  | ||||||
| 				opts.Messages = append(opts.Messages, newMessage) |  | ||||||
| 			} |  | ||||||
| 			fmt.Println("Cleared session context") |  | ||||||
| 			continue |  | ||||||
| 		case strings.HasPrefix(line, "/set"): |  | ||||||
| 			args := strings.Fields(line) |  | ||||||
| 			if len(args) > 1 { |  | ||||||
| 				switch args[1] { |  | ||||||
| 				case "history": |  | ||||||
| 					scanner.HistoryEnable() |  | ||||||
| 				case "nohistory": |  | ||||||
| 					scanner.HistoryDisable() |  | ||||||
| 				case "wordwrap": |  | ||||||
| 					opts.WordWrap = true |  | ||||||
| 					fmt.Println("Set 'wordwrap' mode.") |  | ||||||
| 				case "nowordwrap": |  | ||||||
| 					opts.WordWrap = false |  | ||||||
| 					fmt.Println("Set 'nowordwrap' mode.") |  | ||||||
| 				case "verbose": |  | ||||||
| 					if err := cmd.Flags().Set("verbose", "true"); err != nil { |  | ||||||
| 						return err |  | ||||||
| 					} |  | ||||||
| 					fmt.Println("Set 'verbose' mode.") |  | ||||||
| 				case "quiet": |  | ||||||
| 					if err := cmd.Flags().Set("verbose", "false"); err != nil { |  | ||||||
| 						return err |  | ||||||
| 					} |  | ||||||
| 					fmt.Println("Set 'quiet' mode.") |  | ||||||
| 				case "format": |  | ||||||
| 					if len(args) < 3 || args[2] != "json" { |  | ||||||
| 						fmt.Println("Invalid or missing format. For 'json' mode use '/set format json'") |  | ||||||
| 					} else { |  | ||||||
| 						opts.Format = args[2] |  | ||||||
| 						fmt.Printf("Set format to '%s' mode.\n", args[2]) |  | ||||||
| 					} |  | ||||||
| 				case "noformat": |  | ||||||
| 					opts.Format = "" |  | ||||||
| 					fmt.Println("Disabled format.") |  | ||||||
| 				case "parameter": |  | ||||||
| 					if len(args) < 4 { |  | ||||||
| 						usageParameters() |  | ||||||
| 						continue |  | ||||||
| 					} |  | ||||||
| 					params := args[3:] |  | ||||||
| 					fp, err := api.FormatParams(map[string][]string{args[2]: params}) |  | ||||||
| 					if err != nil { |  | ||||||
| 						fmt.Printf("Couldn't set parameter: %q\n", err) |  | ||||||
| 						continue |  | ||||||
| 					} |  | ||||||
| 					fmt.Printf("Set parameter '%s' to '%s'\n", args[2], strings.Join(params, ", ")) |  | ||||||
| 					opts.Options[args[2]] = fp[args[2]] |  | ||||||
| 				case "system", "template": |  | ||||||
| 					if len(args) < 3 { |  | ||||||
| 						usageSet() |  | ||||||
| 						continue |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					if args[1] == "system" { |  | ||||||
| 						multiline = MultilineSystem |  | ||||||
| 					} else if args[1] == "template" { |  | ||||||
| 						multiline = MultilineTemplate |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					line := strings.Join(args[2:], " ") |  | ||||||
| 					line, ok := strings.CutPrefix(line, `"""`) |  | ||||||
| 					if !ok { |  | ||||||
| 						multiline = MultilineNone |  | ||||||
| 					} else { |  | ||||||
| 						// only cut suffix if the line is multiline |  | ||||||
| 						line, ok = strings.CutSuffix(line, `"""`) |  | ||||||
| 						if ok { |  | ||||||
| 							multiline = MultilineNone |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					sb.WriteString(line) |  | ||||||
| 					if multiline != MultilineNone { |  | ||||||
| 						scanner.Prompt.UseAlt = true |  | ||||||
| 						continue |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					if args[1] == "system" { |  | ||||||
| 						opts.System = sb.String() // for display in modelfile |  | ||||||
| 						newMessage := api.Message{Role: "system", Content: sb.String()} |  | ||||||
| 						// Check if the slice is not empty and the last message is from 'system' |  | ||||||
| 						if len(opts.Messages) > 0 && opts.Messages[len(opts.Messages)-1].Role == "system" { |  | ||||||
| 							// Replace the last message |  | ||||||
| 							opts.Messages[len(opts.Messages)-1] = newMessage |  | ||||||
| 						} else { |  | ||||||
| 							opts.Messages = append(opts.Messages, newMessage) |  | ||||||
| 						} |  | ||||||
| 						fmt.Println("Set system message.") |  | ||||||
| 						sb.Reset() |  | ||||||
| 					} else if args[1] == "template" { |  | ||||||
| 						opts.Template = sb.String() |  | ||||||
| 						fmt.Println("Set prompt template.") |  | ||||||
| 						sb.Reset() |  | ||||||
| 					} |  | ||||||
|  |  | ||||||
| 					sb.Reset() |  | ||||||
| 					continue |  | ||||||
| 				default: |  | ||||||
| 					fmt.Printf("Unknown command '/set %s'. Type /? for help\n", args[1]) |  | ||||||
| 				} |  | ||||||
| 			} else { |  | ||||||
| 				usageSet() |  | ||||||
| 			} |  | ||||||
| 		case strings.HasPrefix(line, "/show"): |  | ||||||
| 			args := strings.Fields(line) |  | ||||||
| 			if len(args) > 1 { |  | ||||||
| 				client, err := api.ClientFromEnvironment() |  | ||||||
| 				if err != nil { |  | ||||||
| 					fmt.Println("error: couldn't connect to ollama server") |  | ||||||
| 					return err |  | ||||||
| 				} |  | ||||||
| 				req := &api.ShowRequest{ |  | ||||||
| 					Name:     opts.Model, |  | ||||||
| 					System:   opts.System, |  | ||||||
| 					Template: opts.Template, |  | ||||||
| 					Options:  opts.Options, |  | ||||||
| 				} |  | ||||||
| 				resp, err := client.Show(cmd.Context(), req) |  | ||||||
| 				if err != nil { |  | ||||||
| 					fmt.Println("error: couldn't get model") |  | ||||||
| 					return err |  | ||||||
| 				} |  | ||||||
|  |  | ||||||
| 				switch args[1] { |  | ||||||
| 				case "info": |  | ||||||
| 					fmt.Println("Model details:") |  | ||||||
| 					if len(resp.Details.Families) > 0 { |  | ||||||
| 						fmt.Printf("Family              %s\n", strings.Join(resp.Details.Families, ", ")) |  | ||||||
| 					} else if resp.Details.Family != "" { |  | ||||||
| 						fmt.Printf("Family              %s\n", resp.Details.Family) |  | ||||||
| 					} |  | ||||||
| 					fmt.Printf("Parameter Size      %s\n", resp.Details.ParameterSize) |  | ||||||
| 					fmt.Printf("Quantization Level  %s\n", resp.Details.QuantizationLevel) |  | ||||||
| 					fmt.Println("") |  | ||||||
| 				case "license": |  | ||||||
| 					if resp.License == "" { |  | ||||||
| 						fmt.Println("No license was specified for this model.") |  | ||||||
| 					} else { |  | ||||||
| 						fmt.Println(resp.License) |  | ||||||
| 					} |  | ||||||
| 				case "modelfile": |  | ||||||
| 					fmt.Println(resp.Modelfile) |  | ||||||
| 				case "parameters": |  | ||||||
| 					if resp.Parameters == "" { |  | ||||||
| 						fmt.Println("No parameters were specified for this model.") |  | ||||||
| 					} else { |  | ||||||
| 						if len(opts.Options) > 0 { |  | ||||||
| 							fmt.Println("User defined parameters:") |  | ||||||
| 							for k, v := range opts.Options { |  | ||||||
| 								fmt.Printf("%-*s %v\n", 30, k, v) |  | ||||||
| 							} |  | ||||||
| 							fmt.Println() |  | ||||||
| 						} |  | ||||||
| 						fmt.Println("Model defined parameters:") |  | ||||||
| 						fmt.Println(resp.Parameters) |  | ||||||
| 					} |  | ||||||
| 				case "system": |  | ||||||
| 					switch { |  | ||||||
| 					case opts.System != "": |  | ||||||
| 						fmt.Println(opts.System + "\n") |  | ||||||
| 					case resp.System != "": |  | ||||||
| 						fmt.Println(resp.System + "\n") |  | ||||||
| 					default: |  | ||||||
| 						fmt.Println("No system message was specified for this model.") |  | ||||||
| 					} |  | ||||||
| 				case "template": |  | ||||||
| 					switch { |  | ||||||
| 					case opts.Template != "": |  | ||||||
| 						fmt.Println(opts.Template + "\n") |  | ||||||
| 					case resp.Template != "": |  | ||||||
| 						fmt.Println(resp.Template) |  | ||||||
| 					default: |  | ||||||
| 						fmt.Println("No prompt template was specified for this model.") |  | ||||||
| 					} |  | ||||||
| 				default: |  | ||||||
| 					fmt.Printf("Unknown command '/show %s'. Type /? for help\n", args[1]) |  | ||||||
| 				} |  | ||||||
| 			} else { |  | ||||||
| 				usageShow() |  | ||||||
| 			} |  | ||||||
| 		case strings.HasPrefix(line, "/help"), strings.HasPrefix(line, "/?"): |  | ||||||
| 			args := strings.Fields(line) |  | ||||||
| 			if len(args) > 1 { |  | ||||||
| 				switch args[1] { |  | ||||||
| 				case "set", "/set": |  | ||||||
| 					usageSet() |  | ||||||
| 				case "show", "/show": |  | ||||||
| 					usageShow() |  | ||||||
| 				case "shortcut", "shortcuts": |  | ||||||
| 					usageShortcuts() |  | ||||||
| 				} |  | ||||||
| 			} else { |  | ||||||
| 				usage() |  | ||||||
| 			} |  | ||||||
| 		case strings.HasPrefix(line, "/exit"), strings.HasPrefix(line, "/bye"): |  | ||||||
| 			return nil |  | ||||||
| 		case strings.HasPrefix(line, "/"): |  | ||||||
| 			args := strings.Fields(line) |  | ||||||
| 			isFile := false |  | ||||||
|  |  | ||||||
| 			if opts.MultiModal { |  | ||||||
| 				for _, f := range extractFileNames(line) { |  | ||||||
| 					if strings.HasPrefix(f, args[0]) { |  | ||||||
| 						isFile = true |  | ||||||
| 						break |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if !isFile { |  | ||||||
| 				fmt.Printf("Unknown command '%s'. Type /? for help\n", args[0]) |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			sb.WriteString(line) |  | ||||||
| 		default: |  | ||||||
| 			sb.WriteString(line) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if sb.Len() > 0 && multiline == MultilineNone { |  | ||||||
| 			newMessage := api.Message{Role: "user", Content: sb.String()} |  | ||||||
|  |  | ||||||
| 			if opts.MultiModal { |  | ||||||
| 				msg, images, err := extractFileData(sb.String()) |  | ||||||
| 				if err != nil { |  | ||||||
| 					return err |  | ||||||
| 				} |  | ||||||
|  |  | ||||||
| 				// clear all previous images for better responses |  | ||||||
| 				if len(images) > 0 { |  | ||||||
| 					for i := range opts.Messages { |  | ||||||
| 						opts.Messages[i].Images = nil |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
|  |  | ||||||
| 				newMessage.Content = msg |  | ||||||
| 				newMessage.Images = images |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			opts.Messages = append(opts.Messages, newMessage) |  | ||||||
|  |  | ||||||
| 			assistant, err := chat(cmd, opts) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			if assistant != nil { |  | ||||||
| 				opts.Messages = append(opts.Messages, *assistant) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			sb.Reset() |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func buildModelfile(opts runOptions) string { |  | ||||||
| 	var mf strings.Builder |  | ||||||
| 	model := opts.ParentModel |  | ||||||
| 	if model == "" { |  | ||||||
| 		model = opts.Model |  | ||||||
| 	} |  | ||||||
| 	fmt.Fprintf(&mf, "FROM %s\n", model) |  | ||||||
| 	if opts.System != "" { |  | ||||||
| 		fmt.Fprintf(&mf, "SYSTEM \"\"\"%s\"\"\"\n", opts.System) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if opts.Template != "" { |  | ||||||
| 		fmt.Fprintf(&mf, "TEMPLATE \"\"\"%s\"\"\"\n", opts.Template) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	keys := make([]string, 0) |  | ||||||
| 	for k := range opts.Options { |  | ||||||
| 		keys = append(keys, k) |  | ||||||
| 	} |  | ||||||
| 	sort.Strings(keys) |  | ||||||
| 	for _, k := range keys { |  | ||||||
| 		fmt.Fprintf(&mf, "PARAMETER %s %v\n", k, opts.Options[k]) |  | ||||||
| 	} |  | ||||||
| 	fmt.Fprintln(&mf) |  | ||||||
|  |  | ||||||
| 	for _, msg := range opts.Messages { |  | ||||||
| 		fmt.Fprintf(&mf, "MESSAGE %s \"\"\"%s\"\"\"\n", msg.Role, msg.Content) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return mf.String() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func normalizeFilePath(fp string) string { |  | ||||||
| 	// Define a map of escaped characters and their replacements |  | ||||||
| 	replacements := map[string]string{ |  | ||||||
| 		"\\ ":  " ",  // Escaped space |  | ||||||
| 		"\\(":  "(",  // Escaped left parenthesis |  | ||||||
| 		"\\)":  ")",  // Escaped right parenthesis |  | ||||||
| 		"\\[":  "[",  // Escaped left square bracket |  | ||||||
| 		"\\]":  "]",  // Escaped right square bracket |  | ||||||
| 		"\\{":  "{",  // Escaped left curly brace |  | ||||||
| 		"\\}":  "}",  // Escaped right curly brace |  | ||||||
| 		"\\$":  "$",  // Escaped dollar sign |  | ||||||
| 		"\\&":  "&",  // Escaped ampersand |  | ||||||
| 		"\\;":  ";",  // Escaped semicolon |  | ||||||
| 		"\\'":  "'",  // Escaped single quote |  | ||||||
| 		"\\\\": "\\", // Escaped backslash |  | ||||||
| 		"\\*":  "*",  // Escaped asterisk |  | ||||||
| 		"\\?":  "?",  // Escaped question mark |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for escaped, actual := range replacements { |  | ||||||
| 		fp = strings.ReplaceAll(fp, escaped, actual) |  | ||||||
| 	} |  | ||||||
| 	return fp |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func extractFileNames(input string) []string { |  | ||||||
| 	// Regex to match file paths starting with optional drive letter, / ./ \ or .\ and include escaped or unescaped spaces (\ or %20) |  | ||||||
| 	// and followed by more characters and a file extension |  | ||||||
| 	// This will capture non filename strings, but we'll check for file existence to remove mismatches |  | ||||||
| 	regexPattern := `(?:[a-zA-Z]:)?(?:\./|/|\\)[\S\\ ]+?\.(?i:jpg|jpeg|png|svg)\b` |  | ||||||
| 	re := regexp.MustCompile(regexPattern) |  | ||||||
|  |  | ||||||
| 	return re.FindAllString(input, -1) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func extractFileData(input string) (string, []api.ImageData, error) { |  | ||||||
| 	filePaths := extractFileNames(input) |  | ||||||
| 	var imgs []api.ImageData |  | ||||||
|  |  | ||||||
| 	for _, fp := range filePaths { |  | ||||||
| 		nfp := normalizeFilePath(fp) |  | ||||||
| 		data, err := getImageData(nfp) |  | ||||||
| 		if err != nil { |  | ||||||
| 			if os.IsNotExist(err) { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			fmt.Fprintf(os.Stderr, "Couldn't process image: %q\n", err) |  | ||||||
| 			return "", imgs, err |  | ||||||
| 		} |  | ||||||
| 		fmt.Fprintf(os.Stderr, "Added image '%s'\n", nfp) |  | ||||||
| 		input = strings.ReplaceAll(input, fp, "") |  | ||||||
| 		imgs = append(imgs, data) |  | ||||||
| 	} |  | ||||||
| 	return input, imgs, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func getImageData(filePath string) ([]byte, error) { |  | ||||||
| 	file, err := os.Open(filePath) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	defer file.Close() |  | ||||||
|  |  | ||||||
| 	buf := make([]byte, 512) |  | ||||||
| 	_, err = file.Read(buf) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	contentType := http.DetectContentType(buf) |  | ||||||
| 	allowedTypes := []string{"image/jpeg", "image/jpg", "image/png"} |  | ||||||
| 	if !slices.Contains(allowedTypes, contentType) { |  | ||||||
| 		return nil, fmt.Errorf("invalid image type: %s", contentType) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	info, err := file.Stat() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Check if the file size exceeds 100MB |  | ||||||
| 	var maxSize int64 = 100 * 1024 * 1024 // 100MB in bytes |  | ||||||
| 	if info.Size() > maxSize { |  | ||||||
| 		return nil, fmt.Errorf("file size exceeds maximum limit (100MB)") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	buf = make([]byte, info.Size()) |  | ||||||
| 	_, err = file.Seek(0, 0) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	_, err = io.ReadFull(file, buf) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return buf, nil |  | ||||||
| } |  | ||||||
| @@ -1,116 +0,0 @@ | |||||||
| package cmd |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"testing" |  | ||||||
| 	"text/template" |  | ||||||
|  |  | ||||||
| 	"github.com/stretchr/testify/assert" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/api" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func TestExtractFilenames(t *testing.T) { |  | ||||||
| 	// Unix style paths |  | ||||||
| 	input := ` some preamble  |  | ||||||
|  ./relative\ path/one.png inbetween1 ./not a valid two.jpg inbetween2 |  | ||||||
| /unescaped space /three.jpeg inbetween3 /valid\ path/dir/four.png "./quoted with spaces/five.svg` |  | ||||||
| 	res := extractFileNames(input) |  | ||||||
| 	assert.Len(t, res, 5) |  | ||||||
| 	assert.Contains(t, res[0], "one.png") |  | ||||||
| 	assert.Contains(t, res[1], "two.jpg") |  | ||||||
| 	assert.Contains(t, res[2], "three.jpeg") |  | ||||||
| 	assert.Contains(t, res[3], "four.png") |  | ||||||
| 	assert.Contains(t, res[4], "five.svg") |  | ||||||
| 	assert.NotContains(t, res[4], '"') |  | ||||||
| 	assert.NotContains(t, res, "inbtween") |  | ||||||
|  |  | ||||||
| 	// Windows style paths |  | ||||||
| 	input = ` some preamble |  | ||||||
|  c:/users/jdoe/one.png inbetween1 c:/program files/someplace/two.jpg inbetween2  |  | ||||||
|  /absolute/nospace/three.jpeg inbetween3 /absolute/with space/four.png inbetween4 |  | ||||||
| ./relative\ path/five.svg inbetween5 "./relative with/spaces/six.png inbetween6 |  | ||||||
| d:\path with\spaces\seven.svg inbetween7 c:\users\jdoe\eight.png inbetween8  |  | ||||||
|  d:\program files\someplace\nine.png inbetween9 "E:\program files\someplace\ten.svg some ending |  | ||||||
| ` |  | ||||||
| 	res = extractFileNames(input) |  | ||||||
| 	assert.Len(t, res, 10) |  | ||||||
| 	assert.NotContains(t, res, "inbtween") |  | ||||||
| 	assert.Contains(t, res[0], "one.png") |  | ||||||
| 	assert.Contains(t, res[0], "c:") |  | ||||||
| 	assert.Contains(t, res[1], "two.jpg") |  | ||||||
| 	assert.Contains(t, res[1], "c:") |  | ||||||
| 	assert.Contains(t, res[2], "three.jpeg") |  | ||||||
| 	assert.Contains(t, res[3], "four.png") |  | ||||||
| 	assert.Contains(t, res[4], "five.svg") |  | ||||||
| 	assert.Contains(t, res[5], "six.png") |  | ||||||
| 	assert.Contains(t, res[6], "seven.svg") |  | ||||||
| 	assert.Contains(t, res[6], "d:") |  | ||||||
| 	assert.Contains(t, res[7], "eight.png") |  | ||||||
| 	assert.Contains(t, res[7], "c:") |  | ||||||
| 	assert.Contains(t, res[8], "nine.png") |  | ||||||
| 	assert.Contains(t, res[8], "d:") |  | ||||||
| 	assert.Contains(t, res[9], "ten.svg") |  | ||||||
| 	assert.Contains(t, res[9], "E:") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestModelfileBuilder(t *testing.T) { |  | ||||||
| 	opts := runOptions{ |  | ||||||
| 		Model:    "hork", |  | ||||||
| 		System:   "You are part horse and part shark, but all hork. Do horklike things", |  | ||||||
| 		Template: "This is a template.", |  | ||||||
| 		Messages: []api.Message{ |  | ||||||
| 			{Role: "user", Content: "Hey there hork!"}, |  | ||||||
| 			{Role: "assistant", Content: "Yes it is true, I am half horse, half shark."}, |  | ||||||
| 		}, |  | ||||||
| 		Options: map[string]interface{}{}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	opts.Options["temperature"] = 0.9 |  | ||||||
| 	opts.Options["seed"] = 42 |  | ||||||
| 	opts.Options["penalize_newline"] = false |  | ||||||
| 	opts.Options["stop"] = []string{"hi", "there"} |  | ||||||
|  |  | ||||||
| 	mf := buildModelfile(opts) |  | ||||||
| 	expectedModelfile := `FROM {{.Model}} |  | ||||||
| SYSTEM """{{.System}}""" |  | ||||||
| TEMPLATE """{{.Template}}""" |  | ||||||
| PARAMETER penalize_newline false |  | ||||||
| PARAMETER seed 42 |  | ||||||
| PARAMETER stop [hi there] |  | ||||||
| PARAMETER temperature 0.9 |  | ||||||
|  |  | ||||||
| MESSAGE user """Hey there hork!""" |  | ||||||
| MESSAGE assistant """Yes it is true, I am half horse, half shark.""" |  | ||||||
| ` |  | ||||||
|  |  | ||||||
| 	tmpl, err := template.New("").Parse(expectedModelfile) |  | ||||||
| 	assert.Nil(t, err) |  | ||||||
|  |  | ||||||
| 	var buf bytes.Buffer |  | ||||||
| 	err = tmpl.Execute(&buf, opts) |  | ||||||
| 	assert.Nil(t, err) |  | ||||||
| 	assert.Equal(t, buf.String(), mf) |  | ||||||
|  |  | ||||||
| 	opts.ParentModel = "horseshark" |  | ||||||
| 	mf = buildModelfile(opts) |  | ||||||
| 	expectedModelfile = `FROM {{.ParentModel}} |  | ||||||
| SYSTEM """{{.System}}""" |  | ||||||
| TEMPLATE """{{.Template}}""" |  | ||||||
| PARAMETER penalize_newline false |  | ||||||
| PARAMETER seed 42 |  | ||||||
| PARAMETER stop [hi there] |  | ||||||
| PARAMETER temperature 0.9 |  | ||||||
|  |  | ||||||
| MESSAGE user """Hey there hork!""" |  | ||||||
| MESSAGE assistant """Yes it is true, I am half horse, half shark.""" |  | ||||||
| ` |  | ||||||
|  |  | ||||||
| 	tmpl, err = template.New("").Parse(expectedModelfile) |  | ||||||
| 	assert.Nil(t, err) |  | ||||||
|  |  | ||||||
| 	var parentBuf bytes.Buffer |  | ||||||
| 	err = tmpl.Execute(&parentBuf, opts) |  | ||||||
| 	assert.Nil(t, err) |  | ||||||
| 	assert.Equal(t, parentBuf.String(), mf) |  | ||||||
| } |  | ||||||
							
								
								
									
										44
									
								
								cmd/spinner.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,44 @@ | |||||||
|  | package cmd | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/jmorganca/ollama/progressbar" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Spinner struct { | ||||||
|  | 	description string | ||||||
|  | 	*progressbar.ProgressBar | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewSpinner(description string) *Spinner { | ||||||
|  | 	return &Spinner{ | ||||||
|  | 		description: description, | ||||||
|  | 		ProgressBar: progressbar.NewOptions(-1, | ||||||
|  | 			progressbar.OptionSetWriter(os.Stderr), | ||||||
|  | 			progressbar.OptionThrottle(60*time.Millisecond), | ||||||
|  | 			progressbar.OptionSpinnerType(14), | ||||||
|  | 			progressbar.OptionSetRenderBlankState(true), | ||||||
|  | 			progressbar.OptionSetElapsedTime(false), | ||||||
|  | 			progressbar.OptionClearOnFinish(), | ||||||
|  | 			progressbar.OptionSetDescription(description), | ||||||
|  | 		), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *Spinner) Spin(tick time.Duration) { | ||||||
|  | 	for range time.Tick(tick) { | ||||||
|  | 		if s.IsFinished() { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		s.Add(1) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s *Spinner) Stop() { | ||||||
|  | 	s.Finish() | ||||||
|  | 	fmt.Println(s.description) | ||||||
|  | } | ||||||
| @@ -1,30 +0,0 @@ | |||||||
| package cmd |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"os/exec" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/api" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func startApp(ctx context.Context, client *api.Client) error { |  | ||||||
| 	exe, err := os.Executable() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	link, err := os.Readlink(exe) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	if !strings.Contains(link, "Ollama.app") { |  | ||||||
| 		return fmt.Errorf("could not find ollama app") |  | ||||||
| 	} |  | ||||||
| 	path := strings.Split(link, "Ollama.app") |  | ||||||
| 	if err := exec.Command("/usr/bin/open", "-a", path[0]+"Ollama.app").Run(); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	return waitForServer(ctx, client) |  | ||||||
| } |  | ||||||
| @@ -1,14 +0,0 @@ | |||||||
| //go:build !windows && !darwin |  | ||||||
|  |  | ||||||
| package cmd |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/api" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func startApp(ctx context.Context, client *api.Client) error { |  | ||||||
| 	return fmt.Errorf("could not connect to ollama server, run 'ollama serve' to start it") |  | ||||||
| } |  | ||||||
| @@ -1,58 +0,0 @@ | |||||||
| package cmd |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"os" |  | ||||||
| 	"os/exec" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"strings" |  | ||||||
| 	"syscall" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/api" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func startApp(ctx context.Context, client *api.Client) error { |  | ||||||
| 	// log.Printf("XXX Attempting to find and start ollama app") |  | ||||||
| 	AppName := "ollama app.exe" |  | ||||||
| 	exe, err := os.Executable() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	appExe := filepath.Join(filepath.Dir(exe), AppName) |  | ||||||
| 	_, err = os.Stat(appExe) |  | ||||||
| 	if errors.Is(err, os.ErrNotExist) { |  | ||||||
| 		// Try the standard install location |  | ||||||
| 		localAppData := os.Getenv("LOCALAPPDATA") |  | ||||||
| 		appExe = filepath.Join(localAppData, "Ollama", AppName) |  | ||||||
| 		_, err := os.Stat(appExe) |  | ||||||
| 		if errors.Is(err, os.ErrNotExist) { |  | ||||||
| 			// Finally look in the path |  | ||||||
| 			appExe, err = exec.LookPath(AppName) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return fmt.Errorf("could not locate ollama app") |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	// log.Printf("XXX attempting to start app %s", appExe) |  | ||||||
|  |  | ||||||
| 	cmd_path := "c:\\Windows\\system32\\cmd.exe" |  | ||||||
| 	cmd := exec.Command(cmd_path, "/c", appExe) |  | ||||||
| 	// TODO - these hide flags aren't working - still pops up a command window for some reason |  | ||||||
| 	cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: 0x08000000, HideWindow: true} |  | ||||||
|  |  | ||||||
| 	// TODO this didn't help either... |  | ||||||
| 	cmd.Stdin = strings.NewReader("") |  | ||||||
| 	cmd.Stdout = os.Stdout |  | ||||||
| 	cmd.Stderr = os.Stderr |  | ||||||
|  |  | ||||||
| 	if err := cmd.Start(); err != nil { |  | ||||||
| 		return fmt.Errorf("unable to start ollama app %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if cmd.Process != nil { |  | ||||||
| 		defer cmd.Process.Release() //nolint:errcheck |  | ||||||
| 	} |  | ||||||
| 	return waitForServer(ctx, client) |  | ||||||
| } |  | ||||||
| @@ -1,200 +0,0 @@ | |||||||
| package convert |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"cmp" |  | ||||||
| 	"encoding/binary" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"slices" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"google.golang.org/protobuf/proto" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/convert/sentencepiece" |  | ||||||
| 	"github.com/ollama/ollama/llm" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	_ int32 = iota |  | ||||||
| 	tokenTypeNormal |  | ||||||
| 	tokenTypeUnknown |  | ||||||
| 	tokenTypeControl |  | ||||||
| 	tokenTypeUserDefined |  | ||||||
| 	tokenTypeUnused |  | ||||||
| 	tokenTypeByte |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type Params struct { |  | ||||||
| 	Architectures     []string `json:"architectures"` |  | ||||||
| 	VocabSize         int      `json:"vocab_size"` |  | ||||||
| 	HiddenSize        int      `json:"hidden_size"`       // n_embd |  | ||||||
| 	HiddenLayers      int      `json:"num_hidden_layers"` // n_layer |  | ||||||
| 	ContextSize       int      `json:"max_position_embeddings"` |  | ||||||
| 	IntermediateSize  int      `json:"intermediate_size"` |  | ||||||
| 	AttentionHeads    int      `json:"num_attention_heads"` // n_head |  | ||||||
| 	KeyValHeads       int      `json:"num_key_value_heads"` |  | ||||||
| 	NormEPS           float64  `json:"rms_norm_eps"` |  | ||||||
| 	BoSTokenID        int      `json:"bos_token_id"` |  | ||||||
| 	EoSTokenID        int      `json:"eos_token_id"` |  | ||||||
| 	HeadDimension     int      `json:"head_dim"` |  | ||||||
| 	PaddingTokenID    int      `json:"pad_token_id"` |  | ||||||
| 	RopeFrequencyBase float64  `json:"rope_theta"` |  | ||||||
|  |  | ||||||
| 	Experts     int `json:"num_local_experts"` |  | ||||||
| 	ExpertsUsed int `json:"num_experts_per_tok"` |  | ||||||
|  |  | ||||||
| 	PreTokenizer string |  | ||||||
|  |  | ||||||
| 	ByteOrder |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type ByteOrder interface { |  | ||||||
| 	binary.ByteOrder |  | ||||||
| 	binary.AppendByteOrder |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type ModelArch interface { |  | ||||||
| 	GetTensors() error |  | ||||||
| 	LoadVocab() error |  | ||||||
| 	WriteGGUF(io.WriteSeeker) error |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type ModelFormat interface { |  | ||||||
| 	GetLayerName(string) (string, error) |  | ||||||
| 	GetTensors(string, *Params) ([]llm.Tensor, error) |  | ||||||
| 	GetParams(string) (*Params, error) |  | ||||||
| 	GetModelArch(string, string, *Params) (ModelArch, error) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type ModelData struct { |  | ||||||
| 	Path    string |  | ||||||
| 	Name    string |  | ||||||
| 	Params  *Params |  | ||||||
| 	Vocab   *Vocab |  | ||||||
| 	Tensors []llm.Tensor |  | ||||||
| 	Format  ModelFormat |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func GetModelFormat(dirname string) (ModelFormat, error) { |  | ||||||
| 	files, err := filepath.Glob(filepath.Join(dirname, "*")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, fn := range files { |  | ||||||
| 		if strings.HasSuffix(fn, ".safetensors") { |  | ||||||
| 			return &SafetensorFormat{}, nil |  | ||||||
| 		} else if strings.HasSuffix(fn, ".bin") || strings.HasSuffix(fn, ".pth") { |  | ||||||
| 			slog.Debug("model is torch") |  | ||||||
| 			return &TorchFormat{}, nil |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil, fmt.Errorf("couldn't determine model format") |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Details on gguf's tokenizer can be found at: |  | ||||||
| // https://github.com/ggerganov/ggml/blob/master/docs/gguf.md#tokenizer |  | ||||||
| type Vocab struct { |  | ||||||
| 	Tokens []string |  | ||||||
| 	Scores []float32 |  | ||||||
| 	Types  []int32 |  | ||||||
| 	Merges []string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func LoadSentencePieceTokens(dirpath string, params *Params) (*Vocab, error) { |  | ||||||
| 	slog.Info(fmt.Sprintf("reading vocab from %s", filepath.Join(dirpath, "tokenizer.model"))) |  | ||||||
| 	in, err := os.ReadFile(filepath.Join(dirpath, "tokenizer.model")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// To regenerate sentencepiece from the protobufs use: |  | ||||||
| 	// protoc -I=./ --go_out=./ sentencepiece_model.proto |  | ||||||
| 	modelProto := &sentencepiece.ModelProto{} |  | ||||||
| 	if err := proto.Unmarshal(in, modelProto); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	v := &Vocab{ |  | ||||||
| 		Tokens: make([]string, 0), |  | ||||||
| 		Scores: make([]float32, 0), |  | ||||||
| 		Types:  make([]int32, 0), |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	pieces := modelProto.GetPieces() |  | ||||||
| 	for _, p := range pieces { |  | ||||||
| 		v.Tokens = append(v.Tokens, p.GetPiece()) |  | ||||||
| 		v.Scores = append(v.Scores, p.GetScore()) |  | ||||||
| 		t := p.GetType() |  | ||||||
| 		switch t { |  | ||||||
| 		case sentencepiece.ModelProto_SentencePiece_UNKNOWN: |  | ||||||
| 		case sentencepiece.ModelProto_SentencePiece_CONTROL: |  | ||||||
| 		case sentencepiece.ModelProto_SentencePiece_UNUSED: |  | ||||||
| 		case sentencepiece.ModelProto_SentencePiece_BYTE: |  | ||||||
| 		default: |  | ||||||
| 			t = sentencepiece.ModelProto_SentencePiece_NORMAL |  | ||||||
| 		} |  | ||||||
| 		v.Types = append(v.Types, int32(t)) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	slog.Info(fmt.Sprintf("vocab size: %d", len(v.Tokens))) |  | ||||||
|  |  | ||||||
| 	// add any additional tokens |  | ||||||
| 	addIn, err := os.ReadFile(filepath.Join(dirpath, "added_tokens.json")) |  | ||||||
| 	if os.IsNotExist(err) { |  | ||||||
| 		return v, nil |  | ||||||
| 	} else if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	slog.Info("reading user defined tokens") |  | ||||||
|  |  | ||||||
| 	var extraTokenData map[string]int |  | ||||||
| 	if err := json.Unmarshal(addIn, &extraTokenData); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	type token struct { |  | ||||||
| 		key string |  | ||||||
| 		pos int |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	extraTokens := make([]token, 0) |  | ||||||
| 	for k, id := range extraTokenData { |  | ||||||
| 		extraTokens = append(extraTokens, token{k, id}) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	slices.SortFunc(extraTokens, func(a, b token) int { |  | ||||||
| 		return cmp.Compare(a.pos, b.pos) |  | ||||||
| 	}) |  | ||||||
|  |  | ||||||
| 	numToks := len(v.Tokens) |  | ||||||
|  |  | ||||||
| 	for cnt, t := range extraTokens { |  | ||||||
| 		// the token id should match the specific index for the total number of tokens |  | ||||||
| 		if t.pos != cnt+numToks { |  | ||||||
| 			return nil, fmt.Errorf("token ID '%d' for '%s' doesn't match total token size", t.pos, t.key) |  | ||||||
| 		} |  | ||||||
| 		v.Tokens = append(v.Tokens, t.key) |  | ||||||
| 		v.Scores = append(v.Scores, -1000.0) |  | ||||||
| 		v.Types = append(v.Types, tokenTypeUserDefined) |  | ||||||
| 	} |  | ||||||
| 	slog.Info(fmt.Sprintf("vocab size w/ extra tokens: %d", len(v.Tokens))) |  | ||||||
|  |  | ||||||
| 	if params.VocabSize > len(v.Tokens) { |  | ||||||
| 		missingTokens := params.VocabSize - len(v.Tokens) |  | ||||||
| 		slog.Warn(fmt.Sprintf("vocab is missing %d tokens", missingTokens)) |  | ||||||
| 		for cnt := 0; cnt < missingTokens; cnt++ { |  | ||||||
| 			v.Tokens = append(v.Tokens, fmt.Sprintf("<dummy%05d>", cnt+1)) |  | ||||||
| 			v.Scores = append(v.Scores, -1) |  | ||||||
| 			v.Types = append(v.Types, tokenTypeUserDefined) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return v, nil |  | ||||||
| } |  | ||||||
| @@ -1,103 +0,0 @@ | |||||||
| //go:build slow |  | ||||||
|  |  | ||||||
| package convert |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"testing" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/llm" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func convertFull(t *testing.T, p string) (llm.KV, llm.Tensors) { |  | ||||||
| 	t.Helper() |  | ||||||
|  |  | ||||||
| 	mf, err := GetModelFormat(p) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	params, err := mf.GetParams(p) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	arch, err := mf.GetModelArch("", p, params) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := arch.LoadVocab(); err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := arch.GetTensors(); err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	f, err := os.CreateTemp(t.TempDir(), "f16") |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 	defer f.Close() |  | ||||||
|  |  | ||||||
| 	if err := arch.WriteGGUF(f); err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	r, err := os.Open(f.Name()) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
| 	defer r.Close() |  | ||||||
|  |  | ||||||
| 	m, _, err := llm.DecodeGGML(r) |  | ||||||
| 	if err != nil { |  | ||||||
| 		t.Fatal(err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return m.KV(), m.Tensors() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestConvertFull(t *testing.T) { |  | ||||||
| 	cases := []struct { |  | ||||||
| 		path    string |  | ||||||
| 		arch    string |  | ||||||
| 		tensors int |  | ||||||
| 		layers  int |  | ||||||
| 	}{ |  | ||||||
| 		{"Meta-Llama-3-8B-Instruct", "llama", 291, 35}, |  | ||||||
| 		{"Mistral-7B-Instruct-v0.2", "llama", 291, 35}, |  | ||||||
| 		{"Mixtral-8x7B-Instruct-v0.1", "llama", 291, 35}, |  | ||||||
| 		{"gemma-2b-it", "gemma", 164, 20}, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, tt := range cases { |  | ||||||
| 		t.Run(tt.path, func(t *testing.T) { |  | ||||||
| 			p := filepath.Join("testdata", tt.path) |  | ||||||
| 			if _, err := os.Stat(p); err != nil { |  | ||||||
| 				t.Skipf("%s not found", p) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			kv, tensors := convertFull(t, p) |  | ||||||
|  |  | ||||||
| 			if kv.Architecture() != tt.arch { |  | ||||||
| 				t.Fatalf("expected llama, got %s", kv.Architecture()) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if kv.FileType().String() != "F16" { |  | ||||||
| 				t.Fatalf("expected F16, got %s", kv.FileType()) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if len(tensors) != tt.tensors { |  | ||||||
| 				t.Fatalf("expected %d tensors, got %d", tt.tensors, len(tensors)) |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			layers := tensors.Layers() |  | ||||||
| 			if len(layers) != tt.layers { |  | ||||||
| 				t.Fatalf("expected %d layers, got %d", tt.layers, len(layers)) |  | ||||||
| 			} |  | ||||||
| 		}) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
							
								
								
									
										103
									
								
								convert/gemma.go
									
									
									
									
									
								
							
							
						
						| @@ -1,103 +0,0 @@ | |||||||
| package convert |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"log/slog" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/pdevine/tensor" |  | ||||||
| 	"github.com/pdevine/tensor/native" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/llm" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type GemmaModel struct { |  | ||||||
| 	ModelData |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func addOnes(data []float32, vectorSize int) ([]float32, error) { |  | ||||||
| 	n := tensor.New(tensor.WithShape(vectorSize), tensor.WithBacking(data)) |  | ||||||
| 	ones := tensor.Ones(tensor.Float32, vectorSize) |  | ||||||
|  |  | ||||||
| 	n, err := n.Add(ones) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ts, err := native.SelectF32(n, 0) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var f32s []float32 |  | ||||||
| 	for _, t := range ts { |  | ||||||
| 		f32s = append(f32s, t...) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 	return f32s, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *GemmaModel) GetTensors() error { |  | ||||||
| 	t, err := m.Format.GetTensors(m.Path, m.Params) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	slog.Debug(fmt.Sprintf("Total tensors: %d", len(t))) |  | ||||||
| 	for _, l := range t { |  | ||||||
| 		if strings.HasSuffix(l.Name, "norm.weight") { |  | ||||||
| 			wt := l.WriterTo.(safetensorWriterTo) |  | ||||||
| 			wt.repacker = m.Repack |  | ||||||
| 			l.WriterTo = wt |  | ||||||
| 		} |  | ||||||
| 		m.Tensors = append(m.Tensors, l) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *GemmaModel) LoadVocab() error { |  | ||||||
| 	v, err := LoadSentencePieceTokens(m.Path, m.Params) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	m.Vocab = v |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *GemmaModel) Repack(_ string, data []float32, shape []uint64) ([]float32, error) { |  | ||||||
| 	return addOnes(data, int(shape[0])) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *GemmaModel) WriteGGUF(ws io.WriteSeeker) error { |  | ||||||
| 	kv := llm.KV{ |  | ||||||
| 		"general.architecture":                   "gemma", |  | ||||||
| 		"general.name":                           m.Name, |  | ||||||
| 		"gemma.context_length":                   uint32(m.Params.ContextSize), |  | ||||||
| 		"gemma.embedding_length":                 uint32(m.Params.HiddenSize), |  | ||||||
| 		"gemma.block_count":                      uint32(m.Params.HiddenLayers), |  | ||||||
| 		"gemma.feed_forward_length":              uint32(m.Params.IntermediateSize), |  | ||||||
| 		"gemma.attention.head_count":             uint32(m.Params.AttentionHeads), |  | ||||||
| 		"gemma.attention.head_count_kv":          uint32(m.Params.KeyValHeads), |  | ||||||
| 		"gemma.attention.layer_norm_rms_epsilon": float32(m.Params.NormEPS), |  | ||||||
| 		"gemma.attention.key_length":             uint32(m.Params.HeadDimension), |  | ||||||
| 		"gemma.attention.value_length":           uint32(m.Params.HeadDimension), |  | ||||||
| 		"general.file_type":                      uint32(1), |  | ||||||
| 		"tokenizer.ggml.model":                   "llama", |  | ||||||
|  |  | ||||||
| 		"tokenizer.ggml.tokens":     m.Vocab.Tokens, |  | ||||||
| 		"tokenizer.ggml.scores":     m.Vocab.Scores, |  | ||||||
| 		"tokenizer.ggml.token_type": m.Vocab.Types, |  | ||||||
|  |  | ||||||
| 		"tokenizer.ggml.bos_token_id":     uint32(m.Params.BoSTokenID), |  | ||||||
| 		"tokenizer.ggml.eos_token_id":     uint32(m.Params.EoSTokenID), |  | ||||||
| 		"tokenizer.ggml.padding_token_id": uint32(m.Params.PaddingTokenID), |  | ||||||
| 		"tokenizer.ggml.unknown_token_id": uint32(3), |  | ||||||
| 		"tokenizer.ggml.add_bos_token":    true, |  | ||||||
| 		"tokenizer.ggml.add_eos_token":    false, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return llm.NewGGUFV3(m.Params.ByteOrder).Encode(ws, kv, m.Tensors) |  | ||||||
| } |  | ||||||
							
								
								
									
										158
									
								
								convert/llama.go
									
									
									
									
									
								
							
							
						
						| @@ -1,158 +0,0 @@ | |||||||
| package convert |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"cmp" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"regexp" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/pdevine/tensor" |  | ||||||
| 	"github.com/pdevine/tensor/native" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/llm" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type LlamaModel struct { |  | ||||||
| 	ModelData |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *LlamaModel) GetTensors() error { |  | ||||||
| 	t, err := m.Format.GetTensors(m.Path, m.Params) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	pattern := `^blk\.[0-9]+\.attn_(?P<layer>q|k)\.weight$` |  | ||||||
| 	re, err := regexp.Compile(pattern) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, l := range t { |  | ||||||
| 		matches := re.FindAllStringSubmatch(l.Name, -1) |  | ||||||
| 		if len(matches) > 0 { |  | ||||||
| 			switch m.Format.(type) { |  | ||||||
| 			case *TorchFormat: |  | ||||||
| 				wt := l.WriterTo.(torchWriterTo) |  | ||||||
| 				wt.repacker = m.Repack |  | ||||||
| 				l.WriterTo = wt |  | ||||||
| 			case *SafetensorFormat: |  | ||||||
| 				wt := l.WriterTo.(safetensorWriterTo) |  | ||||||
| 				wt.repacker = m.Repack |  | ||||||
| 				l.WriterTo = wt |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		m.Tensors = append(m.Tensors, l) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *LlamaModel) LoadVocab() (err error) { |  | ||||||
| 	pre, ts, merges, err := parseTokens(filepath.Join(m.Path, "tokenizer.json")) |  | ||||||
| 	if errors.Is(err, os.ErrNotExist) { |  | ||||||
| 		return nil |  | ||||||
| 	} else if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	m.Vocab = &Vocab{} |  | ||||||
| 	for _, t := range ts { |  | ||||||
| 		m.Vocab.Tokens = append(m.Vocab.Tokens, t.Content) |  | ||||||
| 		m.Vocab.Types = append(m.Vocab.Types, t.Type()) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	m.Vocab.Merges = merges |  | ||||||
| 	m.Params.PreTokenizer = pre |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *LlamaModel) WriteGGUF(ws io.WriteSeeker) error { |  | ||||||
| 	kv := llm.KV{ |  | ||||||
| 		"general.architecture":                   "llama", |  | ||||||
| 		"general.name":                           m.Name, |  | ||||||
| 		"llama.vocab_size":                       uint32(len(m.Vocab.Tokens)), |  | ||||||
| 		"llama.context_length":                   uint32(m.Params.ContextSize), |  | ||||||
| 		"llama.embedding_length":                 uint32(m.Params.HiddenSize), |  | ||||||
| 		"llama.block_count":                      uint32(m.Params.HiddenLayers), |  | ||||||
| 		"llama.feed_forward_length":              uint32(m.Params.IntermediateSize), |  | ||||||
| 		"llama.rope.freq_base":                   float32(m.Params.RopeFrequencyBase), |  | ||||||
| 		"llama.rope.dimension_count":             uint32(m.Params.HiddenSize / m.Params.AttentionHeads), |  | ||||||
| 		"llama.attention.head_count":             uint32(m.Params.AttentionHeads), |  | ||||||
| 		"llama.attention.head_count_kv":          uint32(m.Params.KeyValHeads), |  | ||||||
| 		"llama.attention.layer_norm_rms_epsilon": float32(m.Params.NormEPS), |  | ||||||
| 		"general.file_type":                      uint32(1), |  | ||||||
| 		"tokenizer.ggml.model":                   "gpt2", |  | ||||||
|  |  | ||||||
| 		"tokenizer.ggml.pre":        m.Params.PreTokenizer, |  | ||||||
| 		"tokenizer.ggml.tokens":     m.Vocab.Tokens, |  | ||||||
| 		"tokenizer.ggml.token_type": m.Vocab.Types, |  | ||||||
|  |  | ||||||
| 		"tokenizer.ggml.bos_token_id":     uint32(m.Params.BoSTokenID), |  | ||||||
| 		"tokenizer.ggml.eos_token_id":     uint32(m.Params.EoSTokenID), |  | ||||||
| 		"tokenizer.ggml.unknown_token_id": uint32(0), |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if len(m.Vocab.Merges) > 0 { |  | ||||||
| 		kv["tokenizer.ggml.merges"] = m.Vocab.Merges |  | ||||||
| 	} else { |  | ||||||
| 		kv["tokenizer.ggml.scores"] = m.Vocab.Scores |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return llm.NewGGUFV3(m.Params.ByteOrder).Encode(ws, kv, m.Tensors) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *LlamaModel) Repack(name string, data []float32, shape []uint64) ([]float32, error) { |  | ||||||
| 	return llamaRepack(name, m.Params, data, shape) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func llamaRepack(name string, params *Params, data []float32, shape []uint64) ([]float32, error) { |  | ||||||
| 	var dims []int |  | ||||||
| 	for _, dim := range shape { |  | ||||||
| 		if dim != 0 { |  | ||||||
| 			dims = append(dims, int(dim)) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var heads int |  | ||||||
| 	if strings.HasSuffix(name, "attn_q.weight") { |  | ||||||
| 		heads = params.AttentionHeads |  | ||||||
| 	} else if strings.HasSuffix(name, "attn_k.weight") { |  | ||||||
| 		heads = cmp.Or(params.KeyValHeads, params.AttentionHeads) |  | ||||||
| 	} else { |  | ||||||
| 		return nil, fmt.Errorf("unknown tensor name: %s", name) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	n := tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data)) |  | ||||||
| 	if err := n.Reshape(append([]int{heads, 2, dims[0] / heads / 2}, dims[1:]...)...); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := n.T(0, 2, 1, 3); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := n.Reshape(dims...); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := n.Transpose(); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ts, err := native.SelectF32(n, 1) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var f32s []float32 |  | ||||||
| 	for _, t := range ts { |  | ||||||
| 		f32s = append(f32s, t...) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return f32s, nil |  | ||||||
| } |  | ||||||
| @@ -1,79 +0,0 @@ | |||||||
| package convert |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"io" |  | ||||||
| 	"regexp" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/llm" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type MistralModel struct { |  | ||||||
| 	ModelData |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *MistralModel) GetTensors() error { |  | ||||||
| 	t, err := m.Format.GetTensors(m.Path, m.Params) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	pattern := `^blk\.[0-9]+\.attn_(?P<layer>q|k)\.weight$` |  | ||||||
| 	re, err := regexp.Compile(pattern) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, l := range t { |  | ||||||
| 		matches := re.FindAllStringSubmatch(l.Name, -1) |  | ||||||
| 		if len(matches) > 0 { |  | ||||||
| 			wt := l.WriterTo.(safetensorWriterTo) |  | ||||||
| 			wt.repacker = m.Repack |  | ||||||
| 			l.WriterTo = wt |  | ||||||
| 		} |  | ||||||
| 		m.Tensors = append(m.Tensors, l) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *MistralModel) LoadVocab() error { |  | ||||||
| 	v, err := LoadSentencePieceTokens(m.Path, m.Params) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	m.Vocab = v |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *MistralModel) WriteGGUF(ws io.WriteSeeker) error { |  | ||||||
| 	kv := llm.KV{ |  | ||||||
| 		"general.architecture":                   "llama", |  | ||||||
| 		"general.name":                           m.Name, |  | ||||||
| 		"llama.context_length":                   uint32(m.Params.ContextSize), |  | ||||||
| 		"llama.embedding_length":                 uint32(m.Params.HiddenSize), |  | ||||||
| 		"llama.block_count":                      uint32(m.Params.HiddenLayers), |  | ||||||
| 		"llama.feed_forward_length":              uint32(m.Params.IntermediateSize), |  | ||||||
| 		"llama.rope.dimension_count":             uint32(m.Params.HiddenSize / m.Params.AttentionHeads), |  | ||||||
| 		"llama.attention.head_count":             uint32(m.Params.AttentionHeads), |  | ||||||
| 		"llama.attention.head_count_kv":          uint32(m.Params.KeyValHeads), |  | ||||||
| 		"llama.attention.layer_norm_rms_epsilon": float32(m.Params.NormEPS), |  | ||||||
| 		"general.file_type":                      uint32(1), |  | ||||||
| 		"tokenizer.ggml.model":                   "llama", |  | ||||||
|  |  | ||||||
| 		"tokenizer.ggml.tokens":     m.Vocab.Tokens, |  | ||||||
| 		"tokenizer.ggml.scores":     m.Vocab.Scores, |  | ||||||
| 		"tokenizer.ggml.token_type": m.Vocab.Types, |  | ||||||
|  |  | ||||||
| 		"tokenizer.ggml.bos_token_id":     uint32(m.Params.BoSTokenID), |  | ||||||
| 		"tokenizer.ggml.eos_token_id":     uint32(m.Params.EoSTokenID), |  | ||||||
| 		"tokenizer.ggml.add_bos_token":    true, |  | ||||||
| 		"tokenizer.ggml.add_eos_token":    false, |  | ||||||
| 		"tokenizer.ggml.unknown_token_id": uint32(0), |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return llm.NewGGUFV3(m.Params.ByteOrder).Encode(ws, kv, m.Tensors) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *MistralModel) Repack(name string, data []float32, shape []uint64) ([]float32, error) { |  | ||||||
| 	return llamaRepack(name, m.Params, data, shape) |  | ||||||
| } |  | ||||||
| @@ -1,87 +0,0 @@ | |||||||
| package convert |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"io" |  | ||||||
| 	"regexp" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/llm" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type MixtralModel struct { |  | ||||||
| 	ModelData |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *MixtralModel) GetTensors() error { |  | ||||||
| 	t, err := m.Format.GetTensors(m.Path, m.Params) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	pattern := `^blk\.[0-9]+\.attn_(?P<layer>q|k)\.weight$` |  | ||||||
| 	re, err := regexp.Compile(pattern) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	for _, l := range t { |  | ||||||
| 		matches := re.FindAllStringSubmatch(l.Name, -1) |  | ||||||
| 		if len(matches) > 0 { |  | ||||||
| 			wt := l.WriterTo.(safetensorWriterTo) |  | ||||||
| 			wt.repacker = m.Repack |  | ||||||
| 			l.WriterTo = wt |  | ||||||
| 		} |  | ||||||
| 		m.Tensors = append(m.Tensors, l) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *MixtralModel) LoadVocab() error { |  | ||||||
| 	v, err := LoadSentencePieceTokens(m.Path, m.Params) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	m.Vocab = v |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *MixtralModel) WriteGGUF(ws io.WriteSeeker) error { |  | ||||||
| 	kv := llm.KV{ |  | ||||||
| 		"general.architecture":          "llama", |  | ||||||
| 		"general.name":                  m.Name, |  | ||||||
| 		"llama.block_count":             uint32(m.Params.HiddenLayers), |  | ||||||
| 		"llama.context_length":          uint32(m.Params.ContextSize), |  | ||||||
| 		"llama.embedding_length":        uint32(m.Params.HiddenSize), |  | ||||||
| 		"llama.feed_forward_length":     uint32(m.Params.IntermediateSize), |  | ||||||
| 		"llama.attention.head_count":    uint32(m.Params.AttentionHeads), |  | ||||||
| 		"llama.attention.head_count_kv": uint32(m.Params.KeyValHeads), |  | ||||||
|  |  | ||||||
| 		"llama.rope.freq_base":                   float32(m.Params.RopeFrequencyBase), |  | ||||||
| 		"llama.attention.layer_norm_rms_epsilon": float32(m.Params.NormEPS), |  | ||||||
|  |  | ||||||
| 		"llama.expert_count":      uint32(m.Params.Experts), |  | ||||||
| 		"llama.expert_used_count": uint32(m.Params.ExpertsUsed), |  | ||||||
|  |  | ||||||
| 		"llama.vocab_size":           uint32(len(m.Vocab.Tokens)), |  | ||||||
| 		"llama.rope.dimension_count": uint32(m.Params.HiddenSize / m.Params.AttentionHeads), |  | ||||||
|  |  | ||||||
| 		"general.file_type":    uint32(1), |  | ||||||
| 		"tokenizer.ggml.model": "llama", |  | ||||||
|  |  | ||||||
| 		"tokenizer.ggml.tokens":     m.Vocab.Tokens, |  | ||||||
| 		"tokenizer.ggml.scores":     m.Vocab.Scores, |  | ||||||
| 		"tokenizer.ggml.token_type": m.Vocab.Types, |  | ||||||
|  |  | ||||||
| 		"tokenizer.ggml.bos_token_id":     uint32(m.Params.BoSTokenID), |  | ||||||
| 		"tokenizer.ggml.eos_token_id":     uint32(m.Params.EoSTokenID), |  | ||||||
| 		"tokenizer.ggml.unknown_token_id": uint32(0), |  | ||||||
| 		"tokenizer.ggml.add_bos_token":    true, |  | ||||||
| 		"tokenizer.ggml.add_eos_token":    false, |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return llm.NewGGUFV3(m.Params.ByteOrder).Encode(ws, kv, m.Tensors) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *MixtralModel) Repack(name string, data []float32, shape []uint64) ([]float32, error) { |  | ||||||
| 	return llamaRepack(name, m.Params, data, shape) |  | ||||||
| } |  | ||||||
| @@ -1,309 +0,0 @@ | |||||||
| package convert |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"bytes" |  | ||||||
| 	"encoding/binary" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"fmt" |  | ||||||
| 	"io" |  | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" |  | ||||||
| 	"regexp" |  | ||||||
| 	"slices" |  | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/d4l3k/go-bfloat16" |  | ||||||
| 	"github.com/x448/float16" |  | ||||||
|  |  | ||||||
| 	"github.com/ollama/ollama/llm" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| type safetensorWriterTo struct { |  | ||||||
| 	t *llm.Tensor |  | ||||||
|  |  | ||||||
| 	params *Params |  | ||||||
| 	bo     ByteOrder |  | ||||||
|  |  | ||||||
| 	filename string |  | ||||||
| 	dtype    string |  | ||||||
|  |  | ||||||
| 	offset, size int64 |  | ||||||
| 	repacker     func(string, []float32, []uint64) ([]float32, error) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type safetensorMetadata struct { |  | ||||||
| 	Type    string   `json:"dtype"` |  | ||||||
| 	Shape   []uint64 `json:"shape"` |  | ||||||
| 	Offsets []int64  `json:"data_offsets"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| type SafetensorFormat struct{} |  | ||||||
|  |  | ||||||
| func (m *SafetensorFormat) GetTensors(dirpath string, params *Params) ([]llm.Tensor, error) { |  | ||||||
| 	var tensors []llm.Tensor |  | ||||||
| 	matches, err := filepath.Glob(filepath.Join(dirpath, "*.safetensors")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var offset uint64 |  | ||||||
| 	for _, f := range matches { |  | ||||||
| 		var t []llm.Tensor |  | ||||||
| 		var err error |  | ||||||
| 		t, offset, err = m.readTensors(f, offset, params) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		tensors = append(tensors, t...) |  | ||||||
| 	} |  | ||||||
| 	return tensors, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *SafetensorFormat) readTensors(fn string, offset uint64, params *Params) ([]llm.Tensor, uint64, error) { |  | ||||||
| 	f, err := os.Open(fn) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, 0, err |  | ||||||
| 	} |  | ||||||
| 	defer f.Close() |  | ||||||
|  |  | ||||||
| 	var n int64 |  | ||||||
| 	if err := binary.Read(f, binary.LittleEndian, &n); err != nil { |  | ||||||
| 		return nil, 0, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	b := bytes.NewBuffer(make([]byte, 0, n)) |  | ||||||
| 	if _, err = io.CopyN(b, f, n); err != nil { |  | ||||||
| 		return nil, 0, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var headers map[string]safetensorMetadata |  | ||||||
| 	if err := json.NewDecoder(b).Decode(&headers); err != nil { |  | ||||||
| 		return nil, 0, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var keys []string |  | ||||||
| 	for key := range headers { |  | ||||||
| 		if !strings.HasSuffix(key, "self_attn.rotary_embd.inv_freq") { |  | ||||||
| 			keys = append(keys, key) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	slices.Sort(keys) |  | ||||||
|  |  | ||||||
| 	var tensors []llm.Tensor |  | ||||||
| 	for _, key := range keys { |  | ||||||
| 		value := headers[key] |  | ||||||
|  |  | ||||||
| 		var kind uint32 |  | ||||||
| 		switch len(value.Shape) { |  | ||||||
| 		case 0: |  | ||||||
| 			// valuedata |  | ||||||
| 			continue |  | ||||||
| 		case 2: |  | ||||||
| 			kind = 1 |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		name, err := m.GetLayerName(key) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, 0, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		shape := make([]uint64, len(value.Shape)) |  | ||||||
| 		copy(shape, value.Shape) |  | ||||||
|  |  | ||||||
| 		pad := func(s int64) int64 { |  | ||||||
| 			return 8 + n + s |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		t := llm.Tensor{ |  | ||||||
| 			Name:   name, |  | ||||||
| 			Kind:   kind, |  | ||||||
| 			Offset: offset, |  | ||||||
| 			Shape:  shape[:], |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		t.WriterTo = safetensorWriterTo{ |  | ||||||
| 			t:        &t, |  | ||||||
| 			params:   params, |  | ||||||
| 			bo:       params.ByteOrder, |  | ||||||
| 			filename: fn, |  | ||||||
| 			dtype:    value.Type, |  | ||||||
| 			offset:   pad(value.Offsets[0]), |  | ||||||
| 			size:     pad(value.Offsets[1]) - pad(value.Offsets[0]), |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		offset += t.Size() |  | ||||||
| 		tensors = append(tensors, t) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return tensors, offset, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *SafetensorFormat) GetParams(dirpath string) (*Params, error) { |  | ||||||
| 	f, err := os.Open(filepath.Join(dirpath, "config.json")) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	defer f.Close() |  | ||||||
|  |  | ||||||
| 	var params Params |  | ||||||
|  |  | ||||||
| 	if err := json.NewDecoder(f).Decode(¶ms); err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	params.ByteOrder = binary.LittleEndian |  | ||||||
| 	return ¶ms, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *SafetensorFormat) GetLayerName(n string) (string, error) { |  | ||||||
| 	directMap := map[string]string{ |  | ||||||
| 		"model.embed_tokens.weight": "token_embd.weight", |  | ||||||
| 		"lm_head.weight":            "output.weight", |  | ||||||
| 		"model.norm.weight":         "output_norm.weight", |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	tMap := map[string]string{ |  | ||||||
| 		"model.layers.(\\d+).input_layernorm.weight":                    "blk.$1.attn_norm.weight", |  | ||||||
| 		"model.layers.(\\d+).mlp.down_proj.weight":                      "blk.$1.ffn_down.weight", |  | ||||||
| 		"model.layers.(\\d+).mlp.gate_proj.weight":                      "blk.$1.ffn_gate.weight", |  | ||||||
| 		"model.layers.(\\d+).mlp.up_proj.weight":                        "blk.$1.ffn_up.weight", |  | ||||||
| 		"model.layers.(\\d+).post_attention_layernorm.weight":           "blk.$1.ffn_norm.weight", |  | ||||||
| 		"model.layers.(\\d+).self_attn.k_proj.weight":                   "blk.$1.attn_k.weight", |  | ||||||
| 		"model.layers.(\\d+).self_attn.o_proj.weight":                   "blk.$1.attn_output.weight", |  | ||||||
| 		"model.layers.(\\d+).self_attn.q_proj.weight":                   "blk.$1.attn_q.weight", |  | ||||||
| 		"model.layers.(\\d+).self_attn.v_proj.weight":                   "blk.$1.attn_v.weight", |  | ||||||
| 		"model.layers.(\\d+).block_sparse_moe.gate.weight":              "blk.$1.ffn_gate_inp.weight", |  | ||||||
| 		"model.layers.(\\d+).block_sparse_moe.experts.(\\d+).w1.weight": "blk.$1.ffn_gate.$2.weight", |  | ||||||
| 		"model.layers.(\\d+).block_sparse_moe.experts.(\\d+).w2.weight": "blk.$1.ffn_down.$2.weight", |  | ||||||
| 		"model.layers.(\\d+).block_sparse_moe.experts.(\\d+).w3.weight": "blk.$1.ffn_up.$2.weight", |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	v, ok := directMap[n] |  | ||||||
| 	if ok { |  | ||||||
| 		return v, nil |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// quick hack to rename the layers to gguf format |  | ||||||
| 	for k, v := range tMap { |  | ||||||
| 		re := regexp.MustCompile(k) |  | ||||||
| 		newName := re.ReplaceAllString(n, v) |  | ||||||
| 		if newName != n { |  | ||||||
| 			return newName, nil |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return "", fmt.Errorf("couldn't find a layer name for '%s'", n) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (r safetensorWriterTo) WriteTo(w io.Writer) (n int64, err error) { |  | ||||||
| 	f, err := os.Open(r.filename) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
| 	defer f.Close() |  | ||||||
|  |  | ||||||
| 	if _, err = f.Seek(r.offset, io.SeekStart); err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	var f32s []float32 |  | ||||||
| 	switch r.dtype { |  | ||||||
| 	case "F32": |  | ||||||
| 		f32s = make([]float32, r.size/4) |  | ||||||
| 		if err = binary.Read(f, r.bo, f32s); err != nil { |  | ||||||
| 			return 0, err |  | ||||||
| 		} |  | ||||||
| 	case "F16": |  | ||||||
| 		u16s := make([]uint16, r.size/2) |  | ||||||
| 		if err = binary.Read(f, r.bo, u16s); err != nil { |  | ||||||
| 			return 0, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		for _, b := range u16s { |  | ||||||
| 			f32s = append(f32s, float16.Frombits(b).Float32()) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 	case "BF16": |  | ||||||
| 		u8s := make([]uint8, r.size) |  | ||||||
| 		if err = binary.Read(f, r.bo, u8s); err != nil { |  | ||||||
| 			return 0, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		f32s = bfloat16.DecodeFloat32(u8s) |  | ||||||
| 	default: |  | ||||||
| 		return 0, fmt.Errorf("unknown data type: %s", r.dtype) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if r.repacker != nil { |  | ||||||
| 		f32s, err = r.repacker(r.t.Name, f32s, r.t.Shape) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return 0, err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	switch r.t.Kind { |  | ||||||
| 	case 0: |  | ||||||
| 		return 0, binary.Write(w, r.bo, f32s) |  | ||||||
| 	case 1: |  | ||||||
| 		f16s := make([]uint16, len(f32s)) |  | ||||||
| 		for i := range f32s { |  | ||||||
| 			f16s[i] = float16.Fromfloat32(f32s[i]).Bits() |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		return 0, binary.Write(w, r.bo, f16s) |  | ||||||
| 	default: |  | ||||||
| 		return 0, fmt.Errorf("unknown storage type: %d", r.t.Kind) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func (m *SafetensorFormat) GetModelArch(name, dirPath string, params *Params) (ModelArch, error) { |  | ||||||
| 	switch len(params.Architectures) { |  | ||||||
| 	case 0: |  | ||||||
| 		return nil, fmt.Errorf("No architecture specified to convert") |  | ||||||
| 	case 1: |  | ||||||
| 		switch params.Architectures[0] { |  | ||||||
| 		case "LlamaForCausalLM": |  | ||||||
| 			return &LlamaModel{ |  | ||||||
| 				ModelData{ |  | ||||||
| 					Name:   name, |  | ||||||
| 					Path:   dirPath, |  | ||||||
| 					Params: params, |  | ||||||
| 					Format: m, |  | ||||||
| 				}, |  | ||||||
| 			}, nil |  | ||||||
| 		case "MistralForCausalLM": |  | ||||||
| 			return &MistralModel{ |  | ||||||
| 				ModelData{ |  | ||||||
| 					Name:   name, |  | ||||||
| 					Path:   dirPath, |  | ||||||
| 					Params: params, |  | ||||||
| 					Format: m, |  | ||||||
| 				}, |  | ||||||
| 			}, nil |  | ||||||
| 		case "MixtralForCausalLM": |  | ||||||
| 			return &MixtralModel{ |  | ||||||
| 				ModelData{ |  | ||||||
| 					Name:   name, |  | ||||||
| 					Path:   dirPath, |  | ||||||
| 					Params: params, |  | ||||||
| 					Format: m, |  | ||||||
| 				}, |  | ||||||
| 			}, nil |  | ||||||
| 		case "GemmaForCausalLM": |  | ||||||
| 			return &GemmaModel{ |  | ||||||
| 				ModelData{ |  | ||||||
| 					Name:   name, |  | ||||||
| 					Path:   dirPath, |  | ||||||
| 					Params: params, |  | ||||||
| 					Format: m, |  | ||||||
| 				}, |  | ||||||
| 			}, nil |  | ||||||
| 		default: |  | ||||||
| 			return nil, fmt.Errorf("Models based on '%s' are not yet supported", params.Architectures[0]) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil, fmt.Errorf("Unknown error") |  | ||||||
| } |  | ||||||
| @@ -1,333 +0,0 @@ | |||||||
| // Copyright 2016 Google Inc. |  | ||||||
| // |  | ||||||
| // Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
| // you may not use this file except in compliance with the License. |  | ||||||
| // You may obtain a copy of the License at |  | ||||||
| // |  | ||||||
| //     http://www.apache.org/licenses/LICENSE-2.0 |  | ||||||
| // |  | ||||||
| // Unless required by applicable law or agreed to in writing, software |  | ||||||
| // distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
| // See the License for the specific language governing permissions and |  | ||||||
| // limitations under the License.! |  | ||||||
|  |  | ||||||
| syntax = "proto2"; |  | ||||||
|  |  | ||||||
| // TODO(taku): Needs to use LITE RUNTIME in OSS release. |  | ||||||
| option optimize_for = LITE_RUNTIME; |  | ||||||
| option go_package = "./sentencepiece"; |  | ||||||
|  |  | ||||||
| package sentencepiece; |  | ||||||
|  |  | ||||||
| // TrainerSpec encodes a various parameters for SentencePiece training. |  | ||||||
| // Next id: 55 |  | ||||||
| message TrainerSpec { |  | ||||||
|   /////////////////////////////////////////////////////////////////// |  | ||||||
|   // General parameters |  | ||||||
|   // |  | ||||||
|   // Input corpus files. |  | ||||||
|   //  Trainer accepts the following two formats: |  | ||||||
|   //  A) Monolingual: plain text, one sentence per line. |  | ||||||
|   //  B) Bilingual:   TSV, source sentence <tab> target sentence |  | ||||||
|   //  When bilingual data is passed, shared vocabulary model is built. |  | ||||||
|   //  Note that the input file must be raw corpus, not a preprocessed corpus. |  | ||||||
|   //  Trainer only loads the first `input_sentence_size` sentences specified |  | ||||||
|   //  with this parameter. |  | ||||||
|   repeated string input = 1; |  | ||||||
|  |  | ||||||
|   // Input corpus format: |  | ||||||
|   // "text": one-sentence-per-line text format (default) |  | ||||||
|   // "tsv":  sentence <tab> freq |  | ||||||
|   optional string input_format = 7; |  | ||||||
|  |  | ||||||
|   // Output model file prefix. |  | ||||||
|   // <model_prefix>.model and <model_prefix>.vocab are generated. |  | ||||||
|   optional string model_prefix = 2; |  | ||||||
|  |  | ||||||
|   // Model type. only have UNIGRAM now. |  | ||||||
|   enum ModelType { |  | ||||||
|     UNIGRAM = 1;  // Unigram language model with dynamic algorithm |  | ||||||
|     BPE = 2;      // Byte Pair Encoding |  | ||||||
|     WORD = 3;     // Delimitered by whitespace. |  | ||||||
|     CHAR = 4;     // tokenizes into character sequence |  | ||||||
|   } |  | ||||||
|   optional ModelType model_type = 3 [default = UNIGRAM]; |  | ||||||
|  |  | ||||||
|   // Vocabulary size. 8k is the default size. |  | ||||||
|   optional int32 vocab_size = 4 [default = 8000]; |  | ||||||
|  |  | ||||||
|   // List of the languages this model can accept. |  | ||||||
|   // Since the model is language-agnostic, this field is used as a reference. |  | ||||||
|   repeated string accept_language = 5; |  | ||||||
|  |  | ||||||
|   // Size of self-test samples, which are encoded in the model file. |  | ||||||
|   optional int32 self_test_sample_size = 6 [default = 0]; |  | ||||||
|  |  | ||||||
|   // Whether to use DP version of sentencepiece. Use it with TSV input format |  | ||||||
|   // (requires precomputed word tab counts to work). |  | ||||||
|   optional bool enable_differential_privacy = 50 [default = false]; |  | ||||||
|   // Set these parameters if you need DP version of sentencepiece. |  | ||||||
|   // std of noise to add. |  | ||||||
|   optional float differential_privacy_noise_level = 51 [default = 0.0]; |  | ||||||
|   // Clipping threshold to apply after adding noise. All the words with |  | ||||||
|   // frequency less than this value are dropped. |  | ||||||
|   optional uint64 differential_privacy_clipping_threshold = 52 [default = 0]; |  | ||||||
|  |  | ||||||
|   /////////////////////////////////////////////////////////////////// |  | ||||||
|   // Training parameters. |  | ||||||
|   // |  | ||||||
|   // Uses characters which cover the corpus with the ratio of `chars_coverage`. |  | ||||||
|   // This parameter determines the set of basic Alphabet of sentence piece. |  | ||||||
|   // 1.0 - `chars_coverage` characters are treated as UNK. |  | ||||||
|   // See also required_chars field. |  | ||||||
|   optional float character_coverage = 10 [default = 0.9995]; |  | ||||||
|  |  | ||||||
|   // Maximum size of sentences the trainer loads from `input` parameter. |  | ||||||
|   // Trainer simply loads the `input` files in sequence. |  | ||||||
|   // It is better to shuffle the input corpus randomly. |  | ||||||
|   optional uint64 input_sentence_size = 11 [default = 0]; |  | ||||||
|   optional bool shuffle_input_sentence = 19 [default = true]; |  | ||||||
|  |  | ||||||
|   // Maximum size of sentences to make seed sentence pieces. |  | ||||||
|   // Extended suffix array is constructed to extract frequent |  | ||||||
|   // sub-strings from the corpus. This uses 20N working space, |  | ||||||
|   // where N is the size of corpus. |  | ||||||
|   optional int32 mining_sentence_size = 12 [deprecated = true]; |  | ||||||
|  |  | ||||||
|   // Maximum size of sentences to train sentence pieces. |  | ||||||
|   optional int32 training_sentence_size = 13 [deprecated = true]; |  | ||||||
|  |  | ||||||
|   // The size of seed sentencepieces. |  | ||||||
|   // `seed_sentencepiece_size` must be larger than `vocab_size`. |  | ||||||
|   optional int32 seed_sentencepiece_size = 14 [default = 1000000]; |  | ||||||
|  |  | ||||||
|   // In every EM sub-iterations, keeps top |  | ||||||
|   // `shrinking_factor` * `current sentencepieces size` with respect to |  | ||||||
|   // the loss of the sentence piece. This value should be smaller than 1.0. |  | ||||||
|   optional float shrinking_factor = 15 [default = 0.75]; |  | ||||||
|  |  | ||||||
|   // The maximum sentence length in byte. The sentences with the length |  | ||||||
|   // larger than `max_sentence_length` is simply ignored. |  | ||||||
|   // Longer input tends to bring the following risks: |  | ||||||
|   //  * Overflow during EM training (unigram language model only) |  | ||||||
|   //  * Performance drop because of O(n log n) cost in BPE. |  | ||||||
|   optional int32 max_sentence_length = 18 [default = 4192]; |  | ||||||
|  |  | ||||||
|   // Number of threads in the training. |  | ||||||
|   optional int32 num_threads = 16 [default = 16]; |  | ||||||
|  |  | ||||||
|   // Number of EM sub iterations. |  | ||||||
|   optional int32 num_sub_iterations = 17 [default = 2]; |  | ||||||
|  |  | ||||||
|   /////////////////////////////////////////////////////////////////// |  | ||||||
|   // SentencePiece parameters which control the shapes of sentence piece. |  | ||||||
|   // |  | ||||||
|   // Maximum length of sentencepiece. |  | ||||||
|   optional int32 max_sentencepiece_length = 20 [default = 16]; |  | ||||||
|  |  | ||||||
|   // Uses Unicode script to split sentence pieces. |  | ||||||
|   // When `split_by_unicode_script` is true, we do not allow sentence piece to |  | ||||||
|   // include multiple Unicode scripts, e.g. "F1" is not a valid piece. |  | ||||||
|   // Exception: CJ characters (Hiragana/Katakana/Han) are all handled |  | ||||||
|   // as one script type, since Japanese word can consist of multiple scripts. |  | ||||||
|   // This exception is always applied regardless of the accept-language |  | ||||||
|   // parameter. |  | ||||||
|   optional bool split_by_unicode_script = 21 [default = true]; |  | ||||||
|  |  | ||||||
|   // When `split_by_number` is true, put a boundary between number and |  | ||||||
|   // non-number transition. If we want to treat "F1" is one token, set this flag |  | ||||||
|   // to be false. |  | ||||||
|   optional bool split_by_number = 23 [default = true]; |  | ||||||
|  |  | ||||||
|   // Use a white space to split sentence pieces. |  | ||||||
|   // When `split_by_whitespace` is false, we may have the piece containing |  | ||||||
|   // a white space in the middle. e.g., "in_the". |  | ||||||
|   optional bool split_by_whitespace = 22 [default = true]; |  | ||||||
|  |  | ||||||
|   // Adds whitespace symbol (_) as a suffix instead of prefix. e.g., _hello => |  | ||||||
|   // hello_. When `treat_whitespace_as_suffix` is true, |  | ||||||
|   // NormalizerSpec::add_dummy_prefix will add the dummy whitespace to the end |  | ||||||
|   // of sentence. |  | ||||||
|   optional bool treat_whitespace_as_suffix = 24 [default = false]; |  | ||||||
|  |  | ||||||
|   // Allows pieces that only contain whitespaces instead of appearing only as |  | ||||||
|   // prefix or suffix of other pieces. |  | ||||||
|   optional bool allow_whitespace_only_pieces = 26 [default = false]; |  | ||||||
|  |  | ||||||
|   // Split all digits (0-9) into separate pieces. |  | ||||||
|   optional bool split_digits = 25 [default = false]; |  | ||||||
|  |  | ||||||
|   // Defines the pre-tokenization delimiter. |  | ||||||
|   // When specified, no pieces crossing this delimiter is not included |  | ||||||
|   // in the vocab. Then the delimiter string is virtually ignored |  | ||||||
|   // during the training. This field can allows constraints on the vocabulary |  | ||||||
|   // selection. Note that this field is available on unigram mode. |  | ||||||
|   optional string pretokenization_delimiter = 53 [ default = ""]; |  | ||||||
|  |  | ||||||
|   /////////////////////////////////////////////////////////////////// |  | ||||||
|   // Vocabulary management |  | ||||||
|   // |  | ||||||
|   // Defines control symbols used as an indicator to |  | ||||||
|   // change the behavior of the decoder. <s> and </s> are pre-defined. |  | ||||||
|   // We can use this field to encode various meta information, |  | ||||||
|   // including language indicator in multilingual model. |  | ||||||
|   // These symbols are not visible to users, but visible to |  | ||||||
|   // the decoder. Note that when the input sentence contains control symbols, |  | ||||||
|   // they are not treated as one token, but segmented into normal pieces. |  | ||||||
|   // Control symbols must be inserted independently from the segmentation. |  | ||||||
|   repeated string control_symbols = 30; |  | ||||||
|  |  | ||||||
|   // Defines user defined symbols. |  | ||||||
|   // These symbols are added with extremely high score |  | ||||||
|   // so they are always treated as one unique symbol in any context. |  | ||||||
|   // Typical usage of user_defined_symbols is placeholder for named entities. |  | ||||||
|   repeated string user_defined_symbols = 31; |  | ||||||
|  |  | ||||||
|   // Defines required characters. Each UTF8 character in this string is included |  | ||||||
|   // in the character set regardless of character_coverage value. Unlike |  | ||||||
|   // user_defined_symbols, these characters have scores based on the frequency |  | ||||||
|   // on input sentences, and the model can form subwords using characters |  | ||||||
|   // in this field. |  | ||||||
|   optional string required_chars = 36; |  | ||||||
|  |  | ||||||
|   // Decomposes unknown pieces into UTF-8 bytes. |  | ||||||
|   optional bool byte_fallback = 35 [default = false]; |  | ||||||
|  |  | ||||||
|   // When creating the vocabulary file, defines whether or not to additionally |  | ||||||
|   // output the score for each piece. |  | ||||||
|   optional bool vocabulary_output_piece_score = 32 [default = true]; |  | ||||||
|  |  | ||||||
|   // `vocab_size` is treated as hard limit. Crash if |  | ||||||
|   // the model can not produce the vocab of size `vocab_size`, |  | ||||||
|   // When `hard_vocab_limit` is false, vocab_size is treated |  | ||||||
|   // as soft limit. Note that when model_type=char, |  | ||||||
|   // always assumes hard_vocab_limit = false. |  | ||||||
|   optional bool hard_vocab_limit = 33 [default = true]; |  | ||||||
|  |  | ||||||
|   // use all symbols for vocab extraction. This flag is valid |  | ||||||
|   // if model type is either CHAR or WORD |  | ||||||
|   optional bool use_all_vocab = 34 [default = false]; |  | ||||||
|  |  | ||||||
|   /////////////////////////////////////////////////////////////////// |  | ||||||
|   // Reserved special meta tokens. |  | ||||||
|   // * -1 is not used. |  | ||||||
|   // * unk_id must not be -1. |  | ||||||
|   // Id must starts with 0 and be contigous. |  | ||||||
|   optional int32 unk_id = 40 [default = 0];   // <unk> |  | ||||||
|   optional int32 bos_id = 41 [default = 1];   // <s> |  | ||||||
|   optional int32 eos_id = 42 [default = 2];   // </s> |  | ||||||
|   optional int32 pad_id = 43 [default = -1];  // <pad> (padding) |  | ||||||
|   optional string unk_piece = 45 [default = "<unk>"]; |  | ||||||
|   optional string bos_piece = 46 [default = "<s>"]; |  | ||||||
|   optional string eos_piece = 47 [default = "</s>"]; |  | ||||||
|   optional string pad_piece = 48 [default = "<pad>"]; |  | ||||||
|  |  | ||||||
|   // Encodes <unk> into U+2047 (DOUBLE QUESTION MARK), |  | ||||||
|   // since this character can be useful both for user and |  | ||||||
|   // developer. We can easily figure out that <unk> is emitted. |  | ||||||
|   optional string unk_surface = 44 [default = " \xE2\x81\x87 "]; |  | ||||||
|  |  | ||||||
|   // Increase bit depth to allow unigram model training on large |  | ||||||
|   // (>10M sentences) corpora. A Side-effect of enabling this flag |  | ||||||
|   // is increased memory usage. |  | ||||||
|   optional bool train_extremely_large_corpus = 49 [default = false]; |  | ||||||
|  |  | ||||||
|  // Path to a seed sentencepieces file, with one tab-separated |  | ||||||
|   // seed sentencepiece <tab> frequency per line. |  | ||||||
|   optional string seed_sentencepieces_file = 54 [default = ""]; |  | ||||||
|  |  | ||||||
|   // Customized extensions: the range of field numbers |  | ||||||
|   // are open to third-party extensions. |  | ||||||
|   extensions 200 to max; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // NormalizerSpec encodes a various parameters for string normalizaiton |  | ||||||
| message NormalizerSpec { |  | ||||||
|   // name of normalization rule. |  | ||||||
|   optional string name = 1; |  | ||||||
|  |  | ||||||
|   // Pre-compiled normalization rule created by |  | ||||||
|   // Builder::GetPrecompiledCharsMap() or Builder::CompileCharsMap() method. |  | ||||||
|   // Usually this field is set by Builder::GetNormalizerSpec() method. |  | ||||||
|   optional bytes precompiled_charsmap = 2; |  | ||||||
|  |  | ||||||
|   // Adds dummy whitespace at the beginning of text in order to |  | ||||||
|   // treat "world" in "world" and "hello world" in the same way. |  | ||||||
|   optional bool add_dummy_prefix = 3 [default = true]; |  | ||||||
|  |  | ||||||
|   // Removes leading, trailing, and duplicate internal whitespace. |  | ||||||
|   optional bool remove_extra_whitespaces = 4 [default = true]; |  | ||||||
|  |  | ||||||
|   // Replaces whitespace with meta symbol. |  | ||||||
|   // This field must be true to train sentence piece model. |  | ||||||
|   optional bool escape_whitespaces = 5 [default = true]; |  | ||||||
|  |  | ||||||
|   // Custom normalization rule file in TSV format. |  | ||||||
|   // https://github.com/google/sentencepiece/blob/master/doc/normalization.md |  | ||||||
|   // This field is only used in SentencePieceTrainer::Train() method, which |  | ||||||
|   // compiles the rule into the binary rule stored in `precompiled_charsmap`. |  | ||||||
|   optional string normalization_rule_tsv = 6; |  | ||||||
|  |  | ||||||
|   // Customized extensions: the range of field numbers |  | ||||||
|   // are open to third-party extensions. |  | ||||||
|   extensions 200 to max; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // Proto to store samples for self-testing. |  | ||||||
| message SelfTestData { |  | ||||||
|   message Sample { |  | ||||||
|     optional string input = 1; |  | ||||||
|     optional string expected = 2; |  | ||||||
|   } |  | ||||||
|   repeated Sample samples = 1; |  | ||||||
|  |  | ||||||
|   // Customized extensions: the range of field numbers |  | ||||||
|   // are open to third-party extensions. |  | ||||||
|   extensions 200 to max; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ModelProto stores model parameters. |  | ||||||
| // SentencePieceProcessor is supposed to be self-contained. |  | ||||||
| // All settings/parameters which may change the behavior must be encoded |  | ||||||
| // in ModelProto. |  | ||||||
| message ModelProto { |  | ||||||
|   message SentencePiece { |  | ||||||
|     enum Type { |  | ||||||
|       NORMAL = 1;        // normal symbol |  | ||||||
|       UNKNOWN = 2;       // unknown symbol. only <unk> for now. |  | ||||||
|       CONTROL = 3;       // control symbols. </s>, <s>, <2ja> etc. |  | ||||||
|       USER_DEFINED = 4;  // user defined symbols. |  | ||||||
|                          // Typical usage of USER_DEFINED symbol |  | ||||||
|                          // is placeholder. |  | ||||||
|       BYTE = 6;          // byte symbols. Used when `byte_fallback` is true. |  | ||||||
|       UNUSED = 5;        // this piece is not used. |  | ||||||
|     } |  | ||||||
|     optional string piece = 1;  // piece must not be empty. |  | ||||||
|     optional float score = 2; |  | ||||||
|     optional Type type = 3 [default = NORMAL]; |  | ||||||
|  |  | ||||||
|     // Customized extensions: the range of field numbers |  | ||||||
|     // are open to third-party extensions. |  | ||||||
|     extensions 200 to max; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Sentence pieces with scores. |  | ||||||
|   repeated SentencePiece pieces = 1; |  | ||||||
|  |  | ||||||
|   // Spec used to generate this model file. |  | ||||||
|   optional TrainerSpec trainer_spec = 2; |  | ||||||
|  |  | ||||||
|   // Spec for text normalization. |  | ||||||
|   optional NormalizerSpec normalizer_spec = 3; |  | ||||||
|  |  | ||||||
|   // Stores sample input and its expected segmentation to verify the model. |  | ||||||
|   optional SelfTestData self_test_data = 4; |  | ||||||
|  |  | ||||||
|   // Spec for text de-normalization. |  | ||||||
|   optional NormalizerSpec denormalizer_spec = 5; |  | ||||||
|  |  | ||||||
|   // Customized extensions: the range of field numbers |  | ||||||
|   // are open to third-party extensions. |  | ||||||
|   extensions 200 to max; |  | ||||||
| } |  | ||||||