diff --git a/.dockerignore b/.dockerignore index 58cf1f0f..e28863bf 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,7 +7,6 @@ node_modules /package .env .env.* -!.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* __pycache__ diff --git a/CHANGELOG.md b/CHANGELOG.md index afbe6775..c1c6596a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.110] - 2024-03-06 + +### Added + +- **🌐 Multiple OpenAI Servers Support**: Enjoy seamless integration with multiple OpenAI-compatible APIs, now supported natively. + +### Fixed + +- **🔍 OCR Issue**: Resolved PDF parsing issue caused by OCR malfunction. +- **🚫 RAG Issue**: Fixed the RAG functionality, ensuring it operates smoothly. +- **📄 "Add Docs" Model Button**: Addressed the non-functional behavior of the "Add Docs" model button. + ## [0.1.109] - 2024-03-06 ### Added diff --git a/Dockerfile b/Dockerfile index 2d2f522a..99fd0e61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,6 +53,8 @@ WORKDIR /app/backend # install python dependencies COPY ./backend/requirements.txt ./requirements.txt +RUN apt-get update && apt-get install ffmpeg libsm6 libxext6 -y + RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir RUN pip3 install -r requirements.txt --no-cache-dir diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index f4442d04..f8f166d0 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -222,7 +222,7 @@ async def pull_model( r = requests.request( method="POST", url=f"{url}/api/pull", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), stream=True, ) @@ -294,7 +294,7 @@ async def push_model( r = requests.request( method="POST", url=f"{url}/api/push", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), ) r.raise_for_status() @@ -356,7 +356,7 @@ async def create_model( r = requests.request( method="POST", url=f"{url}/api/create", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), stream=True, ) @@ -419,7 +419,7 @@ async def copy_model( r = requests.request( method="POST", url=f"{url}/api/copy", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), ) r.raise_for_status() @@ -466,7 +466,7 @@ async def delete_model( r = requests.request( method="DELETE", url=f"{url}/api/delete", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), ) r.raise_for_status() @@ -506,7 +506,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use r = requests.request( method="POST", url=f"{url}/api/show", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), ) r.raise_for_status() @@ -558,7 +558,7 @@ async def generate_embeddings( r = requests.request( method="POST", url=f"{url}/api/embeddings", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), ) r.raise_for_status() @@ -644,7 +644,7 @@ async def generate_completion( r = requests.request( method="POST", url=f"{url}/api/generate", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), stream=True, ) @@ -714,7 +714,7 @@ async def generate_chat_completion( r = None - print(form_data.model_dump_json(exclude_none=True)) + print(form_data.model_dump_json(exclude_none=True).encode()) def get_request(): nonlocal form_data @@ -744,7 +744,7 @@ async def generate_chat_completion( r = requests.request( method="POST", url=f"{url}/api/chat", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), stream=True, ) @@ -756,6 +756,7 @@ async def generate_chat_completion( headers=dict(r.headers), ) except Exception as e: + print(e) raise e try: @@ -843,7 +844,7 @@ async def generate_openai_chat_completion( r = requests.request( method="POST", url=f"{url}/v1/chat/completions", - data=form_data.model_dump_json(exclude_none=True), + data=form_data.model_dump_json(exclude_none=True).encode(), stream=True, ) diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index 8d6fdb50..6b9c542e 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -3,7 +3,10 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse, JSONResponse, FileResponse import requests +import aiohttp +import asyncio import json + from pydantic import BaseModel @@ -15,7 +18,9 @@ from utils.utils import ( get_verified_user, get_admin_user, ) -from config import OPENAI_API_BASE_URL, OPENAI_API_KEY, CACHE_DIR +from config import OPENAI_API_BASE_URLS, OPENAI_API_KEYS, CACHE_DIR +from typing import List, Optional + import hashlib from pathlib import Path @@ -29,116 +34,207 @@ app.add_middleware( allow_headers=["*"], ) -app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL -app.state.OPENAI_API_KEY = OPENAI_API_KEY +app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS +app.state.OPENAI_API_KEYS = OPENAI_API_KEYS + +app.state.MODELS = {} -class UrlUpdateForm(BaseModel): - url: str +@app.middleware("http") +async def check_url(request: Request, call_next): + if len(app.state.MODELS) == 0: + await get_all_models() + else: + pass + + response = await call_next(request) + return response -class KeyUpdateForm(BaseModel): - key: str +class UrlsUpdateForm(BaseModel): + urls: List[str] -@app.get("/url") -async def get_openai_url(user=Depends(get_admin_user)): - return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} +class KeysUpdateForm(BaseModel): + keys: List[str] -@app.post("/url/update") -async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)): - app.state.OPENAI_API_BASE_URL = form_data.url - return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} +@app.get("/urls") +async def get_openai_urls(user=Depends(get_admin_user)): + return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS} -@app.get("/key") -async def get_openai_key(user=Depends(get_admin_user)): - return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} +@app.post("/urls/update") +async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)): + app.state.OPENAI_API_BASE_URLS = form_data.urls + return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS} -@app.post("/key/update") -async def update_openai_key(form_data: KeyUpdateForm, user=Depends(get_admin_user)): - app.state.OPENAI_API_KEY = form_data.key - return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} +@app.get("/keys") +async def get_openai_keys(user=Depends(get_admin_user)): + return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS} + + +@app.post("/keys/update") +async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)): + app.state.OPENAI_API_KEYS = form_data.keys + return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS} @app.post("/audio/speech") async def speech(request: Request, user=Depends(get_verified_user)): - target_url = f"{app.state.OPENAI_API_BASE_URL}/audio/speech" - - if app.state.OPENAI_API_KEY == "": - raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) - - body = await request.body() - - name = hashlib.sha256(body).hexdigest() - - SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/") - SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) - file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3") - file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json") - - # Check if the file already exists in the cache - if file_path.is_file(): - return FileResponse(file_path) - - headers = {} - headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" - headers["Content-Type"] = "application/json" - + idx = None try: - print("openai") - r = requests.post( - url=target_url, - data=body, - headers=headers, - stream=True, + idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1") + body = await request.body() + name = hashlib.sha256(body).hexdigest() + + SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/") + SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) + file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3") + file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json") + + # Check if the file already exists in the cache + if file_path.is_file(): + return FileResponse(file_path) + + headers = {} + headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}" + headers["Content-Type"] = "application/json" + + try: + r = requests.post( + url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech", + data=body, + headers=headers, + stream=True, + ) + + r.raise_for_status() + + # Save the streaming content to a file + with open(file_path, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + with open(file_body_path, "w") as f: + json.dump(json.loads(body.decode("utf-8")), f) + + # Return the saved file + return FileResponse(file_path) + + except Exception as e: + print(e) + error_detail = "Open 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) + + except ValueError: + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND) + + +async def fetch_url(url, key): + try: + headers = {"Authorization": f"Bearer {key}"} + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers) as response: + return await response.json() + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + return None + + +def merge_models_lists(model_lists): + merged_list = [] + + for idx, models in enumerate(model_lists): + merged_list.extend( + [ + {**model, "urlIdx": idx} + for model in models + if "api.openai.com" not in app.state.OPENAI_API_BASE_URLS[idx] + or "gpt" in model["id"] + ] ) - r.raise_for_status() + return merged_list - # Save the streaming content to a file - with open(file_path, "wb") as f: - for chunk in r.iter_content(chunk_size=8192): - f.write(chunk) - with open(file_body_path, "w") as f: - json.dump(json.loads(body.decode("utf-8")), f) +async def get_all_models(): + print("get_all_models") + tasks = [ + fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx]) + for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS) + ] + responses = await asyncio.gather(*tasks) + responses = list(filter(lambda x: x is not None and "error" not in x, responses)) + models = { + "data": merge_models_lists( + list(map(lambda response: response["data"], responses)) + ) + } + app.state.MODELS = {model["id"]: model for model in models["data"]} - # Return the saved file - return FileResponse(file_path) + return models - except Exception as e: - print(e) - error_detail = "Open 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) +# , user=Depends(get_current_user) +@app.get("/models") +@app.get("/models/{url_idx}") +async def get_models(url_idx: Optional[int] = None): + if url_idx == None: + return await get_all_models() + else: + url = app.state.OPENAI_API_BASE_URLS[url_idx] + try: + r = requests.request(method="GET", url=f"{url}/models") + r.raise_for_status() + + response_data = r.json() + if "api.openai.com" in url: + 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 = "Open 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 if r else 500, + detail=error_detail, + ) @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) async def proxy(path: str, request: Request, user=Depends(get_verified_user)): - target_url = f"{app.state.OPENAI_API_BASE_URL}/{path}" - print(target_url, app.state.OPENAI_API_KEY) - - if app.state.OPENAI_API_KEY == "": - raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + idx = 0 body = await request.body() - # TODO: Remove below after gpt-4-vision fix from Open AI # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision) try: body = body.decode("utf-8") body = json.loads(body) + idx = app.state.MODELS[body.get("model")]["urlIdx"] + # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000 # This is a workaround until OpenAI fixes the issue with this model if body.get("model") == "gpt-4-vision-preview": @@ -158,8 +254,16 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): except json.JSONDecodeError as e: print("Error loading request body into a dictionary:", e) + url = app.state.OPENAI_API_BASE_URLS[idx] + key = app.state.OPENAI_API_KEYS[idx] + + target_url = f"{url}/{path}" + + if key == "": + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + headers = {} - headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" + headers["Authorization"] = f"Bearer {key}" headers["Content-Type"] = "application/json" try: @@ -181,21 +285,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): 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() - - if "api.openai.com" 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) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index 99aa6959..45ad6970 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -425,7 +425,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str): ] if file_ext == "pdf": - loader = PyPDFLoader(file_path) + loader = PyPDFLoader(file_path, extract_images=True) elif file_ext == "csv": loader = CSVLoader(file_path) elif file_ext == "rst": diff --git a/backend/config.py b/backend/config.py index 8373dc12..2cd01653 100644 --- a/backend/config.py +++ b/backend/config.py @@ -234,9 +234,25 @@ OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")] OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") +if OPENAI_API_KEY == "": + OPENAI_API_KEY = "none" + if OPENAI_API_BASE_URL == "": OPENAI_API_BASE_URL = "https://api.openai.com/v1" +OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "") +OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY + +OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")] + + +OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "") +OPENAI_API_BASE_URLS = ( + OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL +) + +OPENAI_API_BASE_URLS = [url.strip() for url in OPENAI_API_BASE_URL.split(";")] + #################################### # WEBUI diff --git a/backend/constants.py b/backend/constants.py index b2bbe9aa..eacf8a20 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -41,6 +41,7 @@ class ERROR_MESSAGES(str, Enum): 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." PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance." @@ -50,3 +51,4 @@ class ERROR_MESSAGES(str, Enum): RATE_LIMIT_EXCEEDED = "API rate limit exceeded" MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" + OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found" diff --git a/backend/requirements.txt b/backend/requirements.txt index 190d1298..41527a78 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -34,6 +34,8 @@ pandas openpyxl pyxlsb xlrd + +opencv-python-headless rapidocr-onnxruntime faster-whisper diff --git a/package.json b/package.json index 98079399..bd1eb716 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "open-webui", - "version": "0.1.109", + "version": "0.1.110", "private": true, "scripts": { "dev": "vite dev --host", diff --git a/src/app.css b/src/app.css index 63b34465..82b3caa3 100644 --- a/src/app.css +++ b/src/app.css @@ -43,6 +43,10 @@ ol > li { font-weight: 400; } +li p { + display: inline; +} + ::-webkit-scrollbar-thumb { --tw-border-opacity: 1; background-color: rgba(217, 217, 227, 0.8); diff --git a/src/lib/apis/openai/index.ts b/src/lib/apis/openai/index.ts index 3a629eb3..e38314a5 100644 --- a/src/lib/apis/openai/index.ts +++ b/src/lib/apis/openai/index.ts @@ -1,9 +1,9 @@ import { OPENAI_API_BASE_URL } from '$lib/constants'; -export const getOpenAIUrl = async (token: string = '') => { +export const getOpenAIUrls = async (token: string = '') => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/url`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/urls`, { method: 'GET', headers: { Accept: 'application/json', @@ -29,13 +29,13 @@ export const getOpenAIUrl = async (token: string = '') => { throw error; } - return res.OPENAI_API_BASE_URL; + return res.OPENAI_API_BASE_URLS; }; -export const updateOpenAIUrl = async (token: string = '', url: string) => { +export const updateOpenAIUrls = async (token: string = '', urls: string[]) => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/url/update`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/urls/update`, { method: 'POST', headers: { Accept: 'application/json', @@ -43,7 +43,7 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => { ...(token && { authorization: `Bearer ${token}` }) }, body: JSON.stringify({ - url: url + urls: urls }) }) .then(async (res) => { @@ -64,13 +64,13 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => { throw error; } - return res.OPENAI_API_BASE_URL; + return res.OPENAI_API_BASE_URLS; }; -export const getOpenAIKey = async (token: string = '') => { +export const getOpenAIKeys = async (token: string = '') => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/key`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/keys`, { method: 'GET', headers: { Accept: 'application/json', @@ -96,13 +96,13 @@ export const getOpenAIKey = async (token: string = '') => { throw error; } - return res.OPENAI_API_KEY; + return res.OPENAI_API_KEYS; }; -export const updateOpenAIKey = async (token: string = '', key: string) => { +export const updateOpenAIKeys = async (token: string = '', keys: string[]) => { let error = null; - const res = await fetch(`${OPENAI_API_BASE_URL}/key/update`, { + const res = await fetch(`${OPENAI_API_BASE_URL}/keys/update`, { method: 'POST', headers: { Accept: 'application/json', @@ -110,7 +110,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => { ...(token && { authorization: `Bearer ${token}` }) }, body: JSON.stringify({ - key: key + keys: keys }) }) .then(async (res) => { @@ -131,7 +131,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => { throw error; } - return res.OPENAI_API_KEY; + return res.OPENAI_API_KEYS; }; export const getOpenAIModels = async (token: string = '') => { diff --git a/src/lib/components/chat/Settings/Connections.svelte b/src/lib/components/chat/Settings/Connections.svelte index ed9ddf4d..2f0b821d 100644 --- a/src/lib/components/chat/Settings/Connections.svelte +++ b/src/lib/components/chat/Settings/Connections.svelte @@ -4,7 +4,12 @@ const dispatch = createEventDispatcher(); import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama'; - import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai'; + import { + getOpenAIKeys, + getOpenAIUrls, + updateOpenAIKeys, + updateOpenAIUrls + } from '$lib/apis/openai'; import { toast } from 'svelte-sonner'; export let getModels: Function; @@ -16,12 +21,14 @@ let OPENAI_API_KEY = ''; let OPENAI_API_BASE_URL = ''; + let OPENAI_API_KEYS = ['']; + let OPENAI_API_BASE_URLS = ['']; + let showOpenAI = false; - let showLiteLLM = false; 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); + OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS); + OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS); await models.set(await getModels()); }; @@ -43,8 +50,8 @@ onMount(async () => { if ($user.role === 'admin') { OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token); - OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token); - OPENAI_API_KEY = await getOpenAIKey(localStorage.token); + OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token); + OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token); } }); @@ -71,37 +78,74 @@ {#if showOpenAI} -