forked from open-webui/open-webui
		
	Merge pull request #381 from ollama-webui/openai-backend
feat: openai backend support
This commit is contained in:
		
						commit
						e6c6508869
					
				
					 13 changed files with 724 additions and 431 deletions
				
			
		|  | @ -16,6 +16,10 @@ ARG OLLAMA_API_BASE_URL='/ollama/api' | ||||||
| 
 | 
 | ||||||
| ENV ENV=prod | ENV ENV=prod | ||||||
| ENV OLLAMA_API_BASE_URL $OLLAMA_API_BASE_URL | ENV OLLAMA_API_BASE_URL $OLLAMA_API_BASE_URL | ||||||
|  | 
 | ||||||
|  | ENV OPENAI_API_BASE_URL "" | ||||||
|  | ENV OPENAI_API_KEY "" | ||||||
|  | 
 | ||||||
| ENV WEBUI_JWT_SECRET_KEY "SECRET_KEY" | ENV WEBUI_JWT_SECRET_KEY "SECRET_KEY" | ||||||
| 
 | 
 | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
|  |  | ||||||
							
								
								
									
										135
									
								
								backend/apps/openai/main.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								backend/apps/openai/main.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,135 @@ | ||||||
|  | from fastapi import FastAPI, Request, Response, HTTPException, Depends | ||||||
|  | from fastapi.middleware.cors import CORSMiddleware | ||||||
|  | from fastapi.responses import StreamingResponse, JSONResponse | ||||||
|  | 
 | ||||||
|  | import requests | ||||||
|  | import json | ||||||
|  | from pydantic import BaseModel | ||||||
|  | 
 | ||||||
|  | from apps.web.models.users import Users | ||||||
|  | from constants import ERROR_MESSAGES | ||||||
|  | from utils.utils import decode_token, get_current_user | ||||||
|  | from config import OPENAI_API_BASE_URL, OPENAI_API_KEY | ||||||
|  | 
 | ||||||
|  | app = FastAPI() | ||||||
|  | app.add_middleware( | ||||||
|  |     CORSMiddleware, | ||||||
|  |     allow_origins=["*"], | ||||||
|  |     allow_credentials=True, | ||||||
|  |     allow_methods=["*"], | ||||||
|  |     allow_headers=["*"], | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL | ||||||
|  | app.state.OPENAI_API_KEY = OPENAI_API_KEY | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class UrlUpdateForm(BaseModel): | ||||||
|  |     url: str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class KeyUpdateForm(BaseModel): | ||||||
|  |     key: str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.get("/url") | ||||||
|  | async def get_openai_url(user=Depends(get_current_user)): | ||||||
|  |     if user and user.role == "admin": | ||||||
|  |         return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} | ||||||
|  |     else: | ||||||
|  |         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.post("/url/update") | ||||||
|  | async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_current_user)): | ||||||
|  |     if user and user.role == "admin": | ||||||
|  |         app.state.OPENAI_API_BASE_URL = form_data.url | ||||||
|  |         return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} | ||||||
|  |     else: | ||||||
|  |         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.get("/key") | ||||||
|  | async def get_openai_key(user=Depends(get_current_user)): | ||||||
|  |     if user and user.role == "admin": | ||||||
|  |         return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} | ||||||
|  |     else: | ||||||
|  |         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @app.post("/key/update") | ||||||
|  | async def update_openai_key(form_data: KeyUpdateForm, user=Depends(get_current_user)): | ||||||
|  |     if user and user.role == "admin": | ||||||
|  |         app.state.OPENAI_API_KEY = form_data.key | ||||||
|  |         return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} | ||||||
|  |     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.OPENAI_API_BASE_URL}/{path}" | ||||||
|  |     print(target_url, app.state.OPENAI_API_KEY) | ||||||
|  | 
 | ||||||
|  |     if user.role not in ["user", "admin"]: | ||||||
|  |         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) | ||||||
|  |     if app.state.OPENAI_API_KEY == "": | ||||||
|  |         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) | ||||||
|  | 
 | ||||||
|  |     body = await request.body() | ||||||
|  |     # headers = dict(request.headers) | ||||||
|  |     # print(headers) | ||||||
|  | 
 | ||||||
|  |     headers = {} | ||||||
|  |     headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" | ||||||
|  |     headers["Content-Type"] = "application/json" | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         r = requests.request( | ||||||
|  |             method=request.method, | ||||||
|  |             url=target_url, | ||||||
|  |             data=body, | ||||||
|  |             headers=headers, | ||||||
|  |             stream=True, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         r.raise_for_status() | ||||||
|  | 
 | ||||||
|  |         # Check if response is SSE | ||||||
|  |         if "text/event-stream" in r.headers.get("Content-Type", ""): | ||||||
|  |             return StreamingResponse( | ||||||
|  |                 r.iter_content(chunk_size=8192), | ||||||
|  |                 status_code=r.status_code, | ||||||
|  |                 headers=dict(r.headers), | ||||||
|  |             ) | ||||||
|  |         else: | ||||||
|  |             # For non-SSE, read the response and return it | ||||||
|  |             # response_data = ( | ||||||
|  |             #     r.json() | ||||||
|  |             #     if r.headers.get("Content-Type", "") | ||||||
|  |             #     == "application/json" | ||||||
|  |             #     else r.text | ||||||
|  |             # ) | ||||||
|  | 
 | ||||||
|  |             response_data = r.json() | ||||||
|  | 
 | ||||||
|  |             print(type(response_data)) | ||||||
|  | 
 | ||||||
|  |             if "openai" in app.state.OPENAI_API_BASE_URL and path == "models": | ||||||
|  |                 response_data["data"] = list( | ||||||
|  |                     filter(lambda model: "gpt" in model["id"], response_data["data"]) | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             return response_data | ||||||
|  |     except Exception as e: | ||||||
|  |         print(e) | ||||||
|  |         error_detail = "Ollama WebUI: Server Connection Error" | ||||||
|  |         if r is not None: | ||||||
|  |             try: | ||||||
|  |                 res = r.json() | ||||||
|  |                 if "error" in res: | ||||||
|  |                     error_detail = f"External: {res['error']}" | ||||||
|  |             except: | ||||||
|  |                 error_detail = f"External: {e}" | ||||||
|  | 
 | ||||||
|  |         raise HTTPException(status_code=r.status_code, detail=error_detail) | ||||||
|  | @ -27,11 +27,22 @@ if ENV == "prod": | ||||||
|     if OLLAMA_API_BASE_URL == "/ollama/api": |     if OLLAMA_API_BASE_URL == "/ollama/api": | ||||||
|         OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api" |         OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api" | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | #################################### | ||||||
|  | # OPENAI_API | ||||||
|  | #################################### | ||||||
|  | 
 | ||||||
|  | OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") | ||||||
|  | OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") | ||||||
|  | 
 | ||||||
|  | if OPENAI_API_BASE_URL == "": | ||||||
|  |     OPENAI_API_BASE_URL = "https://api.openai.com/v1" | ||||||
|  | 
 | ||||||
| #################################### | #################################### | ||||||
| # WEBUI_VERSION | # WEBUI_VERSION | ||||||
| #################################### | #################################### | ||||||
| 
 | 
 | ||||||
| WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.42") | WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.50") | ||||||
| 
 | 
 | ||||||
| #################################### | #################################### | ||||||
| # WEBUI_AUTH (Required for security) | # WEBUI_AUTH (Required for security) | ||||||
|  |  | ||||||
|  | @ -33,4 +33,5 @@ class ERROR_MESSAGES(str, Enum): | ||||||
|     ) |     ) | ||||||
|     NOT_FOUND = "We could not find what you're looking for :/" |     NOT_FOUND = "We could not find what you're looking for :/" | ||||||
|     USER_NOT_FOUND = "We could not find what you're looking for :/" |     USER_NOT_FOUND = "We could not find what you're looking for :/" | ||||||
|  |     API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature." | ||||||
|     MALICIOUS = "Unusual activities detected, please try again in a few minutes." |     MALICIOUS = "Unusual activities detected, please try again in a few minutes." | ||||||
|  |  | ||||||
|  | @ -6,6 +6,8 @@ from fastapi.middleware.cors import CORSMiddleware | ||||||
| from starlette.exceptions import HTTPException as StarletteHTTPException | from starlette.exceptions import HTTPException as StarletteHTTPException | ||||||
| 
 | 
 | ||||||
