Compare commits
	
		
			1 Commits
		
	
	
		
			v0.3.3
			...
			fix-unknow
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					3168f51125 | 
@@ -1,9 +1,7 @@
 | 
			
		||||
build
 | 
			
		||||
llama/build
 | 
			
		||||
.venv
 | 
			
		||||
.vscode
 | 
			
		||||
ollama
 | 
			
		||||
app
 | 
			
		||||
macapp
 | 
			
		||||
dist
 | 
			
		||||
llm/llama.cpp
 | 
			
		||||
.env
 | 
			
		||||
.cache
 | 
			
		||||
test_data
 | 
			
		||||
web
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
							
								
								
									
										480
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,480 +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: "stable"
 | 
			
		||||
          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: "stable"
 | 
			
		||||
          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: "stable"
 | 
			
		||||
          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-24.Q3-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: "stable"
 | 
			
		||||
          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: "stable"
 | 
			
		||||
          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'
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    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
 | 
			
		||||
      - name: Create or update Release
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "Looking for existing release for ${{ env.RELEASE_VERSION }}"
 | 
			
		||||
          OLD_TAG=$(gh release ls --json name,tagName | jq -r ".[] | select(.name == \"${{ env.RELEASE_VERSION }}\") | .tagName")
 | 
			
		||||
          if [ -n "$OLD_TAG" ]; then
 | 
			
		||||
            echo "Updating release ${{ env.RELEASE_VERSION }} to point to new tag ${GITHUB_REF_NAME}"
 | 
			
		||||
            gh release edit ${OLD_TAG} --tag ${GITHUB_REF_NAME}
 | 
			
		||||
          else
 | 
			
		||||
            echo "Creating new release ${{ env.RELEASE_VERSION }} pointing to tag ${GITHUB_REF_NAME}"
 | 
			
		||||
            gh release create ${GITHUB_REF_NAME} \
 | 
			
		||||
              --title ${{ env.RELEASE_VERSION }} \
 | 
			
		||||
              --draft \
 | 
			
		||||
              --generate-notes \
 | 
			
		||||
              --prerelease
 | 
			
		||||
          fi
 | 
			
		||||
          echo "Uploading artifacts for tag ${GITHUB_REF_NAME}"
 | 
			
		||||
          gh release upload ${GITHUB_REF_NAME} dist/* --clobber
 | 
			
		||||
							
								
								
									
										323
									
								
								.github/workflows/test.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,323 +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 }}
 | 
			
		||||
      CGO_ENABLED: '1'
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
      - uses: actions/setup-go@v5
 | 
			
		||||
        with:
 | 
			
		||||
          go-version: "stable"
 | 
			
		||||
          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'
 | 
			
		||||
      - run: go build .
 | 
			
		||||
      - 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.1.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: "stable"
 | 
			
		||||
          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-24.Q3-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: "stable"
 | 
			
		||||
          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: "stable"
 | 
			
		||||
          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@v6
 | 
			
		||||
        with:
 | 
			
		||||
          args: --timeout 8m0s -v ${{ startsWith(matrix.os, 'windows-') && '' || '--disable gofmt --disable goimports' }}
 | 
			
		||||
  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: "stable"
 | 
			
		||||
          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
 | 
			
		||||
							
								
								
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -2,14 +2,5 @@
 | 
			
		||||
.vscode
 | 
			
		||||
.env
 | 
			
		||||
.venv
 | 
			
		||||
.swp
 | 
			
		||||
dist
 | 
			
		||||
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,34 +0,0 @@
 | 
			
		||||
run:
 | 
			
		||||
  timeout: 5m
 | 
			
		||||
linters:
 | 
			
		||||
  enable:
 | 
			
		||||
    - asasalint
 | 
			
		||||
    - bidichk
 | 
			
		||||
    - bodyclose
 | 
			
		||||
    - containedctx
 | 
			
		||||
    - contextcheck
 | 
			
		||||
    - exportloopref
 | 
			
		||||
    - gocheckcompilerdirectives
 | 
			
		||||
    # conditionally enable this on linux/macos
 | 
			
		||||
    # - gofmt
 | 
			
		||||
    # - goimports
 | 
			
		||||
    - intrange
 | 
			
		||||
    - misspell
 | 
			
		||||
    - nilerr
 | 
			
		||||
    - nolintlint
 | 
			
		||||
    - nosprintfhostport
 | 
			
		||||
    - testifylint
 | 
			
		||||
    - unconvert
 | 
			
		||||
    - unused
 | 
			
		||||
    - wastedassign
 | 
			
		||||
    - whitespace
 | 
			
		||||
    - usestdlibvars
 | 
			
		||||
severity:
 | 
			
		||||
  default-severity: error
 | 
			
		||||
  rules:
 | 
			
		||||
    - linters:
 | 
			
		||||
        - gofmt
 | 
			
		||||
        - goimports
 | 
			
		||||
        - intrange
 | 
			
		||||
        - usestdlibvars
 | 
			
		||||
      severity: info
 | 
			
		||||
							
								
								
									
										147
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						@@ -1,144 +1,15 @@
 | 
			
		||||
ARG GOLANG_VERSION=1.22.5
 | 
			
		||||
ARG CMAKE_VERSION=3.22.1
 | 
			
		||||
# this CUDA_VERSION corresponds with the one specified in docs/gpu.md
 | 
			
		||||
ARG CUDA_VERSION=11.3.1
 | 
			
		||||
ARG ROCM_VERSION=6.1.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 rockylinux:8 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/gcc-toolset-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
 | 
			
		||||
FROM golang:1.20
 | 
			
		||||
WORKDIR /go/src/github.com/jmorganca/ollama
 | 
			
		||||
COPY . .
 | 
			
		||||
COPY --from=static-build-amd64 /go/src/github.com/ollama/ollama/llm/build/linux/ llm/build/linux/
 | 
			
		||||
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 .
 | 
			
		||||
RUN CGO_ENABLED=1 go build -ldflags '-linkmode external -extldflags "-static"' .
 | 
			
		||||
 | 
			
		||||
# Intermediate stage used for ./scripts/build_linux.sh
 | 
			
		||||
FROM --platform=linux/arm64 cpu-build-arm64 AS build-arm64
 | 
			
		||||
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
 | 
			
		||||
FROM alpine
 | 
			
		||||
COPY --from=0 /go/src/github.com/jmorganca/ollama/ollama /bin/ollama
 | 
			
		||||
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"]
 | 
			
		||||
CMD ["serve"]
 | 
			
		||||
 | 
			
		||||
FROM runtime-$TARGETARCH
 | 
			
		||||
EXPOSE 11434
 | 
			
		||||
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"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										393
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -1,118 +1,74 @@
 | 
			
		||||
<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>
 | 
			
		||||
 | 
			
		||||
# Ollama
 | 
			
		||||
 | 
			
		||||
[](https://discord.gg/ollama)
 | 
			
		||||
 | 
			
		||||
Get up and running with large language models.
 | 
			
		||||
> 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.com/download/OllamaSetup.exe)
 | 
			
		||||
 | 
			
		||||
### 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)
 | 
			
		||||
- [Download](https://ollama.ai/download) for macOS on Apple Silicon (Intel coming soon)
 | 
			
		||||
- Download for Windows and Linux (coming soon)
 | 
			
		||||
- Build [from source](#building)
 | 
			
		||||
 | 
			
		||||
## Quickstart
 | 
			
		||||
 | 
			
		||||
To run and chat with [Llama 3.1](https://ollama.com/library/llama3.1):
 | 
			
		||||
To run and chat with [Llama 2](https://ai.meta.com/llama), the new model by Meta:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama run llama3.1
 | 
			
		||||
ollama run llama2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 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                    |
 | 
			
		||||
| ------------------------ | ---------- | ----- | --------------------------- |
 | 
			
		||||
| Llama2                   | 7B         | 3.8GB | `ollama pull llama2`        |
 | 
			
		||||
| Llama2 13B               | 13B        | 7.3GB | `ollama pull llama2:13b`    |
 | 
			
		||||
| Orca Mini                | 3B         | 1.9GB | `ollama pull orca`          |
 | 
			
		||||
| Vicuna                   | 7B         | 3.8GB | `ollama pull vicuna`        |
 | 
			
		||||
| Nous-Hermes              | 13B        | 7.3GB | `ollama pull nous-hermes`   |
 | 
			
		||||
| Wizard Vicuna Uncensored | 13B        | 7.3GB | `ollama pull wizard-vicuna` |
 | 
			
		||||
 | 
			
		||||
| Model              | Parameters | Size  | Download                       |
 | 
			
		||||
| ------------------ | ---------- | ----- | ------------------------------ |
 | 
			
		||||
| Llama 3.1          | 8B         | 4.7GB | `ollama run llama3.1`          |
 | 
			
		||||
| Llama 3.1          | 70B        | 40GB  | `ollama run llama3.1:70b`      |
 | 
			
		||||
| Llama 3.1          | 405B       | 231GB | `ollama run llama3.1:405b`     |
 | 
			
		||||
| Phi 3 Mini         | 3.8B       | 2.3GB | `ollama run phi3`              |
 | 
			
		||||
| Phi 3 Medium       | 14B        | 7.9GB | `ollama run phi3:medium`       |
 | 
			
		||||
| Gemma 2            | 9B         | 5.5GB | `ollama run gemma2`            |
 | 
			
		||||
| Gemma 2            | 27B        | 16GB  | `ollama run gemma2:27b`        |
 | 
			
		||||
| 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 to run the 3B models, 16 GB to run the 7B models, and 32 GB to run the 13B models.
 | 
			
		||||
 | 
			
		||||
> [!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.
 | 
			
		||||
## Examples
 | 
			
		||||
 | 
			
		||||
## Customize a model
 | 
			
		||||
 | 
			
		||||
### Import from GGUF
 | 
			
		||||
 | 
			
		||||
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.1` model:
 | 
			
		||||
### Run a model
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama pull llama3.1
 | 
			
		||||
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`:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
FROM llama3.1
 | 
			
		||||
FROM llama2
 | 
			
		||||
 | 
			
		||||
# set the temperature to 1 [higher is more creative, lower is more coherent]
 | 
			
		||||
PARAMETER temperature 1
 | 
			
		||||
 | 
			
		||||
# set the system message
 | 
			
		||||
# set the system prompt
 | 
			
		||||
SYSTEM """
 | 
			
		||||
You are Mario from Super Mario Bros. Answer as Mario, the assistant, only.
 | 
			
		||||
"""
 | 
			
		||||
@@ -127,280 +83,55 @@ ollama run 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
 | 
			
		||||
 | 
			
		||||
### Create a model
 | 
			
		||||
 | 
			
		||||
`ollama create` is used to create a model from a Modelfile.
 | 
			
		||||
### Pull a model from the registry
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama create mymodel -f ./Modelfile
 | 
			
		||||
ollama pull orca
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Pull a model
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama pull llama3.1
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> This command can also be used to update a local model. Only the diff will be pulled.
 | 
			
		||||
 | 
			
		||||
### Remove a model
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama rm llama3.1
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Copy a model
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama cp llama3.1 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
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama run llava "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.1 "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.
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Show model information
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
ollama show llama3.1
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### List models on your computer
 | 
			
		||||
### Listing local models
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
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.1
 | 
			
		||||
./ollama serve &
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Finally, run a model!
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
./ollama run llama2
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 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 '{
 | 
			
		||||
  "model": "llama3.1",
 | 
			
		||||
  "prompt":"Why is the sky blue?"
 | 
			
		||||
}'
 | 
			
		||||
curl -X POST http://localhost:11434/api/generate -d '{"model": "llama2", "prompt":"Why is the sky blue?"}'
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Chat with a model
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
curl http://localhost:11434/api/chat -d '{
 | 
			
		||||
  "model": "llama3.1",
 | 
			
		||||
  "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)
 | 
			
		||||
- [LLocal.in](https://github.com/kartikm7/llocal) (Easy to use Electron Desktop Client for Ollama)
 | 
			
		||||
- [Ollama with Google Mesop](https://github.com/rapidarchitect/ollama_mesop/) (Mesop Chat Client implementation with Ollama)
 | 
			
		||||
- [Kerlig AI](https://www.kerlig.com/) (AI writing assistant for macOS)
 | 
			
		||||
- [AI Studio](https://github.com/MindWorkAI/AI-Studio)
 | 
			
		||||
- [Sidellama](https://github.com/gyopak/sidellama) (browser-based LLM client)
 | 
			
		||||
- [LLMStack](https://github.com/trypromptly/LLMStack) (No-code multi-agent framework to build LLM agents and workflows)
 | 
			
		||||
- [BoltAI for Mac](https://boltai.com) (AI Chat Client for Mac)
 | 
			
		||||
 | 
			
		||||
### 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)
 | 
			
		||||
- [gollama](https://github.com/sammcj/gollama)
 | 
			
		||||
 | 
			
		||||
### 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)
 | 
			
		||||
- [Firebase Genkit](https://firebase.google.com/docs/genkit/plugins/ollama)
 | 
			
		||||
- [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)
 | 
			
		||||
- [LangChainRust](https://github.com/Abraxas-365/langchain-rust) with [example](https://github.com/Abraxas-365/langchain-rust/blob/main/examples/llm_ollama.rs)
 | 
			
		||||
- [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)
 | 
			
		||||
- [Ollama-hpp for C++](https://github.com/jmont-dev/ollama-hpp)
 | 
			
		||||
- [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 Hugging Face)
 | 
			
		||||
- [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.
 | 
			
		||||
- [Headless Ollama](https://github.com/nischalj10/headless-ollama) (Scripts to automatically install ollama client & models on any OS for apps that depends on ollama server)
 | 
			
		||||
 | 
			
		||||
### Supported backends
 | 
			
		||||
 | 
			
		||||
- [llama.cpp](https://github.com/ggerganov/llama.cpp) project founded by Georgi Gerganov.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								SECURITY.md
									
									
									
									
									
								
							
							
						
						@@ -1,25 +0,0 @@
 | 
			
		||||
# Security
 | 
			
		||||
 | 
			
		||||
The Ollama maintainer team takes security seriously and will actively work to resolve security issues.
 | 
			
		||||
 | 
			
		||||
## Reporting a vulnerability
 | 
			
		||||
 | 
			
		||||
If you discover a security vulnerability, please do not open a public issue. Instead, please report it by emailing hello@ollama.com. We ask that you give us sufficient time to investigate and address the vulnerability before disclosing it publicly.
 | 
			
		||||
 | 
			
		||||
Please include the following details in your report:
 | 
			
		||||
- A description of the vulnerability
 | 
			
		||||
- Steps to reproduce the issue
 | 
			
		||||
- Your assessment of the potential impact
 | 
			
		||||
- Any possible mitigations
 | 
			
		||||
 | 
			
		||||
## Security best practices
 | 
			
		||||
 | 
			
		||||
While the maintainer team does their best to secure Ollama, users are encouraged to implement their own security best practices, such as:
 | 
			
		||||
 | 
			
		||||
- Regularly updating to the latest version of Ollama
 | 
			
		||||
- Securing access to hosted instances of Ollama
 | 
			
		||||
- Monitoring systems for unusual activity
 | 
			
		||||
 | 
			
		||||
## Contact
 | 
			
		||||
 | 
			
		||||
For any other questions or concerns related to security, please contact us at hello@ollama.com
 | 
			
		||||
							
								
								
									
										219
									
								
								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
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
@@ -22,22 +9,16 @@ import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"runtime"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/envconfig"
 | 
			
		||||
	"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 {
 | 
			
		||||
	base *url.URL
 | 
			
		||||
	http *http.Client
 | 
			
		||||
	base    url.URL
 | 
			
		||||
	HTTP    http.Client
 | 
			
		||||
	Headers http.Header
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func checkError(resp *http.Response, body []byte) error {
 | 
			
		||||
	if resp.StatusCode < http.StatusBadRequest {
 | 
			
		||||
	if resp.StatusCode >= 200 && resp.StatusCode < 400 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -52,26 +33,15 @@ func checkError(resp *http.Response, body []byte) error {
 | 
			
		||||
	return apiError
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClientFromEnvironment creates a new [Client] using configuration from the
 | 
			
		||||
// environment variable OLLAMA_HOST, which points to the network host and
 | 
			
		||||
// port on which the ollama service is listenting. The format of this variable
 | 
			
		||||
// is:
 | 
			
		||||
//
 | 
			
		||||
//	<scheme>://<host>:<port>
 | 
			
		||||
//
 | 
			
		||||
// If the variable is not specified, a default ollama host and port will be
 | 
			
		||||
// used.
 | 
			
		||||
func ClientFromEnvironment() (*Client, error) {
 | 
			
		||||
	return &Client{
 | 
			
		||||
		base: envconfig.Host(),
 | 
			
		||||
		http: http.DefaultClient,
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
func NewClient(hosts ...string) *Client {
 | 
			
		||||
	host := "127.0.0.1:11434"
 | 
			
		||||
	if len(hosts) > 0 {
 | 
			
		||||
		host = hosts[0]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
func NewClient(base *url.URL, http *http.Client) *Client {
 | 
			
		||||
	return &Client{
 | 
			
		||||
		base: base,
 | 
			
		||||
		http: http,
 | 
			
		||||
		base: url.URL{Scheme: "http", Host: host},
 | 
			
		||||
		HTTP: http.Client{},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -79,33 +49,29 @@ func (c *Client) do(ctx context.Context, method, path string, reqData, respData
 | 
			
		||||
	var reqBody io.Reader
 | 
			
		||||
	var data []byte
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	switch reqData := reqData.(type) {
 | 
			
		||||
	case io.Reader:
 | 
			
		||||
		// reqData is already an io.Reader
 | 
			
		||||
		reqBody = reqData
 | 
			
		||||
	case nil:
 | 
			
		||||
		// noop
 | 
			
		||||
	default:
 | 
			
		||||
	if reqData != nil {
 | 
			
		||||
		data, err = json.Marshal(reqData)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		reqBody = bytes.NewReader(data)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestURL := c.base.JoinPath(path)
 | 
			
		||||
	request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), reqBody)
 | 
			
		||||
	url := c.base.JoinPath(path).String()
 | 
			
		||||
 | 
			
		||||
	req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	request.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	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()))
 | 
			
		||||
	req.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	req.Header.Set("Accept", "application/json")
 | 
			
		||||
 | 
			
		||||
	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 {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -128,8 +94,6 @@ func (c *Client) do(ctx context.Context, method, path string, reqData, respData
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const maxBufferSize = 512 * format.KiloByte
 | 
			
		||||
 | 
			
		||||
func (c *Client) stream(ctx context.Context, method, path string, data any, fn func([]byte) error) error {
 | 
			
		||||
	var buf *bytes.Buffer
 | 
			
		||||
	if data != nil {
 | 
			
		||||
@@ -141,26 +105,21 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f
 | 
			
		||||
		buf = bytes.NewBuffer(bts)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	requestURL := c.base.JoinPath(path)
 | 
			
		||||
	request, err := http.NewRequestWithContext(ctx, method, requestURL.String(), buf)
 | 
			
		||||
	request, err := http.NewRequestWithContext(ctx, method, c.base.JoinPath(path).String(), buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	request.Header.Set("Content-Type", "application/json")
 | 
			
		||||
	request.Header.Set("Accept", "application/x-ndjson")
 | 
			
		||||
	request.Header.Set("User-Agent", fmt.Sprintf("ollama/%s (%s %s) Go/%s", version.Version, runtime.GOARCH, runtime.GOOS, runtime.Version()))
 | 
			
		||||
	request.Header.Set("Accept", "application/json")
 | 
			
		||||
 | 
			
		||||
	response, err := c.http.Do(request)
 | 
			
		||||
	response, err := http.DefaultClient.Do(request)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer response.Body.Close()
 | 
			
		||||
 | 
			
		||||
	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() {
 | 
			
		||||
		var errorResponse struct {
 | 
			
		||||
			Error string `json:"error,omitempty"`
 | 
			
		||||
@@ -172,10 +131,10 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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{
 | 
			
		||||
				StatusCode:   response.StatusCode,
 | 
			
		||||
				Status:       response.Status,
 | 
			
		||||
@@ -191,14 +150,8 @@ func (c *Client) stream(ctx context.Context, method, path string, data any, fn f
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
	return c.stream(ctx, http.MethodPost, "/api/generate", req, func(bts []byte) error {
 | 
			
		||||
		var resp GenerateResponse
 | 
			
		||||
@@ -210,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
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
	return c.stream(ctx, http.MethodPost, "/api/pull", req, func(bts []byte) error {
 | 
			
		||||
		var resp ProgressResponse
 | 
			
		||||
@@ -249,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
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
	return c.stream(ctx, http.MethodPost, "/api/push", req, func(bts []byte) error {
 | 
			
		||||
		var resp ProgressResponse
 | 
			
		||||
@@ -268,18 +189,11 @@ func (c *Client) Push(ctx context.Context, req *PushRequest, fn PushProgressFunc
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CreateProgressFunc is a function that [Client.Create] invokes when progress
 | 
			
		||||
// is made.
 | 
			
		||||
// It's similar to other progress function types like [PullProgressFunc].
 | 
			
		||||
type CreateProgressFunc func(ProgressResponse) error
 | 
			
		||||
type CreateProgressFunc func(CreateProgress) 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 {
 | 
			
		||||
	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 {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
@@ -288,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) {
 | 
			
		||||
	var lr ListResponse
 | 
			
		||||
	if err := c.do(ctx, http.MethodGet, "/api/tags", nil, &lr); err != nil {
 | 
			
		||||
@@ -297,83 +210,9 @@ func (c *Client) List(ctx context.Context) (*ListResponse, error) {
 | 
			
		||||
	return &lr, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// List running models.
 | 
			
		||||
func (c *Client) ListRunning(ctx context.Context) (*ProcessResponse, error) {
 | 
			
		||||
	var lr ProcessResponse
 | 
			
		||||
	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 {
 | 
			
		||||
	if err := c.do(ctx, http.MethodPost, "/api/copy", req, nil); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Delete deletes a model and its data.
 | 
			
		||||
func (c *Client) Delete(ctx context.Context, req *DeleteRequest) error {
 | 
			
		||||
	if err := c.do(ctx, http.MethodDelete, "/api/delete", req, nil); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Embed generates embeddings from a model.
 | 
			
		||||
func (c *Client) Embed(ctx context.Context, req *EmbedRequest) (*EmbedResponse, error) {
 | 
			
		||||
	var resp EmbedResponse
 | 
			
		||||
	if err := c.do(ctx, http.MethodPost, "/api/embed", req, &resp); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return &resp, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Embeddings generates an embedding 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,45 +0,0 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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())
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										797
									
								
								api/types.go
									
									
									
									
									
								
							
							
						
						@@ -1,18 +1,12 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"math"
 | 
			
		||||
	"os"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// StatusError is an error with and HTTP status code.
 | 
			
		||||
type StatusError struct {
 | 
			
		||||
	StatusCode   int
 | 
			
		||||
	Status       string
 | 
			
		||||
@@ -33,701 +27,160 @@ 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 {
 | 
			
		||||
	// 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  string `json:"prompt"`
 | 
			
		||||
	Context []int  `json:"context,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Prompt is the textual prompt to send to the model.
 | 
			
		||||
	Prompt string `json:"prompt"`
 | 
			
		||||
	Options `json:"options"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	// Suffix is the text that comes after the inserted text.
 | 
			
		||||
	Suffix string `json:"suffix"`
 | 
			
		||||
type CreateRequest struct {
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
	Path string `json:"path"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	// System overrides the model's default system message/prompt.
 | 
			
		||||
	System string `json:"system"`
 | 
			
		||||
type CreateProgress struct {
 | 
			
		||||
	Status string `json:"status"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	// Template overrides the model's default prompt template.
 | 
			
		||||
	Template string `json:"template"`
 | 
			
		||||
type DeleteRequest struct {
 | 
			
		||||
	Name string `json:"name"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	// Context is the context parameter returned from a previous call to
 | 
			
		||||
	// Generate call. It can be used to keep a short conversational memory.
 | 
			
		||||
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"`
 | 
			
		||||
	CreatedAt time.Time `json:"created_at"`
 | 
			
		||||
	Response  string    `json:"response,omitempty"`
 | 
			
		||||
 | 
			
		||||
	Done    bool  `json:"done"`
 | 
			
		||||
	Context []int `json:"context,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Stream specifies whether the response is streaming; it is true by default.
 | 
			
		||||
	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 ChatRequest struct {
 | 
			
		||||
	// Model is the model name, as in [GenerateRequest].
 | 
			
		||||
	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"`
 | 
			
		||||
 | 
			
		||||
	// Tools is an optional list of tools the model has access to.
 | 
			
		||||
	Tools `json:"tools,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Options lists model-specific options.
 | 
			
		||||
	Options map[string]interface{} `json:"options"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Tools []Tool
 | 
			
		||||
 | 
			
		||||
func (t Tools) String() string {
 | 
			
		||||
	bts, _ := json.Marshal(t)
 | 
			
		||||
	return string(bts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t Tool) String() string {
 | 
			
		||||
	bts, _ := json.Marshal(t)
 | 
			
		||||
	return string(bts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Message is a single message in a chat sequence. The message contains the
 | 
			
		||||
// role ("system", "user", or "assistant"), the content and an optional list
 | 
			
		||||
// of images.
 | 
			
		||||
type Message struct {
 | 
			
		||||
	Role      string      `json:"role"`
 | 
			
		||||
	Content   string      `json:"content"`
 | 
			
		||||
	Images    []ImageData `json:"images,omitempty"`
 | 
			
		||||
	ToolCalls []ToolCall  `json:"tool_calls,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *Message) UnmarshalJSON(b []byte) error {
 | 
			
		||||
	type Alias Message
 | 
			
		||||
	var a Alias
 | 
			
		||||
	if err := json.Unmarshal(b, &a); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*m = Message(a)
 | 
			
		||||
	m.Role = strings.ToLower(m.Role)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ToolCall struct {
 | 
			
		||||
	Function ToolCallFunction `json:"function"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ToolCallFunction struct {
 | 
			
		||||
	Name      string                    `json:"name"`
 | 
			
		||||
	Arguments ToolCallFunctionArguments `json:"arguments"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ToolCallFunctionArguments map[string]any
 | 
			
		||||
 | 
			
		||||
func (t *ToolCallFunctionArguments) String() string {
 | 
			
		||||
	bts, _ := json.Marshal(t)
 | 
			
		||||
	return string(bts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Tool struct {
 | 
			
		||||
	Type     string       `json:"type"`
 | 
			
		||||
	Function ToolFunction `json:"function"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ToolFunction struct {
 | 
			
		||||
	Name        string `json:"name"`
 | 
			
		||||
	Description string `json:"description"`
 | 
			
		||||
	Parameters  struct {
 | 
			
		||||
		Type       string   `json:"type"`
 | 
			
		||||
		Required   []string `json:"required"`
 | 
			
		||||
		Properties map[string]struct {
 | 
			
		||||
			Type        string   `json:"type"`
 | 
			
		||||
			Description string   `json:"description"`
 | 
			
		||||
			Enum        []string `json:"enum,omitempty"`
 | 
			
		||||
		} `json:"properties"`
 | 
			
		||||
	} `json:"parameters"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *ToolFunction) String() string {
 | 
			
		||||
	bts, _ := json.Marshal(t)
 | 
			
		||||
	return string(bts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ChatResponse is the response returned by [Client.Chat]. Its fields are
 | 
			
		||||
// similar to [GenerateResponse].
 | 
			
		||||
type ChatResponse struct {
 | 
			
		||||
	Model      string    `json:"model"`
 | 
			
		||||
	CreatedAt  time.Time `json:"created_at"`
 | 
			
		||||
	Message    Message   `json:"message"`
 | 
			
		||||
	DoneReason string    `json:"done_reason,omitempty"`
 | 
			
		||||
 | 
			
		||||
	Done bool `json:"done"`
 | 
			
		||||
 | 
			
		||||
	Metrics
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Metrics struct {
 | 
			
		||||
	TotalDuration      time.Duration `json:"total_duration,omitempty"`
 | 
			
		||||
	LoadDuration       time.Duration `json:"load_duration,omitempty"`
 | 
			
		||||
	PromptEvalCount    int           `json:"prompt_eval_count,omitempty"`
 | 
			
		||||
	PromptEvalDuration time.Duration `json:"prompt_eval_duration,omitempty"`
 | 
			
		||||
	EvalCount          int           `json:"eval_count,omitempty"`
 | 
			
		||||
	EvalDuration       time.Duration `json:"eval_duration,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Options specified in [GenerateRequest], if you add a new option here add it
 | 
			
		||||
// to the API docs also.
 | 
			
		||||
func (r *GenerateResponse) Summary() {
 | 
			
		||||
	if r.TotalDuration > 0 {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "total duration:       %v\n", r.TotalDuration)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.PromptEvalCount > 0 {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "prompt eval count:    %d token(s)\n", r.PromptEvalCount)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.PromptEvalDuration > 0 {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "prompt eval duration: %s\n", r.PromptEvalDuration)
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "prompt eval rate:     %.2f tokens/s\n", float64(r.PromptEvalCount)/r.PromptEvalDuration.Seconds())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.EvalCount > 0 {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "eval count:           %d token(s)\n", r.EvalCount)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r.EvalDuration > 0 {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "eval duration:        %s\n", r.EvalDuration)
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "eval rate:            %.2f tokens/s\n", float64(r.EvalCount)/r.EvalDuration.Seconds())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Options struct {
 | 
			
		||||
	Runner
 | 
			
		||||
	Seed int `json:"seed,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Predict options used at runtime
 | 
			
		||||
	NumKeep          int      `json:"num_keep,omitempty"`
 | 
			
		||||
	Seed             int      `json:"seed,omitempty"`
 | 
			
		||||
	NumPredict       int      `json:"num_predict,omitempty"`
 | 
			
		||||
	TopK             int      `json:"top_k,omitempty"`
 | 
			
		||||
	TopP             float32  `json:"top_p,omitempty"`
 | 
			
		||||
	MinP             float32  `json:"min_p,omitempty"`
 | 
			
		||||
	TFSZ             float32  `json:"tfs_z,omitempty"`
 | 
			
		||||
	TypicalP         float32  `json:"typical_p,omitempty"`
 | 
			
		||||
	RepeatLastN      int      `json:"repeat_last_n,omitempty"`
 | 
			
		||||
	Temperature      float32  `json:"temperature,omitempty"`
 | 
			
		||||
	RepeatPenalty    float32  `json:"repeat_penalty,omitempty"`
 | 
			
		||||
	PresencePenalty  float32  `json:"presence_penalty,omitempty"`
 | 
			
		||||
	FrequencyPenalty float32  `json:"frequency_penalty,omitempty"`
 | 
			
		||||
	Mirostat         int      `json:"mirostat,omitempty"`
 | 
			
		||||
	MirostatTau      float32  `json:"mirostat_tau,omitempty"`
 | 
			
		||||
	MirostatEta      float32  `json:"mirostat_eta,omitempty"`
 | 
			
		||||
	PenalizeNewline  bool     `json:"penalize_newline,omitempty"`
 | 
			
		||||
	Stop             []string `json:"stop,omitempty"`
 | 
			
		||||
	// Backend options
 | 
			
		||||
	UseNUMA bool `json:"numa,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Model options
 | 
			
		||||
	NumCtx        int  `json:"num_ctx,omitempty"`
 | 
			
		||||
	NumBatch      int  `json:"num_batch,omitempty"`
 | 
			
		||||
	NumGPU        int  `json:"num_gpu,omitempty"`
 | 
			
		||||
	MainGPU       int  `json:"main_gpu,omitempty"`
 | 
			
		||||
	LowVRAM       bool `json:"low_vram,omitempty"`
 | 
			
		||||
	F16KV         bool `json:"f16_kv,omitempty"`
 | 
			
		||||
	LogitsAll     bool `json:"logits_all,omitempty"`
 | 
			
		||||
	VocabOnly     bool `json:"vocab_only,omitempty"`
 | 
			
		||||
	UseMMap       bool `json:"use_mmap,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"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Runner options which must be set when the model is loaded into memory
 | 
			
		||||
type Runner struct {
 | 
			
		||||
	UseNUMA   bool  `json:"numa,omitempty"`
 | 
			
		||||
	NumCtx    int   `json:"num_ctx,omitempty"`
 | 
			
		||||
	NumBatch  int   `json:"num_batch,omitempty"`
 | 
			
		||||
	NumGPU    int   `json:"num_gpu,omitempty"`
 | 
			
		||||
	MainGPU   int   `json:"main_gpu,omitempty"`
 | 
			
		||||
	LowVRAM   bool  `json:"low_vram,omitempty"`
 | 
			
		||||
	F16KV     bool  `json:"f16_kv,omitempty"`
 | 
			
		||||
	LogitsAll bool  `json:"logits_all,omitempty"`
 | 
			
		||||
	VocabOnly bool  `json:"vocab_only,omitempty"`
 | 
			
		||||
	UseMMap   *bool `json:"use_mmap,omitempty"`
 | 
			
		||||
	UseMLock  bool  `json:"use_mlock,omitempty"`
 | 
			
		||||
	NumThread int   `json:"num_thread,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EmbedRequest is the request passed to [Client.Embed].
 | 
			
		||||
type EmbedRequest struct {
 | 
			
		||||
	// Model is the model name.
 | 
			
		||||
	Model string `json:"model"`
 | 
			
		||||
 | 
			
		||||
	// Input is the input to embed.
 | 
			
		||||
	Input any `json:"input"`
 | 
			
		||||
 | 
			
		||||
	// KeepAlive controls how long the model will stay loaded in memory following
 | 
			
		||||
	// this request.
 | 
			
		||||
	KeepAlive *Duration `json:"keep_alive,omitempty"`
 | 
			
		||||
 | 
			
		||||
	Truncate *bool `json:"truncate,omitempty"`
 | 
			
		||||
 | 
			
		||||
	// Options lists model-specific options.
 | 
			
		||||
	Options map[string]interface{} `json:"options"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// EmbedResponse is the response from [Client.Embed].
 | 
			
		||||
type EmbedResponse struct {
 | 
			
		||||
	Model      string      `json:"model"`
 | 
			
		||||
	Embeddings [][]float32 `json:"embeddings"`
 | 
			
		||||
 | 
			
		||||
	TotalDuration   time.Duration `json:"total_duration,omitempty"`
 | 
			
		||||
	LoadDuration    time.Duration `json:"load_duration,omitempty"`
 | 
			
		||||
	PromptEvalCount int           `json:"prompt_eval_count,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 is deprecated
 | 
			
		||||
	Template string `json:"template"`
 | 
			
		||||
	Verbose  bool   `json:"verbose"`
 | 
			
		||||
 | 
			
		||||
	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"`
 | 
			
		||||
	ModelInfo     map[string]any `json:"model_info,omitempty"`
 | 
			
		||||
	ProjectorInfo map[string]any `json:"projector_info,omitempty"`
 | 
			
		||||
	ModifiedAt    time.Time      `json:"modified_at,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 []ListModelResponse `json:"models"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ProcessResponse is the response from [Client.Process].
 | 
			
		||||
type ProcessResponse struct {
 | 
			
		||||
	Models []ProcessModelResponse `json:"models"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ListModelResponse is a single model description in [ListResponse].
 | 
			
		||||
type ListModelResponse struct {
 | 
			
		||||
	Name       string       `json:"name"`
 | 
			
		||||
	Model      string       `json:"model"`
 | 
			
		||||
	ModifiedAt time.Time    `json:"modified_at"`
 | 
			
		||||
	Size       int64        `json:"size"`
 | 
			
		||||
	Digest     string       `json:"digest"`
 | 
			
		||||
	Details    ModelDetails `json:"details,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ProcessModelResponse is a single model description in [ProcessResponse].
 | 
			
		||||
type ProcessModelResponse struct {
 | 
			
		||||
	Name      string       `json:"name"`
 | 
			
		||||
	Model     string       `json:"model"`
 | 
			
		||||
	Size      int64        `json:"size"`
 | 
			
		||||
	Digest    string       `json:"digest"`
 | 
			
		||||
	Details   ModelDetails `json:"details,omitempty"`
 | 
			
		||||
	ExpiresAt time.Time    `json:"expires_at"`
 | 
			
		||||
	SizeVRAM  int64        `json:"size_vram"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RetrieveModelResponse struct {
 | 
			
		||||
	Id      string `json:"id"`
 | 
			
		||||
	Object  string `json:"object"`
 | 
			
		||||
	Created int64  `json:"created"`
 | 
			
		||||
	OwnedBy string `json:"owned_by"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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))
 | 
			
		||||
			case reflect.Pointer:
 | 
			
		||||
				var b bool
 | 
			
		||||
				if field.Type() == reflect.TypeOf(&b) {
 | 
			
		||||
					val, ok := val.(bool)
 | 
			
		||||
					if !ok {
 | 
			
		||||
						return fmt.Errorf("option %q must be of type boolean", key)
 | 
			
		||||
					}
 | 
			
		||||
					field.Set(reflect.ValueOf(&val))
 | 
			
		||||
				} else {
 | 
			
		||||
					return fmt.Errorf("unknown type loading config params: %v %v", field.Kind(), field.Type())
 | 
			
		||||
				}
 | 
			
		||||
			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 {
 | 
			
		||||
	return Options{
 | 
			
		||||
		// options set on request to runner
 | 
			
		||||
		NumPredict: -1,
 | 
			
		||||
		Seed: -1,
 | 
			
		||||
 | 
			
		||||
		// set a minimal num_keep to avoid issues on context shifts
 | 
			
		||||
		NumKeep:          4,
 | 
			
		||||
		UseNUMA: false,
 | 
			
		||||
 | 
			
		||||
		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,
 | 
			
		||||
		TopK:             40,
 | 
			
		||||
		TopP:             0.9,
 | 
			
		||||
		TFSZ:             1.0,
 | 
			
		||||
		TypicalP:         1.0,
 | 
			
		||||
		RepeatLastN:      64,
 | 
			
		||||
		RepeatPenalty:    1.1,
 | 
			
		||||
		PresencePenalty:  0.0,
 | 
			
		||||
		FrequencyPenalty: 0.0,
 | 
			
		||||
		Mirostat:         0,
 | 
			
		||||
		MirostatTau:      5.0,
 | 
			
		||||
		MirostatEta:      0.1,
 | 
			
		||||
		PenalizeNewline:  true,
 | 
			
		||||
		Seed:             -1,
 | 
			
		||||
 | 
			
		||||
		Runner: Runner{
 | 
			
		||||
			// 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:   nil,
 | 
			
		||||
			UseNUMA:   false,
 | 
			
		||||
		},
 | 
			
		||||
		NumThread: runtime.NumCPU(),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
				case reflect.Pointer:
 | 
			
		||||
					var b bool
 | 
			
		||||
					if field.Type() == reflect.TypeOf(&b) {
 | 
			
		||||
						boolVal, err := strconv.ParseBool(vals[0])
 | 
			
		||||
						if err != nil {
 | 
			
		||||
							return nil, fmt.Errorf("invalid bool value %s", vals)
 | 
			
		||||
						}
 | 
			
		||||
						out[key] = &boolVal
 | 
			
		||||
					} else {
 | 
			
		||||
						return nil, fmt.Errorf("unknown type %s for %s", field.Kind(), key)
 | 
			
		||||
					}
 | 
			
		||||
				default:
 | 
			
		||||
					return nil, fmt.Errorf("unknown type %s for %s", field.Kind(), key)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,233 +0,0 @@
 | 
			
		||||
package api
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"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",
 | 
			
		||||
			42 * time.Second,
 | 
			
		||||
			42 * time.Second,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			"another positive duration",
 | 
			
		||||
			42 * time.Minute,
 | 
			
		||||
			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)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUseMmapParsingFromJSON(t *testing.T) {
 | 
			
		||||
	tr := true
 | 
			
		||||
	fa := false
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		req  string
 | 
			
		||||
		exp  *bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Undefined",
 | 
			
		||||
			req:  `{ }`,
 | 
			
		||||
			exp:  nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "True",
 | 
			
		||||
			req:  `{ "use_mmap": true }`,
 | 
			
		||||
			exp:  &tr,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "False",
 | 
			
		||||
			req:  `{ "use_mmap": false }`,
 | 
			
		||||
			exp:  &fa,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.name, func(t *testing.T) {
 | 
			
		||||
			var oMap map[string]interface{}
 | 
			
		||||
			err := json.Unmarshal([]byte(test.req), &oMap)
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
			opts := DefaultOptions()
 | 
			
		||||
			err = opts.FromMap(oMap)
 | 
			
		||||
			require.NoError(t, err)
 | 
			
		||||
			assert.Equal(t, test.exp, opts.UseMMap)
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestUseMmapFormatParams(t *testing.T) {
 | 
			
		||||
	tr := true
 | 
			
		||||
	fa := false
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		req  map[string][]string
 | 
			
		||||
		exp  *bool
 | 
			
		||||
		err  error
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "True",
 | 
			
		||||
			req: map[string][]string{
 | 
			
		||||
				"use_mmap": {"true"},
 | 
			
		||||
			},
 | 
			
		||||
			exp: &tr,
 | 
			
		||||
			err: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "False",
 | 
			
		||||
			req: map[string][]string{
 | 
			
		||||
				"use_mmap": {"false"},
 | 
			
		||||
			},
 | 
			
		||||
			exp: &fa,
 | 
			
		||||
			err: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Numeric True",
 | 
			
		||||
			req: map[string][]string{
 | 
			
		||||
				"use_mmap": {"1"},
 | 
			
		||||
			},
 | 
			
		||||
			exp: &tr,
 | 
			
		||||
			err: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Numeric False",
 | 
			
		||||
			req: map[string][]string{
 | 
			
		||||
				"use_mmap": {"0"},
 | 
			
		||||
			},
 | 
			
		||||
			exp: &fa,
 | 
			
		||||
			err: nil,
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "invalid string",
 | 
			
		||||
			req: map[string][]string{
 | 
			
		||||
				"use_mmap": {"foo"},
 | 
			
		||||
			},
 | 
			
		||||
			exp: nil,
 | 
			
		||||
			err: fmt.Errorf("invalid bool value [foo]"),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		t.Run(test.name, func(t *testing.T) {
 | 
			
		||||
			resp, err := FormatParams(test.req)
 | 
			
		||||
			require.Equal(t, test.err, err)
 | 
			
		||||
			respVal, ok := resp["use_mmap"]
 | 
			
		||||
			if test.exp != nil {
 | 
			
		||||
				assert.True(t, ok, "resp: %v", resp)
 | 
			
		||||
				assert.Equal(t, *test.exp, *respVal.(*bool))
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMessage_UnmarshalJSON(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		input    string
 | 
			
		||||
		expected string
 | 
			
		||||
	}{
 | 
			
		||||
		{`{"role": "USER", "content": "Hello!"}`, "user"},
 | 
			
		||||
		{`{"role": "System", "content": "Initialization complete."}`, "system"},
 | 
			
		||||
		{`{"role": "assistant", "content": "How can I help you?"}`, "assistant"},
 | 
			
		||||
		{`{"role": "TOOl", "content": "Access granted."}`, "tool"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, test := range tests {
 | 
			
		||||
		var msg Message
 | 
			
		||||
		if err := json.Unmarshal([]byte(test.input), &msg); err != nil {
 | 
			
		||||
			t.Errorf("Unexpected error: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if msg.Role != test.expected {
 | 
			
		||||
			t.Errorf("role not lowercased: got %v, expected %v", msg.Role, test.expected)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
## 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.
 | 
			
		||||
First, build the `ollama` binary:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
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,
 | 
			
		||||
    icon: './assets/icon.icns',
 | 
			
		||||
    extraResource: [
 | 
			
		||||
      '../dist/ollama',
 | 
			
		||||
      path.join(__dirname, './assets/iconTemplate.png'),
 | 
			
		||||
      path.join(__dirname, './assets/iconTemplate@2x.png'),
 | 
			
		||||
      path.join(__dirname, './assets/iconUpdateTemplate.png'),
 | 
			
		||||
      path.join(__dirname, './assets/iconUpdateTemplate@2x.png'),
 | 
			
		||||
      path.join(__dirname, './assets/iconDarkTemplate.png'),
 | 
			
		||||
      path.join(__dirname, './assets/iconDarkTemplate@2x.png'),
 | 
			
		||||
      path.join(__dirname, './assets/iconDarkUpdateTemplate.png'),
 | 
			
		||||
      path.join(__dirname, './assets/iconDarkUpdateTemplate@2x.png'),
 | 
			
		||||
      '../ollama',
 | 
			
		||||
      path.join(__dirname, './assets/ollama_icon_16x16Template.png'),
 | 
			
		||||
      path.join(__dirname, './assets/ollama_icon_16x16Template@2x.png'),
 | 
			
		||||
      path.join(__dirname, './assets/ollama_outline_icon_16x16Template.png'),
 | 
			
		||||
      path.join(__dirname, './assets/ollama_outline_icon_16x16Template@2x.png'),
 | 
			
		||||
      ...(process.platform === 'darwin' ? ['../llama/ggml-metal.metal'] : []),
 | 
			
		||||
    ],
 | 
			
		||||
    ...(process.env.SIGN
 | 
			
		||||
      ? {
 | 
			
		||||
@@ -41,12 +38,19 @@ const config: ForgeConfig = {
 | 
			
		||||
          },
 | 
			
		||||
        }
 | 
			
		||||
      : {}),
 | 
			
		||||
    osxUniversal: {
 | 
			
		||||
      x64ArchFiles: '**/ollama',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  rebuildConfig: {},
 | 
			
		||||
  makers: [new MakerSquirrel({}), new MakerZIP({}, ['darwin'])],
 | 
			
		||||
  publishers: [
 | 
			
		||||
    new PublisherGithub({
 | 
			
		||||
      repository: {
 | 
			
		||||
        name: 'ollama',
 | 
			
		||||
        owner: 'jmorganca',
 | 
			
		||||
      },
 | 
			
		||||
      draft: false,
 | 
			
		||||
      prerelease: true,
 | 
			
		||||
    }),
 | 
			
		||||
  ],
 | 
			
		||||
  hooks: {
 | 
			
		||||
    readPackageJson: async (_, packageJson) => {
 | 
			
		||||
      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,80 +0,0 @@
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"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 {
 | 
			
		||||
		rotateLogs(AppLogFile)
 | 
			
		||||
		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")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func rotateLogs(logFile string) {
 | 
			
		||||
	if _, err := os.Stat(logFile); os.IsNotExist(err) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	index := strings.LastIndex(logFile, ".")
 | 
			
		||||
	pre := logFile[:index]
 | 
			
		||||
	post := "." + logFile[index+1:]
 | 
			
		||||
	for i := LogRotationCount; i > 0; i-- {
 | 
			
		||||
		older := pre + "-" + strconv.Itoa(i) + post
 | 
			
		||||
		newer := pre + "-" + strconv.Itoa(i-1) + post
 | 
			
		||||
		if i == 1 {
 | 
			
		||||
			newer = pre + post
 | 
			
		||||
		}
 | 
			
		||||
		if _, err := os.Stat(newer); err == nil {
 | 
			
		||||
			if _, err := os.Stat(older); err == nil {
 | 
			
		||||
				err := os.Remove(older)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					slog.Warn("Failed to remove older log", "older", older, "error", err)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			err := os.Rename(newer, older)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				slog.Warn("Failed to rotate log", "older", older, "newer", newer, "error", err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
//go:build !windows
 | 
			
		||||
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import "log/slog"
 | 
			
		||||
 | 
			
		||||
func ShowLogs() {
 | 
			
		||||
	slog.Warn("ShowLogs not yet implemented")
 | 
			
		||||
}
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
package lifecycle
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"github.com/stretchr/testify/require"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestRotateLogs(t *testing.T) {
 | 
			
		||||
	logDir := t.TempDir()
 | 
			
		||||
	logFile := filepath.Join(logDir, "testlog.log")
 | 
			
		||||
 | 
			
		||||
	// No log exists
 | 
			
		||||
	rotateLogs(logFile)
 | 
			
		||||
 | 
			
		||||
	require.NoError(t, os.WriteFile(logFile, []byte("1"), 0644))
 | 
			
		||||
	assert.FileExists(t, logFile)
 | 
			
		||||
	// First rotation
 | 
			
		||||
	rotateLogs(logFile)
 | 
			
		||||
	assert.FileExists(t, filepath.Join(logDir, "testlog-1.log"))
 | 
			
		||||
	assert.NoFileExists(t, filepath.Join(logDir, "testlog-2.log"))
 | 
			
		||||
	assert.NoFileExists(t, logFile)
 | 
			
		||||
 | 
			
		||||
	// Should be a no-op without a new log
 | 
			
		||||
	rotateLogs(logFile)
 | 
			
		||||
	assert.FileExists(t, filepath.Join(logDir, "testlog-1.log"))
 | 
			
		||||
	assert.NoFileExists(t, filepath.Join(logDir, "testlog-2.log"))
 | 
			
		||||
	assert.NoFileExists(t, logFile)
 | 
			
		||||
 | 
			
		||||
	for i := 2; i <= LogRotationCount+1; i++ {
 | 
			
		||||
		require.NoError(t, os.WriteFile(logFile, []byte(strconv.Itoa(i)), 0644))
 | 
			
		||||
		assert.FileExists(t, logFile)
 | 
			
		||||
		rotateLogs(logFile)
 | 
			
		||||
		assert.NoFileExists(t, logFile)
 | 
			
		||||
		for j := 1; j < i; j++ {
 | 
			
		||||
			assert.FileExists(t, filepath.Join(logDir, "testlog-"+strconv.Itoa(j)+".log"))
 | 
			
		||||
		}
 | 
			
		||||
		assert.NoFileExists(t, filepath.Join(logDir, "testlog-"+strconv.Itoa(i+1)+".log"))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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"
 | 
			
		||||
	LogRotationCount = 5
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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,180 +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 {
 | 
			
		||||
	var cmdPath string
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rotateLogs(ServerLogFile)
 | 
			
		||||
	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,91 +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
 | 
			
		||||
	}
 | 
			
		||||
	//nolint:errcheck
 | 
			
		||||
	defer dll.Release()
 | 
			
		||||
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
	//nolint:errcheck
 | 
			
		||||
	defer windows.CloseHandle(hProcess)
 | 
			
		||||
 | 
			
		||||
	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 == http.StatusNoContent {
 | 
			
		||||
		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 != http.StatusOK {
 | 
			
		||||
		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 != http.StatusOK {
 | 
			
		||||
		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()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										165
									
								
								app/ollama.iss
									
									
									
									
									
								
							
							
						
						@@ -1,165 +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}\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\cuda")
 | 
			
		||||
  Source: "..\dist\windows-amd64\cuda\*"; DestDir: "{app}\cuda\"; Flags: ignoreversion recursesubdirs
 | 
			
		||||
#endif
 | 
			
		||||
#if DirExists("..\dist\windows-amd64\oneapi")
 | 
			
		||||
  Source: "..\dist\windows-amd64\oneapi\*"; DestDir: "{app}\oneapi\"; Flags: ignoreversion recursesubdirs
 | 
			
		||||
#endif
 | 
			
		||||
#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
 | 
			
		||||
 | 
			
		||||
[InstallDelete]
 | 
			
		||||
Type: filesandordirs; Name: "{%TEMP}\ollama*"
 | 
			
		||||
Type: filesandordirs; Name: "{%LOCALAPPDATA}\Programs\Ollama"
 | 
			
		||||
 | 
			
		||||
[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.1
 | 
			
		||||
;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 llama3.1"
 | 
			
		||||
write-host ""
 | 
			
		||||
							
								
								
									
										997
									
								
								macapp/package-lock.json → app/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -6,14 +6,12 @@
 | 
			
		||||
  "main": ".webpack/main",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "electron-forge start",
 | 
			
		||||
    "package": "electron-forge package --arch universal",
 | 
			
		||||
    "package:sign": "SIGN=1 electron-forge package --arch universal",
 | 
			
		||||
    "make": "electron-forge make --arch universal",
 | 
			
		||||
    "make:sign": "SIGN=1 electron-forge make --arch universal",
 | 
			
		||||
    "package": "electron-forge package",
 | 
			
		||||
    "package:sign": "SIGN=1 electron-forge package",
 | 
			
		||||
    "make": "electron-forge make",
 | 
			
		||||
    "make:sign": "SIGN=1 electron-forge make",
 | 
			
		||||
    "publish": "SIGN=1 electron-forge publish",
 | 
			
		||||
    "lint": "eslint --ext .ts,.tsx .",
 | 
			
		||||
    "format": "prettier --check . --ignore-path .gitignore",
 | 
			
		||||
    "format:fix": "prettier --write . --ignore-path .gitignore"
 | 
			
		||||
    "lint": "eslint --ext .ts,.tsx ."
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [],
 | 
			
		||||
  "author": {
 | 
			
		||||
@@ -32,7 +30,6 @@
 | 
			
		||||
    "@electron-forge/plugin-auto-unpack-natives": "^6.2.1",
 | 
			
		||||
    "@electron-forge/plugin-webpack": "^6.2.1",
 | 
			
		||||
    "@electron-forge/publisher-github": "^6.2.1",
 | 
			
		||||
    "@electron/universal": "^1.4.1",
 | 
			
		||||
    "@svgr/webpack": "^8.0.1",
 | 
			
		||||
    "@types/chmodr": "^1.0.0",
 | 
			
		||||
    "@types/node": "^20.4.0",
 | 
			
		||||
@@ -46,7 +43,7 @@
 | 
			
		||||
    "chmodr": "^1.2.0",
 | 
			
		||||
    "copy-webpack-plugin": "^11.0.0",
 | 
			
		||||
    "css-loader": "^6.8.1",
 | 
			
		||||
    "electron": "25.9.2",
 | 
			
		||||
    "electron": "25.2.0",
 | 
			
		||||
    "eslint": "^8.43.0",
 | 
			
		||||
    "eslint-plugin-import": "^2.27.5",
 | 
			
		||||
    "fork-ts-checker-webpack-plugin": "^7.3.0",
 | 
			
		||||
@@ -2,7 +2,7 @@ import { useState } from 'react'
 | 
			
		||||
import copy from 'copy-to-clipboard'
 | 
			
		||||
import { CheckIcon, DocumentDuplicateIcon } from '@heroicons/react/24/outline'
 | 
			
		||||
import Store from 'electron-store'
 | 
			
		||||
import { getCurrentWindow, app } from '@electron/remote'
 | 
			
		||||
import { getCurrentWindow } from '@electron/remote'
 | 
			
		||||
 | 
			
		||||
import { install } from './install'
 | 
			
		||||
import OllamaIcon from './ollama.svg'
 | 
			
		||||
@@ -19,7 +19,7 @@ export default function () {
 | 
			
		||||
  const [step, setStep] = useState<Step>(Step.WELCOME)
 | 
			
		||||
  const [commandCopied, setCommandCopied] = useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  const command = 'ollama run llama3.1'
 | 
			
		||||
  const command = 'ollama run llama2'
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='drag'>
 | 
			
		||||
@@ -51,15 +51,10 @@ export default function () {
 | 
			
		||||
              <div className='mx-auto'>
 | 
			
		||||
                <button
 | 
			
		||||
                  onClick={async () => {
 | 
			
		||||
                    try {
 | 
			
		||||
                      await install()
 | 
			
		||||
                      setStep(Step.FINISH)
 | 
			
		||||
                    } catch (e) {
 | 
			
		||||
                      console.error('could not install: ', e)
 | 
			
		||||
                    } finally {
 | 
			
		||||
                      getCurrentWindow().show()
 | 
			
		||||
                      getCurrentWindow().focus()
 | 
			
		||||
                    }
 | 
			
		||||
                    await install()
 | 
			
		||||
                    getCurrentWindow().show()
 | 
			
		||||
                    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'
 | 
			
		||||
                >
 | 
			
		||||
							
								
								
									
										4
									
								
								app/src/declarations.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
			
		||||
declare module '*.svg' {
 | 
			
		||||
  const content: string;
 | 
			
		||||
  export default content;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,21 +1,17 @@
 | 
			
		||||
import { spawn, ChildProcess } from 'child_process'
 | 
			
		||||
import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow, MenuItemConstructorOptions, nativeTheme } from 'electron'
 | 
			
		||||
import { spawn } from 'child_process'
 | 
			
		||||
import { app, autoUpdater, dialog, Tray, Menu, BrowserWindow, nativeTheme } from 'electron'
 | 
			
		||||
import Store from 'electron-store'
 | 
			
		||||
import winston from 'winston'
 | 
			
		||||
import 'winston-daily-rotate-file'
 | 
			
		||||
import * as path from 'path'
 | 
			
		||||
 | 
			
		||||
import { v4 as uuidv4 } from 'uuid'
 | 
			
		||||
import { analytics, id } from './telemetry'
 | 
			
		||||
import { installed } from './install'
 | 
			
		||||
 | 
			
		||||
require('@electron/remote/main').initialize()
 | 
			
		||||
 | 
			
		||||
if (require('electron-squirrel-startup')) {
 | 
			
		||||
  app.quit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const store = new Store()
 | 
			
		||||
 | 
			
		||||
let tray: Tray | null = null
 | 
			
		||||
let welcomeWindow: BrowserWindow | null = null
 | 
			
		||||
 | 
			
		||||
declare const MAIN_WINDOW_WEBPACK_ENTRY: string
 | 
			
		||||
@@ -32,30 +28,10 @@ const logger = winston.createLogger({
 | 
			
		||||
  format: winston.format.printf(info => info.message),
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
app.on('ready', () => {
 | 
			
		||||
  const gotTheLock = app.requestSingleInstanceLock()
 | 
			
		||||
  if (!gotTheLock) {
 | 
			
		||||
    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()
 | 
			
		||||
})
 | 
			
		||||
const SingleInstanceLock = app.requestSingleInstanceLock()
 | 
			
		||||
if (!SingleInstanceLock) {
 | 
			
		||||
  app.quit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function firstRunWindow() {
 | 
			
		||||
  // Create the browser window.
 | 
			
		||||
@@ -71,74 +47,65 @@ function firstRunWindow() {
 | 
			
		||||
      nodeIntegration: true,
 | 
			
		||||
      contextIsolation: false,
 | 
			
		||||
    },
 | 
			
		||||
    alwaysOnTop: true,
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  require('@electron/remote/main').enable(welcomeWindow.webContents)
 | 
			
		||||
 | 
			
		||||
  // and load the index.html of the app.
 | 
			
		||||
  welcomeWindow.loadURL(MAIN_WINDOW_WEBPACK_ENTRY)
 | 
			
		||||
 | 
			
		||||
  welcomeWindow.on('ready-to-show', () => welcomeWindow.show())
 | 
			
		||||
  welcomeWindow.on('closed', () => {
 | 
			
		||||
    if (process.platform === 'darwin') {
 | 
			
		||||
      app.dock.hide()
 | 
			
		||||
 | 
			
		||||
  // for debugging
 | 
			
		||||
  // welcomeWindow.webContents.openDevTools()
 | 
			
		||||
 | 
			
		||||
  if (process.platform === 'darwin') {
 | 
			
		||||
    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
 | 
			
		||||
let updateAvailable = false
 | 
			
		||||
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')
 | 
			
		||||
if (require('electron-squirrel-startup')) {
 | 
			
		||||
  app.quit()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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() {
 | 
			
		||||
  const binary = app.isPackaged
 | 
			
		||||
    ? path.join(process.resourcesPath, 'ollama')
 | 
			
		||||
    : path.resolve(process.cwd(), '..', 'ollama')
 | 
			
		||||
 | 
			
		||||
  proc = spawn(binary, ['serve'])
 | 
			
		||||
  const proc = spawn(binary, ['serve'])
 | 
			
		||||
 | 
			
		||||
  proc.stdout.on('data', data => {
 | 
			
		||||
    logger.info(data.toString().trim())
 | 
			
		||||
@@ -148,75 +115,24 @@ function server() {
 | 
			
		||||
    logger.error(data.toString().trim())
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  function restart() {
 | 
			
		||||
    logger.info('Restarting the server...')
 | 
			
		||||
    server()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  proc.on('exit', restart)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function restart() {
 | 
			
		||||
  setTimeout(server, 1000)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
app.on('before-quit', () => {
 | 
			
		||||
  if (proc) {
 | 
			
		||||
  app.on('before-quit', () => {
 | 
			
		||||
    proc.off('exit', restart)
 | 
			
		||||
    proc.kill('SIGINT') // send SIGINT signal to the server, which also stops any loaded llms
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
  }
 | 
			
		||||
    proc.kill()
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function checkUpdate() {
 | 
			
		||||
  const available = await isNewReleaseAvailable()
 | 
			
		||||
  if (available) {
 | 
			
		||||
    logger.info('checking for update')
 | 
			
		||||
    autoUpdater.checkForUpdates()
 | 
			
		||||
  }
 | 
			
		||||
if (process.platform === 'darwin') {
 | 
			
		||||
  app.dock.hide()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function init() {
 | 
			
		||||
  if (app.isPackaged) {
 | 
			
		||||
    checkUpdate()
 | 
			
		||||
    setInterval(() => {
 | 
			
		||||
      checkUpdate()
 | 
			
		||||
    }, 60 * 60 * 1000)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateTray()
 | 
			
		||||
 | 
			
		||||
app.on('ready', () => {
 | 
			
		||||
  if (process.platform === 'darwin') {
 | 
			
		||||
    if (app.isPackaged) {
 | 
			
		||||
      if (!app.isInApplicationsFolder()) {
 | 
			
		||||
@@ -252,13 +168,10 @@ function init() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createSystemtray()
 | 
			
		||||
  server()
 | 
			
		||||
 | 
			
		||||
  if (store.get('first-time-run') && installed()) {
 | 
			
		||||
    if (process.platform === 'darwin') {
 | 
			
		||||
      app.dock.hide()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    app.setLoginItemSettings({ openAtLogin: app.getLoginItemSettings().openAtLogin })
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
@@ -266,7 +179,7 @@ function init() {
 | 
			
		||||
  // This is the first run or the CLI is no longer installed
 | 
			
		||||
  app.setLoginItemSettings({ openAtLogin: true })
 | 
			
		||||
  firstRunWindow()
 | 
			
		||||
}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
@@ -277,26 +190,45 @@ app.on('window-all-closed', () => {
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function id(): string {
 | 
			
		||||
  const id = store.get('id') as string
 | 
			
		||||
// In this file you can include the rest of your app's specific main process
 | 
			
		||||
// 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) {
 | 
			
		||||
    return id
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const uuid = uuidv4()
 | 
			
		||||
  store.set('id', uuid)
 | 
			
		||||
  return uuid
 | 
			
		||||
async function heartbeat() {
 | 
			
		||||
  analytics.track({
 | 
			
		||||
    anonymousId: id(),
 | 
			
		||||
    event: 'heartbeat',
 | 
			
		||||
    properties: {
 | 
			
		||||
      version: app.getVersion(),
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
autoUpdater.setFeedURL({ url: updateURL })
 | 
			
		||||
if (app.isPackaged) {
 | 
			
		||||
  heartbeat()
 | 
			
		||||
  autoUpdater.checkForUpdates()
 | 
			
		||||
  setInterval(() => {
 | 
			
		||||
    heartbeat()
 | 
			
		||||
    autoUpdater.checkForUpdates()
 | 
			
		||||
  }, 60 * 60 * 1000)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
autoUpdater.on('error', e => {
 | 
			
		||||
  logger.error(`update check failed - ${e.message}`)
 | 
			
		||||
  console.error(`update check failed - ${e.message}`)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
autoUpdater.on('update-downloaded', () => {
 | 
			
		||||
  updateAvailable = true
 | 
			
		||||
  updateTray()
 | 
			
		||||
autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName) => {
 | 
			
		||||
  dialog
 | 
			
		||||
    .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() {
 | 
			
		||||
  const command = `do shell script "mkdir -p ${path.dirname(
 | 
			
		||||
    symlinkPath
 | 
			
		||||
  )} && ln -F -s \\"${ollama}\\" \\"${symlinkPath}\\"" with administrator privileges`
 | 
			
		||||
  )} && ln -F -s ${ollama} ${symlinkPath}" with administrator privileges`
 | 
			
		||||
 | 
			
		||||
  await exec(`osascript -e '${command}'`)
 | 
			
		||||
  try {
 | 
			
		||||
    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,97 +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,183 +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),
 | 
			
		||||
			wParam,
 | 
			
		||||
			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,481 +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:             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:       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:   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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1404
									
								
								cmd/cmd.go
									
									
									
									
									
								
							
							
						
						@@ -1,622 +0,0 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
	"golang.org/x/exp/maps"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
	"github.com/ollama/ollama/envconfig"
 | 
			
		||||
	"github.com/ollama/ollama/parser"
 | 
			
		||||
	"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
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func loadModel(cmd *cobra.Command, opts *runOptions) error {
 | 
			
		||||
	p := progress.NewProgress(os.Stderr)
 | 
			
		||||
	defer p.StopAndClear()
 | 
			
		||||
 | 
			
		||||
	spinner := progress.NewSpinner("")
 | 
			
		||||
	p.Add("", spinner)
 | 
			
		||||
 | 
			
		||||
	client, err := api.ClientFromEnvironment()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	chatReq := &api.ChatRequest{
 | 
			
		||||
		Model:     opts.Model,
 | 
			
		||||
		KeepAlive: opts.KeepAlive,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return client.Chat(cmd.Context(), chatReq, func(api.ChatResponse) error { return nil })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func generateInteractive(cmd *cobra.Command, opts runOptions) error {
 | 
			
		||||
	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 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 min_p <float>          Pick token based on top token probability * min_p")
 | 
			
		||||
		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()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			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":
 | 
			
		||||
					if len(args) < 3 {
 | 
			
		||||
						usageSet()
 | 
			
		||||
						continue
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					multiline = MultilineSystem
 | 
			
		||||
 | 
			
		||||
					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
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					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()
 | 
			
		||||
 | 
			
		||||
					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,
 | 
			
		||||
					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":
 | 
			
		||||
					showInfo(resp)
 | 
			
		||||
				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":
 | 
			
		||||
					if resp.Template != "" {
 | 
			
		||||
						fmt.Println(resp.Template)
 | 
			
		||||
					} else {
 | 
			
		||||
						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 f parser.File
 | 
			
		||||
	f.Commands = append(f.Commands, parser.Command{Name: "model", Args: cmp.Or(opts.ParentModel, opts.Model)})
 | 
			
		||||
 | 
			
		||||
	if opts.System != "" {
 | 
			
		||||
		f.Commands = append(f.Commands, parser.Command{Name: "system", Args: opts.System})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	keys := maps.Keys(opts.Options)
 | 
			
		||||
	slices.Sort(keys)
 | 
			
		||||
	for _, k := range keys {
 | 
			
		||||
		v := opts.Options[k]
 | 
			
		||||
		var cmds []parser.Command
 | 
			
		||||
		switch t := v.(type) {
 | 
			
		||||
		case []string:
 | 
			
		||||
			for _, s := range t {
 | 
			
		||||
				cmds = append(cmds, parser.Command{Name: k, Args: s})
 | 
			
		||||
			}
 | 
			
		||||
		default:
 | 
			
		||||
			cmds = append(cmds, parser.Command{Name: k, Args: fmt.Sprintf("%v", t)})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		f.Commands = append(f.Commands, cmds...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, msg := range opts.Messages {
 | 
			
		||||
		f.Commands = append(f.Commands, parser.Command{Name: "message", Args: fmt.Sprintf("%s: %s", msg.Role, msg.Content)})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return f.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,107 +0,0 @@
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/go-cmp/cmp"
 | 
			
		||||
	"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",
 | 
			
		||||
		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]any{
 | 
			
		||||
			"temperature":      0.9,
 | 
			
		||||
			"seed":             42,
 | 
			
		||||
			"penalize_newline": false,
 | 
			
		||||
			"stop":             []string{"hi", "there"},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("model", func(t *testing.T) {
 | 
			
		||||
		expect := `FROM hork
 | 
			
		||||
SYSTEM You are part horse and part shark, but all hork. Do horklike things
 | 
			
		||||
PARAMETER penalize_newline false
 | 
			
		||||
PARAMETER seed 42
 | 
			
		||||
PARAMETER stop hi
 | 
			
		||||
PARAMETER stop there
 | 
			
		||||
PARAMETER temperature 0.9
 | 
			
		||||
MESSAGE user Hey there hork!
 | 
			
		||||
MESSAGE assistant Yes it is true, I am half horse, half shark.
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
		actual := buildModelfile(opts)
 | 
			
		||||
		if diff := cmp.Diff(expect, actual); diff != "" {
 | 
			
		||||
			t.Errorf("mismatch (-want +got):\n%s", diff)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("parent model", func(t *testing.T) {
 | 
			
		||||
		opts.ParentModel = "horseshark"
 | 
			
		||||
		expect := `FROM horseshark
 | 
			
		||||
SYSTEM You are part horse and part shark, but all hork. Do horklike things
 | 
			
		||||
PARAMETER penalize_newline false
 | 
			
		||||
PARAMETER seed 42
 | 
			
		||||
PARAMETER stop hi
 | 
			
		||||
PARAMETER stop there
 | 
			
		||||
PARAMETER temperature 0.9
 | 
			
		||||
MESSAGE user Hey there hork!
 | 
			
		||||
MESSAGE assistant Yes it is true, I am half horse, half shark.
 | 
			
		||||
`
 | 
			
		||||
		actual := buildModelfile(opts)
 | 
			
		||||
		if diff := cmp.Diff(expect, actual); diff != "" {
 | 
			
		||||
			t.Errorf("mismatch (-want +got):\n%s", diff)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								cmd/start.go
									
									
									
									
									
								
							
							
						
						@@ -1,27 +0,0 @@
 | 
			
		||||
//go:build darwin || windows
 | 
			
		||||
 | 
			
		||||
package cmd
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/api"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func waitForServer(ctx context.Context, client *api.Client) error {
 | 
			
		||||
	// wait for the server to start
 | 
			
		||||
	timeout := time.After(5 * time.Second)
 | 
			
		||||
	tick := time.Tick(500 * time.Millisecond)
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-timeout:
 | 
			
		||||
			return errors.New("timed out waiting for server to start")
 | 
			
		||||
		case <-tick:
 | 
			
		||||
			if err := client.Heartbeat(ctx); err == nil {
 | 
			
		||||
				return nil // server has started
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -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,122 +0,0 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/llm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Parameters struct {
 | 
			
		||||
	Architectures []string `json:"architectures"`
 | 
			
		||||
	VocabSize     uint32   `json:"vocab_size"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (Parameters) KV(t *Tokenizer) llm.KV {
 | 
			
		||||
	kv := llm.KV{
 | 
			
		||||
		"general.file_type":            uint32(1),
 | 
			
		||||
		"general.quantization_version": uint32(2),
 | 
			
		||||
		"tokenizer.ggml.pre":           t.Pre,
 | 
			
		||||
		"tokenizer.ggml.model":         t.Vocabulary.Model,
 | 
			
		||||
		"tokenizer.ggml.tokens":        t.Vocabulary.Tokens,
 | 
			
		||||
		"tokenizer.ggml.scores":        t.Vocabulary.Scores,
 | 
			
		||||
		"tokenizer.ggml.token_type":    t.Vocabulary.Types,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if t.Template != "" {
 | 
			
		||||
		kv["tokenizer.chat_template"] = t.Template
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, sv := range t.SpecialVocabulary {
 | 
			
		||||
		kv[fmt.Sprintf("tokenizer.ggml.%s_token_id", sv.Key())] = uint32(sv.ID)
 | 
			
		||||
		kv[fmt.Sprintf("tokenizer.ggml.add_%s_token", sv.Key())] = sv.AddToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (Parameters) specialTokenTypes() []string {
 | 
			
		||||
	return []string{
 | 
			
		||||
		"bos", "eos", "unk", "sep", "pad", "cls", "mask",
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (Parameters) writeFile(ws io.WriteSeeker, kv llm.KV, ts []llm.Tensor) error {
 | 
			
		||||
	return llm.WriteGGUF(ws, kv, ts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Converter interface {
 | 
			
		||||
	// KV maps parameters to LLM key-values
 | 
			
		||||
	KV(*Tokenizer) llm.KV
 | 
			
		||||
	// Tensors maps input tensors to LLM tensors. Model specific modifications can be done here.
 | 
			
		||||
	Tensors([]Tensor) []llm.Tensor
 | 
			
		||||
 | 
			
		||||
	// tensorName returns the LLM tensor name for a specific input name
 | 
			
		||||
	tensorName(string) string
 | 
			
		||||
	// specialTokenTypes returns any special token types the model uses
 | 
			
		||||
	specialTokenTypes() []string
 | 
			
		||||
	writeFile(io.WriteSeeker, llm.KV, []llm.Tensor) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Convert writes an Ollama compatible model to the provided io.WriteSeeker based on configurations
 | 
			
		||||
// and files it finds in the input path.
 | 
			
		||||
// Supported input model formats include safetensors.
 | 
			
		||||
// Supported input tokenizers files include tokenizer.json (preferred) and tokenizer.model.
 | 
			
		||||
func Convert(fsys fs.FS, ws io.WriteSeeker) error {
 | 
			
		||||
	bts, err := fs.ReadFile(fsys, "config.json")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var p Parameters
 | 
			
		||||
	if err := json.Unmarshal(bts, &p); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(p.Architectures) < 1 {
 | 
			
		||||
		return errors.New("unknown architecture")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var conv Converter
 | 
			
		||||
	switch p.Architectures[0] {
 | 
			
		||||
	case "LlamaForCausalLM", "MistralForCausalLM":
 | 
			
		||||
		conv = &llama{}
 | 
			
		||||
	case "MixtralForCausalLM":
 | 
			
		||||
		conv = &mixtral{}
 | 
			
		||||
	case "GemmaForCausalLM":
 | 
			
		||||
		conv = &gemma{}
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("unsupported architecture")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := json.Unmarshal(bts, conv); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t, err := parseTokenizer(fsys, conv.specialTokenTypes())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if vocabSize := int(p.VocabSize); vocabSize > len(t.Vocabulary.Tokens) {
 | 
			
		||||
		slog.Warn("vocabulary is smaller than expected, padding with dummy tokens", "expect", p.VocabSize, "actual", len(t.Vocabulary.Tokens))
 | 
			
		||||
		for i := range vocabSize - len(t.Vocabulary.Tokens) {
 | 
			
		||||
			t.Vocabulary.Tokens = append(t.Vocabulary.Tokens, fmt.Sprintf("[PAD%d]", i))
 | 
			
		||||
			t.Vocabulary.Scores = append(t.Vocabulary.Scores, -1)
 | 
			
		||||
			t.Vocabulary.Types = append(t.Vocabulary.Types, tokenTypeUserDefined)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		slog.Debug("vocabulary", "size", len(t.Vocabulary.Tokens))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ts, err := parseTensors(fsys)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return conv.writeFile(ws, conv.KV(t), conv.Tensors(ts))
 | 
			
		||||
}
 | 
			
		||||
@@ -1,103 +0,0 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/pdevine/tensor"
 | 
			
		||||
	"github.com/pdevine/tensor/native"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/llm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type gemma struct {
 | 
			
		||||
	Parameters
 | 
			
		||||
	MaxPositionEmbeddings uint32  `json:"max_position_embeddings"`
 | 
			
		||||
	HiddenSize            uint32  `json:"hidden_size"`
 | 
			
		||||
	HiddenLayers          uint32  `json:"num_hidden_layers"`
 | 
			
		||||
	IntermediateSize      uint32  `json:"intermediate_size"`
 | 
			
		||||
	NumAttentionHeads     uint32  `json:"num_attention_heads"`
 | 
			
		||||
	NumKeyValueHeads      uint32  `json:"num_key_value_heads"`
 | 
			
		||||
	RMSNormEPS            float32 `json:"rms_norm_eps"`
 | 
			
		||||
	HeadDim               uint32  `json:"head_dim"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Converter = (*gemma)(nil)
 | 
			
		||||
 | 
			
		||||
func (p *gemma) KV(t *Tokenizer) llm.KV {
 | 
			
		||||
	kv := p.Parameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "gemma"
 | 
			
		||||
	kv["general.name"] = "gemma"
 | 
			
		||||
	kv["gemma.context_length"] = p.MaxPositionEmbeddings
 | 
			
		||||
	kv["gemma.embedding_length"] = p.HiddenSize
 | 
			
		||||
	kv["gemma.block_count"] = p.HiddenLayers
 | 
			
		||||
	kv["gemma.feed_forward_length"] = p.IntermediateSize
 | 
			
		||||
	kv["gemma.attention.head_count"] = p.NumAttentionHeads
 | 
			
		||||
	kv["gemma.attention.head_count_kv"] = p.NumKeyValueHeads
 | 
			
		||||
	kv["gemma.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
 | 
			
		||||
	kv["gemma.attention.key_length"] = p.HeadDim
 | 
			
		||||
	kv["gemma.attention.value_length"] = p.HeadDim
 | 
			
		||||
	kv["tokenizer.ggml.eot_token_id"] = uint32(107)
 | 
			
		||||
	kv["tokenizer.ggml.middle_token_id"] = uint32(68)
 | 
			
		||||
	kv["tokenizer.ggml.prefix_token_id"] = uint32(67)
 | 
			
		||||
	kv["tokenizer.ggml.suffix_token_id"] = uint32(69)
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *gemma) Tensors(ts []Tensor) []llm.Tensor {
 | 
			
		||||
	var out []llm.Tensor
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		name := p.tensorName(t.Name())
 | 
			
		||||
		if strings.HasSuffix(name, "_norm.weight") {
 | 
			
		||||
			t.SetRepacker(p.addOne)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, llm.Tensor{
 | 
			
		||||
			Name:     name,
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    t.Shape(),
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *gemma) tensorName(n string) string {
 | 
			
		||||
	return strings.NewReplacer(
 | 
			
		||||
		"model.embed_tokens", "token_embd",
 | 
			
		||||
		"model.norm", "output_norm",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"input_layernorm", "attn_norm",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"post_attention_layernorm", "ffn_norm",
 | 
			
		||||
		"block_sparse_moe.gate", "ffn_inp",
 | 
			
		||||
	).Replace(n)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*gemma) addOne(_ string, data []float32, shape []uint64) ([]float32, error) {
 | 
			
		||||
	n := tensor.New(tensor.WithShape(int(shape[0])), tensor.WithBacking(data))
 | 
			
		||||
	ones := tensor.Ones(tensor.Float32, int(shape[0]))
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
@@ -1,182 +0,0 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"cmp"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/llm"
 | 
			
		||||
	"github.com/pdevine/tensor"
 | 
			
		||||
	"github.com/pdevine/tensor/native"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type llama struct {
 | 
			
		||||
	Parameters
 | 
			
		||||
	NLayers               uint32  `json:"n_layers"`
 | 
			
		||||
	NumHiddenLayers       uint32  `json:"num_hidden_layers"`
 | 
			
		||||
	NLayer                uint32  `json:"n_layer"`
 | 
			
		||||
	MaxPositionEmbeddings uint32  `json:"max_position_embeddings"`
 | 
			
		||||
	NCtx                  uint32  `json:"n_ctx"`
 | 
			
		||||
	HiddenSize            uint32  `json:"hidden_size"`
 | 
			
		||||
	NEmbd                 uint32  `json:"n_embd"`
 | 
			
		||||
	IntermediateSize      uint32  `json:"intermediate_size"`
 | 
			
		||||
	NInner                uint32  `json:"n_inner"`
 | 
			
		||||
	NumAttentionHeads     uint32  `json:"num_attention_heads"`
 | 
			
		||||
	NHead                 uint32  `json:"n_head"`
 | 
			
		||||
	NumKeyValueHeads      uint32  `json:"num_key_value_heads"`
 | 
			
		||||
	RopeTheta             float32 `json:"rope_theta"`
 | 
			
		||||
	RopeScaling           struct {
 | 
			
		||||
		Type   string  `json:"type"`
 | 
			
		||||
		Factor float32 `json:"factor"`
 | 
			
		||||
	} `json:"rope_scaling"`
 | 
			
		||||
	RMSNormEPS       float32 `json:"rms_norm_eps"`
 | 
			
		||||
	LayerNormEPS     float32 `json:"layer_norm_eps"`
 | 
			
		||||
	LayerNormEpsilon float32 `json:"layer_norm_epsilon"`
 | 
			
		||||
	NormEpsilon      float32 `json:"norm_epsilon"`
 | 
			
		||||
	HeadDim          uint32  `json:"head_dim"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Converter = (*llama)(nil)
 | 
			
		||||
 | 
			
		||||
func (p *llama) KV(t *Tokenizer) llm.KV {
 | 
			
		||||
	kv := p.Parameters.KV(t)
 | 
			
		||||
	kv["general.architecture"] = "llama"
 | 
			
		||||
	kv["general.name"] = "llama"
 | 
			
		||||
	kv["llama.vocab_size"] = p.VocabSize
 | 
			
		||||
 | 
			
		||||
	kv["llama.block_count"] = cmp.Or(p.NLayers, p.NumHiddenLayers, p.NLayer)
 | 
			
		||||
 | 
			
		||||
	if contextLength := cmp.Or(p.MaxPositionEmbeddings, p.NCtx); contextLength > 0 {
 | 
			
		||||
		kv["llama.context_length"] = contextLength
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if embeddingLength := cmp.Or(p.HiddenSize, p.NEmbd); embeddingLength > 0 {
 | 
			
		||||
		kv["llama.embedding_length"] = cmp.Or(p.HiddenSize, p.NEmbd)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if feedForwardLength := cmp.Or(p.IntermediateSize, p.NInner); feedForwardLength > 0 {
 | 
			
		||||
		kv["llama.feed_forward_length"] = cmp.Or(p.IntermediateSize, p.NInner)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if headCount := cmp.Or(p.NumAttentionHeads, p.NHead); headCount > 0 {
 | 
			
		||||
		kv["llama.attention.head_count"] = cmp.Or(p.NumAttentionHeads, p.NHead)
 | 
			
		||||
		kv["llama.rope.dimension_count"] = p.HiddenSize / headCount
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.RopeTheta > 0 {
 | 
			
		||||
		kv["llama.rope.freq_base"] = p.RopeTheta
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.RopeScaling.Type == "linear" {
 | 
			
		||||
		kv["llama.rope.scaling.type"] = p.RopeScaling.Type
 | 
			
		||||
		kv["llama.rope.scaling.factor"] = p.RopeScaling.Factor
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.NumKeyValueHeads > 0 {
 | 
			
		||||
		kv["llama.attention.head_count_kv"] = p.NumKeyValueHeads
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.RMSNormEPS > 0 {
 | 
			
		||||
		kv["llama.attention.layer_norm_rms_epsilon"] = p.RMSNormEPS
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if layerNormEpsilon := cmp.Or(p.LayerNormEPS, p.LayerNormEpsilon, p.NormEpsilon); layerNormEpsilon > 0 {
 | 
			
		||||
		kv["llama.attention.layer_norm_epsilon"] = layerNormEpsilon
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.HeadDim > 0 {
 | 
			
		||||
		kv["llama.attention.key_length"] = p.HeadDim
 | 
			
		||||
		kv["llama.attention.value_length"] = p.HeadDim
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(t.Merges) > 0 {
 | 
			
		||||
		kv["tokenizer.ggml.merges"] = t.Merges
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llama) Tensors(ts []Tensor) []llm.Tensor {
 | 
			
		||||
	var out []llm.Tensor
 | 
			
		||||
	for _, t := range ts {
 | 
			
		||||
		name := p.tensorName(t.Name())
 | 
			
		||||
		if strings.HasSuffix(name, "attn_q.weight") ||
 | 
			
		||||
			strings.HasSuffix(name, "attn_k.weight") {
 | 
			
		||||
			t.SetRepacker(p.repack)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, llm.Tensor{
 | 
			
		||||
			Name:     name,
 | 
			
		||||
			Kind:     t.Kind(),
 | 
			
		||||
			Shape:    t.Shape(),
 | 
			
		||||
			WriterTo: t,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llama) tensorName(n string) string {
 | 
			
		||||
	return strings.NewReplacer(
 | 
			
		||||
		"lm_head", "output",
 | 
			
		||||
		"model.embed_tokens", "token_embd",
 | 
			
		||||
		"model.norm", "output_norm",
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"input_layernorm", "attn_norm",
 | 
			
		||||
		"self_attn.q_proj", "attn_q",
 | 
			
		||||
		"self_attn.k_proj", "attn_k",
 | 
			
		||||
		"self_attn.v_proj", "attn_v",
 | 
			
		||||
		"self_attn.o_proj", "attn_output",
 | 
			
		||||
		"mlp.gate_proj", "ffn_gate",
 | 
			
		||||
		"mlp.down_proj", "ffn_down",
 | 
			
		||||
		"mlp.up_proj", "ffn_up",
 | 
			
		||||
		"post_attention_layernorm", "ffn_norm",
 | 
			
		||||
		// mixtral
 | 
			
		||||
		"block_sparse_moe.gate", "ffn_gate_inp",
 | 
			
		||||
	).Replace(n)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *llama) repack(name string, data []float32, shape []uint64) ([]float32, error) {
 | 
			
		||||
	var dims []int
 | 
			
		||||
	for _, dim := range shape {
 | 
			
		||||
		dims = append(dims, int(dim))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var heads uint32
 | 
			
		||||
	if strings.HasSuffix(name, "q_proj.weight") {
 | 
			
		||||
		heads = p.NumAttentionHeads
 | 
			
		||||
	} else if strings.HasSuffix(name, "k_proj.weight") {
 | 
			
		||||
		heads = cmp.Or(p.NumKeyValueHeads, p.NumAttentionHeads)
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil, fmt.Errorf("unknown tensor for repack: %s", name)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n := tensor.New(tensor.WithShape(dims...), tensor.WithBacking(data))
 | 
			
		||||
	if err := n.Reshape(append([]int{int(heads), 2, dims[0] / int(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,89 +0,0 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/llm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type mixtral struct {
 | 
			
		||||
	llama
 | 
			
		||||
	NumLocalExperts    uint32 `json:"num_local_experts"`
 | 
			
		||||
	NumExpertsPerToken uint32 `json:"num_experts_per_tok"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var _ Converter = (*mixtral)(nil)
 | 
			
		||||
 | 
			
		||||
func (p *mixtral) KV(t *Tokenizer) llm.KV {
 | 
			
		||||
	kv := p.llama.KV(t)
 | 
			
		||||
 | 
			
		||||
	if p.NumLocalExperts > 0 {
 | 
			
		||||
		kv["llama.expert_count"] = p.NumLocalExperts
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if p.NumExpertsPerToken > 0 {
 | 
			
		||||
		kv["llama.expert_used_count"] = p.NumExpertsPerToken
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return kv
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *mixtral) Tensors(ts []Tensor) []llm.Tensor {
 | 
			
		||||
	oldnew := []string{
 | 
			
		||||
		"model.layers", "blk",
 | 
			
		||||
		"w1", "ffn_gate_exps",
 | 
			
		||||
		"w2", "ffn_down_exps",
 | 
			
		||||
		"w3", "ffn_up_exps",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := range p.NumLocalExperts {
 | 
			
		||||
		oldnew = append(oldnew, fmt.Sprintf(".block_sparse_moe.experts.%d.", i), ".")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// group experts of the same layer (model.layers.%d) and type (w[123]) into a single tensor
 | 
			
		||||
	namer := strings.NewReplacer(oldnew...)
 | 
			
		||||
	experts := make(map[string]experts)
 | 
			
		||||
 | 
			
		||||
	// merge experts into a single tensor while removing them from ts
 | 
			
		||||
	ts = slices.DeleteFunc(ts, func(t Tensor) bool {
 | 
			
		||||
		if !strings.Contains(t.Name(), ".block_sparse_moe.experts.") {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		name := namer.Replace(t.Name())
 | 
			
		||||
		experts[name] = append(experts[name], t)
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	var out []llm.Tensor
 | 
			
		||||
	for n, e := range experts {
 | 
			
		||||
		// TODO(mxyng): sanity check experts
 | 
			
		||||
		out = append(out, llm.Tensor{
 | 
			
		||||
			Name:     n,
 | 
			
		||||
			Kind:     e[0].Kind(),
 | 
			
		||||
			Shape:    append([]uint64{uint64(len(e))}, e[0].Shape()...),
 | 
			
		||||
			WriterTo: e,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return append(out, p.llama.Tensors(ts)...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type experts []Tensor
 | 
			
		||||
 | 
			
		||||
func (e experts) WriteTo(w io.Writer) (int64, error) {
 | 
			
		||||
	// TODO(mxyng): experts _should_ be numerically sorted by expert but this should check
 | 
			
		||||
	for _, t := range e {
 | 
			
		||||
		// the canonical merged experts tensor stacks all experts along a new, 0 axis,
 | 
			
		||||
		// e.g. `tensor.Stack(0, e[0], e[1:]...)`, which requires allocating temporary buffers
 | 
			
		||||
		// this accomplishes the same thing by writing each expert tensor in sequence
 | 
			
		||||
		if _, err := t.WriteTo(w); err != nil {
 | 
			
		||||
			return 0, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return 0, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -1,126 +0,0 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log/slog"
 | 
			
		||||
	"math"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"slices"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/ollama/ollama/llm"
 | 
			
		||||
	"golang.org/x/exp/maps"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func convertFull(t *testing.T, fsys fs.FS) (*os.File, llm.KV, llm.Tensors) {
 | 
			
		||||
	t.Helper()
 | 
			
		||||
 | 
			
		||||
	f, err := os.CreateTemp(t.TempDir(), "f16")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	defer f.Close()
 | 
			
		||||
 | 
			
		||||
	if err := Convert(fsys, f); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := os.Open(f.Name())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
	t.Cleanup(func() { r.Close() })
 | 
			
		||||
 | 
			
		||||
	m, _, err := llm.DecodeGGML(r, math.MaxInt)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, err := r.Seek(0, io.SeekStart); err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return r, m.KV(), m.Tensors()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestMain(m *testing.M) {
 | 
			
		||||
	var level slog.Level
 | 
			
		||||
	flag.TextVar(&level, "level", slog.LevelInfo, "log level")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
	slog.SetLogLoggerLevel(level)
 | 
			
		||||
	os.Exit(m.Run())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestConvertFull(t *testing.T) {
 | 
			
		||||
	cases := []string{
 | 
			
		||||
		"Meta-Llama-3-8B-Instruct",
 | 
			
		||||
		"Mistral-7B-Instruct-v0.2",
 | 
			
		||||
		"Mixtral-8x7B-Instruct-v0.1",
 | 
			
		||||
		"gemma-2b-it",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for i := range cases {
 | 
			
		||||
		tt := cases[i]
 | 
			
		||||
		t.Run(tt, func(t *testing.T) {
 | 
			
		||||
			t.Parallel()
 | 
			
		||||
 | 
			
		||||
			p := filepath.Join("testdata", tt)
 | 
			
		||||
			if testing.Short() {
 | 
			
		||||
				t.Skip("skipping in short mode")
 | 
			
		||||
			} else if _, err := os.Stat(p); err != nil {
 | 
			
		||||
				t.Skipf("%s not found", p)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			f, kv, tensors := convertFull(t, os.DirFS(p))
 | 
			
		||||
			actual := make(map[string]string)
 | 
			
		||||
			for k, v := range kv {
 | 
			
		||||
				if s, ok := v.(json.Marshaler); !ok {
 | 
			
		||||
					actual[k] = fmt.Sprintf("%v", v)
 | 
			
		||||
				} else {
 | 
			
		||||
					bts, err := json.Marshal(s)
 | 
			
		||||
					if err != nil {
 | 
			
		||||
						t.Fatal(err)
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					actual[k] = fmt.Sprintf("%x", sha256.Sum256(bts))
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, tensor := range tensors.Items {
 | 
			
		||||
				sha256sum := sha256.New()
 | 
			
		||||
				sr := io.NewSectionReader(f, int64(tensors.Offset+tensor.Offset), int64(tensor.Size()))
 | 
			
		||||
				if _, err := io.Copy(sha256sum, sr); err != nil {
 | 
			
		||||
					t.Fatal(err)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				actual[tensor.Name] = fmt.Sprintf("%x", sha256sum.Sum(nil))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			expectFile, err := os.Open(filepath.Join("testdata", fmt.Sprintf("%s.json", tt)))
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var expect map[string]string
 | 
			
		||||
			if err := json.NewDecoder(expectFile).Decode(&expect); err != nil {
 | 
			
		||||
				t.Fatal(err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			keys := maps.Keys(expect)
 | 
			
		||||
			slices.Sort(keys)
 | 
			
		||||
			for _, k := range keys {
 | 
			
		||||
				if v, ok := actual[k]; !ok {
 | 
			
		||||
					t.Errorf("missing %s", k)
 | 
			
		||||
				} else if v != expect[k] {
 | 
			
		||||
					t.Errorf("unexpected %s: want %s, got %s", k, expect[k], v)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
package convert
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"archive/zip"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ZipReader struct {
 | 
			
		||||
	r     *zip.Reader
 | 
			
		||||
	p     string
 | 
			
		||||
 | 
			
		||||
	// limit is the maximum size of a file that can be read directly
 | 
			
		||||
	// from the zip archive. Files larger than this size will be extracted
 | 
			
		||||
	limit int64
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewZipReader(r *zip.Reader, p string, limit int64) fs.FS {
 | 
			
		||||
	return &ZipReader{r, p, limit}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (z *ZipReader) Open(name string) (fs.File, error) {
 | 
			
		||||
	r, err := z.r.Open(name)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer r.Close()
 | 
			
		||||
 | 
			
		||||
	if fi, err := r.Stat(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else if fi.Size() < z.limit {
 | 
			
		||||
		return r, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !filepath.IsLocal(name) {
 | 
			
		||||
		return nil, zip.ErrInsecurePath
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	n := filepath.Join(z.p, name)
 | 
			
		||||
	if _, err := os.Stat(n); errors.Is(err, os.ErrNotExist) {
 | 
			
		||||
		w, err := os.Create(n)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		defer w.Close()
 | 
			
		||||
 | 
			
		||||
		if _, err := io.Copy(w, r); err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return os.Open(n)
 | 
			
		||||
}
 | 
			
		||||