forked from open-webui/open-webui
		
	feat: enable backend ollama url update
This commit is contained in:
		
							parent
							
								
									bdb3f50771
								
							
						
					
					
						commit
						30aff2db53
					
				
					 13 changed files with 554 additions and 409 deletions
				
			
		|  | @ -2,12 +2,6 @@ | ||||||
| 
 | 
 | ||||||
| FROM node:alpine as build | FROM node:alpine as build | ||||||
| 
 | 
 | ||||||
| ARG OLLAMA_API_BASE_URL='/ollama/api' |  | ||||||
| RUN echo $OLLAMA_API_BASE_URL |  | ||||||
| 
 |  | ||||||
| ENV PUBLIC_API_BASE_URL $OLLAMA_API_BASE_URL |  | ||||||
| RUN echo $PUBLIC_API_BASE_URL |  | ||||||
| 
 |  | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
| 
 | 
 | ||||||
| COPY package.json package-lock.json ./  | COPY package.json package-lock.json ./  | ||||||
|  |  | ||||||
|  | @ -1,69 +1,68 @@ | ||||||
| from flask import Flask, request, Response, jsonify | from fastapi import FastAPI, Request, Response, HTTPException, Depends | ||||||
| from flask_cors import CORS | from fastapi.middleware.cors import CORSMiddleware | ||||||
| 
 | from fastapi.responses import StreamingResponse | ||||||
| 
 | 
 | ||||||
| import requests | import requests | ||||||
| import json | import json | ||||||
| 
 | from pydantic import BaseModel | ||||||
| 
 | 
 | ||||||
| from apps.web.models.users import Users | from apps.web.models.users import Users | ||||||
| from constants import ERROR_MESSAGES | from constants import ERROR_MESSAGES | ||||||
| from utils.utils import decode_token | from utils.utils import decode_token, get_current_user | ||||||
| from config import OLLAMA_API_BASE_URL, WEBUI_AUTH | from config import OLLAMA_API_BASE_URL, WEBUI_AUTH | ||||||
| 
 | 
 | ||||||
| app = Flask(__name__) | app = FastAPI() | ||||||
| CORS( | app.add_middleware( | ||||||
|     app |     CORSMiddleware, | ||||||
| )  # Enable Cross-Origin Resource Sharing (CORS) to allow requests from different domains |     allow_origins=["*"], | ||||||
|  |     allow_credentials=True, | ||||||
|  |     allow_methods=["*"], | ||||||
|  |     allow_headers=["*"], | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| # Define the target server URL | app.state.OLLAMA_API_BASE_URL = OLLAMA_API_BASE_URL | ||||||
| TARGET_SERVER_URL = OLLAMA_API_BASE_URL | 
 | ||||||
|  | # TARGET_SERVER_URL = OLLAMA_API_BASE_URL | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.route("/", defaults={"path": ""}, methods=["GET", "POST", "PUT", "DELETE"]) | @app.get("/url") | ||||||
| @app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE"]) | async def get_ollama_api_url(user=Depends(get_current_user)): | ||||||
| def proxy(path): |     if user and user.role == "admin": | ||||||
|     # Combine the base URL of the target server with the requested path |         return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} | ||||||
|     target_url = f"{TARGET_SERVER_URL}/{path}" |     else: | ||||||
|     print(target_url) |         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) | ||||||
| 
 | 
 | ||||||
|     # Get data from the original request | 
 | ||||||
|     data = request.get_data() | class UrlUpdateForm(BaseModel): | ||||||
|  |     url: str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.post("/url/update") | ||||||
|  | async def update_ollama_api_url( | ||||||
|  |     form_data: UrlUpdateForm, user=Depends(get_current_user) | ||||||
|  | ): | ||||||
|  |     if user and user.role == "admin": | ||||||
|  |         app.state.OLLAMA_API_BASE_URL = form_data.url | ||||||
|  |         return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} | ||||||
|  |     else: | ||||||
|  |         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) | ||||||
|  | async def proxy(path: str, request: Request, user=Depends(get_current_user)): | ||||||
|  |     target_url = f"{app.state.OLLAMA_API_BASE_URL}/{path}" | ||||||
|  | 
 | ||||||
|  |     body = await request.body() | ||||||
|     headers = dict(request.headers) |     headers = dict(request.headers) | ||||||
| 
 | 
 | ||||||
|     # Basic RBAC support |     if user.role in ["user", "admin"]: | ||||||
|     if WEBUI_AUTH: |         if path in ["pull", "delete", "push", "copy", "create"]: | ||||||
|         if "Authorization" in headers: |             if user.role != "admin": | ||||||
|             _, credentials = headers["Authorization"].split() |                 raise HTTPException( | ||||||
|             token_data = decode_token(credentials) |                     status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED | ||||||
|             if token_data is None or "email" not in token_data: |                 ) | ||||||
|                 return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 |  | ||||||
| 
 |  | ||||||
|             user = Users.get_user_by_email(token_data["email"]) |  | ||||||
|             if user: |  | ||||||
|                 # Only user and admin roles can access |  | ||||||
|                 if user.role in ["user", "admin"]: |  | ||||||
|                     if path in ["pull", "delete", "push", "copy", "create"]: |  | ||||||
|                         # Only admin role can perform actions above |  | ||||||
|                         if user.role == "admin": |  | ||||||
|                             pass |  | ||||||
|                         else: |  | ||||||
|                             return ( |  | ||||||
|                                 jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), |  | ||||||
|                                 401, |  | ||||||
|                             ) |  | ||||||
|                     else: |  | ||||||
|                         pass |  | ||||||
|                 else: |  | ||||||
|                     return jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), 401 |  | ||||||
|             else: |  | ||||||
|                 return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 |  | ||||||
|         else: |  | ||||||
|             return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 |  | ||||||
|     else: |     else: | ||||||
|         pass |         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) | ||||||
| 
 |  | ||||||
|     r = None |  | ||||||
| 
 | 
 | ||||||
|     headers.pop("Host", None) |     headers.pop("Host", None) | ||||||
|     headers.pop("Authorization", None) |     headers.pop("Authorization", None) | ||||||
|  | @ -71,49 +70,30 @@ def proxy(path): | ||||||
|     headers.pop("Referer", None) |     headers.pop("Referer", None) | ||||||
| 
 | 
 | ||||||
|     try: |     try: | ||||||
|         # Make a request to the target server |  | ||||||
|         r = requests.request( |         r = requests.request( | ||||||
|             method=request.method, |             method=request.method, | ||||||
|             url=target_url, |             url=target_url, | ||||||
|             data=data, |             data=body, | ||||||
|             headers=headers, |             headers=headers, | ||||||
|             stream=True,  # Enable streaming for server-sent events |             stream=True, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         r.raise_for_status() |         r.raise_for_status() | ||||||
| 
 | 
 | ||||||
|         # Proxy the target server's response to the client |         return StreamingResponse( | ||||||
|         def generate(): |             r.iter_content(chunk_size=8192), | ||||||
|             for chunk in r.iter_content(chunk_size=8192): |             status_code=r.status_code, | ||||||
|                 yield chunk |             headers=dict(r.headers), | ||||||
| 
 |         ) | ||||||
|         response = Response(generate(), status=r.status_code) |  | ||||||
| 
 |  | ||||||
|         # Copy headers from the target server's response to the client's response |  | ||||||
|         for key, value in r.headers.items(): |  | ||||||
|             response.headers[key] = value |  | ||||||
| 
 |  | ||||||
|         return response |  | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         print(e) |         print(e) | ||||||
|         error_detail = "Ollama WebUI: Server Connection Error" |         error_detail = "Ollama WebUI: Server Connection Error" | ||||||
|         if r != None: |         if r is not None: | ||||||
|             print(r.text) |             try: | ||||||
|             res = r.json() |                 res = r.json() | ||||||
|             if "error" in res: |                 if "error" in res: | ||||||
|                 error_detail = f"Ollama: {res['error']}" |                     error_detail = f"Ollama: {res['error']}" | ||||||
|             print(res) |             except: | ||||||
|  |                 error_detail = f"Ollama: {e}" | ||||||
| 
 | 
 | ||||||
|         return ( |         raise HTTPException(status_code=r.status_code, detail=error_detail) | ||||||
|             jsonify( |  | ||||||
|                 { |  | ||||||
|                     "detail": error_detail, |  | ||||||
|                     "message": str(e), |  | ||||||
|                 } |  | ||||||
|             ), |  | ||||||
|             400, |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| if __name__ == "__main__": |  | ||||||
|     app.run(debug=True) |  | ||||||
|  |  | ||||||
							
								
								
									
										176
									
								
								backend/apps/ollama/old_main.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								backend/apps/ollama/old_main.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,176 @@ | ||||||
|  | from flask import Flask, request, Response, jsonify | ||||||
|  | from flask_cors import CORS | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | import requests | ||||||
|  | import json | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from apps.web.models.users import Users | ||||||
|  | from constants import ERROR_MESSAGES | ||||||
|  | from utils.utils import decode_token | ||||||
|  | from config import OLLAMA_API_BASE_URL, WEBUI_AUTH | ||||||
|  | 
 | ||||||
|  | app = Flask(__name__) | ||||||
|  | CORS( | ||||||
|  |     app | ||||||
|  | )  # Enable Cross-Origin Resource Sharing (CORS) to allow requests from different domains | ||||||
|  | 
 | ||||||
|  | # Define the target server URL | ||||||
|  | TARGET_SERVER_URL = OLLAMA_API_BASE_URL | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.route("/url", methods=["GET"]) | ||||||
|  | def get_ollama_api_url(): | ||||||
|  |     headers = dict(request.headers) | ||||||
|  |     if "Authorization" in headers: | ||||||
|  |         _, credentials = headers["Authorization"].split() | ||||||
|  |         token_data = decode_token(credentials) | ||||||
|  |         if token_data is None or "email" not in token_data: | ||||||
|  |             return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 | ||||||
|  | 
 | ||||||
|  |         user = Users.get_user_by_email(token_data["email"]) | ||||||
|  |         if user and user.role == "admin": | ||||||
|  |             return ( | ||||||
|  |                 jsonify({"OLLAMA_API_BASE_URL": TARGET_SERVER_URL}), | ||||||
|  |                 200, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             return ( | ||||||
|  |                 jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), | ||||||
|  |                 401, | ||||||
|  |             ) | ||||||
|  |     else: | ||||||
|  |         return ( | ||||||
|  |             jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), | ||||||
|  |             401, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.route("/url/update", methods=["POST"]) | ||||||
|  | def update_ollama_api_url(): | ||||||
|  |     headers = dict(request.headers) | ||||||
|  |     data = request.get_json(force=True) | ||||||
|  | 
 | ||||||
|  |     if "Authorization" in headers: | ||||||
|  |         _, credentials = headers["Authorization"].split() | ||||||
|  |         token_data = decode_token(credentials) | ||||||
|  |         if token_data is None or "email" not in token_data: | ||||||
|  |             return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 | ||||||
|  | 
 | ||||||
|  |         user = Users.get_user_by_email(token_data["email"]) | ||||||
|  |         if user and user.role == "admin": | ||||||
|  |             TARGET_SERVER_URL = data["url"] | ||||||
|  |             return ( | ||||||
|  |                 jsonify({"OLLAMA_API_BASE_URL": TARGET_SERVER_URL}), | ||||||
|  |                 200, | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             return ( | ||||||
|  |                 jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), | ||||||
|  |                 401, | ||||||
|  |             ) | ||||||
|  |     else: | ||||||
|  |         return ( | ||||||
|  |             jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), | ||||||
|  |             401, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.route("/", defaults={"path": ""}, methods=["GET", "POST", "PUT", "DELETE"]) | ||||||
|  | @app.route("/<path:path>", methods=["GET", "POST", "PUT", "DELETE"]) | ||||||
|  | def proxy(path): | ||||||
|  |     # Combine the base URL of the target server with the requested path | ||||||
|  |     target_url = f"{TARGET_SERVER_URL}/{path}" | ||||||
|  |     print(target_url) | ||||||
|  | 
 | ||||||
|  |     # Get data from the original request | ||||||
|  |     data = request.get_data() | ||||||
|  |     headers = dict(request.headers) | ||||||
|  | 
 | ||||||
|  |     # Basic RBAC support | ||||||
|  |     if WEBUI_AUTH: | ||||||
|  |         if "Authorization" in headers: | ||||||
|  |             _, credentials = headers["Authorization"].split() | ||||||
|  |             token_data = decode_token(credentials) | ||||||
|  |             if token_data is None or "email" not in token_data: | ||||||
|  |                 return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 | ||||||
|  | 
 | ||||||
|  |             user = Users.get_user_by_email(token_data["email"]) | ||||||
|  |             if user: | ||||||
|  |                 # Only user and admin roles can access | ||||||
|  |                 if user.role in ["user", "admin"]: | ||||||
|  |                     if path in ["pull", "delete", "push", "copy", "create"]: | ||||||
|  |                         # Only admin role can perform actions above | ||||||
|  |                         if user.role == "admin": | ||||||
|  |                             pass | ||||||
|  |                         else: | ||||||
|  |                             return ( | ||||||
|  |                                 jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), | ||||||
|  |                                 401, | ||||||
|  |                             ) | ||||||
|  |                     else: | ||||||
|  |                         pass | ||||||
|  |                 else: | ||||||
|  |                     return jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), 401 | ||||||
|  |             else: | ||||||
|  |                 return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 | ||||||
|  |         else: | ||||||
|  |             return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 | ||||||
|  |     else: | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     r = None | ||||||
|  | 
 | ||||||
|  |     headers.pop("Host", None) | ||||||
|  |     headers.pop("Authorization", None) | ||||||
|  |     headers.pop("Origin", None) | ||||||
|  |     headers.pop("Referer", None) | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         # Make a request to the target server | ||||||
|  |         r = requests.request( | ||||||
|  |             method=request.method, | ||||||
|  |             url=target_url, | ||||||
|  |             data=data, | ||||||
|  |             headers=headers, | ||||||
|  |             stream=True,  # Enable streaming for server-sent events | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         r.raise_for_status() | ||||||
|  | 
 | ||||||
|  |         # Proxy the target server's response to the client | ||||||
|  |         def generate(): | ||||||
|  |             for chunk in r.iter_content(chunk_size=8192): | ||||||
|  |                 yield chunk | ||||||
|  | 
 | ||||||
|  |         response = Response(generate(), status=r.status_code) | ||||||
|  | 
 | ||||||
|  |         # Copy headers from the target server's response to the client's response | ||||||
|  |         for key, value in r.headers.items(): | ||||||
|  |             response.headers[key] = value | ||||||
|  | 
 | ||||||
|  |         return response | ||||||
|  |     except Exception as e: | ||||||
|  |         print(e) | ||||||
|  |         error_detail = "Ollama WebUI: Server Connection Error" | ||||||
|  |         if r != None: | ||||||
|  |             print(r.text) | ||||||
|  |             res = r.json() | ||||||
|  |             if "error" in res: | ||||||
|  |                 error_detail = f"Ollama: {res['error']}" | ||||||
|  |             print(res) | ||||||
|  | 
 | ||||||
|  |         return ( | ||||||
|  |             jsonify( | ||||||
|  |                 { | ||||||
|  |                     "detail": error_detail, | ||||||
|  |                     "message": str(e), | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |             400, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     app.run(debug=True) | ||||||
|  | @ -46,5 +46,7 @@ async def check_url(request: Request, call_next): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| app.mount("/api/v1", webui_app) | app.mount("/api/v1", webui_app) | ||||||
| app.mount("/ollama/api", WSGIMiddleware(ollama_app)) | # app.mount("/ollama/api", WSGIMiddleware(ollama_app)) | ||||||
|  | app.mount("/ollama/api", ollama_app) | ||||||
|  | 
 | ||||||
| app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files") | app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files") | ||||||
|  |  | ||||||
|  | @ -1,12 +1,76 @@ | ||||||
| import { OLLAMA_API_BASE_URL } from '$lib/constants'; | import { OLLAMA_API_BASE_URL } from '$lib/constants'; | ||||||
| 
 | 
 | ||||||
| export const getOllamaVersion = async ( | export const getOllamaAPIUrl = async (token: string = '') => { | ||||||
| 	base_url: string = OLLAMA_API_BASE_URL, |  | ||||||
| 	token: string = '' |  | ||||||
| ) => { |  | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${base_url}/version`, { | 	const res = await fetch(`${OLLAMA_API_BASE_URL}/url`, { | ||||||
|  | 		method: 'GET', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			...(token && { authorization: `Bearer ${token}` }) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			console.log(err); | ||||||
|  | 			if ('detail' in err) { | ||||||
|  | 				error = err.detail; | ||||||
|  | 			} else { | ||||||
|  | 				error = 'Server connection failed'; | ||||||
|  | 			} | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res.OLLAMA_API_BASE_URL; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const updateOllamaAPIUrl = async (token: string = '', url: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${OLLAMA_API_BASE_URL}/url/update`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			...(token && { authorization: `Bearer ${token}` }) | ||||||
|  | 		}, | ||||||
|  | 		body: JSON.stringify({ | ||||||
|  | 			url: url | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			console.log(err); | ||||||
|  | 			if ('detail' in err) { | ||||||
|  | 				error = err.detail; | ||||||
|  | 			} else { | ||||||
|  | 				error = 'Server connection failed'; | ||||||
|  | 			} | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res.OLLAMA_API_BASE_URL; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const getOllamaVersion = async (token: string = '') => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${OLLAMA_API_BASE_URL}/version`, { | ||||||
| 		method: 'GET', | 		method: 'GET', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			Accept: 'application/json', | 			Accept: 'application/json', | ||||||
|  | @ -35,13 +99,10 @@ export const getOllamaVersion = async ( | ||||||
| 	return res?.version ?? ''; | 	return res?.version ?? ''; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const getOllamaModels = async ( | export const getOllamaModels = async (token: string = '') => { | ||||||
| 	base_url: string = OLLAMA_API_BASE_URL, |  | ||||||
| 	token: string = '' |  | ||||||
| ) => { |  | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${base_url}/tags`, { | 	const res = await fetch(`${OLLAMA_API_BASE_URL}/tags`, { | ||||||
| 		method: 'GET', | 		method: 'GET', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			Accept: 'application/json', | 			Accept: 'application/json', | ||||||
|  | @ -72,15 +133,10 @@ export const getOllamaModels = async ( | ||||||
| 	}); | 	}); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const generateTitle = async ( | export const generateTitle = async (token: string = '', model: string, prompt: string) => { | ||||||
| 	base_url: string = OLLAMA_API_BASE_URL, |  | ||||||
| 	token: string = '', |  | ||||||
| 	model: string, |  | ||||||
| 	prompt: string |  | ||||||
| ) => { |  | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${base_url}/generate`, { | 	const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, { | ||||||
| 		method: 'POST', | 		method: 'POST', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			'Content-Type': 'text/event-stream', | 			'Content-Type': 'text/event-stream', | ||||||
|  | @ -111,14 +167,10 @@ export const generateTitle = async ( | ||||||
| 	return res?.response ?? 'New Chat'; | 	return res?.response ?? 'New Chat'; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const generateChatCompletion = async ( | export const generateChatCompletion = async (token: string = '', body: object) => { | ||||||
| 	base_url: string = OLLAMA_API_BASE_URL, |  | ||||||
| 	token: string = '', |  | ||||||
| 	body: object |  | ||||||
| ) => { |  | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${base_url}/chat`, { | 	const res = await fetch(`${OLLAMA_API_BASE_URL}/chat`, { | ||||||
| 		method: 'POST', | 		method: 'POST', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			'Content-Type': 'text/event-stream', | 			'Content-Type': 'text/event-stream', | ||||||
|  | @ -137,15 +189,10 @@ export const generateChatCompletion = async ( | ||||||
| 	return res; | 	return res; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const createModel = async ( | export const createModel = async (token: string, tagName: string, content: string) => { | ||||||
| 	base_url: string = OLLAMA_API_BASE_URL, |  | ||||||
| 	token: string, |  | ||||||
| 	tagName: string, |  | ||||||
| 	content: string |  | ||||||
| ) => { |  | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${base_url}/create`, { | 	const res = await fetch(`${OLLAMA_API_BASE_URL}/create`, { | ||||||
| 		method: 'POST', | 		method: 'POST', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			'Content-Type': 'text/event-stream', | 			'Content-Type': 'text/event-stream', | ||||||
|  | @ -167,14 +214,10 @@ export const createModel = async ( | ||||||
| 	return res; | 	return res; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const deleteModel = async ( | export const deleteModel = async (token: string, tagName: string) => { | ||||||
| 	base_url: string = OLLAMA_API_BASE_URL, |  | ||||||
| 	token: string, |  | ||||||
| 	tagName: string |  | ||||||
| ) => { |  | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${base_url}/delete`, { | 	const res = await fetch(`${OLLAMA_API_BASE_URL}/delete`, { | ||||||
| 		method: 'DELETE', | 		method: 'DELETE', | ||||||
| 		headers: { | 		headers: { | ||||||
| 			'Content-Type': 'text/event-stream', | 			'Content-Type': 'text/event-stream', | ||||||
|  | @ -204,3 +247,27 @@ export const deleteModel = async ( | ||||||
| 
 | 
 | ||||||
| 	return res; | 	return res; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const pullModel = async (token: string, tagName: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${OLLAMA_API_BASE_URL}/pull`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			'Content-Type': 'text/event-stream', | ||||||
|  | 			Authorization: `Bearer ${token}` | ||||||
|  | 		}, | ||||||
|  | 		body: JSON.stringify({ | ||||||
|  | 			name: tagName | ||||||
|  | 		}) | ||||||
|  | 	}).catch((err) => { | ||||||
|  | 		error = err; | ||||||
|  | 		return null; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -7,19 +7,23 @@ | ||||||
| 	import { config, models, settings, user, chats } from '$lib/stores'; | 	import { config, models, settings, user, chats } from '$lib/stores'; | ||||||
| 	import { splitStream, getGravatarURL } from '$lib/utils'; | 	import { splitStream, getGravatarURL } from '$lib/utils'; | ||||||
| 
 | 
 | ||||||
| 	import { getOllamaVersion, getOllamaModels } from '$lib/apis/ollama'; |  | ||||||
| 	import { createNewChat, deleteAllChats, getAllChats, getChatList } from '$lib/apis/chats'; |  | ||||||
| 	import { | 	import { | ||||||
| 		WEB_UI_VERSION, | 		getOllamaVersion, | ||||||
| 		OLLAMA_API_BASE_URL, | 		getOllamaModels, | ||||||
| 		WEBUI_API_BASE_URL, | 		getOllamaAPIUrl, | ||||||
| 		WEBUI_BASE_URL | 		updateOllamaAPIUrl, | ||||||
| 	} from '$lib/constants'; | 		pullModel, | ||||||
|  | 		createModel, | ||||||
|  | 		deleteModel | ||||||
|  | 	} from '$lib/apis/ollama'; | ||||||
|  | 	import { createNewChat, deleteAllChats, getAllChats, getChatList } from '$lib/apis/chats'; | ||||||
|  | 	import { WEB_UI_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; | ||||||
| 
 | 
 | ||||||
| 	import Advanced from './Settings/Advanced.svelte'; | 	import Advanced from './Settings/Advanced.svelte'; | ||||||
| 	import Modal from '../common/Modal.svelte'; | 	import Modal from '../common/Modal.svelte'; | ||||||
| 	import { updateUserPassword } from '$lib/apis/auths'; | 	import { updateUserPassword } from '$lib/apis/auths'; | ||||||
| 	import { goto } from '$app/navigation'; | 	import { goto } from '$app/navigation'; | ||||||
|  | 	import Page from '../../../routes/(app)/+page.svelte'; | ||||||
| 
 | 
 | ||||||
| 	export let show = false; | 	export let show = false; | ||||||
| 
 | 
 | ||||||
|  | @ -33,7 +37,7 @@ | ||||||
| 	let selectedTab = 'general'; | 	let selectedTab = 'general'; | ||||||
| 
 | 
 | ||||||
| 	// General | 	// General | ||||||
| 	let API_BASE_URL = OLLAMA_API_BASE_URL; | 	let API_BASE_URL = ''; | ||||||
| 	let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light']; | 	let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light']; | ||||||
| 	let theme = 'dark'; | 	let theme = 'dark'; | ||||||
| 	let notificationEnabled = false; | 	let notificationEnabled = false; | ||||||
|  | @ -139,19 +143,13 @@ | ||||||
| 	// About | 	// About | ||||||
| 	let ollamaVersion = ''; | 	let ollamaVersion = ''; | ||||||
| 
 | 
 | ||||||
| 	const checkOllamaConnection = async () => { | 	const updateOllamaAPIUrlHandler = async () => { | ||||||
| 		if (API_BASE_URL === '') { | 		API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL); | ||||||
| 			API_BASE_URL = OLLAMA_API_BASE_URL; | 		const _models = await getModels('ollama'); | ||||||
| 		} |  | ||||||
| 		const _models = await getModels(API_BASE_URL, 'ollama'); |  | ||||||
| 
 | 
 | ||||||
| 		if (_models.length > 0) { | 		if (_models.length > 0) { | ||||||
| 			toast.success('Server connection verified'); | 			toast.success('Server connection verified'); | ||||||
| 			await models.set(_models); | 			await models.set(_models); | ||||||
| 
 |  | ||||||
| 			saveSettings({ |  | ||||||
| 				API_BASE_URL: API_BASE_URL |  | ||||||
| 			}); |  | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | @ -229,67 +227,60 @@ | ||||||
| 
 | 
 | ||||||
| 	const pullModelHandler = async () => { | 	const pullModelHandler = async () => { | ||||||
| 		modelTransferring = true; | 		modelTransferring = true; | ||||||
| 		const res = await fetch(`${API_BASE_URL}/pull`, { |  | ||||||
| 			method: 'POST', |  | ||||||
| 			headers: { |  | ||||||
| 				'Content-Type': 'text/event-stream', |  | ||||||
| 				...($settings.authHeader && { Authorization: $settings.authHeader }), |  | ||||||
| 				...($user && { Authorization: `Bearer ${localStorage.token}` }) |  | ||||||
| 			}, |  | ||||||
| 			body: JSON.stringify({ |  | ||||||
| 				name: modelTag |  | ||||||
| 			}) |  | ||||||
| 		}); |  | ||||||
| 
 | 
 | ||||||
| 		const reader = res.body | 		const res = await pullModel(localStorage.token, modelTag); | ||||||
| 			.pipeThrough(new TextDecoderStream()) |  | ||||||
| 			.pipeThrough(splitStream('\n')) |  | ||||||
| 			.getReader(); |  | ||||||
| 
 | 
 | ||||||
| 		while (true) { | 		if (res) { | ||||||
| 			const { value, done } = await reader.read(); | 			const reader = res.body | ||||||
| 			if (done) break; | 				.pipeThrough(new TextDecoderStream()) | ||||||
|  | 				.pipeThrough(splitStream('\n')) | ||||||
|  | 				.getReader(); | ||||||
| 
 | 
 | ||||||
| 			try { | 			while (true) { | ||||||
| 				let lines = value.split('\n'); | 				const { value, done } = await reader.read(); | ||||||
|  | 				if (done) break; | ||||||
| 
 | 
 | ||||||
| 				for (const line of lines) { | 				try { | ||||||
| 					if (line !== '') { | 					let lines = value.split('\n'); | ||||||
| 						console.log(line); |  | ||||||
| 						let data = JSON.parse(line); |  | ||||||
| 						console.log(data); |  | ||||||
| 
 | 
 | ||||||
| 						if (data.error) { | 					for (const line of lines) { | ||||||
| 							throw data.error; | 						if (line !== '') { | ||||||
| 						} | 							console.log(line); | ||||||
|  | 							let data = JSON.parse(line); | ||||||
|  | 							console.log(data); | ||||||
| 
 | 
 | ||||||
| 						if (data.detail) { | 							if (data.error) { | ||||||
| 							throw data.detail; | 								throw data.error; | ||||||
| 						} | 							} | ||||||
| 						if (data.status) { |  | ||||||
| 							if (!data.digest) { |  | ||||||
| 								toast.success(data.status); |  | ||||||
| 
 | 
 | ||||||
| 								if (data.status === 'success') { | 							if (data.detail) { | ||||||
| 									const notification = new Notification(`Ollama`, { | 								throw data.detail; | ||||||
| 										body: `Model '${modelTag}' has been successfully downloaded.`, | 							} | ||||||
| 										icon: '/favicon.png' | 							if (data.status) { | ||||||
| 									}); | 								if (!data.digest) { | ||||||
| 								} | 									toast.success(data.status); | ||||||
| 							} else { | 
 | ||||||
| 								digest = data.digest; | 									if (data.status === 'success') { | ||||||
| 								if (data.completed) { | 										const notification = new Notification(`Ollama`, { | ||||||
| 									pullProgress = Math.round((data.completed / data.total) * 1000) / 10; | 											body: `Model '${modelTag}' has been successfully downloaded.`, | ||||||
|  | 											icon: '/favicon.png' | ||||||
|  | 										}); | ||||||
|  | 									} | ||||||
| 								} else { | 								} else { | ||||||
| 									pullProgress = 100; | 									digest = data.digest; | ||||||
|  | 									if (data.completed) { | ||||||
|  | 										pullProgress = Math.round((data.completed / data.total) * 1000) / 10; | ||||||
|  | 									} else { | ||||||
|  | 										pullProgress = 100; | ||||||
|  | 									} | ||||||
| 								} | 								} | ||||||
| 							} | 							} | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
|  | 				} catch (error) { | ||||||
|  | 					console.log(error); | ||||||
|  | 					toast.error(error); | ||||||
| 				} | 				} | ||||||
| 			} catch (error) { |  | ||||||
| 				console.log(error); |  | ||||||
| 				toast.error(error); |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -410,21 +401,11 @@ | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (uploaded) { | 		if (uploaded) { | ||||||
| 			const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/create`, { | 			const res = await createModel( | ||||||
| 				method: 'POST', | 				localStorage.token, | ||||||
| 				headers: { | 				`${name}:latest`, | ||||||
| 					'Content-Type': 'text/event-stream', | 				`FROM @${modelFileDigest}\n${modelFileContent}` | ||||||
| 					...($settings.authHeader && { Authorization: $settings.authHeader }), | 			); | ||||||
| 					...($user && { Authorization: `Bearer ${localStorage.token}` }) |  | ||||||
| 				}, |  | ||||||
| 				body: JSON.stringify({ |  | ||||||
| 					name: `${name}:latest`, |  | ||||||
| 					modelfile: `FROM @${modelFileDigest}\n${modelFileContent}` |  | ||||||
| 				}) |  | ||||||
| 			}).catch((err) => { |  | ||||||
| 				console.log(err); |  | ||||||
| 				return null; |  | ||||||
| 			}); |  | ||||||
| 
 | 
 | ||||||
| 			if (res && res.ok) { | 			if (res && res.ok) { | ||||||
| 				const reader = res.body | 				const reader = res.body | ||||||
|  | @ -490,52 +471,44 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const deleteModelHandler = async () => { | 	const deleteModelHandler = async () => { | ||||||
| 		const res = await fetch(`${API_BASE_URL}/delete`, { | 		const res = await deleteModel(localStorage.token, deleteModelTag); | ||||||
| 			method: 'DELETE', |  | ||||||
| 			headers: { |  | ||||||
| 				'Content-Type': 'text/event-stream', |  | ||||||
| 				...($settings.authHeader && { Authorization: $settings.authHeader }), |  | ||||||
| 				...($user && { Authorization: `Bearer ${localStorage.token}` }) |  | ||||||
| 			}, |  | ||||||
| 			body: JSON.stringify({ |  | ||||||
| 				name: deleteModelTag |  | ||||||
| 			}) |  | ||||||
| 		}); |  | ||||||
| 
 | 
 | ||||||
| 		const reader = res.body | 		if (res) { | ||||||
| 			.pipeThrough(new TextDecoderStream()) | 			const reader = res.body | ||||||
| 			.pipeThrough(splitStream('\n')) | 				.pipeThrough(new TextDecoderStream()) | ||||||
| 			.getReader(); | 				.pipeThrough(splitStream('\n')) | ||||||
|  | 				.getReader(); | ||||||
| 
 | 
 | ||||||
| 		while (true) { | 			while (true) { | ||||||
| 			const { value, done } = await reader.read(); | 				const { value, done } = await reader.read(); | ||||||
| 			if (done) break; | 				if (done) break; | ||||||
| 
 | 
 | ||||||
| 			try { | 				try { | ||||||
| 				let lines = value.split('\n'); | 					let lines = value.split('\n'); | ||||||
| 
 | 
 | ||||||
| 				for (const line of lines) { | 					for (const line of lines) { | ||||||
| 					if (line !== '' && line !== 'null') { | 						if (line !== '' && line !== 'null') { | ||||||
| 						console.log(line); | 							console.log(line); | ||||||
| 						let data = JSON.parse(line); | 							let data = JSON.parse(line); | ||||||
| 						console.log(data); | 							console.log(data); | ||||||
| 
 | 
 | ||||||
| 						if (data.error) { | 							if (data.error) { | ||||||
| 							throw data.error; | 								throw data.error; | ||||||
|  | 							} | ||||||
|  | 							if (data.detail) { | ||||||
|  | 								throw data.detail; | ||||||
|  | 							} | ||||||
|  | 
 | ||||||
|  | 							if (data.status) { | ||||||
|  | 							} | ||||||
|  | 						} else { | ||||||
|  | 							toast.success(`Deleted ${deleteModelTag}`); | ||||||
| 						} | 						} | ||||||
| 						if (data.detail) { |  | ||||||
| 							throw data.detail; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						if (data.status) { |  | ||||||
| 						} |  | ||||||
| 					} else { |  | ||||||
| 						toast.success(`Deleted ${deleteModelTag}`); |  | ||||||
| 					} | 					} | ||||||
|  | 				} catch (error) { | ||||||
|  | 					console.log(error); | ||||||
|  | 					toast.error(error); | ||||||
| 				} | 				} | ||||||
| 			} catch (error) { |  | ||||||
| 				console.log(error); |  | ||||||
| 				toast.error(error); |  | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -543,13 +516,10 @@ | ||||||
| 		models.set(await getModels()); | 		models.set(await getModels()); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const getModels = async (url = '', type = 'all') => { | 	const getModels = async (type = 'all') => { | ||||||
| 		let models = []; | 		let models = []; | ||||||
| 		models.push( | 		models.push( | ||||||
| 			...(await getOllamaModels( | 			...(await getOllamaModels(localStorage.token).catch((error) => { | ||||||
| 				url ? url : $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, |  | ||||||
| 				localStorage.token |  | ||||||
| 			).catch((error) => { |  | ||||||
| 				toast.error(error); | 				toast.error(error); | ||||||
| 				return []; | 				return []; | ||||||
| 			})) | 			})) | ||||||
|  | @ -557,10 +527,10 @@ | ||||||
| 
 | 
 | ||||||
| 		// If OpenAI API Key exists | 		// If OpenAI API Key exists | ||||||
| 		if (type === 'all' && $settings.OPENAI_API_KEY) { | 		if (type === 'all' && $settings.OPENAI_API_KEY) { | ||||||
| 			const API_BASE_URL = $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'; | 			const OPENAI_API_BASE_URL = $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'; | ||||||
| 
 | 
 | ||||||
| 			// Validate OPENAI_API_KEY | 			// Validate OPENAI_API_KEY | ||||||
| 			const openaiModelRes = await fetch(`${API_BASE_URL}/models`, { | 			const openaiModelRes = await fetch(`${OPENAI_API_BASE_URL}/models`, { | ||||||
| 				method: 'GET', | 				method: 'GET', | ||||||
| 				headers: { | 				headers: { | ||||||
| 					'Content-Type': 'application/json', | 					'Content-Type': 'application/json', | ||||||
|  | @ -588,7 +558,7 @@ | ||||||
| 							...openAIModels | 							...openAIModels | ||||||
| 								.map((model) => ({ name: model.id, external: true })) | 								.map((model) => ({ name: model.id, external: true })) | ||||||
| 								.filter((model) => | 								.filter((model) => | ||||||
| 									API_BASE_URL.includes('openai') ? model.name.includes('gpt') : true | 									OPENAI_API_BASE_URL.includes('openai') ? model.name.includes('gpt') : true | ||||||
| 								) | 								) | ||||||
| 					  ] | 					  ] | ||||||
| 					: []) | 					: []) | ||||||
|  | @ -624,15 +594,18 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
|  | 		console.log('settings', $user.role === 'admin'); | ||||||
|  | 		if ($user.role === 'admin') { | ||||||
|  | 			API_BASE_URL = await getOllamaAPIUrl(localStorage.token); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||||
| 		console.log(settings); | 		console.log(settings); | ||||||
| 
 | 
 | ||||||
| 		theme = localStorage.theme ?? 'dark'; | 		theme = localStorage.theme ?? 'dark'; | ||||||
| 		notificationEnabled = settings.notificationEnabled ?? false; | 		notificationEnabled = settings.notificationEnabled ?? false; | ||||||
| 
 | 
 | ||||||
| 		API_BASE_URL = settings.API_BASE_URL ?? OLLAMA_API_BASE_URL; |  | ||||||
| 		system = settings.system ?? ''; | 		system = settings.system ?? ''; | ||||||
| 
 |  | ||||||
| 		requestFormat = settings.requestFormat ?? ''; | 		requestFormat = settings.requestFormat ?? ''; | ||||||
| 
 | 
 | ||||||
| 		options.seed = settings.seed ?? 0; | 		options.seed = settings.seed ?? 0; | ||||||
|  | @ -659,10 +632,7 @@ | ||||||
| 			authContent = settings.authHeader.split(' ')[1]; | 			authContent = settings.authHeader.split(' ')[1]; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		ollamaVersion = await getOllamaVersion( | 		ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => { | ||||||
| 			API_BASE_URL ?? OLLAMA_API_BASE_URL, |  | ||||||
| 			localStorage.token |  | ||||||
| 		).catch((error) => { |  | ||||||
| 			return ''; | 			return ''; | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
|  | @ -1026,51 +996,53 @@ | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						</div> | ||||||
| 
 | 
 | ||||||
| 						<hr class=" dark:border-gray-700" /> | 						{#if $user.role === 'admin'} | ||||||
| 						<div> | 							<hr class=" dark:border-gray-700" /> | ||||||
| 							<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div> | 							<div> | ||||||
| 							<div class="flex w-full"> | 								<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div> | ||||||
| 								<div class="flex-1 mr-2"> | 								<div class="flex w-full"> | ||||||
| 									<input | 									<div class="flex-1 mr-2"> | ||||||
| 										class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | 										<input | ||||||
| 										placeholder="Enter URL (e.g. http://localhost:8080/ollama/api)" | 											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | ||||||
| 										bind:value={API_BASE_URL} | 											placeholder="Enter URL (e.g. http://localhost:8080/ollama/api)" | ||||||
| 									/> | 											bind:value={API_BASE_URL} | ||||||
| 								</div> |  | ||||||
| 								<button |  | ||||||
| 									class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition" |  | ||||||
| 									on:click={() => { |  | ||||||
| 										checkOllamaConnection(); |  | ||||||
| 									}} |  | ||||||
| 								> |  | ||||||
| 									<svg |  | ||||||
| 										xmlns="http://www.w3.org/2000/svg" |  | ||||||
| 										viewBox="0 0 20 20" |  | ||||||
| 										fill="currentColor" |  | ||||||
| 										class="w-4 h-4" |  | ||||||
| 									> |  | ||||||
| 										<path |  | ||||||
| 											fill-rule="evenodd" |  | ||||||
| 											d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" |  | ||||||
| 											clip-rule="evenodd" |  | ||||||
| 										/> | 										/> | ||||||
| 									</svg> | 									</div> | ||||||
| 								</button> | 									<button | ||||||
| 							</div> | 										class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition" | ||||||
|  | 										on:click={() => { | ||||||
|  | 											updateOllamaAPIUrlHandler(); | ||||||
|  | 										}} | ||||||
|  | 									> | ||||||
|  | 										<svg | ||||||
|  | 											xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 											viewBox="0 0 20 20" | ||||||
|  | 											fill="currentColor" | ||||||
|  | 											class="w-4 h-4" | ||||||
|  | 										> | ||||||
|  | 											<path | ||||||
|  | 												fill-rule="evenodd" | ||||||
|  | 												d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" | ||||||
|  | 												clip-rule="evenodd" | ||||||
|  | 											/> | ||||||
|  | 										</svg> | ||||||
|  | 									</button> | ||||||
|  | 								</div> | ||||||
| 
 | 
 | ||||||
| 							<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | 								<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||||
| 								The field above should be set to <span | 									The field above should be set to <span | ||||||
| 									class=" text-gray-500 dark:text-gray-300 font-medium">'/ollama/api'</span | 										class=" text-gray-500 dark:text-gray-300 font-medium">'/ollama/api'</span | ||||||
| 								>; | 									>; | ||||||
| 								<a | 									<a | ||||||
| 									class=" text-gray-500 dark:text-gray-300 font-medium" | 										class=" text-gray-500 dark:text-gray-300 font-medium" | ||||||
| 									href="https://github.com/ollama-webui/ollama-webui#troubleshooting" | 										href="https://github.com/ollama-webui/ollama-webui#troubleshooting" | ||||||
| 									target="_blank" | 										target="_blank" | ||||||
| 								> | 									> | ||||||
| 									Click here for help. | 										Click here for help. | ||||||
| 								</a> | 									</a> | ||||||
|  | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						{/if} | ||||||
| 
 | 
 | ||||||
| 						<hr class=" dark:border-gray-700" /> | 						<hr class=" dark:border-gray-700" /> | ||||||
| 
 | 
 | ||||||
|  | @ -1088,7 +1060,6 @@ | ||||||
| 								class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | 								class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||||
| 								on:click={() => { | 								on:click={() => { | ||||||
| 									saveSettings({ | 									saveSettings({ | ||||||
| 										API_BASE_URL: API_BASE_URL === '' ? OLLAMA_API_BASE_URL : API_BASE_URL, |  | ||||||
| 										system: system !== '' ? system : undefined | 										system: system !== '' ? system : undefined | ||||||
| 									}); | 									}); | ||||||
| 									show = false; | 									show = false; | ||||||
|  |  | ||||||
|  | @ -1,13 +1,8 @@ | ||||||
| import { dev, browser } from '$app/environment'; | import { dev } from '$app/environment'; | ||||||
| import { PUBLIC_API_BASE_URL } from '$env/static/public'; |  | ||||||
| 
 | 
 | ||||||
| export const OLLAMA_API_BASE_URL = dev | export const OLLAMA_API_BASE_URL = dev | ||||||
| 	? `http://${location.hostname}:8080/ollama/api` | 	? `http://${location.hostname}:8080/ollama/api` | ||||||
| 	: PUBLIC_API_BASE_URL === '' | 	: '/ollama/api'; | ||||||
| 	? browser |  | ||||||
| 		? `http://${location.hostname}:11434/api` |  | ||||||
| 		: `http://localhost:11434/api` |  | ||||||
| 	: PUBLIC_API_BASE_URL; |  | ||||||
| 
 | 
 | ||||||
| export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``; | export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``; | ||||||
| export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`; | export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`; | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ | ||||||
| 	import { getOpenAIModels } from '$lib/apis/openai'; | 	import { getOpenAIModels } from '$lib/apis/openai'; | ||||||
| 
 | 
 | ||||||
| 	import { user, showSettings, settings, models, modelfiles, prompts } from '$lib/stores'; | 	import { user, showSettings, settings, models, modelfiles, prompts } from '$lib/stores'; | ||||||
| 	import { OLLAMA_API_BASE_URL, REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; | 	import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; | ||||||
| 
 | 
 | ||||||
| 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | 	import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; | ||||||
| 	import Sidebar from '$lib/components/layout/Sidebar.svelte'; | 	import Sidebar from '$lib/components/layout/Sidebar.svelte'; | ||||||
|  | @ -32,10 +32,7 @@ | ||||||
| 	const getModels = async () => { | 	const getModels = async () => { | ||||||
| 		let models = []; | 		let models = []; | ||||||
| 		models.push( | 		models.push( | ||||||
| 			...(await getOllamaModels( | 			...(await getOllamaModels(localStorage.token).catch((error) => { | ||||||
| 				$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, |  | ||||||
| 				localStorage.token |  | ||||||
| 			).catch((error) => { |  | ||||||
| 				toast.error(error); | 				toast.error(error); | ||||||
| 				return []; | 				return []; | ||||||
| 			})) | 			})) | ||||||
|  | @ -58,10 +55,7 @@ | ||||||
| 
 | 
 | ||||||
| 	const setOllamaVersion = async (version: string = '') => { | 	const setOllamaVersion = async (version: string = '') => { | ||||||
| 		if (version === '') { | 		if (version === '') { | ||||||
| 			version = await getOllamaVersion( | 			version = await getOllamaVersion(localStorage.token).catch((error) => { | ||||||
| 				$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, |  | ||||||
| 				localStorage.token |  | ||||||
| 			).catch((error) => { |  | ||||||
| 				return ''; | 				return ''; | ||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -7,7 +7,6 @@ | ||||||
| 	import { page } from '$app/stores'; | 	import { page } from '$app/stores'; | ||||||
| 
 | 
 | ||||||
| 	import { models, modelfiles, user, settings, chats, chatId, config } from '$lib/stores'; | 	import { models, modelfiles, user, settings, chats, chatId, config } from '$lib/stores'; | ||||||
| 	import { OLLAMA_API_BASE_URL } from '$lib/constants'; |  | ||||||
| 
 | 
 | ||||||
| 	import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; | 	import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; | ||||||
| 	import { copyToClipboard, splitStream } from '$lib/utils'; | 	import { copyToClipboard, splitStream } from '$lib/utils'; | ||||||
|  | @ -163,36 +162,32 @@ | ||||||
| 		// Scroll down | 		// Scroll down | ||||||
| 		window.scrollTo({ top: document.body.scrollHeight }); | 		window.scrollTo({ top: document.body.scrollHeight }); | ||||||
| 
 | 
 | ||||||
| 		const res = await generateChatCompletion( | 		const res = await generateChatCompletion(localStorage.token, { | ||||||
| 			$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, | 			model: model, | ||||||
| 			localStorage.token, | 			messages: [ | ||||||
| 			{ | 				$settings.system | ||||||
| 				model: model, | 					? { | ||||||
| 				messages: [ | 							role: 'system', | ||||||
| 					$settings.system | 							content: $settings.system | ||||||
| 						? { | 					  } | ||||||
| 								role: 'system', | 					: undefined, | ||||||
| 								content: $settings.system | 				...messages | ||||||
| 						  } | 			] | ||||||
| 						: undefined, | 				.filter((message) => message) | ||||||
| 					...messages | 				.map((message) => ({ | ||||||
| 				] | 					role: message.role, | ||||||
| 					.filter((message) => message) | 					content: message.content, | ||||||
| 					.map((message) => ({ | 					...(message.files && { | ||||||
| 						role: message.role, | 						images: message.files | ||||||
| 						content: message.content, | 							.filter((file) => file.type === 'image') | ||||||
| 						...(message.files && { | 							.map((file) => file.url.slice(file.url.indexOf(',') + 1)) | ||||||
| 							images: message.files | 					}) | ||||||
| 								.filter((file) => file.type === 'image') | 				})), | ||||||
| 								.map((file) => file.url.slice(file.url.indexOf(',') + 1)) | 			options: { | ||||||
| 						}) | 				...($settings.options ?? {}) | ||||||
| 					})), | 			}, | ||||||
| 				options: { | 			format: $settings.requestFormat ?? undefined | ||||||
| 					...($settings.options ?? {}) | 		}); | ||||||
| 				}, |  | ||||||
| 				format: $settings.requestFormat ?? undefined |  | ||||||
| 			} |  | ||||||
| 		); |  | ||||||
| 
 | 
 | ||||||
| 		if (res && res.ok) { | 		if (res && res.ok) { | ||||||
| 			const reader = res.body | 			const reader = res.body | ||||||
|  | @ -595,7 +590,6 @@ | ||||||
| 	const generateChatTitle = async (_chatId, userPrompt) => { | 	const generateChatTitle = async (_chatId, userPrompt) => { | ||||||
| 		if ($settings.titleAutoGenerate ?? true) { | 		if ($settings.titleAutoGenerate ?? true) { | ||||||
| 			const title = await generateTitle( | 			const title = await generateTitle( | ||||||
| 				$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, |  | ||||||
| 				localStorage.token, | 				localStorage.token, | ||||||
| 				$settings?.titleAutoGenerateModel ?? selectedModels[0], | 				$settings?.titleAutoGenerateModel ?? selectedModels[0], | ||||||
| 				userPrompt | 				userPrompt | ||||||
|  |  | ||||||
|  | @ -7,7 +7,6 @@ | ||||||
| 	import { page } from '$app/stores'; | 	import { page } from '$app/stores'; | ||||||
| 
 | 
 | ||||||
| 	import { models, modelfiles, user, settings, chats, chatId } from '$lib/stores'; | 	import { models, modelfiles, user, settings, chats, chatId } from '$lib/stores'; | ||||||
| 	import { OLLAMA_API_BASE_URL } from '$lib/constants'; |  | ||||||
| 
 | 
 | ||||||
| 	import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; | 	import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; | ||||||
| 	import { copyToClipboard, splitStream } from '$lib/utils'; | 	import { copyToClipboard, splitStream } from '$lib/utils'; | ||||||
|  | @ -180,36 +179,32 @@ | ||||||
| 		// Scroll down | 		// Scroll down | ||||||
| 		window.scrollTo({ top: document.body.scrollHeight }); | 		window.scrollTo({ top: document.body.scrollHeight }); | ||||||
| 
 | 
 | ||||||
| 		const res = await generateChatCompletion( | 		const res = await generateChatCompletion(localStorage.token, { | ||||||
| 			$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, | 			model: model, | ||||||
| 			localStorage.token, | 			messages: [ | ||||||
| 			{ | 				$settings.system | ||||||
| 				model: model, | 					? { | ||||||
| 				messages: [ | 							role: 'system', | ||||||
| 					$settings.system | 							content: $settings.system | ||||||
| 						? { | 					  } | ||||||
| 								role: 'system', | 					: undefined, | ||||||
| 								content: $settings.system | 				...messages | ||||||
| 						  } | 			] | ||||||
| 						: undefined, | 				.filter((message) => message) | ||||||
| 					...messages | 				.map((message) => ({ | ||||||
| 				] | 					role: message.role, | ||||||
| 					.filter((message) => message) | 					content: message.content, | ||||||
| 					.map((message) => ({ | 					...(message.files && { | ||||||
| 						role: message.role, | 						images: message.files | ||||||
| 						content: message.content, | 							.filter((file) => file.type === 'image') | ||||||
| 						...(message.files && { | 							.map((file) => file.url.slice(file.url.indexOf(',') + 1)) | ||||||
| 							images: message.files | 					}) | ||||||
| 								.filter((file) => file.type === 'image') | 				})), | ||||||
| 								.map((file) => file.url.slice(file.url.indexOf(',') + 1)) | 			options: { | ||||||
| 						}) | 				...($settings.options ?? {}) | ||||||
| 					})), | 			}, | ||||||
| 				options: { | 			format: $settings.requestFormat ?? undefined | ||||||
| 					...($settings.options ?? {}) | 		}); | ||||||
| 				}, |  | ||||||
| 				format: $settings.requestFormat ?? undefined |  | ||||||
| 			} |  | ||||||
| 		); |  | ||||||
| 
 | 
 | ||||||
| 		if (res && res.ok) { | 		if (res && res.ok) { | ||||||
| 			const reader = res.body | 			const reader = res.body | ||||||
|  | @ -611,12 +606,7 @@ | ||||||
| 
 | 
 | ||||||
| 	const generateChatTitle = async (_chatId, userPrompt) => { | 	const generateChatTitle = async (_chatId, userPrompt) => { | ||||||
| 		if ($settings.titleAutoGenerate ?? true) { | 		if ($settings.titleAutoGenerate ?? true) { | ||||||
| 			const title = await generateTitle( | 			const title = await generateTitle(localStorage.token, selectedModels[0], userPrompt); | ||||||
| 				$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, |  | ||||||
| 				localStorage.token, |  | ||||||
| 				selectedModels[0], |  | ||||||
| 				userPrompt |  | ||||||
| 			); |  | ||||||
| 
 | 
 | ||||||
| 			if (title) { | 			if (title) { | ||||||
| 				await setChatTitle(_chatId, title); | 				await setChatTitle(_chatId, title); | ||||||
|  |  | ||||||
|  | @ -6,7 +6,6 @@ | ||||||
| 	import { onMount } from 'svelte'; | 	import { onMount } from 'svelte'; | ||||||
| 
 | 
 | ||||||
| 	import { modelfiles, settings, user } from '$lib/stores'; | 	import { modelfiles, settings, user } from '$lib/stores'; | ||||||
| 	import { OLLAMA_API_BASE_URL } from '$lib/constants'; |  | ||||||
| 	import { createModel, deleteModel } from '$lib/apis/ollama'; | 	import { createModel, deleteModel } from '$lib/apis/ollama'; | ||||||
| 	import { | 	import { | ||||||
| 		createNewModelfile, | 		createNewModelfile, | ||||||
|  | @ -20,11 +19,7 @@ | ||||||
| 	const deleteModelHandler = async (tagName) => { | 	const deleteModelHandler = async (tagName) => { | ||||||
| 		let success = null; | 		let success = null; | ||||||
| 
 | 
 | ||||||
| 		success = await deleteModel( | 		success = await deleteModel(localStorage.token, tagName); | ||||||
| 			$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, |  | ||||||
| 			localStorage.token, |  | ||||||
| 			tagName |  | ||||||
| 		); |  | ||||||
| 
 | 
 | ||||||
| 		if (success) { | 		if (success) { | ||||||
| 			toast.success(`Deleted ${tagName}`); | 			toast.success(`Deleted ${tagName}`); | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ | ||||||
| 	import { v4 as uuidv4 } from 'uuid'; | 	import { v4 as uuidv4 } from 'uuid'; | ||||||
| 	import { toast } from 'svelte-french-toast'; | 	import { toast } from 'svelte-french-toast'; | ||||||
| 	import { goto } from '$app/navigation'; | 	import { goto } from '$app/navigation'; | ||||||
| 	import { OLLAMA_API_BASE_URL } from '$lib/constants'; |  | ||||||
| 	import { settings, user, config, modelfiles, models } from '$lib/stores'; | 	import { settings, user, config, modelfiles, models } from '$lib/stores'; | ||||||
| 
 | 
 | ||||||
| 	import Advanced from '$lib/components/chat/Settings/Advanced.svelte'; | 	import Advanced from '$lib/components/chat/Settings/Advanced.svelte'; | ||||||
|  | @ -132,12 +131,7 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, ''); | ||||||
| 			Object.keys(categories).filter((category) => categories[category]).length > 0 && | 			Object.keys(categories).filter((category) => categories[category]).length > 0 && | ||||||
| 			!$models.includes(tagName) | 			!$models.includes(tagName) | ||||||
| 		) { | 		) { | ||||||
| 			const res = await createModel( | 			const res = await createModel(localStorage.token, tagName, content); | ||||||
| 				$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, |  | ||||||
| 				localStorage.token, |  | ||||||
| 				tagName, |  | ||||||
| 				content |  | ||||||
| 			); |  | ||||||
| 
 | 
 | ||||||
| 			if (res) { | 			if (res) { | ||||||
| 				const reader = res.body | 				const reader = res.body | ||||||
|  |  | ||||||
|  | @ -7,8 +7,6 @@ | ||||||
| 	import { page } from '$app/stores'; | 	import { page } from '$app/stores'; | ||||||
| 
 | 
 | ||||||
| 	import { settings, user, config, modelfiles } from '$lib/stores'; | 	import { settings, user, config, modelfiles } from '$lib/stores'; | ||||||
| 
 |  | ||||||
| 	import { OLLAMA_API_BASE_URL } from '$lib/constants'; |  | ||||||
| 	import { splitStream } from '$lib/utils'; | 	import { splitStream } from '$lib/utils'; | ||||||
| 
 | 
 | ||||||
| 	import { createModel } from '$lib/apis/ollama'; | 	import { createModel } from '$lib/apis/ollama'; | ||||||
|  | @ -104,12 +102,7 @@ | ||||||
| 			content !== '' && | 			content !== '' && | ||||||
| 			Object.keys(categories).filter((category) => categories[category]).length > 0 | 			Object.keys(categories).filter((category) => categories[category]).length > 0 | ||||||
| 		) { | 		) { | ||||||
| 			const res = await createModel( | 			const res = await createModel(localStorage.token, tagName, content); | ||||||
| 				$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, |  | ||||||
| 				localStorage.token, |  | ||||||
| 				tagName, |  | ||||||
| 				content |  | ||||||
| 			); |  | ||||||
| 
 | 
 | ||||||
| 			if (res) { | 			if (res) { | ||||||
| 				const reader = res.body | 				const reader = res.body | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek