├── demo.png ├── .gitattributes ├── sonar-project.properties ├── .dockerignore ├── docker-compose.yaml ├── docker-compose-prebuilt.yaml ├── Dockerfile ├── .github ├── dependabot.yml └── workflows │ ├── stale.yml │ └── build.yml ├── package.json ├── docs ├── unraid.md └── api.md ├── LICENSE ├── eslint.config.js ├── public ├── swagger.html ├── script.js ├── styles.css ├── swagger.json └── index.html ├── .gitignore ├── README.md └── server.js /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/d3v0ps-cloud/OllamaModelManager/HEAD/demo.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=d3v0ps-cloud_OllamaModelManager_31f3370e-89cb-4039-a803-018716922431 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .gitattributes 4 | .github 5 | README.md 6 | demo.png 7 | node_modules 8 | npm-debug.log 9 | *.log 10 | .env 11 | .env.* 12 | .DS_Store 13 | docker-compose.yml 14 | docker-compose-prebuilt.yml 15 | Dockerfile 16 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | ollamamodelmanager: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | # image: ollamamodelmanager:latest 7 | container_name: ollamamodelmanager 8 | restart: unless-stopped 9 | ports: 10 | - "3000:3000" 11 | environment: 12 | - OLLAMA_ENDPOINTS=${OLLAMA_ENDPOINTS:-ollama:11434} -------------------------------------------------------------------------------- /docker-compose-prebuilt.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | ollamamodelmanager: 3 | #image: ghcr.io/d3v0ps-cloud/ollamamodelmanager:latest 4 | image: aaronbolton78/ollamamodelmanager:latest 5 | ports: 6 | - "3000:3000" 7 | environment: 8 | - OLLAMA_ENDPOINTS=http://192.168.1.10:11434,https://ollama1.remote.net,https://ollama2.remote.net 9 | restart: unless-stopped 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-slim 2 | 3 | WORKDIR /app 4 | 5 | # Copy package files 6 | COPY package*.json ./ 7 | 8 | # Install dependencies 9 | RUN npm install --production 10 | 11 | # Install curl for healthcheck 12 | RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* 13 | 14 | # Copy application files 15 | COPY . . 16 | 17 | # Expose the application port 18 | EXPOSE 3000 19 | 20 | # Add healthcheck 21 | HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ 22 | CMD curl -f http://localhost:3000/ || exit 1 23 | 24 | # Start the application 25 | CMD ["npm", "start"] 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic `dependabot.yml` file with 2 | # minimum configuration for two package managers 3 | 4 | version: 2 5 | updates: 6 | # Enable version updates for npm 7 | - package-ecosystem: "npm" 8 | # Look for `package.json` and `lock` files in the `root` directory 9 | directory: "/" 10 | # Check the npm registry for updates every day (weekdays) 11 | schedule: 12 | interval: "daily" 13 | 14 | # Enable version updates for Docker 15 | - package-ecosystem: "docker" 16 | # Look for a `Dockerfile` in the `root` directory 17 | directory: "/" 18 | # Check for updates once a week 19 | schedule: 20 | interval: "weekly" 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ollamamodelmanager", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "node server.js", 8 | "debug": "node --inspect server.js", 9 | "debug:dev": "nodemon --inspect server.js", 10 | "dev": "nodemon server.js", 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "lint": "eslint .", 13 | "lint:fix": "eslint . --fix" 14 | }, 15 | "keywords": [], 16 | "author": "", 17 | "license": "ISC", 18 | "description": "", 19 | "dependencies": { 20 | "axios": "^1.7.9", 21 | "cors": "^2.8.5", 22 | "dotenv": "^16.4.7", 23 | "express": "^4.21.2" 24 | }, 25 | "devDependencies": { 26 | "@eslint/js": "^9.20.0", 27 | "eslint": "^9.20.0", 28 | "nodemon": "^3.1.9" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | workflow_dispatch: 10 | schedule: 11 | - cron: '26 2 * * *' 12 | 13 | jobs: 14 | stale: 15 | 16 | runs-on: ubuntu-latest 17 | permissions: 18 | issues: write 19 | pull-requests: write 20 | 21 | steps: 22 | - uses: actions/stale@v5 23 | with: 24 | repo-token: ${{ secrets.GITHUB_TOKEN }} 25 | stale-issue-message: 'Stale issue message' 26 | stale-pr-message: 'Stale pull request message' 27 | stale-issue-label: 'no-issue-activity' 28 | stale-pr-label: 'no-pr-activity' 29 | -------------------------------------------------------------------------------- /docs/unraid.md: -------------------------------------------------------------------------------- 1 | ### (Untested I dont have unraid to confirm) 2 | 3 | # Unraid Deployment Guide for Ollama Model Manager 4 | 5 | This guide covers how to deploy the Ollama Model Manager on Unraid using Docker. 6 | 7 | ## Prerequisites 8 | 9 | - Unraid server running version 6.9.0 or higher 10 | - Community Applications (CA) plugin installed 11 | - Docker enabled on your Unraid server 12 | - One or more Ollama instances accessible from your network 13 | 14 | ## Installation Steps 15 | 16 | ### 1. Install via Docker 17 | 18 | #### Manual Configuration 19 | 20 | 1. Click "Add Container" in the Docker tab 21 | 2. Fill in the following fields: 22 | 23 | ```yaml 24 | Repository: ghcr.io/d3v0ps-cloud/ollamamodelmanager:latest 25 | Name: ollamamodelmanager 26 | Network Type: Bridge 27 | 28 | # Port Mappings 29 | Host Port: 3000 30 | Container Port: 3000 31 | Protocol: TCP 32 | 33 | # Environment Variables 34 | Variable: OLLAMA_ENDPOINTS 35 | Value: http://your-ollama-ip:11434,https://ollama2.remote.net 36 | 37 | # Optional: Auto-start container 38 | Start on array boot: Yes 39 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Aaron Bolton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | 3 | export default [ 4 | js.configs.recommended, 5 | { 6 | languageOptions: { 7 | ecmaVersion: 'latest', 8 | sourceType: 'module', 9 | globals: { 10 | // Browser globals 11 | window: 'readonly', 12 | document: 'readonly', 13 | fetch: 'readonly', 14 | TextDecoder: 'readonly', 15 | setTimeout: 'readonly', 16 | // Functions defined in HTML 17 | updateSelectedModels: 'readonly', 18 | updateSelectedModelsInBulk: 'readonly', 19 | displayModels: 'readonly', 20 | formatBytes: 'readonly', 21 | refreshModels: 'readonly', 22 | // Node.js globals 23 | console: 'readonly', 24 | process: 'readonly', 25 | __dirname: 'readonly', 26 | __filename: 'readonly', 27 | exports: 'readonly', 28 | module: 'readonly', 29 | require: 'readonly', 30 | }, 31 | }, 32 | rules: { 33 | // Additional custom rules 34 | 'no-unused-vars': 'warn', 35 | 'no-console': 'off', // Allow console for server-side logging 36 | 'semi': ['error', 'always'], 37 | 'quotes': ['error', 'single'], 38 | }, 39 | // Files to lint 40 | files: ['**/*.js'], 41 | // Files to ignore 42 | ignores: ['node_modules/**', 'build/**', 'dist/**'], 43 | }, 44 | ]; 45 | -------------------------------------------------------------------------------- /public/swagger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ollama Model Manager API Documentation 7 | 8 | 17 | 18 | 19 |
20 | 21 | 22 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ollama Model Manager 2 | 3 | A web-based management interface for Ollama endpoints, allowing you to manage and interact with multiple Ollama instances from a single dashboard. 4 | 5 | Demo Screenshot 6 | 7 | 8 | ## Features 9 | 10 | - Connect to multiple Ollama endpoints simultaneously 11 | - Web-based interface for model management 12 | - Support for both local and remote Ollama instances 13 | - Filter Models 14 | - Sort Models\ 15 | - Select Multiple Models or All Models 16 | - Delete Selected Models 17 | - Light & Dark Theme (defaults dark) 18 | - Update Models 19 | - Running Models Stats 20 | - Pull Models from Ollama Hub 21 | - Swagger API Documentation 22 | - [Unraid Deployment Guide (untested)](https://github.com/d3v0ps-cloud/OllamaModelManager/blob/main/docs/unraid.md) 23 | 24 | ## Prerequisites 25 | 26 | - Node.js 20.x or later (for npm installation) 27 | - Docker and Docker Compose (for Docker installation) 28 | - One or more running Ollama instances 29 | 30 | ## Installation 31 | 32 | ### Using npm 33 | 34 | 1. Clone the repository: 35 | ```bash 36 | git clone https://github.com/d3v0ps-cloud/OllamaModelManager.git 37 | ``` 38 | 39 | ```bash 40 | cd OllamaModelManager 41 | ``` 42 | 43 | 2. Install dependencies: 44 | ```bash 45 | npm install 46 | ``` 47 | 48 | 3. Create a `.env` file in the root directory and configure your Ollama endpoints: 49 | ```bash 50 | OLLAMA_ENDPOINTS=http://localhost:11434,https://ollama1.remote.net,https://ollama2.remote.net 51 | ``` 52 | 53 | 4. Start the application: 54 | 55 | Development mode (with hot reload): 56 | ```bash 57 | npm run dev 58 | ``` 59 | 60 | Production mode: 61 | ```bash 62 | npm start 63 | ``` 64 | 65 | The application will be available at `http://localhost:3000` 66 | 67 | ### Using Docker 68 | 69 | #### Option 1 - Build your own image 70 | 71 | 1. Clone the repository: 72 | ```bash 73 | git clone https://github.com/d3v0ps-cloud/OllamaModelManager.git 74 | ``` 75 | 76 | ```bash 77 | cd OllamaModelManager 78 | ``` 79 | 80 | 2. Configure your Ollama endpoints in `docker-compose.yml`: 81 | ```yaml 82 | environment: 83 | - OLLAMA_ENDPOINTS=http://your-ollama-ip:11434,https://ollama1.remote.net 84 | ``` 85 | 86 | 3. Build and start the container: 87 | ```bash 88 | docker compose up -d 89 | ``` 90 | 91 | The application will be available at `http://localhost:3000` 92 | 93 | #### Option 2 - Use the prebuilt image 94 | 95 | 1. Copy down the Pre-Built Compose file: 96 | ```bash 97 | docker-compose-prebuilt.yml 98 | ``` 99 | 100 | 2. Configure your Ollama endpoints in `docker-compose-prebuilt.yml`: 101 | ```yaml 102 | environment: 103 | - OLLAMA_ENDPOINTS=http://your-ollama-ip:11434,https://ollama1.remote.net 104 | ``` 105 | 106 | 3. Build and start the container: 107 | ```bash 108 | docker compose up -d 109 | ``` 110 | 111 | The application will be available at `http://localhost:3000` 112 | 113 | ## Configuration 114 | 115 | ### Environment Variables 116 | 117 | - `OLLAMA_ENDPOINTS`: Comma-separated list of Ollama API endpoints (required) 118 | - Format: `http://host1:port,http://host2:port` 119 | - Example: `http://192.168.1.10:11434,https://ollama1.remote.net` 120 | 121 | ## Development 122 | 123 | To run the application in development mode with hot reload: 124 | 125 | ```bash 126 | npm run dev 127 | ``` 128 | 129 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - 'README.md' 10 | - 'demo.png' 11 | - 'docs/**' 12 | pull_request: 13 | branches: 14 | - main 15 | paths-ignore: 16 | - 'README.md' 17 | - 'demo.png' 18 | - 'docs/**' 19 | 20 | env: 21 | GHCR_REGISTRY: ghcr.io 22 | DOCKERHUB_REGISTRY: docker.io 23 | IMAGE_NAME: ${{ github.repository }} 24 | 25 | jobs: 26 | 27 | # analyse: 28 | # name: Analyse code 29 | # runs-on: ubuntu-latest 30 | 31 | # steps: 32 | # - uses: actions/checkout@v4 33 | # with: 34 | # fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 35 | # - uses: sonarsource/sonarqube-scan-action@v4 36 | # env: 37 | # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 38 | # SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} 39 | # # If you wish to fail your job when the Quality Gate is red, uncomment the 40 | # # following lines. This would typically be used to fail a deployment. 41 | # # - uses: sonarsource/sonarqube-quality-gate-action@v1 42 | # # timeout-minutes: 5 43 | # # env: 44 | # # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 45 | 46 | build-and-push: 47 | runs-on: ubuntu-latest 48 | # needs: analyse 49 | permissions: 50 | contents: read 51 | packages: write 52 | 53 | steps: 54 | - name: Checkout repository 55 | uses: actions/checkout@v4 56 | 57 | - name: Set up QEMU 58 | uses: docker/setup-qemu-action@v3 59 | 60 | - name: Set up Docker Buildx 61 | uses: docker/setup-buildx-action@v3 62 | 63 | - name: Log in to GitHub Container Registry 64 | uses: docker/login-action@v3 65 | with: 66 | registry: ${{ env.GHCR_REGISTRY }} 67 | username: ${{ github.actor }} 68 | password: ${{ secrets.GITHUB_TOKEN }} 69 | 70 | - name: Log in to Docker Hub 71 | uses: docker/login-action@v3 72 | with: 73 | registry: ${{ env.DOCKERHUB_REGISTRY }} 74 | username: ${{ secrets.DOCKERHUB_USERNAME }} 75 | password: ${{ secrets.DOCKERHUB_TOKEN }} 76 | 77 | - name: Extract metadata (tags, labels) for Docker 78 | id: meta 79 | uses: docker/metadata-action@v5 80 | with: 81 | images: | 82 | ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }} 83 | ${{ env.DOCKERHUB_REGISTRY }}/${{ secrets.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }} 84 | tags: | 85 | type=raw,value=latest,enable={{is_default_branch}} 86 | type=ref,event=branch 87 | type=ref,event=pr 88 | type=semver,pattern={{version}} 89 | type=semver,pattern={{major}}.{{minor}} 90 | type=sha 91 | 92 | - name: Build and push Docker image 93 | uses: docker/build-push-action@v5 94 | with: 95 | context: . 96 | file: Dockerfile 97 | platforms: linux/amd64,linux/arm64 98 | push: ${{ github.event_name != 'pull_request' }} 99 | tags: ${{ steps.meta.outputs.tags }} 100 | labels: ${{ steps.meta.outputs.labels }} 101 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # Ollama Model Manager API Documentation 2 | 3 | This document outlines the REST API endpoints exposed by the Ollama Model Manager application. 4 | 5 | ## Base URL 6 | 7 | By default, the server runs on `http://localhost:3000` 8 | 9 | ## Environment Configuration 10 | 11 | - `OLLAMA_ENDPOINTS`: Comma-separated list of Ollama endpoints (default: 'http://localhost:11434') 12 | 13 | ## Endpoints 14 | 15 | ### Get Available Ollama Endpoints 16 | 17 | ```http 18 | GET /api/endpoints 19 | ``` 20 | 21 | Returns a list of configured Ollama endpoints. 22 | 23 | **Response** 24 | ```json 25 | [ 26 | "http://localhost:11434", 27 | "http://other-endpoint:11434" 28 | ] 29 | ``` 30 | 31 | ### Set Active Ollama Endpoint 32 | 33 | ```http 34 | POST /api/set-endpoint 35 | ``` 36 | 37 | Sets and validates the active Ollama endpoint. 38 | 39 | **Request Body** 40 | ```json 41 | { 42 | "endpoint": "http://localhost:11434" 43 | } 44 | ``` 45 | 46 | **Response** 47 | ```json 48 | { 49 | "success": true, 50 | "message": "Endpoint set successfully" 51 | } 52 | ``` 53 | 54 | **Error Response** (500) 55 | ```json 56 | { 57 | "success": false, 58 | "message": "Failed to connect to Ollama endpoint", 59 | "error": "error message" 60 | } 61 | ``` 62 | 63 | ### Get Running Models 64 | 65 | ```http 66 | GET /api/ps 67 | ``` 68 | 69 | Returns a list of currently running Ollama models. 70 | 71 | **Response** 72 | ```json 73 | { 74 | // Response format matches Ollama's /api/ps endpoint 75 | } 76 | ``` 77 | 78 | **Error Response** (500) 79 | ```json 80 | { 81 | "success": false, 82 | "message": "Failed to fetch running models", 83 | "error": "error message" 84 | } 85 | ``` 86 | 87 | ### Get Available Models 88 | 89 | ```http 90 | GET /api/models 91 | ``` 92 | 93 | Returns a list of available models with their details. 94 | 95 | **Response** 96 | ```json 97 | [ 98 | { 99 | "name": "model-name", 100 | "size": 12345678, 101 | "details": { 102 | "parent_model": "base-model", 103 | "format": "gguf", 104 | "family": "llama", 105 | "families": ["llama", "llama2"], 106 | "parameter_size": "7B", 107 | "quantization_level": "Q4_K_M" 108 | } 109 | } 110 | ] 111 | ``` 112 | 113 | **Error Response** (500) 114 | ```json 115 | { 116 | "success": false, 117 | "message": "Failed to fetch models", 118 | "error": "error message" 119 | } 120 | ``` 121 | 122 | ### Delete Models 123 | 124 | ```http 125 | DELETE /api/models 126 | ``` 127 | 128 | Deletes one or more models from Ollama. 129 | 130 | **Request Body** 131 | ```json 132 | { 133 | "models": ["model1", "model2"] 134 | } 135 | ``` 136 | 137 | **Success Response** 138 | ```json 139 | { 140 | "success": true, 141 | "message": "Models deleted successfully" 142 | } 143 | ``` 144 | 145 | **Error Response** (500) 146 | ```json 147 | { 148 | "success": false, 149 | "message": "Failed to delete models: model1, model2", 150 | } 151 | ``` 152 | 153 | ### Pull Model 154 | 155 | ```http 156 | POST /api/pull 157 | ``` 158 | 159 | Pulls a new model from Ollama. Returns a streaming response with progress updates. 160 | 161 | **Request Body** 162 | ```json 163 | { 164 | "model": "model-name" 165 | } 166 | ``` 167 | 168 | **Streaming Response Format** 169 | ```json 170 | {"status": "downloading", "completed": 1234, "total": 5678} 171 | {"status": "verifying digest"} 172 | {"status": "writing manifest"} 173 | ``` 174 | 175 | **Error Response** (400) 176 | ```json 177 | { 178 | "success": false, 179 | "message": "Model name is required" 180 | } 181 | ``` 182 | 183 | ### Update Model 184 | 185 | ```http 186 | POST /api/update-model 187 | ``` 188 | 189 | Updates an existing model. Returns a streaming response with progress updates. 190 | 191 | **Request Body** 192 | ```json 193 | { 194 | "modelName": "model-name" 195 | } 196 | ``` 197 | 198 | **Streaming Response Format** 199 | ```json 200 | {"status": "downloading", "completed": 1234, "total": 5678} 201 | {"status": "verifying digest"} 202 | {"status": "writing manifest"} 203 | ``` 204 | 205 | **Error Response** (400) 206 | ```json 207 | { 208 | "success": false, 209 | "message": "Model name is required" 210 | } 211 | ``` 212 | 213 | ## Error Handling 214 | 215 | All endpoints follow a consistent error response format: 216 | 217 | ```json 218 | { 219 | "success": false, 220 | "message": "Human readable error message", 221 | "error": "Detailed error information (optional)" 222 | } 223 | ``` 224 | 225 | ## Streaming Responses 226 | 227 | For endpoints that return streaming responses (pull and update-model): 228 | 229 | 1. The response is chunked and each chunk contains a JSON object 230 | 2. Each JSON object has a `status` field indicating the current operation 231 | 3. For download progress, the response includes `completed` and `total` bytes 232 | 4. The stream ends when the operation is complete or an error occurs 233 | 5. Error responses in streams include `status: "error"` and an `error` message 234 | -------------------------------------------------------------------------------- /public/script.js: -------------------------------------------------------------------------------- 1 | let selectedModels = new Set(); 2 | 3 | /* eslint-disable no-unused-vars */ 4 | function updateSelectedModels(checkbox) { 5 | if (checkbox.checked) { 6 | selectedModels.add(checkbox.value); 7 | } else { 8 | selectedModels.delete(checkbox.value); 9 | } 10 | updateBulkActionButtons(); 11 | } 12 | 13 | function updateBulkActionButtons() { 14 | const updateSelectedBtn = document.getElementById('updateSelectedBtn'); 15 | if (updateSelectedBtn) { 16 | updateSelectedBtn.disabled = selectedModels.size === 0; 17 | } 18 | } 19 | 20 | async function updateSelectedModelsInBulk() { 21 | const models = Array.from(selectedModels); 22 | const updatePromises = models.map(modelName => updateModel(modelName)); 23 | await Promise.all(updatePromises); 24 | selectedModels.clear(); 25 | updateBulkActionButtons(); 26 | } 27 | 28 | function displayModels(models) { 29 | /* eslint-enable no-unused-vars */ 30 | const container = document.getElementById('modelsListContent'); 31 | container.innerHTML = ` 32 |
33 | 34 |
35 | `; 36 | 37 | models.forEach(model => { 38 | const row = document.createElement('div'); 39 | row.className = 'model-row'; 40 | row.innerHTML = ` 41 |
42 | 43 |
44 |
${model.name}
45 |
${formatBytes(model.size)}
46 |
${model.parameter_size || 'N/A'}
47 |
${model.family || 'N/A'}
48 |
${model.format || 'N/A'}
49 |
${model.quantization_level || 'N/A'}
50 |
51 | 52 |
53 |
54 | `; 55 | container.appendChild(row); 56 | }); 57 | } 58 | 59 | async function updateModel(modelName) { 60 | const statusElement = document.getElementById(`status-${modelName}`); 61 | statusElement.textContent = 'Starting update...'; 62 | 63 | try { 64 | const response = await fetch('/api/update-model', { 65 | method: 'POST', 66 | headers: { 67 | 'Content-Type': 'application/json' 68 | }, 69 | body: JSON.stringify({ modelName }) 70 | }); 71 | 72 | if (!response.ok) { 73 | const error = await response.json(); 74 | throw new Error(error.message || 'Failed to start update'); 75 | } 76 | 77 | const reader = response.body.getReader(); 78 | let lastStatus = ''; 79 | 80 | while (true) { 81 | const { done, value } = await reader.read(); 82 | if (done) break; 83 | 84 | const text = new TextDecoder().decode(value); 85 | const lines = text.split('\n'); 86 | 87 | for (const line of lines) { 88 | if (line.trim()) { 89 | try { 90 | const update = JSON.parse(line); 91 | 92 | if (update.status === 'error') { 93 | throw new Error(update.error || 'Update failed'); 94 | } 95 | 96 | if (update.status === 'downloading') { 97 | const progress = ((update.completed / update.total) * 100).toFixed(1); 98 | statusElement.textContent = `Downloading: ${progress}%`; 99 | } else if (update.status === 'verifying digest') { 100 | statusElement.textContent = 'Verifying download...'; 101 | } else if (update.status === 'writing manifest') { 102 | statusElement.textContent = 'Finalizing update...'; 103 | } else if (update.status !== lastStatus) { 104 | statusElement.textContent = update.status; 105 | lastStatus = update.status; 106 | } 107 | } catch (e) { 108 | if (e.message === 'Update failed') { 109 | throw e; 110 | } 111 | console.error('Error parsing update:', e); 112 | } 113 | } 114 | } 115 | } 116 | 117 | statusElement.textContent = 'Update complete'; 118 | setTimeout(() => { 119 | statusElement.textContent = ''; 120 | refreshModels(); 121 | }, 2000); 122 | 123 | } catch (error) { 124 | console.error('Update error:', error); 125 | statusElement.textContent = `Error: ${error.message}`; 126 | setTimeout(() => { 127 | statusElement.textContent = ''; 128 | }, 5000); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import express from 'express'; 3 | import cors from 'cors'; 4 | import axios from 'axios'; 5 | import { readFile } from 'fs/promises'; 6 | import { join } from 'path'; 7 | const app = express(); 8 | 9 | app.use(cors({ 10 | origin: '*', 11 | methods: ['GET', 'POST', 'DELETE', 'OPTIONS'], 12 | allowedHeaders: ['Content-Type'] 13 | })); 14 | app.use(express.json()); 15 | 16 | // Serve swagger.json with explicit route 17 | app.get('/swagger.json', async (req, res) => { 18 | try { 19 | const swaggerPath = join(process.cwd(), 'public', 'swagger.json'); 20 | const swaggerContent = await readFile(swaggerPath, 'utf8'); 21 | res.setHeader('Content-Type', 'application/json'); 22 | res.send(swaggerContent); 23 | } catch (error) { 24 | console.error('Error serving swagger.json:', error); 25 | res.status(500).send({ error: 'Failed to load swagger.json' }); 26 | } 27 | }); 28 | 29 | // Serve static files after routes 30 | app.use(express.static('public')); 31 | 32 | // Store the Ollama endpoint 33 | let ollamaEndpoint = 'http://localhost:11434'; 34 | 35 | // Get endpoints from environment variable 36 | const getEndpoints = () => { 37 | const endpoints = process.env.OLLAMA_ENDPOINTS || 'http://localhost:11434'; 38 | return endpoints.split(',').map(endpoint => endpoint.trim()); 39 | }; 40 | 41 | // Endpoint to get available Ollama endpoints 42 | app.get('/api/endpoints', (req, res) => { 43 | res.json(getEndpoints()); 44 | }); 45 | 46 | // Endpoint to set Ollama API URL 47 | app.post('/api/set-endpoint', async (req, res) => { 48 | const { endpoint } = req.body; 49 | ollamaEndpoint = endpoint; 50 | try { 51 | // Test the connection 52 | await axios.get(`${endpoint}/api/tags`); 53 | res.json({ success: true, message: 'Endpoint set successfully' }); 54 | } catch (error) { 55 | res.status(500).json({ 56 | success: false, 57 | message: 'Failed to connect to Ollama endpoint', 58 | error: error.message 59 | }); 60 | } 61 | }); 62 | 63 | // Get running models from Ollama 64 | app.get('/api/ps', async (req, res) => { 65 | try { 66 | const response = await axios.get(`${ollamaEndpoint}/api/ps`); 67 | res.json(response.data); 68 | } catch (error) { 69 | res.status(500).json({ 70 | success: false, 71 | message: 'Failed to fetch running models', 72 | error: error.message 73 | }); 74 | } 75 | }); 76 | 77 | app.get('/api/models', async (req, res) => { 78 | try { 79 | const response = await axios.get(`${ollamaEndpoint}/api/tags`); 80 | 81 | // Get details for each model 82 | const modelsWithDetails = await Promise.all(response.data.models.map(async (model) => { 83 | try { 84 | const detailsResponse = await axios.post(`${ollamaEndpoint}/api/show`, { 85 | name: model.name 86 | }); 87 | return { 88 | ...model, 89 | details: { 90 | parent_model: detailsResponse.data.details?.parent_model || '', 91 | format: detailsResponse.data.details?.format || '', 92 | family: detailsResponse.data.details?.family || '', 93 | families: detailsResponse.data.details?.families || [], 94 | parameter_size: detailsResponse.data.details?.parameter_size || '', 95 | quantization_level: detailsResponse.data.details?.quantization_level || '' 96 | } 97 | }; 98 | } catch { 99 | // If we can't get details, return the model without them 100 | return model; 101 | } 102 | })); 103 | 104 | // Sort models alphabetically 105 | const sortedModels = modelsWithDetails.sort((a, b) => 106 | a.name.localeCompare(b.name) 107 | ); 108 | res.json(sortedModels); 109 | } catch (error) { 110 | res.status(500).json({ 111 | success: false, 112 | message: 'Failed to fetch models', 113 | error: error.message 114 | }); 115 | } 116 | }); 117 | 118 | // Delete models from Ollama 119 | app.delete('/api/models', async (req, res) => { 120 | const { models } = req.body; 121 | try { 122 | const results = await Promise.allSettled(models.map(model => 123 | axios.delete(`${ollamaEndpoint}/api/delete`, { 124 | data: { name: model } 125 | }) 126 | )); 127 | 128 | const failed = results 129 | .filter(r => r.status === 'rejected') 130 | .map((r, i) => models[i]); 131 | 132 | if (failed.length > 0) { 133 | res.status(500).json({ 134 | success: false, 135 | message: `Failed to delete models: ${failed.join(', ')}`, 136 | }); 137 | } else { 138 | res.json({ success: true, message: 'Models deleted successfully' }); 139 | } 140 | } catch (error) { 141 | res.status(500).json({ 142 | success: false, 143 | message: 'Failed to process delete request', 144 | error: error.message 145 | }); 146 | } 147 | }); 148 | 149 | // Handle streaming response for model operations 150 | const handleModelOperation = async (req, res, operation) => { 151 | res.setHeader('Content-Type', 'application/json'); 152 | res.setHeader('Transfer-Encoding', 'chunked'); 153 | 154 | let hasEnded = false; 155 | const endResponse = (error) => { 156 | if (!hasEnded) { 157 | hasEnded = true; 158 | if (error) { 159 | res.write(JSON.stringify({ 160 | status: 'error', 161 | error: error.message 162 | }) + '\n'); 163 | } 164 | res.end(); 165 | } 166 | }; 167 | 168 | try { 169 | const response = await axios({ 170 | method: 'post', 171 | url: `${ollamaEndpoint}/api/pull`, 172 | data: operation, 173 | responseType: 'stream' 174 | }); 175 | 176 | response.data.on('data', (chunk) => { 177 | try { 178 | const lines = chunk.toString().split('\n'); 179 | lines.forEach(line => { 180 | if (line.trim()) { 181 | try { 182 | JSON.parse(line); 183 | res.write(line + '\n'); 184 | } catch { 185 | console.error('Invalid JSON in response:', line); 186 | } 187 | } 188 | }); 189 | } catch (error) { 190 | console.error('Error processing chunk:', error); 191 | endResponse(error); 192 | } 193 | }); 194 | 195 | response.data.on('end', () => endResponse()); 196 | response.data.on('error', (error) => { 197 | console.error('Stream error:', error); 198 | endResponse(error); 199 | }); 200 | 201 | req.on('close', () => endResponse()); 202 | 203 | } catch (error) { 204 | console.error('Failed to start operation:', error); 205 | endResponse(error); 206 | } 207 | }; 208 | 209 | // Endpoint to pull a model 210 | app.post('/api/pull', async (req, res) => { 211 | const { model } = req.body; 212 | if (!model) { 213 | return res.status(400).json({ 214 | success: false, 215 | message: 'Model name is required' 216 | }); 217 | } 218 | await handleModelOperation(req, res, { model }); 219 | }); 220 | 221 | // Endpoint to update a model 222 | app.post('/api/update-model', async (req, res) => { 223 | const { modelName } = req.body; 224 | if (!modelName) { 225 | return res.status(400).json({ 226 | success: false, 227 | message: 'Model name is required' 228 | }); 229 | } 230 | await handleModelOperation(req, res, { model: modelName }); 231 | }); 232 | 233 | const PORT = 3000; 234 | app.listen(PORT, () => { 235 | console.log(`Server running on http://localhost:${PORT}`); 236 | }); 237 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | /* Light theme */ 2 | :root { 3 | --bg-color: #f2f2f7; 4 | --container-bg: rgba(255, 255, 255, 0.95); 5 | --text-color: #000; 6 | --secondary-text: #6c6c70; 7 | --border-color: rgba(60, 60, 67, 0.12); 8 | --hover-bg: rgba(60, 60, 67, 0.03); 9 | --header-bg: rgba(255, 255, 255, 0.85); 10 | --pill-bg: #e5e5ea; 11 | --pill-text: #3a3a3c; 12 | --shadow-color: rgba(0,0,0,0.05); 13 | --success-bg: #e4f8ef; 14 | --success-text: #1d804b; 15 | --error-bg: #ffe5e5; 16 | --error-text: #ff3b30; 17 | --button-bg: #007aff; 18 | --button-hover: #0063cc; 19 | --button-text: white; 20 | --delete-button-bg: #ff3b30; 21 | --delete-button-hover: #d70015; 22 | --checkbox-bg: white; 23 | --input-bg: rgba(118, 118, 128, 0.12); 24 | --input-text: #000; 25 | --input-border: transparent; 26 | --select-bg: rgba(118, 118, 128, 0.12); 27 | } 28 | 29 | [data-theme="dark"] { 30 | --bg-color: #000; 31 | --container-bg: rgba(28, 28, 30, 0.95); 32 | --text-color: #fff; 33 | --secondary-text: #98989d; 34 | --border-color: rgba(84, 84, 88, 0.65); 35 | --hover-bg: rgba(84, 84, 88, 0.2); 36 | --header-bg: rgba(28, 28, 30, 0.85); 37 | --pill-bg: #48484a; 38 | --pill-text: #e5e5ea; 39 | --shadow-color: rgba(0,0,0,0.3); 40 | --success-bg: #1c3829; 41 | --success-text: #30d158; 42 | --error-bg: #3b1715; 43 | --error-text: #ff453a; 44 | --button-bg: #0a84ff; 45 | --button-hover: #0066cc; 46 | --button-text: white; 47 | --delete-button-bg: #ff453a; 48 | --delete-button-hover: #d70015; 49 | --checkbox-bg: #48484a; 50 | --input-bg: rgba(118, 118, 128, 0.24); 51 | --input-text: #fff; 52 | --input-border: transparent; 53 | --select-bg: rgba(118, 118, 128, 0.24); 54 | } 55 | 56 | @supports (-webkit-backdrop-filter: none) or (backdrop-filter: none) { 57 | .container, .models-header { 58 | backdrop-filter: blur(20px); 59 | -webkit-backdrop-filter: blur(20px); 60 | } 61 | } 62 | 63 | body { 64 | font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif; 65 | max-width: 900px; 66 | margin: 0 auto; 67 | padding: 20px; 68 | background-color: var(--bg-color); 69 | color: var(--text-color); 70 | line-height: 1.5; 71 | -webkit-font-smoothing: antialiased; 72 | } 73 | 74 | .container { 75 | background-color: var(--container-bg); 76 | padding: 24px; 77 | border-radius: 16px; 78 | box-shadow: 0 8px 32px var(--shadow-color); 79 | transition: all 0.3s ease; 80 | } 81 | 82 | h1 { 83 | font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", sans-serif; 84 | font-size: 34px; 85 | font-weight: 700; 86 | margin-bottom: 24px; 87 | } 88 | 89 | h2 { 90 | font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", sans-serif; 91 | font-size: 22px; 92 | font-weight: 600; 93 | margin-bottom: 16px; 94 | } 95 | 96 | .endpoint-section { 97 | margin-bottom: 32px; 98 | padding-bottom: 24px; 99 | border-bottom: 1px solid var(--border-color); 100 | } 101 | 102 | .endpoint-container { 103 | display: flex; 104 | gap: 24px; 105 | align-items: flex-start; 106 | } 107 | 108 | .endpoint-select-wrapper { 109 | flex: 0 0 auto; 110 | } 111 | 112 | .running-models-stats { 113 | flex: 1; 114 | background: var(--container-bg); 115 | border: 1px solid var(--border-color); 116 | border-radius: 12px; 117 | padding: 16px; 118 | max-width: 400px; 119 | } 120 | 121 | .running-models-stats h3 { 122 | font-size: 16px; 123 | font-weight: 600; 124 | margin: 0 0 12px 0; 125 | color: var(--text-color); 126 | } 127 | 128 | .running-model-item { 129 | padding: 12px; 130 | border: 1px solid var(--border-color); 131 | border-radius: 8px; 132 | margin-bottom: 8px; 133 | background: var(--hover-bg); 134 | } 135 | 136 | .running-model-item:last-child { 137 | margin-bottom: 0; 138 | } 139 | 140 | .model-stat { 141 | display: flex; 142 | justify-content: space-between; 143 | margin-bottom: 4px; 144 | font-size: 14px; 145 | } 146 | 147 | .model-stat:last-child { 148 | margin-bottom: 0; 149 | } 150 | 151 | .stat-label { 152 | color: var(--secondary-text); 153 | } 154 | 155 | .stat-value { 156 | color: var(--text-color); 157 | font-weight: 500; 158 | } 159 | 160 | .endpoint-select { 161 | width: 300px; 162 | padding: 12px 16px; 163 | margin-right: 12px; 164 | background-color: var(--input-bg); 165 | color: var(--input-text); 166 | border: none; 167 | border-radius: 10px; 168 | font-size: 16px; 169 | cursor: pointer; 170 | transition: all 0.2s ease; 171 | -webkit-appearance: none; 172 | appearance: none; 173 | background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2214%22%20height%3D%228%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M1%201l6%206%206-6%22%20stroke%3D%22%23999%22%20stroke-width%3D%222%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%2F%3E%3C%2Fsvg%3E"); 174 | background-repeat: no-repeat; 175 | background-position: right 16px center; 176 | padding-right: 40px; 177 | } 178 | 179 | .endpoint-select:focus { 180 | outline: none; 181 | box-shadow: 0 0 0 2px var(--button-bg); 182 | } 183 | 184 | .endpoint-select option { 185 | background-color: var(--select-bg); 186 | color: var(--input-text); 187 | padding: 12px; 188 | } 189 | 190 | .models-list { 191 | margin-top: 24px; 192 | border-radius: 12px; 193 | overflow: hidden; 194 | box-shadow: 0 2px 8px var(--shadow-color); 195 | } 196 | 197 | .models-header { 198 | display: flex; 199 | align-items: center; 200 | padding: 14px 16px; 201 | background-color: var(--header-bg); 202 | font-weight: 600; 203 | font-size: 14px; 204 | color: var(--secondary-text); 205 | position: sticky; 206 | top: 0; 207 | z-index: 1; 208 | } 209 | 210 | .models-header > div { 211 | cursor: pointer; 212 | display: flex; 213 | align-items: center; 214 | user-select: none; 215 | transition: color 0.2s ease; 216 | } 217 | 218 | .models-header > div:hover { 219 | color: var(--button-bg); 220 | } 221 | 222 | .sort-indicator::after { 223 | content: '↕'; 224 | margin-left: 6px; 225 | font-size: 12px; 226 | opacity: 0.5; 227 | } 228 | 229 | .sort-asc::after { 230 | content: '↑'; 231 | opacity: 1; 232 | } 233 | 234 | .sort-desc::after { 235 | content: '↓'; 236 | opacity: 1; 237 | } 238 | 239 | .model-item { 240 | display: flex; 241 | align-items: center; 242 | padding: 14px 16px; 243 | border-bottom: 1px solid var(--border-color); 244 | transition: background-color 0.2s ease; 245 | animation: fadeIn 0.3s ease; 246 | } 247 | 248 | @keyframes fadeIn { 249 | from { opacity: 0; transform: translateY(10px); } 250 | to { opacity: 1; transform: translateY(0); } 251 | } 252 | 253 | .model-item:hover { 254 | background-color: var(--hover-bg); 255 | } 256 | 257 | .checkbox-col { 258 | width: 24px; 259 | } 260 | 261 | .checkbox-col input[type="checkbox"] { 262 | width: 20px; 263 | height: 20px; 264 | border-radius: 6px; 265 | border: 2px solid var(--border-color); 266 | appearance: none; 267 | -webkit-appearance: none; 268 | outline: none; 269 | cursor: pointer; 270 | position: relative; 271 | transition: all 0.2s ease; 272 | } 273 | 274 | .checkbox-col input[type="checkbox"]:checked { 275 | background-color: var(--button-bg); 276 | border-color: var(--button-bg); 277 | } 278 | 279 | .checkbox-col input[type="checkbox"]:checked::after { 280 | content: ''; 281 | position: absolute; 282 | left: 6px; 283 | top: 2px; 284 | width: 4px; 285 | height: 9px; 286 | border: solid white; 287 | border-width: 0 2px 2px 0; 288 | transform: rotate(45deg); 289 | } 290 | 291 | .name-col { flex: 2; min-width: 150px; } 292 | .size-col { width: 100px; } 293 | .param-col { width: 80px; } 294 | .family-col { width: 100px; } 295 | .format-col { width: 80px; } 296 | .quant-col { width: 100px; } 297 | 298 | .actions-col { 299 | flex: 0.8; 300 | text-align: center; 301 | } 302 | 303 | .action-buttons { 304 | display: flex; 305 | gap: 8px; 306 | justify-content: center; 307 | } 308 | 309 | .action-buttons button { 310 | padding: 6px 12px; 311 | margin: 0; 312 | font-size: 14px; 313 | } 314 | 315 | .update-btn { 316 | background-color: var(--button-bg); 317 | } 318 | 319 | .update-btn:hover { 320 | background-color: var(--button-hover); 321 | } 322 | 323 | button { 324 | padding: 10px 20px; 325 | background-color: var(--button-bg); 326 | color: var(--button-text); 327 | border: none; 328 | border-radius: 10px; 329 | font-size: 15px; 330 | font-weight: 500; 331 | cursor: pointer; 332 | transition: all 0.2s ease; 333 | margin-right: 12px; 334 | margin-bottom: 16px; 335 | } 336 | 337 | button:hover { 338 | background-color: var(--button-hover); 339 | transform: translateY(-1px); 340 | } 341 | 342 | button:active { 343 | transform: translateY(0); 344 | } 345 | 346 | button:disabled { 347 | background-color: var(--border-color); 348 | cursor: not-allowed; 349 | transform: none; 350 | } 351 | 352 | .delete-btn { 353 | background-color: var(--delete-button-bg); 354 | } 355 | 356 | .delete-btn:hover { 357 | background-color: var(--delete-button-hover); 358 | } 359 | 360 | .update-status { 361 | font-size: 0.8em; 362 | color: var(--text-color); 363 | margin-top: 4px; 364 | } 365 | 366 | .filter-container { 367 | margin-bottom: 16px; 368 | } 369 | 370 | .filter-input { 371 | width: 100%; 372 | padding: 12px 16px; 373 | background-color: var(--input-bg); 374 | color: var(--input-text); 375 | border: none; 376 | border-radius: 10px; 377 | font-size: 16px; 378 | transition: all 0.2s ease; 379 | } 380 | 381 | .filter-input:focus { 382 | outline: none; 383 | box-shadow: 0 0 0 2px var(--button-bg); 384 | } 385 | 386 | .filter-input::placeholder { 387 | color: var(--secondary-text); 388 | } 389 | 390 | .api-docs-link { 391 | position: fixed; 392 | top: 20px; 393 | right: 80px; 394 | text-decoration: none; 395 | padding: 10px; 396 | border-radius: 8px; 397 | width: 44px; 398 | height: 44px; 399 | display: flex; 400 | align-items: center; 401 | justify-content: center; 402 | font-size: 12px; 403 | background-color: var(--container-bg); 404 | color: var(--text-color); 405 | box-shadow: 0 4px 12px var(--shadow-color); 406 | transition: all 0.3s ease; 407 | } 408 | 409 | .api-docs-link:hover { 410 | transform: scale(1.05); 411 | background-color: var(--container-bg); 412 | } 413 | 414 | .theme-toggle { 415 | position: fixed; 416 | top: 20px; 417 | right: 20px; 418 | padding: 10px; 419 | border-radius: 8px; 420 | width: 44px; 421 | height: 44px; 422 | display: flex; 423 | align-items: center; 424 | justify-content: center; 425 | font-size: 22px; 426 | background-color: var(--container-bg); 427 | color: var(--text-color); 428 | cursor: pointer; 429 | border: none; 430 | box-shadow: 0 4px 12px var(--shadow-color); 431 | transition: all 0.3s ease; 432 | } 433 | 434 | .theme-toggle:hover { 435 | transform: scale(1.05); 436 | background-color: var(--container-bg); 437 | } 438 | 439 | #endpointStatus { 440 | margin-top: 12px; 441 | padding: 12px 16px; 442 | border-radius: 10px; 443 | font-size: 15px; 444 | animation: slideIn 0.3s ease; 445 | } 446 | 447 | @keyframes slideIn { 448 | from { opacity: 0; transform: translateX(-10px); } 449 | to { opacity: 1; transform: translateX(0); } 450 | } 451 | 452 | .success { 453 | background-color: var(--success-bg); 454 | color: var(--success-text); 455 | } 456 | 457 | .error { 458 | background-color: var(--error-bg); 459 | color: var(--error-text); 460 | } 461 | 462 | .progress-text { 463 | font-size: 14px; 464 | color: var(--secondary-text); 465 | text-align: center; 466 | margin-top: 12px; 467 | } 468 | -------------------------------------------------------------------------------- /public/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "title": "Ollama Model Manager API", 5 | "description": "REST API for managing Ollama models and endpoints", 6 | "version": "1.0.0" 7 | }, 8 | "servers": [ 9 | { 10 | "url": "/", 11 | "description": "Current server (relative URL)" 12 | }, 13 | { 14 | "url": "{protocol}://{hostname}:{port}", 15 | "description": "Custom server URL", 16 | "variables": { 17 | "protocol": { 18 | "enum": ["http", "https"], 19 | "default": "http" 20 | }, 21 | "hostname": { 22 | "default": "localhost" 23 | }, 24 | "port": { 25 | "default": "3000" 26 | } 27 | } 28 | } 29 | ], 30 | "components": { 31 | "schemas": { 32 | "Error": { 33 | "type": "object", 34 | "properties": { 35 | "success": { 36 | "type": "boolean", 37 | "example": false 38 | }, 39 | "message": { 40 | "type": "string", 41 | "description": "Human readable error message" 42 | }, 43 | "error": { 44 | "type": "string", 45 | "description": "Detailed error information (optional)" 46 | } 47 | } 48 | }, 49 | "SuccessResponse": { 50 | "type": "object", 51 | "properties": { 52 | "success": { 53 | "type": "boolean", 54 | "example": true 55 | }, 56 | "message": { 57 | "type": "string" 58 | } 59 | } 60 | }, 61 | "ModelDetails": { 62 | "type": "object", 63 | "properties": { 64 | "parent_model": { 65 | "type": "string" 66 | }, 67 | "format": { 68 | "type": "string" 69 | }, 70 | "family": { 71 | "type": "string" 72 | }, 73 | "families": { 74 | "type": "array", 75 | "items": { 76 | "type": "string" 77 | } 78 | }, 79 | "parameter_size": { 80 | "type": "string" 81 | }, 82 | "quantization_level": { 83 | "type": "string" 84 | } 85 | } 86 | } 87 | } 88 | }, 89 | "paths": { 90 | "/api/endpoints": { 91 | "get": { 92 | "summary": "Get Available Ollama Endpoints", 93 | "description": "Returns a list of configured Ollama endpoints", 94 | "responses": { 95 | "200": { 96 | "description": "List of endpoints", 97 | "content": { 98 | "application/json": { 99 | "schema": { 100 | "type": "array", 101 | "items": { 102 | "type": "string" 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } 110 | }, 111 | "/api/set-endpoint": { 112 | "post": { 113 | "summary": "Set Active Ollama Endpoint", 114 | "description": "Sets and validates the active Ollama endpoint", 115 | "requestBody": { 116 | "required": true, 117 | "content": { 118 | "application/json": { 119 | "schema": { 120 | "type": "object", 121 | "properties": { 122 | "endpoint": { 123 | "type": "string", 124 | "example": "http://localhost:11434" 125 | } 126 | }, 127 | "required": ["endpoint"] 128 | } 129 | } 130 | } 131 | }, 132 | "responses": { 133 | "200": { 134 | "description": "Endpoint set successfully", 135 | "content": { 136 | "application/json": { 137 | "schema": { 138 | "$ref": "#/components/schemas/SuccessResponse" 139 | } 140 | } 141 | } 142 | }, 143 | "500": { 144 | "description": "Error setting endpoint", 145 | "content": { 146 | "application/json": { 147 | "schema": { 148 | "$ref": "#/components/schemas/Error" 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | }, 156 | "/api/ps": { 157 | "get": { 158 | "summary": "Get Running Models", 159 | "description": "Returns a list of currently running Ollama models", 160 | "responses": { 161 | "200": { 162 | "description": "List of running models", 163 | "content": { 164 | "application/json": { 165 | "schema": { 166 | "type": "object", 167 | "description": "Response format matches Ollama's /api/ps endpoint" 168 | } 169 | } 170 | } 171 | }, 172 | "500": { 173 | "description": "Error fetching running models", 174 | "content": { 175 | "application/json": { 176 | "schema": { 177 | "$ref": "#/components/schemas/Error" 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | }, 185 | "/api/models": { 186 | "get": { 187 | "summary": "Get Available Models", 188 | "description": "Returns a list of available models with their details", 189 | "responses": { 190 | "200": { 191 | "description": "List of models", 192 | "content": { 193 | "application/json": { 194 | "schema": { 195 | "type": "array", 196 | "items": { 197 | "type": "object", 198 | "properties": { 199 | "name": { 200 | "type": "string" 201 | }, 202 | "size": { 203 | "type": "integer" 204 | }, 205 | "details": { 206 | "$ref": "#/components/schemas/ModelDetails" 207 | } 208 | } 209 | } 210 | } 211 | } 212 | } 213 | }, 214 | "500": { 215 | "description": "Error fetching models", 216 | "content": { 217 | "application/json": { 218 | "schema": { 219 | "$ref": "#/components/schemas/Error" 220 | } 221 | } 222 | } 223 | } 224 | } 225 | }, 226 | "delete": { 227 | "summary": "Delete Models", 228 | "description": "Deletes one or more models from Ollama", 229 | "requestBody": { 230 | "required": true, 231 | "content": { 232 | "application/json": { 233 | "schema": { 234 | "type": "object", 235 | "properties": { 236 | "models": { 237 | "type": "array", 238 | "items": { 239 | "type": "string" 240 | } 241 | } 242 | }, 243 | "required": ["models"] 244 | } 245 | } 246 | } 247 | }, 248 | "responses": { 249 | "200": { 250 | "description": "Models deleted successfully", 251 | "content": { 252 | "application/json": { 253 | "schema": { 254 | "$ref": "#/components/schemas/SuccessResponse" 255 | } 256 | } 257 | } 258 | }, 259 | "500": { 260 | "description": "Error deleting models", 261 | "content": { 262 | "application/json": { 263 | "schema": { 264 | "$ref": "#/components/schemas/Error" 265 | } 266 | } 267 | } 268 | } 269 | } 270 | } 271 | }, 272 | "/api/pull": { 273 | "post": { 274 | "summary": "Pull Model", 275 | "description": "Pulls a new model from Ollama. Returns a streaming response with progress updates", 276 | "requestBody": { 277 | "required": true, 278 | "content": { 279 | "application/json": { 280 | "schema": { 281 | "type": "object", 282 | "properties": { 283 | "model": { 284 | "type": "string" 285 | } 286 | }, 287 | "required": ["model"] 288 | } 289 | } 290 | } 291 | }, 292 | "responses": { 293 | "200": { 294 | "description": "Streaming response with progress updates", 295 | "content": { 296 | "application/x-ndjson": { 297 | "schema": { 298 | "type": "object", 299 | "properties": { 300 | "status": { 301 | "type": "string", 302 | "enum": ["downloading", "verifying digest", "writing manifest", "error"] 303 | }, 304 | "completed": { 305 | "type": "integer" 306 | }, 307 | "total": { 308 | "type": "integer" 309 | }, 310 | "error": { 311 | "type": "string" 312 | } 313 | } 314 | } 315 | } 316 | } 317 | }, 318 | "400": { 319 | "description": "Invalid request", 320 | "content": { 321 | "application/json": { 322 | "schema": { 323 | "$ref": "#/components/schemas/Error" 324 | } 325 | } 326 | } 327 | } 328 | } 329 | } 330 | }, 331 | "/api/update-model": { 332 | "post": { 333 | "summary": "Update Model", 334 | "description": "Updates an existing model. Returns a streaming response with progress updates", 335 | "requestBody": { 336 | "required": true, 337 | "content": { 338 | "application/json": { 339 | "schema": { 340 | "type": "object", 341 | "properties": { 342 | "modelName": { 343 | "type": "string" 344 | } 345 | }, 346 | "required": ["modelName"] 347 | } 348 | } 349 | } 350 | }, 351 | "responses": { 352 | "200": { 353 | "description": "Streaming response with progress updates", 354 | "content": { 355 | "application/x-ndjson": { 356 | "schema": { 357 | "type": "object", 358 | "properties": { 359 | "status": { 360 | "type": "string", 361 | "enum": ["downloading", "verifying digest", "writing manifest", "error"] 362 | }, 363 | "completed": { 364 | "type": "integer" 365 | }, 366 | "total": { 367 | "type": "integer" 368 | }, 369 | "error": { 370 | "type": "string" 371 | } 372 | } 373 | } 374 | } 375 | } 376 | }, 377 | "400": { 378 | "description": "Invalid request", 379 | "content": { 380 | "application/json": { 381 | "schema": { 382 | "$ref": "#/components/schemas/Error" 383 | } 384 | } 385 | } 386 | } 387 | } 388 | } 389 | } 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ollama Model Manager 7 | 8 | 9 | 10 | 13 | 14 | 15 | API Docs 16 | 17 | 18 |
19 |

Ollama Model Manager

20 | 21 |
22 |

Ollama Endpoints

23 |
24 |
25 | 28 |
29 |
30 |
31 |

Running Models

32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 | 40 | 41 |
42 | 43 |
44 |

Available Models

45 | 46 | 47 | 48 |
49 | 50 |
51 |
52 |
53 |
54 | 55 |
56 |
Name
57 |
Size
58 |
Params
59 |
Family
60 |
Format
61 |
Q-Level
62 |
Actions
63 |
64 |
65 |
66 |
67 |
68 | 69 | 596 | 597 | 598 | --------------------------------------------------------------------------------