├── 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 |
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 |
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 |
66 |
67 |
68 |
69 |
596 |
597 |
598 |
--------------------------------------------------------------------------------