| from apps.ollama.main import app as ollama_app | from apps.ollama.main import app as ollama_app | ||||||
|  | from apps.openai.main import app as openai_app | ||||||
|  | 
 | ||||||
| from apps.web.main import app as webui_app | from apps.web.main import app as webui_app | ||||||
| 
 | 
 | ||||||
| import time | import time | ||||||
|  | @ -46,7 +48,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", ollama_app) | app.mount("/ollama/api", ollama_app) | ||||||
|  | app.mount("/openai/api", openai_app) | ||||||
| 
 | 
 | ||||||
| app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files") | app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files") | ||||||
|  |  | ||||||
							
								
								
									
										12
									
								
								example.env
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								example.env
									
										
									
									
									
								
							|  | @ -1,12 +1,6 @@ | ||||||
| # If you're serving both the frontend and backend (Recommended) |  | ||||||
| # Set the public API base URL for seamless communication |  | ||||||
| PUBLIC_API_BASE_URL='/ollama/api' |  | ||||||
| 
 |  | ||||||
| # If you're serving only the frontend (Not recommended and not fully supported) |  | ||||||
| # Comment above and Uncomment below |  | ||||||
| # You can use the default value or specify a custom path, e.g., '/api' |  | ||||||
| # PUBLIC_API_BASE_URL='http://{location.hostname}:11434/api' |  | ||||||
| 
 |  | ||||||
| # Ollama URL for the backend to connect | # Ollama URL for the backend to connect | ||||||
| # The path '/ollama/api' will be redirected to the specified backend URL | # The path '/ollama/api' will be redirected to the specified backend URL | ||||||
| OLLAMA_API_BASE_URL='http://localhost:11434/api' | OLLAMA_API_BASE_URL='http://localhost:11434/api' | ||||||
|  | 
 | ||||||
