forked from open-webui/open-webui
commit
8ed5759d0e
15 changed files with 328 additions and 157 deletions
|
@ -7,7 +7,6 @@ node_modules
|
||||||
/package
|
/package
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
12
CHANGELOG.md
12
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/),
|
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).
|
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
|
## [0.1.109] - 2024-03-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -53,6 +53,8 @@ WORKDIR /app/backend
|
||||||
# install python dependencies
|
# install python dependencies
|
||||||
COPY ./backend/requirements.txt ./requirements.txt
|
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 torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir
|
||||||
RUN pip3 install -r requirements.txt --no-cache-dir
|
RUN pip3 install -r requirements.txt --no-cache-dir
|
||||||
|
|
||||||
|
|
|
@ -222,7 +222,7 @@ async def pull_model(
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/api/pull",
|
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,
|
stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -294,7 +294,7 @@ async def push_model(
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/api/push",
|
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()
|
r.raise_for_status()
|
||||||
|
@ -356,7 +356,7 @@ async def create_model(
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/api/create",
|
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,
|
stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -419,7 +419,7 @@ async def copy_model(
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/api/copy",
|
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()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@ -466,7 +466,7 @@ async def delete_model(
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="DELETE",
|
method="DELETE",
|
||||||
url=f"{url}/api/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()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@ -506,7 +506,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/api/show",
|
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()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@ -558,7 +558,7 @@ async def generate_embeddings(
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/api/embeddings",
|
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()
|
r.raise_for_status()
|
||||||
|
|
||||||
|
@ -644,7 +644,7 @@ async def generate_completion(
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/api/generate",
|
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,
|
stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -714,7 +714,7 @@ async def generate_chat_completion(
|
||||||
|
|
||||||
r = None
|
r = None
|
||||||
|
|
||||||
print(form_data.model_dump_json(exclude_none=True))
|
print(form_data.model_dump_json(exclude_none=True).encode())
|
||||||
|
|
||||||
def get_request():
|
def get_request():
|
||||||
nonlocal form_data
|
nonlocal form_data
|
||||||
|
@ -744,7 +744,7 @@ async def generate_chat_completion(
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/api/chat",
|
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,
|
stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -756,6 +756,7 @@ async def generate_chat_completion(
|
||||||
headers=dict(r.headers),
|
headers=dict(r.headers),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -843,7 +844,7 @@ async def generate_openai_chat_completion(
|
||||||
r = requests.request(
|
r = requests.request(
|
||||||
method="POST",
|
method="POST",
|
||||||
url=f"{url}/v1/chat/completions",
|
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,
|
stream=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,10 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +18,9 @@ from utils.utils import (
|
||||||
get_verified_user,
|
get_verified_user,
|
||||||
get_admin_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
|
import hashlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -29,116 +34,207 @@ app.add_middleware(
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL
|
app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
|
||||||
app.state.OPENAI_API_KEY = OPENAI_API_KEY
|
app.state.OPENAI_API_KEYS = OPENAI_API_KEYS
|
||||||
|
|
||||||
|
app.state.MODELS = {}
|
||||||
|
|
||||||
|
|
||||||
class UrlUpdateForm(BaseModel):
|
@app.middleware("http")
|
||||||
url: str
|
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):
|
class UrlsUpdateForm(BaseModel):
|
||||||
key: str
|
urls: List[str]
|
||||||
|
|
||||||
|
|
||||||
@app.get("/url")
|
class KeysUpdateForm(BaseModel):
|
||||||
async def get_openai_url(user=Depends(get_admin_user)):
|
keys: List[str]
|
||||||
return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/url/update")
|
@app.get("/urls")
|
||||||
async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
|
async def get_openai_urls(user=Depends(get_admin_user)):
|
||||||
app.state.OPENAI_API_BASE_URL = form_data.url
|
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
|
||||||
return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/key")
|
@app.post("/urls/update")
|
||||||
async def get_openai_key(user=Depends(get_admin_user)):
|
async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
|
||||||
return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
|
app.state.OPENAI_API_BASE_URLS = form_data.urls
|
||||||
|
return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/key/update")
|
@app.get("/keys")
|
||||||
async def update_openai_key(form_data: KeyUpdateForm, user=Depends(get_admin_user)):
|
async def get_openai_keys(user=Depends(get_admin_user)):
|
||||||
app.state.OPENAI_API_KEY = form_data.key
|
return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
|
||||||
return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
|
|
||||||
|
|
||||||
|
@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")
|
@app.post("/audio/speech")
|
||||||
async def speech(request: Request, user=Depends(get_verified_user)):
|
async def speech(request: Request, user=Depends(get_verified_user)):
|
||||||
target_url = f"{app.state.OPENAI_API_BASE_URL}/audio/speech"
|
idx = None
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("openai")
|
idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
|
||||||
r = requests.post(
|
body = await request.body()
|
||||||
url=target_url,
|
name = hashlib.sha256(body).hexdigest()
|
||||||
data=body,
|
|
||||||
headers=headers,
|
SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
|
||||||
stream=True,
|
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:
|
async def get_all_models():
|
||||||
json.dump(json.loads(body.decode("utf-8")), f)
|
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 models
|
||||||
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)
|
# , 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"])
|
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
||||||
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
target_url = f"{app.state.OPENAI_API_BASE_URL}/{path}"
|
idx = 0
|
||||||
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)
|
|
||||||
|
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
|
|
||||||
# TODO: Remove below after gpt-4-vision fix from Open AI
|
# 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 to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
|
||||||
try:
|
try:
|
||||||
body = body.decode("utf-8")
|
body = body.decode("utf-8")
|
||||||
body = json.loads(body)
|
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
|
# 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
|
# This is a workaround until OpenAI fixes the issue with this model
|
||||||
if body.get("model") == "gpt-4-vision-preview":
|
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:
|
except json.JSONDecodeError as e:
|
||||||
print("Error loading request body into a dictionary:", 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 = {}
|
||||||
headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
|
headers["Authorization"] = f"Bearer {key}"
|
||||||
headers["Content-Type"] = "application/json"
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -181,21 +285,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
|
||||||
headers=dict(r.headers),
|
headers=dict(r.headers),
|
||||||
)
|
)
|
||||||
else:
|
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()
|
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
|
return response_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
|
@ -425,7 +425,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
|
||||||
]
|
]
|
||||||
|
|
||||||
if file_ext == "pdf":
|
if file_ext == "pdf":
|
||||||
loader = PyPDFLoader(file_path)
|
loader = PyPDFLoader(file_path, extract_images=True)
|
||||||
elif file_ext == "csv":
|
elif file_ext == "csv":
|
||||||
loader = CSVLoader(file_path)
|
loader = CSVLoader(file_path)
|
||||||
elif file_ext == "rst":
|
elif file_ext == "rst":
|
||||||
|
|
|
@ -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_KEY = os.environ.get("OPENAI_API_KEY", "")
|
||||||
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
|
OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
|
||||||
|
|
||||||
|
if OPENAI_API_KEY == "":
|
||||||
|
OPENAI_API_KEY = "none"
|
||||||
|
|
||||||
if OPENAI_API_BASE_URL == "":
|
if OPENAI_API_BASE_URL == "":
|
||||||
OPENAI_API_BASE_URL = "https://api.openai.com/v1"
|
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
|
# WEBUI
|
||||||
|
|
|
@ -41,6 +41,7 @@ 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."
|
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."
|
||||||
|
|
||||||
PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
|
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"
|
RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
|
||||||
|
|
||||||
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
|
MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
|
||||||
|
OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found"
|
||||||
|
|
|
@ -34,6 +34,8 @@ pandas
|
||||||
openpyxl
|
openpyxl
|
||||||
pyxlsb
|
pyxlsb
|
||||||
xlrd
|
xlrd
|
||||||
|
|
||||||
|
opencv-python-headless
|
||||||
rapidocr-onnxruntime
|
rapidocr-onnxruntime
|
||||||
|
|
||||||
faster-whisper
|
faster-whisper
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "open-webui",
|
"name": "open-webui",
|
||||||
"version": "0.1.109",
|
"version": "0.1.110",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host",
|
"dev": "vite dev --host",
|
||||||
|
|
|
@ -43,6 +43,10 @@ ol > li {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li p {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
background-color: rgba(217, 217, 227, 0.8);
|
background-color: rgba(217, 217, 227, 0.8);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { OPENAI_API_BASE_URL } from '$lib/constants';
|
import { OPENAI_API_BASE_URL } from '$lib/constants';
|
||||||
|
|
||||||
export const getOpenAIUrl = async (token: string = '') => {
|
export const getOpenAIUrls = async (token: string = '') => {
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OPENAI_API_BASE_URL}/url`, {
|
const res = await fetch(`${OPENAI_API_BASE_URL}/urls`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
@ -29,13 +29,13 @@ export const getOpenAIUrl = async (token: string = '') => {
|
||||||
throw error;
|
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;
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
@ -43,7 +43,7 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => {
|
||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: url
|
urls: urls
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
@ -64,13 +64,13 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => {
|
||||||
throw error;
|
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;
|
let error = null;
|
||||||
|
|
||||||
const res = await fetch(`${OPENAI_API_BASE_URL}/key`, {
|
const res = await fetch(`${OPENAI_API_BASE_URL}/keys`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
@ -96,13 +96,13 @@ export const getOpenAIKey = async (token: string = '') => {
|
||||||
throw error;
|
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;
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
|
@ -110,7 +110,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
|
||||||
...(token && { authorization: `Bearer ${token}` })
|
...(token && { authorization: `Bearer ${token}` })
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
key: key
|
keys: keys
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
@ -131,7 +131,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.OPENAI_API_KEY;
|
return res.OPENAI_API_KEYS;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getOpenAIModels = async (token: string = '') => {
|
export const getOpenAIModels = async (token: string = '') => {
|
||||||
|
|
|
@ -4,7 +4,12 @@
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
|
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';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
export let getModels: Function;
|
export let getModels: Function;
|
||||||
|
@ -16,12 +21,14 @@
|
||||||
let OPENAI_API_KEY = '';
|
let OPENAI_API_KEY = '';
|
||||||
let OPENAI_API_BASE_URL = '';
|
let OPENAI_API_BASE_URL = '';
|
||||||
|
|
||||||
|
let OPENAI_API_KEYS = [''];
|
||||||
|
let OPENAI_API_BASE_URLS = [''];
|
||||||
|
|
||||||
let showOpenAI = false;
|
let showOpenAI = false;
|
||||||
let showLiteLLM = false;
|
|
||||||
|
|
||||||
const updateOpenAIHandler = async () => {
|
const updateOpenAIHandler = async () => {
|
||||||
OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL);
|
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
|
||||||
OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
|
OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
|
||||||
|
|
||||||
await models.set(await getModels());
|
await models.set(await getModels());
|
||||||
};
|
};
|
||||||
|
@ -43,8 +50,8 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($user.role === 'admin') {
|
if ($user.role === 'admin') {
|
||||||
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
|
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
|
||||||
OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
|
OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
|
||||||
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -71,37 +78,74 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showOpenAI}
|
{#if showOpenAI}
|
||||||
<div>
|
<div class="flex flex-col gap-1">
|
||||||
<div class=" mb-2.5 text-sm font-medium">API Key</div>
|
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||||
<div class="flex w-full">
|
<div class="flex w-full gap-2">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<input
|
<input
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
placeholder="Enter OpenAI API Key"
|
placeholder="API Base URL"
|
||||||
bind:value={OPENAI_API_KEY}
|
bind:value={url}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<div class=" mb-2.5 text-sm font-medium">API Base URL</div>
|
<input
|
||||||
<div class="flex w-full">
|
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||||
<div class="flex-1">
|
placeholder="API Key"
|
||||||
<input
|
bind:value={OPENAI_API_KEYS[idx]}
|
||||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
autocomplete="off"
|
||||||
placeholder="Enter OpenAI API Base URL"
|
/>
|
||||||
bind:value={OPENAI_API_BASE_URL}
|
</div>
|
||||||
autocomplete="off"
|
<div class="self-center flex items-center">
|
||||||
/>
|
{#if idx === 0}
|
||||||
|
<button
|
||||||
|
class="px-1"
|
||||||
|
on:click={() => {
|
||||||
|
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, ''];
|
||||||
|
OPENAI_API_KEYS = [...OPENAI_API_KEYS, ''];
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="px-1"
|
||||||
|
on:click={() => {
|
||||||
|
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
|
||||||
|
(url, urlIdx) => idx !== urlIdx
|
||||||
|
);
|
||||||
|
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="currentColor"
|
||||||
|
class="w-4 h-4"
|
||||||
|
>
|
||||||
|
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
WebUI will make requests to <span class=" text-gray-200">'{url}/models'</span>
|
||||||
WebUI will make requests to <span class=" text-gray-200"
|
</div>
|
||||||
>'{OPENAI_API_BASE_URL}/chat'</span
|
{/each}
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -138,7 +138,9 @@
|
||||||
<button
|
<button
|
||||||
class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
|
class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
|
||||||
type="button"
|
type="button"
|
||||||
on:click={uploadDocInputElement.click}
|
on:click={() => {
|
||||||
|
uploadDocInputElement.click();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{#if inputFiles}
|
{#if inputFiles}
|
||||||
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
|
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
|
||||||
|
|
|
@ -97,14 +97,11 @@
|
||||||
if (localDBChats.length === 0) {
|
if (localDBChats.length === 0) {
|
||||||
await deleteDB('Chats');
|
await deleteDB('Chats');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('localdb', localDBChats);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(DB);
|
console.log(DB);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// IndexedDB Not Found
|
// IndexedDB Not Found
|
||||||
console.log('IDB Not Found');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
|
|
Loading…
Reference in a new issue