|  | OPENAI_API_BASE_URL='' | ||||||
|  | OPENAI_API_KEY='' | ||||||
|  | @ -1,4 +1,176 @@ | ||||||
| export const getOpenAIModels = async ( | import { OPENAI_API_BASE_URL } from '$lib/constants'; | ||||||
|  | 
 | ||||||
|  | export const getOpenAIUrl = async (token: string = '') => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${OPENAI_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.OPENAI_API_BASE_URL; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const updateOpenAIUrl = async (token: string = '', url: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${OPENAI_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.OPENAI_API_BASE_URL; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const getOpenAIKey = async (token: string = '') => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${OPENAI_API_BASE_URL}/key`, { | ||||||
|  | 		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.OPENAI_API_KEY; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const updateOpenAIKey = async (token: string = '', key: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${OPENAI_API_BASE_URL}/key/update`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			Accept: 'application/json', | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			...(token && { authorization: `Bearer ${token}` }) | ||||||
|  | 		}, | ||||||
|  | 		body: JSON.stringify({ | ||||||
|  | 			key: key | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 		.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.OPENAI_API_KEY; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const getOpenAIModels = async (token: string = '') => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${OPENAI_API_BASE_URL}/models`, { | ||||||
|  | 		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); | ||||||
|  | 			error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; | ||||||
|  | 			return []; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	const models = Array.isArray(res) ? res : res?.data ?? null; | ||||||
|  | 
 | ||||||
|  | 	return models | ||||||
|  | 		? models | ||||||
|  | 				.map((model) => ({ name: model.id, external: true })) | ||||||
|  | 				.sort((a, b) => { | ||||||
|  | 					return a.name.localeCompare(b.name); | ||||||
|  | 				}) | ||||||
|  | 		: models; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const getOpenAIModelsDirect = async ( | ||||||
| 	base_url: string = 'https://api.openai.com/v1', | 	base_url: string = 'https://api.openai.com/v1', | ||||||
| 	api_key: string = '' | 	api_key: string = '' | ||||||
| ) => { | ) => { | ||||||
|  | @ -34,3 +206,26 @@ export const getOpenAIModels = async ( | ||||||
| 			return a.name.localeCompare(b.name); | 			return a.name.localeCompare(b.name); | ||||||
| 		}); | 		}); | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const generateOpenAIChatCompletion = async (token: string = '', body: object) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${OPENAI_API_BASE_URL}/chat/completions`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			Authorization: `Bearer ${token}`, | ||||||
|  | 			'Content-Type': 'application/json' | ||||||
|  | 		}, | ||||||
|  | 		body: JSON.stringify(body) | ||||||
|  | 	}).catch((err) => { | ||||||
|  | 		console.log(err); | ||||||
|  | 		error = err; | ||||||
|  | 		return null; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -27,7 +27,7 @@ | ||||||
| 					> | 					> | ||||||
| 						{#if model in modelfiles} | 						{#if model in modelfiles} | ||||||
| 							<img | 							<img | ||||||
| 								src={modelfiles[model]?.imageUrl} | 								src={modelfiles[model]?.imageUrl ?? '/ollama-dark.png'} | ||||||
| 								alt="modelfile" | 								alt="modelfile" | ||||||
| 								class=" w-20 mb-2 rounded-full {models.length > 1 | 								class=" w-20 mb-2 rounded-full {models.length > 1 | ||||||
| 									? ' border-[5px] border-white dark:border-gray-800' | 									? ' border-[5px] border-white dark:border-gray-800' | ||||||
|  |  | ||||||
|  | @ -24,6 +24,13 @@ | ||||||
| 	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'; | 	import Page from '../../../routes/(app)/+page.svelte'; | ||||||
|  | 	import { | ||||||
|  | 		getOpenAIKey, | ||||||
|  | 		getOpenAIModels, | ||||||
|  | 		getOpenAIUrl, | ||||||
|  | 		updateOpenAIKey, | ||||||
|  | 		updateOpenAIUrl | ||||||
|  | 	} from '$lib/apis/openai'; | ||||||
| 
 | 
 | ||||||
| 	export let show = false; | 	export let show = false; | ||||||
| 
 | 
 | ||||||
|  | @ -153,6 +160,13 @@ | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | 	const updateOpenAIHandler = async () => { | ||||||
|  | 		OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL); | ||||||
|  | 		OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY); | ||||||
|  | 
 | ||||||
|  | 		await models.set(await getModels()); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
| 	const toggleTheme = async () => { | 	const toggleTheme = async () => { | ||||||
| 		if (theme === 'dark') { | 		if (theme === 'dark') { | ||||||
| 			theme = 'light'; | 			theme = 'light'; | ||||||
|  | @ -484,7 +498,7 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const getModels = async (type = 'all') => { | 	const getModels = async (type = 'all') => { | ||||||
| 		let models = []; | 		const models = []; | ||||||
| 		models.push( | 		models.push( | ||||||
| 			...(await getOllamaModels(localStorage.token).catch((error) => { | 			...(await getOllamaModels(localStorage.token).catch((error) => { | ||||||
| 				toast.error(error); | 				toast.error(error); | ||||||
|  | @ -493,43 +507,13 @@ | ||||||
| 		); | 		); | ||||||
| 
 | 
 | ||||||
| 		// If OpenAI API Key exists | 		// If OpenAI API Key exists | ||||||
| 		if (type === 'all' && $settings.OPENAI_API_KEY) { | 		if (type === 'all' && OPENAI_API_KEY) { | ||||||
| 			const OPENAI_API_BASE_URL = $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'; | 			const openAIModels = await getOpenAIModels(localStorage.token).catch((error) => { | ||||||
|  | 				console.log(error); | ||||||
|  | 				return null; | ||||||
|  | 			}); | ||||||
| 
 | 
 | ||||||
| 			// Validate OPENAI_API_KEY | 			models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : [])); | ||||||
| 			const openaiModelRes = await fetch(`${OPENAI_API_BASE_URL}/models`, { |  | ||||||
| 				method: 'GET', |  | ||||||
| 				headers: { |  | ||||||
| 					'Content-Type': 'application/json', |  | ||||||
| 					Authorization: `Bearer ${$settings.OPENAI_API_KEY}` |  | ||||||
| 				} |  | ||||||
| 			}) |  | ||||||
| 				.then(async (res) => { |  | ||||||
| 					if (!res.ok) throw await res.json(); |  | ||||||
| 					return res.json(); |  | ||||||
| 				}) |  | ||||||
| 				.catch((error) => { |  | ||||||
| 					console.log(error); |  | ||||||
| 					toast.error(`OpenAI: ${error?.error?.message ?? 'Network Problem'}`); |  | ||||||
| 					return null; |  | ||||||
| 				}); |  | ||||||
| 
 |  | ||||||
| 			const openAIModels = Array.isArray(openaiModelRes) |  | ||||||
| 				? openaiModelRes |  | ||||||
| 				: openaiModelRes?.data ?? null; |  | ||||||
| 
 |  | ||||||
| 			models.push( |  | ||||||
| 				...(openAIModels |  | ||||||
| 					? [ |  | ||||||
| 							{ name: 'hr' }, |  | ||||||
| 							...openAIModels |  | ||||||
| 								.map((model) => ({ name: model.id, external: true })) |  | ||||||
| 								.filter((model) => |  | ||||||
| 									OPENAI_API_BASE_URL.includes('openai') ? model.name.includes('gpt') : true |  | ||||||
| 								) |  | ||||||
| 					  ] |  | ||||||
| 					: []) |  | ||||||
| 			); |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return models; | 		return models; | ||||||
|  | @ -564,6 +548,8 @@ | ||||||
| 		console.log('settings', $user.role === 'admin'); | 		console.log('settings', $user.role === 'admin'); | ||||||
| 		if ($user.role === 'admin') { | 		if ($user.role === 'admin') { | ||||||
| 			API_BASE_URL = await getOllamaAPIUrl(localStorage.token); | 			API_BASE_URL = await getOllamaAPIUrl(localStorage.token); | ||||||
|  | 			OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token); | ||||||
|  | 			OPENAI_API_KEY = await getOpenAIKey(localStorage.token); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||||
|  | @ -584,9 +570,6 @@ | ||||||
| 		options = { ...options, ...settings.options }; | 		options = { ...options, ...settings.options }; | ||||||
| 		options.stop = (settings?.options?.stop ?? []).join(','); | 		options.stop = (settings?.options?.stop ?? []).join(','); | ||||||
| 
 | 
 | ||||||
| 		OPENAI_API_KEY = settings.OPENAI_API_KEY ?? ''; |  | ||||||
| 		OPENAI_API_BASE_URL = settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'; |  | ||||||
| 
 |  | ||||||
| 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | ||||||
| 		speechAutoSend = settings.speechAutoSend ?? false; | 		speechAutoSend = settings.speechAutoSend ?? false; | ||||||
| 		responseAutoCopy = settings.responseAutoCopy ?? false; | 		responseAutoCopy = settings.responseAutoCopy ?? false; | ||||||
|  | @ -709,31 +692,31 @@ | ||||||
| 						</div> | 						</div> | ||||||
| 						<div class=" self-center">Models</div> | 						<div class=" self-center">Models</div> | ||||||
| 					</button> | 					</button> | ||||||
| 				{/if} |  | ||||||
| 
 | 
 | ||||||
| 				<button | 					<button | ||||||
| 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | 						class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | ||||||
| 					'external' | 						'external' | ||||||
| 						? 'bg-gray-200 dark:bg-gray-700' | 							? 'bg-gray-200 dark:bg-gray-700' | ||||||
| 						: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" | 							: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" | ||||||
| 					on:click={() => { | 						on:click={() => { | ||||||
| 						selectedTab = 'external'; | 							selectedTab = 'external'; | ||||||
| 					}} | 						}} | ||||||
| 				> | 					> | ||||||
| 					<div class=" self-center mr-2"> | 						<div class=" self-center mr-2"> | ||||||
| 						<svg | 							<svg | ||||||
| 							xmlns="http://www.w3.org/2000/svg" | 								xmlns="http://www.w3.org/2000/svg" | ||||||
| 							viewBox="0 0 16 16" | 								viewBox="0 0 16 16" | ||||||
| 							fill="currentColor" | 								fill="currentColor" | ||||||
| 							class="w-4 h-4" | 								class="w-4 h-4" | ||||||
| 						> | 							> | ||||||
| 							<path | 								<path | ||||||
| 								d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z" | 									d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z" | ||||||
| 							/> | 								/> | ||||||
| 						</svg> | 							</svg> | ||||||
| 					</div> | 						</div> | ||||||
| 					<div class=" self-center">External</div> | 						<div class=" self-center">External</div> | ||||||
| 				</button> | 					</button> | ||||||
|  | 				{/if} | ||||||
| 
 | 
 | ||||||
| 				<button | 				<button | ||||||
| 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | 					class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === | ||||||
|  | @ -1415,10 +1398,12 @@ | ||||||
| 					<form | 					<form | ||||||
| 						class="flex flex-col h-full justify-between space-y-3 text-sm" | 						class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||||
| 						on:submit|preventDefault={() => { | 						on:submit|preventDefault={() => { | ||||||
| 							saveSettings({ | 							updateOpenAIHandler(); | ||||||
| 								OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined, | 
 | ||||||
| 								OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined | 							// saveSettings({ | ||||||
| 							}); | 							// 	OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined, | ||||||
|  | 							// 	OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined | ||||||
|  | 							// }); | ||||||
| 							show = false; | 							show = false; | ||||||
| 						}} | 						}} | ||||||
| 					> | 					> | ||||||
|  |  | ||||||
|  | @ -1,11 +1,10 @@ | ||||||
| import { dev } from '$app/environment'; | import { dev } from '$app/environment'; | ||||||
| 
 | 
 | ||||||
| export const OLLAMA_API_BASE_URL = dev |  | ||||||
| 	? `http://${location.hostname}:8080/ollama/api` |  | ||||||
| 	: '/ollama/api'; |  | ||||||
| 
 |  | ||||||
| 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`; | ||||||
|  | export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`; | ||||||
|  | export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`; | ||||||
| 
 | 
 | ||||||
| export const WEB_UI_VERSION = 'v1.0.0-alpha-static'; | export const WEB_UI_VERSION = 'v1.0.0-alpha-static'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -37,19 +37,17 @@ | ||||||
| 				return []; | 				return []; | ||||||
| 			})) | 			})) | ||||||
| 		); | 		); | ||||||
| 		// If OpenAI API Key exists |  | ||||||
| 		if ($settings.OPENAI_API_KEY) { |  | ||||||
| 			const openAIModels = await getOpenAIModels( |  | ||||||
| 				$settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1', |  | ||||||
| 				$settings.OPENAI_API_KEY |  | ||||||
| 			).catch((error) => { |  | ||||||
| 				console.log(error); |  | ||||||
| 				toast.error(error); |  | ||||||
| 				return null; |  | ||||||
| 			}); |  | ||||||
| 
 | 
 | ||||||
| 			models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : [])); | 		// $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1', | ||||||
| 		} | 		// 		$settings.OPENAI_API_KEY | ||||||
|  | 
 | ||||||
|  | 		const openAIModels = await getOpenAIModels(localStorage.token).catch((error) => { | ||||||
|  | 			console.log(error); | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : [])); | ||||||
|  | 
 | ||||||
| 		return models; | 		return models; | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ | ||||||
| 	import ModelSelector from '$lib/components/chat/ModelSelector.svelte'; | 	import ModelSelector from '$lib/components/chat/ModelSelector.svelte'; | ||||||
| 	import Navbar from '$lib/components/layout/Navbar.svelte'; | 	import Navbar from '$lib/components/layout/Navbar.svelte'; | ||||||
| 	import { createNewChat, getChatList, updateChatById } from '$lib/apis/chats'; | 	import { createNewChat, getChatList, updateChatById } from '$lib/apis/chats'; | ||||||
|  | 	import { generateOpenAIChatCompletion } from '$lib/apis/openai'; | ||||||
| 
 | 
 | ||||||
| 	let stopResponseFlag = false; | 	let stopResponseFlag = false; | ||||||
| 	let autoScroll = true; | 	let autoScroll = true; | ||||||
|  | @ -321,188 +322,171 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const sendPromptOpenAI = async (model, userPrompt, parentId, _chatId) => { | 	const sendPromptOpenAI = async (model, userPrompt, parentId, _chatId) => { | ||||||
| 		if ($settings.OPENAI_API_KEY) { | 		let responseMessageId = uuidv4(); | ||||||
| 			if (models) { |  | ||||||
| 				let responseMessageId = uuidv4(); |  | ||||||
| 
 | 
 | ||||||
| 				let responseMessage = { | 		let responseMessage = { | ||||||
| 					parentId: parentId, | 			parentId: parentId, | ||||||
| 					id: responseMessageId, | 			id: responseMessageId, | ||||||
| 					childrenIds: [], | 			childrenIds: [], | ||||||
| 					role: 'assistant', | 			role: 'assistant', | ||||||
| 					content: '', | 			content: '', | ||||||
| 					model: model | 			model: model | ||||||
| 				}; | 		}; | ||||||
| 
 | 
 | ||||||
| 				history.messages[responseMessageId] = responseMessage; | 		history.messages[responseMessageId] = responseMessage; | ||||||
| 				history.currentId = responseMessageId; | 		history.currentId = responseMessageId; | ||||||
| 				if (parentId !== null) { | 		if (parentId !== null) { | ||||||
| 					history.messages[parentId].childrenIds = [ | 			history.messages[parentId].childrenIds = [ | ||||||
| 						...history.messages[parentId].childrenIds, | 				...history.messages[parentId].childrenIds, | ||||||
| 						responseMessageId | 				responseMessageId | ||||||
| 					]; | 			]; | ||||||
| 				} | 		} | ||||||
| 
 | 
 | ||||||
| 				window.scrollTo({ top: document.body.scrollHeight }); | 		window.scrollTo({ top: document.body.scrollHeight }); | ||||||
| 
 | 
 | ||||||
| 				const res = await fetch( | 		const res = await generateOpenAIChatCompletion(localStorage.token, { | ||||||
| 					`${$settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'}/chat/completions`, | 			model: model, | ||||||
| 					{ | 			stream: true, | ||||||
| 						method: 'POST', | 			messages: [ | ||||||
| 						headers: { | 				$settings.system | ||||||
| 							Authorization: `Bearer ${$settings.OPENAI_API_KEY}`, | 					? { | ||||||
| 							'Content-Type': 'application/json' | 							role: 'system', | ||||||
| 						}, | 							content: $settings.system | ||||||
| 						body: JSON.stringify({ | 					  } | ||||||
| 							model: model, | 					: undefined, | ||||||
| 							stream: true, | 				...messages | ||||||
| 							messages: [ | 			] | ||||||
| 								$settings.system | 				.filter((message) => message) | ||||||
| 									? { | 				.map((message) => ({ | ||||||
| 											role: 'system', | 					role: message.role, | ||||||
| 											content: $settings.system | 					...(message.files | ||||||
| 									  } | 						? { | ||||||
| 									: undefined, | 								content: [ | ||||||
| 								...messages | 									{ | ||||||
| 							] | 										type: 'text', | ||||||
| 								.filter((message) => message) | 										text: message.content | ||||||
| 								.map((message) => ({ | 									}, | ||||||
| 									role: message.role, | 									...message.files | ||||||
| 									...(message.files | 										.filter((file) => file.type === 'image') | ||||||
| 										? { | 										.map((file) => ({ | ||||||
| 												content: [ | 											type: 'image_url', | ||||||
| 													{ | 											image_url: { | ||||||
| 														type: 'text', | 												url: file.url | ||||||
| 														text: message.content | 											} | ||||||
| 													}, | 										})) | ||||||
| 													...message.files | 								] | ||||||
| 														.filter((file) => file.type === 'image') | 						  } | ||||||
| 														.map((file) => ({ | 						: { content: message.content }) | ||||||
| 															type: 'image_url', | 				})), | ||||||
| 															image_url: { | 			seed: $settings?.options?.seed ?? undefined, | ||||||
| 																url: file.url | 			stop: $settings?.options?.stop ?? undefined, | ||||||
| 															} | 			temperature: $settings?.options?.temperature ?? undefined, | ||||||
| 														})) | 			top_p: $settings?.options?.top_p ?? undefined, | ||||||
| 												] | 			num_ctx: $settings?.options?.num_ctx ?? undefined, | ||||||
| 										  } | 			frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, | ||||||
| 										: { content: message.content }) | 			max_tokens: $settings?.options?.num_predict ?? undefined | ||||||
| 								})), | 		}); | ||||||
| 							seed: $settings?.options?.seed ?? undefined, |  | ||||||
| 							stop: $settings?.options?.stop ?? undefined, |  | ||||||
| 							temperature: $settings?.options?.temperature ?? undefined, |  | ||||||
| 							top_p: $settings?.options?.top_p ?? undefined, |  | ||||||
| 							num_ctx: $settings?.options?.num_ctx ?? undefined, |  | ||||||
| 							frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, |  | ||||||
| 							max_tokens: $settings?.options?.num_predict ?? undefined |  | ||||||
| 						}) |  | ||||||
| 					} |  | ||||||
| 				).catch((err) => { |  | ||||||
| 					console.log(err); |  | ||||||
| 					return null; |  | ||||||
| 				}); |  | ||||||
| 
 | 
 | ||||||
| 				if (res && res.ok) { | 		if (res && res.ok) { | ||||||
| 					const reader = res.body | 			const reader = res.body | ||||||
| 						.pipeThrough(new TextDecoderStream()) | 				.pipeThrough(new TextDecoderStream()) | ||||||
| 						.pipeThrough(splitStream('\n')) | 				.pipeThrough(splitStream('\n')) | ||||||
| 						.getReader(); | 				.getReader(); | ||||||
| 
 | 
 | ||||||
| 					while (true) { | 			while (true) { | ||||||
| 						const { value, done } = await reader.read(); | 				const { value, done } = await reader.read(); | ||||||
| 						if (done || stopResponseFlag || _chatId !== $chatId) { | 				if (done || stopResponseFlag || _chatId !== $chatId) { | ||||||
| 							responseMessage.done = true; |  | ||||||
| 							messages = messages; |  | ||||||
| 							break; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						try { |  | ||||||
| 							let lines = value.split('\n'); |  | ||||||
| 
 |  | ||||||
| 							for (const line of lines) { |  | ||||||
| 								if (line !== '') { |  | ||||||
| 									console.log(line); |  | ||||||
| 									if (line === 'data: [DONE]') { |  | ||||||
| 										responseMessage.done = true; |  | ||||||
| 										messages = messages; |  | ||||||
| 									} else { |  | ||||||
| 										let data = JSON.parse(line.replace(/^data: /, '')); |  | ||||||
| 										console.log(data); |  | ||||||
| 
 |  | ||||||
| 										if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { |  | ||||||
| 											continue; |  | ||||||
| 										} else { |  | ||||||
| 											responseMessage.content += data.choices[0].delta.content ?? ''; |  | ||||||
| 											messages = messages; |  | ||||||
| 										} |  | ||||||
| 									} |  | ||||||
| 								} |  | ||||||
| 							} |  | ||||||
| 						} catch (error) { |  | ||||||
| 							console.log(error); |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						if ($settings.notificationEnabled && !document.hasFocus()) { |  | ||||||
| 							const notification = new Notification(`OpenAI ${model}`, { |  | ||||||
| 								body: responseMessage.content, |  | ||||||
| 								icon: '/favicon.png' |  | ||||||
| 							}); |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						if ($settings.responseAutoCopy) { |  | ||||||
| 							copyToClipboard(responseMessage.content); |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						if (autoScroll) { |  | ||||||
| 							window.scrollTo({ top: document.body.scrollHeight }); |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					if ($chatId == _chatId) { |  | ||||||
| 						chat = await updateChatById(localStorage.token, _chatId, { |  | ||||||
| 							messages: messages, |  | ||||||
| 							history: history |  | ||||||
| 						}); |  | ||||||
| 						await chats.set(await getChatList(localStorage.token)); |  | ||||||
| 					} |  | ||||||
| 				} else { |  | ||||||
| 					if (res !== null) { |  | ||||||
| 						const error = await res.json(); |  | ||||||
| 						console.log(error); |  | ||||||
| 						if ('detail' in error) { |  | ||||||
| 							toast.error(error.detail); |  | ||||||
| 							responseMessage.content = error.detail; |  | ||||||
| 						} else { |  | ||||||
| 							if ('message' in error.error) { |  | ||||||
| 								toast.error(error.error.message); |  | ||||||
| 								responseMessage.content = error.error.message; |  | ||||||
| 							} else { |  | ||||||
| 								toast.error(error.error); |  | ||||||
| 								responseMessage.content = error.error; |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					} else { |  | ||||||
| 						toast.error(`Uh-oh! There was an issue connecting to ${model}.`); |  | ||||||
| 						responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					responseMessage.error = true; |  | ||||||
| 					responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; |  | ||||||
| 					responseMessage.done = true; | 					responseMessage.done = true; | ||||||
| 					messages = messages; | 					messages = messages; | ||||||
|  | 					break; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				stopResponseFlag = false; | 				try { | ||||||
| 				await tick(); | 					let lines = value.split('\n'); | ||||||
|  | 
 | ||||||
|  | 					for (const line of lines) { | ||||||
|  | 						if (line !== '') { | ||||||
|  | 							console.log(line); | ||||||
|  | 							if (line === 'data: [DONE]') { | ||||||
|  | 								responseMessage.done = true; | ||||||
|  | 								messages = messages; | ||||||
|  | 							} else { | ||||||
|  | 								let data = JSON.parse(line.replace(/^data: /, '')); | ||||||
|  | 								console.log(data); | ||||||
|  | 
 | ||||||
|  | 								if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { | ||||||
|  | 									continue; | ||||||
|  | 								} else { | ||||||
|  | 									responseMessage.content += data.choices[0].delta.content ?? ''; | ||||||
|  | 									messages = messages; | ||||||
|  | 								} | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} catch (error) { | ||||||
|  | 					console.log(error); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if ($settings.notificationEnabled && !document.hasFocus()) { | ||||||
|  | 					const notification = new Notification(`OpenAI ${model}`, { | ||||||
|  | 						body: responseMessage.content, | ||||||
|  | 						icon: '/favicon.png' | ||||||
|  | 					}); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if ($settings.responseAutoCopy) { | ||||||
|  | 					copyToClipboard(responseMessage.content); | ||||||
|  | 				} | ||||||
| 
 | 
 | ||||||
| 				if (autoScroll) { | 				if (autoScroll) { | ||||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | 					window.scrollTo({ top: document.body.scrollHeight }); | ||||||
| 				} | 				} | ||||||
| 
 |  | ||||||
| 				if (messages.length == 2) { |  | ||||||
| 					window.history.replaceState(history.state, '', `/c/${_chatId}`); |  | ||||||
| 					await setChatTitle(_chatId, userPrompt); |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
|  | 			if ($chatId == _chatId) { | ||||||
|  | 				chat = await updateChatById(localStorage.token, _chatId, { | ||||||
|  | 					messages: messages, | ||||||
|  | 					history: history | ||||||
|  | 				}); | ||||||
|  | 				await chats.set(await getChatList(localStorage.token)); | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			if (res !== null) { | ||||||
|  | 				const error = await res.json(); | ||||||
|  | 				console.log(error); | ||||||
|  | 				if ('detail' in error) { | ||||||
|  | 					toast.error(error.detail); | ||||||
|  | 					responseMessage.content = error.detail; | ||||||
|  | 				} else { | ||||||
|  | 					if ('message' in error.error) { | ||||||
|  | 						toast.error(error.error.message); | ||||||
|  | 						responseMessage.content = error.error.message; | ||||||
|  | 					} else { | ||||||
|  | 						toast.error(error.error); | ||||||
|  | 						responseMessage.content = error.error; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				toast.error(`Uh-oh! There was an issue connecting to ${model}.`); | ||||||
|  | 				responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			responseMessage.error = true; | ||||||
|  | 			responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; | ||||||
|  | 			responseMessage.done = true; | ||||||
|  | 			messages = messages; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		stopResponseFlag = false; | ||||||
|  | 		await tick(); | ||||||
|  | 
 | ||||||
|  | 		if (autoScroll) { | ||||||
|  | 			window.scrollTo({ top: document.body.scrollHeight }); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (messages.length == 2) { | ||||||
|  | 			window.history.replaceState(history.state, '', `/c/${_chatId}`); | ||||||
|  | 			await setChatTitle(_chatId, userPrompt); | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,6 +9,8 @@ | ||||||
| 	import { models, modelfiles, user, settings, chats, chatId } from '$lib/stores'; | 	import { models, modelfiles, user, settings, chats, chatId } from '$lib/stores'; | ||||||
| 
 | 
 | ||||||
| 	import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; | 	import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; | ||||||
|  | 	import { generateOpenAIChatCompletion } from '$lib/apis/openai'; | ||||||
|  | 
 | ||||||
| 	import { copyToClipboard, splitStream } from '$lib/utils'; | 	import { copyToClipboard, splitStream } from '$lib/utils'; | ||||||
| 
 | 
 | ||||||
| 	import MessageInput from '$lib/components/chat/MessageInput.svelte'; | 	import MessageInput from '$lib/components/chat/MessageInput.svelte'; | ||||||
|  | @ -338,188 +340,171 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const sendPromptOpenAI = async (model, userPrompt, parentId, _chatId) => { | 	const sendPromptOpenAI = async (model, userPrompt, parentId, _chatId) => { | ||||||
| 		if ($settings.OPENAI_API_KEY) { | 		let responseMessageId = uuidv4(); | ||||||
| 			if (models) { |  | ||||||
| 				let responseMessageId = uuidv4(); |  | ||||||
| 
 | 
 | ||||||
| 				let responseMessage = { | 		let responseMessage = { | ||||||
| 					parentId: parentId, | 			parentId: parentId, | ||||||
| 					id: responseMessageId, | 			id: responseMessageId, | ||||||
| 					childrenIds: [], | 			childrenIds: [], | ||||||
| 					role: 'assistant', | 			role: 'assistant', | ||||||
| 					content: '', | 			content: '', | ||||||
| 					model: model | 			model: model | ||||||
| 				}; | 		}; | ||||||
| 
 | 
 | ||||||
| 				history.messages[responseMessageId] = responseMessage; | 		history.messages[responseMessageId] = responseMessage; | ||||||
| 				history.currentId = responseMessageId; | 		history.currentId = responseMessageId; | ||||||
| 				if (parentId !== null) { | 		if (parentId !== null) { | ||||||
| 					history.messages[parentId].childrenIds = [ | 			history.messages[parentId].childrenIds = [ | ||||||
| 						...history.messages[parentId].childrenIds, | 				...history.messages[parentId].childrenIds, | ||||||
| 						responseMessageId | 				responseMessageId | ||||||
| 					]; | 			]; | ||||||
| 				} | 		} | ||||||
| 
 | 
 | ||||||
| 				window.scrollTo({ top: document.body.scrollHeight }); | 		window.scrollTo({ top: document.body.scrollHeight }); | ||||||
| 
 | 
 | ||||||
| 				const res = await fetch( | 		const res = await generateOpenAIChatCompletion(localStorage.token, { | ||||||
| 					`${$settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'}/chat/completions`, | 			model: model, | ||||||
| 					{ | 			stream: true, | ||||||
| 						method: 'POST', | 			messages: [ | ||||||
| 						headers: { | 				$settings.system | ||||||
| 							Authorization: `Bearer ${$settings.OPENAI_API_KEY}`, | 					? { | ||||||
| 							'Content-Type': 'application/json' | 							role: 'system', | ||||||
| 						}, | 							content: $settings.system | ||||||
| 						body: JSON.stringify({ | 					  } | ||||||
| 							model: model, | 					: undefined, | ||||||
| 							stream: true, | 				...messages | ||||||
| 							messages: [ | 			] | ||||||
| 								$settings.system | 				.filter((message) => message) | ||||||
| 									? { | 				.map((message) => ({ | ||||||
| 											role: 'system', | 					role: message.role, | ||||||
| 											content: $settings.system | 					...(message.files | ||||||
| 									  } | 						? { | ||||||
| 									: undefined, | 								content: [ | ||||||
| 								...messages | 									{ | ||||||
| 							] | 										type: 'text', | ||||||
| 								.filter((message) => message) | 										text: message.content | ||||||
| 								.map((message) => ({ | 									}, | ||||||
| 									role: message.role, | 									...message.files | ||||||
| 									...(message.files | 										.filter((file) => file.type === 'image') | ||||||
| 										? { | 										.map((file) => ({ | ||||||
| 												content: [ | 											type: 'image_url', | ||||||
| 													{ | 											image_url: { | ||||||
| 														type: 'text', | 												url: file.url | ||||||
| 														text: message.content | 											} | ||||||
| 													}, | 										})) | ||||||
| 													...message.files | 								] | ||||||
| 														.filter((file) => file.type === 'image') | 						  } | ||||||
| 														.map((file) => ({ | 						: { content: message.content }) | ||||||
| 															type: 'image_url', | 				})), | ||||||
| 															image_url: { | 			seed: $settings?.options?.seed ?? undefined, | ||||||
| 																url: file.url | 			stop: $settings?.options?.stop ?? undefined, | ||||||
| 															} | 			temperature: $settings?.options?.temperature ?? undefined, | ||||||
| 														})) | 			top_p: $settings?.options?.top_p ?? undefined, | ||||||
| 												] | 			num_ctx: $settings?.options?.num_ctx ?? undefined, | ||||||
| 										  } | 			frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, | ||||||
| 										: { content: message.content }) | 			max_tokens: $settings?.options?.num_predict ?? undefined | ||||||
| 								})), | 		}); | ||||||
| 							seed: $settings?.options?.seed ?? undefined, |  | ||||||
| 							stop: $settings?.options?.stop ?? undefined, |  | ||||||
| 							temperature: $settings?.options?.temperature ?? undefined, |  | ||||||
| 							top_p: $settings?.options?.top_p ?? undefined, |  | ||||||
| 							num_ctx: $settings?.options?.num_ctx ?? undefined, |  | ||||||
| 							frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, |  | ||||||
| 							max_tokens: $settings?.options?.num_predict ?? undefined |  | ||||||
| 						}) |  | ||||||
| 					} |  | ||||||
| 				).catch((err) => { |  | ||||||
| 					console.log(err); |  | ||||||
| 					return null; |  | ||||||
| 				}); |  | ||||||
| 
 | 
 | ||||||
| 				if (res && res.ok) { | 		if (res && res.ok) { | ||||||
| 					const reader = res.body | 			const reader = res.body | ||||||
| 						.pipeThrough(new TextDecoderStream()) | 				.pipeThrough(new TextDecoderStream()) | ||||||
| 						.pipeThrough(splitStream('\n')) | 				.pipeThrough(splitStream('\n')) | ||||||
| 						.getReader(); | 				.getReader(); | ||||||
| 
 | 
 | ||||||
| 					while (true) { | 			while (true) { | ||||||
| 						const { value, done } = await reader.read(); | 				const { value, done } = await reader.read(); | ||||||
| 						if (done || stopResponseFlag || _chatId !== $chatId) { | 				if (done || stopResponseFlag || _chatId !== $chatId) { | ||||||
| 							responseMessage.done = true; |  | ||||||
| 							messages = messages; |  | ||||||
| 							break; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						try { |  | ||||||
| 							let lines = value.split('\n'); |  | ||||||
| 
 |  | ||||||
| 							for (const line of lines) { |  | ||||||
| 								if (line !== '') { |  | ||||||
| 									console.log(line); |  | ||||||
| 									if (line === 'data: [DONE]') { |  | ||||||
| 										responseMessage.done = true; |  | ||||||
| 										messages = messages; |  | ||||||
| 									} else { |  | ||||||
| 										let data = JSON.parse(line.replace(/^data: /, '')); |  | ||||||
| 										console.log(data); |  | ||||||
| 
 |  | ||||||
| 										if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { |  | ||||||
| 											continue; |  | ||||||
| 										} else { |  | ||||||
| 											responseMessage.content += data.choices[0].delta.content ?? ''; |  | ||||||
| 											messages = messages; |  | ||||||
| 										} |  | ||||||
| 									} |  | ||||||
| 								} |  | ||||||
| 							} |  | ||||||
| 						} catch (error) { |  | ||||||
| 							console.log(error); |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						if ($settings.notificationEnabled && !document.hasFocus()) { |  | ||||||
| 							const notification = new Notification(`OpenAI ${model}`, { |  | ||||||
| 								body: responseMessage.content, |  | ||||||
| 								icon: '/favicon.png' |  | ||||||
| 							}); |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						if ($settings.responseAutoCopy) { |  | ||||||
| 							copyToClipboard(responseMessage.content); |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						if (autoScroll) { |  | ||||||
| 							window.scrollTo({ top: document.body.scrollHeight }); |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					if ($chatId == _chatId) { |  | ||||||
| 						chat = await updateChatById(localStorage.token, _chatId, { |  | ||||||
| 							messages: messages, |  | ||||||
| 							history: history |  | ||||||
| 						}); |  | ||||||
| 						await chats.set(await getChatList(localStorage.token)); |  | ||||||
| 					} |  | ||||||
| 				} else { |  | ||||||
| 					if (res !== null) { |  | ||||||
| 						const error = await res.json(); |  | ||||||
| 						console.log(error); |  | ||||||
| 						if ('detail' in error) { |  | ||||||
| 							toast.error(error.detail); |  | ||||||
| 							responseMessage.content = error.detail; |  | ||||||
| 						} else { |  | ||||||
| 							if ('message' in error.error) { |  | ||||||
| 								toast.error(error.error.message); |  | ||||||
| 								responseMessage.content = error.error.message; |  | ||||||
| 							} else { |  | ||||||
| 								toast.error(error.error); |  | ||||||
| 								responseMessage.content = error.error; |  | ||||||
| 							} |  | ||||||
| 						} |  | ||||||
| 					} else { |  | ||||||
| 						toast.error(`Uh-oh! There was an issue connecting to ${model}.`); |  | ||||||
| 						responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; |  | ||||||
| 					} |  | ||||||
| 
 |  | ||||||
| 					responseMessage.error = true; |  | ||||||
| 					responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; |  | ||||||
| 					responseMessage.done = true; | 					responseMessage.done = true; | ||||||
| 					messages = messages; | 					messages = messages; | ||||||
|  | 					break; | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				stopResponseFlag = false; | 				try { | ||||||
| 				await tick(); | 					let lines = value.split('\n'); | ||||||
|  | 
 | ||||||
|  | 					for (const line of lines) { | ||||||
|  | 						if (line !== '') { | ||||||
|  | 							console.log(line); | ||||||
|  | 							if (line === 'data: [DONE]') { | ||||||
|  | 								responseMessage.done = true; | ||||||
|  | 								messages = messages; | ||||||
|  | 							} else { | ||||||
|  | 								let data = JSON.parse(line.replace(/^data: /, '')); | ||||||
|  | 								console.log(data); | ||||||
|  | 
 | ||||||
|  | 								if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { | ||||||
|  | 									continue; | ||||||
|  | 								} else { | ||||||
|  | 									responseMessage.content += data.choices[0].delta.content ?? ''; | ||||||
|  | 									messages = messages; | ||||||
|  | 								} | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} catch (error) { | ||||||
|  | 					console.log(error); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if ($settings.notificationEnabled && !document.hasFocus()) { | ||||||
|  | 					const notification = new Notification(`OpenAI ${model}`, { | ||||||
|  | 						body: responseMessage.content, | ||||||
|  | 						icon: '/favicon.png' | ||||||
|  | 					}); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if ($settings.responseAutoCopy) { | ||||||
|  | 					copyToClipboard(responseMessage.content); | ||||||
|  | 				} | ||||||
| 
 | 
 | ||||||
| 				if (autoScroll) { | 				if (autoScroll) { | ||||||
| 					window.scrollTo({ top: document.body.scrollHeight }); | 					window.scrollTo({ top: document.body.scrollHeight }); | ||||||
| 				} | 				} | ||||||
| 
 |  | ||||||
| 				if (messages.length == 2) { |  | ||||||
| 					window.history.replaceState(history.state, '', `/c/${_chatId}`); |  | ||||||
| 					await setChatTitle(_chatId, userPrompt); |  | ||||||
| 				} |  | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
|  | 			if ($chatId == _chatId) { | ||||||
|  | 				chat = await updateChatById(localStorage.token, _chatId, { | ||||||
|  | 					messages: messages, | ||||||
|  | 					history: history | ||||||
|  | 				}); | ||||||
|  | 				await chats.set(await getChatList(localStorage.token)); | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			if (res !== null) { | ||||||
|  | 				const error = await res.json(); | ||||||
|  | 				console.log(error); | ||||||
|  | 				if ('detail' in error) { | ||||||
|  | 					toast.error(error.detail); | ||||||
|  | 					responseMessage.content = error.detail; | ||||||
|  | 				} else { | ||||||
|  | 					if ('message' in error.error) { | ||||||
|  | 						toast.error(error.error.message); | ||||||
|  | 						responseMessage.content = error.error.message; | ||||||
|  | 					} else { | ||||||
|  | 						toast.error(error.error); | ||||||
|  | 						responseMessage.content = error.error; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				toast.error(`Uh-oh! There was an issue connecting to ${model}.`); | ||||||
|  | 				responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			responseMessage.error = true; | ||||||
|  | 			responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; | ||||||
|  | 			responseMessage.done = true; | ||||||
|  | 			messages = messages; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		stopResponseFlag = false; | ||||||
|  | 		await tick(); | ||||||
|  | 
 | ||||||
|  | 		if (autoScroll) { | ||||||
|  | 			window.scrollTo({ top: document.body.scrollHeight }); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (messages.length == 2) { | ||||||
|  | 			window.history.replaceState(history.state, '', `/c/${_chatId}`); | ||||||
|  | 			await setChatTitle(_chatId, userPrompt); | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek