Unverified Commit d17dc592 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #2574 from cheahjs/feat/oauth

feat: experimental SSO support for Google, Microsoft, and OIDC
parents 09082a07 79f8620b
"""Peewee migrations -- 017_add_user_oauth_sub.py.
Some examples (model - class or model name)::
> Model = migrator.orm['table_name'] # Return model in current state by name
> Model = migrator.ModelClass # Return model in current state by name
> migrator.sql(sql) # Run custom SQL
> migrator.run(func, *args, **kwargs) # Run python function with the given args
> migrator.create_model(Model) # Create a model (could be used as decorator)
> migrator.remove_model(model, cascade=True) # Remove a model
> migrator.add_fields(model, **fields) # Add fields to a model
> migrator.change_fields(model, **fields) # Change fields
> migrator.remove_fields(model, *field_names, cascade=True)
> migrator.rename_field(model, old_field_name, new_field_name)
> migrator.rename_table(model, new_table_name)
> migrator.add_index(model, *col_names, unique=False)
> migrator.add_not_null(model, *field_names)
> migrator.add_default(model, field_name, default)
> migrator.add_constraint(model, name, sql)
> migrator.drop_index(model, *col_names)
> migrator.drop_not_null(model, *field_names)
> migrator.drop_constraints(model, *constraints)
"""
from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
def migrate(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your migrations here."""
migrator.add_fields(
"user",
oauth_sub=pw.TextField(null=True, unique=True),
)
def rollback(migrator: Migrator, database: pw.Database, *, fake=False):
"""Write your rollback migrations here."""
migrator.remove_fields("user", "oauth_sub")
...@@ -2,6 +2,8 @@ from fastapi import FastAPI, Depends ...@@ -2,6 +2,8 @@ from fastapi import FastAPI, Depends
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from apps.webui.routers import ( from apps.webui.routers import (
auths, auths,
users, users,
......
...@@ -105,6 +105,7 @@ class AuthsTable: ...@@ -105,6 +105,7 @@ class AuthsTable:
name: str, name: str,
profile_image_url: str = "/user.png", profile_image_url: str = "/user.png",
role: str = "pending", role: str = "pending",
oauth_sub: Optional[str] = None,
) -> Optional[UserModel]: ) -> Optional[UserModel]:
log.info("insert_new_auth") log.info("insert_new_auth")
...@@ -115,7 +116,9 @@ class AuthsTable: ...@@ -115,7 +116,9 @@ class AuthsTable:
) )
result = Auth.create(**auth.model_dump()) result = Auth.create(**auth.model_dump())
user = Users.insert_new_user(id, name, email, profile_image_url, role) user = Users.insert_new_user(
id, name, email, profile_image_url, role, oauth_sub
)
if result and user: if result and user:
return user return user
......
...@@ -28,6 +28,8 @@ class User(Model): ...@@ -28,6 +28,8 @@ class User(Model):
settings = JSONField(null=True) settings = JSONField(null=True)
info = JSONField(null=True) info = JSONField(null=True)
oauth_sub = TextField(null=True, unique=True)
class Meta: class Meta:
database = DB database = DB
...@@ -53,6 +55,8 @@ class UserModel(BaseModel): ...@@ -53,6 +55,8 @@ class UserModel(BaseModel):
settings: Optional[UserSettings] = None settings: Optional[UserSettings] = None
info: Optional[dict] = None info: Optional[dict] = None
oauth_sub: Optional[str] = None
#################### ####################
# Forms # Forms
...@@ -83,6 +87,7 @@ class UsersTable: ...@@ -83,6 +87,7 @@ class UsersTable:
email: str, email: str,
profile_image_url: str = "/user.png", profile_image_url: str = "/user.png",
role: str = "pending", role: str = "pending",
oauth_sub: Optional[str] = None,
) -> Optional[UserModel]: ) -> Optional[UserModel]:
user = UserModel( user = UserModel(
**{ **{
...@@ -94,6 +99,7 @@ class UsersTable: ...@@ -94,6 +99,7 @@ class UsersTable:
"last_active_at": int(time.time()), "last_active_at": int(time.time()),
"created_at": int(time.time()), "created_at": int(time.time()),
"updated_at": int(time.time()), "updated_at": int(time.time()),
"oauth_sub": oauth_sub,
} }
) )
result = User.create(**user.model_dump()) result = User.create(**user.model_dump())
...@@ -123,6 +129,13 @@ class UsersTable: ...@@ -123,6 +129,13 @@ class UsersTable:
except: except:
return None return None
def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]:
try:
user = User.get(User.oauth_sub == sub)
return UserModel(**model_to_dict(user))
except:
return None
def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]: def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]:
return [ return [
UserModel(**model_to_dict(user)) UserModel(**model_to_dict(user))
...@@ -174,6 +187,18 @@ class UsersTable: ...@@ -174,6 +187,18 @@ class UsersTable:
except: except:
return None return None
def update_user_oauth_sub_by_id(
self, id: str, oauth_sub: str
) -> Optional[UserModel]:
try:
query = User.update(oauth_sub=oauth_sub).where(User.id == id)
query.execute()
user = User.get(User.id == id)
return UserModel(**model_to_dict(user))
except:
return None
def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]: def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]:
try: try:
query = User.update(**updated).where(User.id == id) query = User.update(**updated).where(User.id == id)
......
...@@ -10,7 +10,6 @@ import re ...@@ -10,7 +10,6 @@ import re
import uuid import uuid
import csv import csv
from apps.webui.models.auths import ( from apps.webui.models.auths import (
SigninForm, SigninForm,
SignupForm, SignupForm,
......
...@@ -305,6 +305,135 @@ JWT_EXPIRES_IN = PersistentConfig( ...@@ -305,6 +305,135 @@ JWT_EXPIRES_IN = PersistentConfig(
"JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1") "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1")
) )
####################################
# OAuth config
####################################
ENABLE_OAUTH_SIGNUP = PersistentConfig(
"ENABLE_OAUTH_SIGNUP",
"oauth.enable_signup",
os.environ.get("ENABLE_OAUTH_SIGNUP", "False").lower() == "true",
)
OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig(
"OAUTH_MERGE_ACCOUNTS_BY_EMAIL",
"oauth.merge_accounts_by_email",
os.environ.get("OAUTH_MERGE_ACCOUNTS_BY_EMAIL", "False").lower() == "true",
)
OAUTH_PROVIDERS = {}
GOOGLE_CLIENT_ID = PersistentConfig(
"GOOGLE_CLIENT_ID",
"oauth.google.client_id",
os.environ.get("GOOGLE_CLIENT_ID", ""),
)
GOOGLE_CLIENT_SECRET = PersistentConfig(
"GOOGLE_CLIENT_SECRET",
"oauth.google.client_secret",
os.environ.get("GOOGLE_CLIENT_SECRET", ""),
)
GOOGLE_OAUTH_SCOPE = PersistentConfig(
"GOOGLE_OAUTH_SCOPE",
"oauth.google.scope",
os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"),
)
MICROSOFT_CLIENT_ID = PersistentConfig(
"MICROSOFT_CLIENT_ID",
"oauth.microsoft.client_id",
os.environ.get("MICROSOFT_CLIENT_ID", ""),
)
MICROSOFT_CLIENT_SECRET = PersistentConfig(
"MICROSOFT_CLIENT_SECRET",
"oauth.microsoft.client_secret",
os.environ.get("MICROSOFT_CLIENT_SECRET", ""),
)
MICROSOFT_CLIENT_TENANT_ID = PersistentConfig(
"MICROSOFT_CLIENT_TENANT_ID",
"oauth.microsoft.tenant_id",
os.environ.get("MICROSOFT_CLIENT_TENANT_ID", ""),
)
MICROSOFT_OAUTH_SCOPE = PersistentConfig(
"MICROSOFT_OAUTH_SCOPE",
"oauth.microsoft.scope",
os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"),
)
OAUTH_CLIENT_ID = PersistentConfig(
"OAUTH_CLIENT_ID",
"oauth.oidc.client_id",
os.environ.get("OAUTH_CLIENT_ID", ""),
)
OAUTH_CLIENT_SECRET = PersistentConfig(
"OAUTH_CLIENT_SECRET",
"oauth.oidc.client_secret",
os.environ.get("OAUTH_CLIENT_SECRET", ""),
)
OPENID_PROVIDER_URL = PersistentConfig(
"OPENID_PROVIDER_URL",
"oauth.oidc.provider_url",
os.environ.get("OPENID_PROVIDER_URL", ""),
)
OAUTH_SCOPES = PersistentConfig(
"OAUTH_SCOPES",
"oauth.oidc.scopes",
os.environ.get("OAUTH_SCOPES", "openid email profile"),
)
OAUTH_PROVIDER_NAME = PersistentConfig(
"OAUTH_PROVIDER_NAME",
"oauth.oidc.provider_name",
os.environ.get("OAUTH_PROVIDER_NAME", "SSO"),
)
def load_oauth_providers():
OAUTH_PROVIDERS.clear()
if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value:
OAUTH_PROVIDERS["google"] = {
"client_id": GOOGLE_CLIENT_ID.value,
"client_secret": GOOGLE_CLIENT_SECRET.value,
"server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration",
"scope": GOOGLE_OAUTH_SCOPE.value,
}
if (
MICROSOFT_CLIENT_ID.value
and MICROSOFT_CLIENT_SECRET.value
and MICROSOFT_CLIENT_TENANT_ID.value
):
OAUTH_PROVIDERS["microsoft"] = {
"client_id": MICROSOFT_CLIENT_ID.value,
"client_secret": MICROSOFT_CLIENT_SECRET.value,
"server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration",
"scope": MICROSOFT_OAUTH_SCOPE.value,
}
if (
OAUTH_CLIENT_ID.value
and OAUTH_CLIENT_SECRET.value
and OPENID_PROVIDER_URL.value
):
OAUTH_PROVIDERS["oidc"] = {
"client_id": OAUTH_CLIENT_ID.value,
"client_secret": OAUTH_CLIENT_SECRET.value,
"server_metadata_url": OPENID_PROVIDER_URL.value,
"scope": OAUTH_SCOPES.value,
"name": OAUTH_PROVIDER_NAME.value,
}
load_oauth_providers()
#################################### ####################################
# Static DIR # Static DIR
#################################### ####################################
...@@ -733,6 +862,16 @@ WEBUI_SECRET_KEY = os.environ.get( ...@@ -733,6 +862,16 @@ WEBUI_SECRET_KEY = os.environ.get(
), # DEPRECATED: remove at next major version ), # DEPRECATED: remove at next major version
) )
WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get(
"WEBUI_SESSION_COOKIE_SAME_SITE",
os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"),
)
WEBUI_SESSION_COOKIE_SECURE = os.environ.get(
"WEBUI_SESSION_COOKIE_SECURE",
os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true",
)
if WEBUI_AUTH and WEBUI_SECRET_KEY == "": if WEBUI_AUTH and WEBUI_SECRET_KEY == "":
raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND) raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND)
......
import base64
import uuid
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from authlib.integrations.starlette_client import OAuth
from authlib.oidc.core import UserInfo
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import json import json
import markdown import markdown
...@@ -24,7 +29,8 @@ from fastapi.middleware.wsgi import WSGIMiddleware ...@@ -24,7 +29,8 @@ from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import StreamingResponse, Response from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import StreamingResponse, Response, RedirectResponse
from apps.socket.main import app as socket_app from apps.socket.main import app as socket_app
...@@ -53,9 +59,11 @@ from apps.webui.main import ( ...@@ -53,9 +59,11 @@ from apps.webui.main import (
from pydantic import BaseModel from pydantic import BaseModel
from typing import List, Optional, Iterator, Generator, Union from typing import List, Optional, Iterator, Generator, Union
from apps.webui.models.auths import Auths
from apps.webui.models.models import Models, ModelModel from apps.webui.models.models import Models, ModelModel
from apps.webui.models.tools import Tools from apps.webui.models.tools import Tools
from apps.webui.models.functions import Functions from apps.webui.models.functions import Functions
from apps.webui.models.users import Users
from apps.webui.utils import load_toolkit_module_by_id, load_function_module_by_id from apps.webui.utils import load_toolkit_module_by_id, load_function_module_by_id
...@@ -64,6 +72,8 @@ from utils.utils import ( ...@@ -64,6 +72,8 @@ from utils.utils import (
get_verified_user, get_verified_user,
get_current_user, get_current_user,
get_http_authorization_cred, get_http_authorization_cred,
get_password_hash,
create_token,
) )
from utils.task import ( from utils.task import (
title_generation_template, title_generation_template,
...@@ -74,6 +84,7 @@ from utils.misc import ( ...@@ -74,6 +84,7 @@ from utils.misc import (
get_last_user_message, get_last_user_message,
add_or_update_system_message, add_or_update_system_message,
stream_message_template, stream_message_template,
parse_duration,
) )
from apps.rag.utils import get_rag_context, rag_template from apps.rag.utils import get_rag_context, rag_template
...@@ -106,9 +117,16 @@ from config import ( ...@@ -106,9 +117,16 @@ from config import (
SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD, SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD,
TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE,
SAFE_MODE, SAFE_MODE,
OAUTH_PROVIDERS,
ENABLE_OAUTH_SIGNUP,
OAUTH_MERGE_ACCOUNTS_BY_EMAIL,
WEBUI_SECRET_KEY,
WEBUI_SESSION_COOKIE_SAME_SITE,
WEBUI_SESSION_COOKIE_SECURE,
AppConfig, AppConfig,
) )
from constants import ERROR_MESSAGES from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES
from utils.webhook import post_webhook
if SAFE_MODE: if SAFE_MODE:
print("SAFE MODE ENABLED") print("SAFE MODE ENABLED")
...@@ -1725,6 +1743,12 @@ async def get_app_config(): ...@@ -1725,6 +1743,12 @@ async def get_app_config():
"engine": audio_app.state.config.STT_ENGINE, "engine": audio_app.state.config.STT_ENGINE,
}, },
}, },
"oauth": {
"providers": {
name: config.get("name", name)
for name, config in OAUTH_PROVIDERS.items()
}
},
} }
...@@ -1806,6 +1830,154 @@ async def get_app_latest_release_version(): ...@@ -1806,6 +1830,154 @@ async def get_app_latest_release_version():
) )
############################
# OAuth Login & Callback
############################
oauth = OAuth()
for provider_name, provider_config in OAUTH_PROVIDERS.items():
oauth.register(
name=provider_name,
client_id=provider_config["client_id"],
client_secret=provider_config["client_secret"],
server_metadata_url=provider_config["server_metadata_url"],
client_kwargs={
"scope": provider_config["scope"],
},
)
# SessionMiddleware is used by authlib for oauth
if len(OAUTH_PROVIDERS) > 0:
app.add_middleware(
SessionMiddleware,
secret_key=WEBUI_SECRET_KEY,
session_cookie="oui-session",
same_site=WEBUI_SESSION_COOKIE_SAME_SITE,
https_only=WEBUI_SESSION_COOKIE_SECURE,
)
@app.get("/oauth/{provider}/login")
async def oauth_login(provider: str, request: Request):
if provider not in OAUTH_PROVIDERS:
raise HTTPException(404)
redirect_uri = request.url_for("oauth_callback", provider=provider)
return await oauth.create_client(provider).authorize_redirect(request, redirect_uri)
# OAuth login logic is as follows:
# 1. Attempt to find a user with matching subject ID, tied to the provider
# 2. If OAUTH_MERGE_ACCOUNTS_BY_EMAIL is true, find a user with the email address provided via OAuth
# - This is considered insecure in general, as OAuth providers do not always verify email addresses
# 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user
# - Email addresses are considered unique, so we fail registration if the email address is alreayd taken
@app.get("/oauth/{provider}/callback")
async def oauth_callback(provider: str, request: Request, response: Response):
if provider not in OAUTH_PROVIDERS:
raise HTTPException(404)
client = oauth.create_client(provider)
try:
token = await client.authorize_access_token(request)
except Exception as e:
log.warning(f"OAuth callback error: {e}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
user_data: UserInfo = token["userinfo"]
sub = user_data.get("sub")
if not sub:
log.warning(f"OAuth callback failed, sub is missing: {user_data}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
provider_sub = f"{provider}@{sub}"
email = user_data.get("email", "").lower()
# We currently mandate that email addresses are provided
if not email:
log.warning(f"OAuth callback failed, email is missing: {user_data}")
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
# Check if the user exists
user = Users.get_user_by_oauth_sub(provider_sub)
if not user:
# If the user does not exist, check if merging is enabled
if OAUTH_MERGE_ACCOUNTS_BY_EMAIL.value:
# Check if the user exists by email
user = Users.get_user_by_email(email)
if user:
# Update the user with the new oauth sub
Users.update_user_oauth_sub_by_id(user.id, provider_sub)
if not user:
# If the user does not exist, check if signups are enabled
if ENABLE_OAUTH_SIGNUP.value:
# Check if an existing user with the same email already exists
existing_user = Users.get_user_by_email(user_data.get("email", "").lower())
if existing_user:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
picture_url = user_data.get("picture", "")
if picture_url:
# Download the profile image into a base64 string
try:
async with aiohttp.ClientSession() as session:
async with session.get(picture_url) as resp:
picture = await resp.read()
base64_encoded_picture = base64.b64encode(picture).decode(
"utf-8"
)
guessed_mime_type = mimetypes.guess_type(picture_url)[0]
if guessed_mime_type is None:
# assume JPG, browsers are tolerant enough of image formats
guessed_mime_type = "image/jpeg"
picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}"
except Exception as e:
log.error(f"Error downloading profile image '{picture_url}': {e}")
picture_url = ""
if not picture_url:
picture_url = "/user.png"
user = Auths.insert_new_auth(
email=email,
password=get_password_hash(
str(uuid.uuid4())
), # Random password, not used
name=user_data.get("name", "User"),
profile_image_url=picture_url,
role=webui_app.state.config.DEFAULT_USER_ROLE,
oauth_sub=provider_sub,
)
if webui_app.state.config.WEBHOOK_URL:
post_webhook(
webui_app.state.config.WEBHOOK_URL,
WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
{
"action": "signup",
"message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name),
"user": user.model_dump_json(exclude_none=True),
},
)
else:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED
)
jwt_token = create_token(
data={"id": user.id},
expires_delta=parse_duration(webui_app.state.config.JWT_EXPIRES_IN),
)
# Set the cookie token
response.set_cookie(
key="token",
value=token,
httponly=True, # Ensures the cookie is not accessible via JavaScript
)
# Redirect back to the frontend with the JWT token
redirect_url = f"{request.base_url}auth#token={jwt_token}"
return RedirectResponse(url=redirect_url)
@app.get("/manifest.json") @app.get("/manifest.json")
async def get_manifest_json(): async def get_manifest_json():
return { return {
......
...@@ -58,6 +58,7 @@ rank-bm25==0.2.2 ...@@ -58,6 +58,7 @@ rank-bm25==0.2.2
faster-whisper==1.0.2 faster-whisper==1.0.2
PyJWT[crypto]==2.8.0 PyJWT[crypto]==2.8.0
authlib==1.3.0
black==24.4.2 black==24.4.2
langfuse==2.33.0 langfuse==2.33.0
......
...@@ -59,6 +59,7 @@ dependencies = [ ...@@ -59,6 +59,7 @@ dependencies = [
"faster-whisper==1.0.2", "faster-whisper==1.0.2",
"PyJWT[crypto]==2.8.0", "PyJWT[crypto]==2.8.0",
"authlib==1.3.0",
"black==24.4.2", "black==24.4.2",
"langfuse==2.33.0", "langfuse==2.33.0",
......
...@@ -31,6 +31,8 @@ asgiref==3.8.1 ...@@ -31,6 +31,8 @@ asgiref==3.8.1
# via opentelemetry-instrumentation-asgi # via opentelemetry-instrumentation-asgi
attrs==23.2.0 attrs==23.2.0
# via aiohttp # via aiohttp
authlib==1.3.0
# via open-webui
av==11.0.0 av==11.0.0
# via faster-whisper # via faster-whisper
backoff==2.2.1 backoff==2.2.1
...@@ -93,6 +95,7 @@ coloredlogs==15.0.1 ...@@ -93,6 +95,7 @@ coloredlogs==15.0.1
compressed-rtf==1.0.6 compressed-rtf==1.0.6
# via extract-msg # via extract-msg
cryptography==42.0.7 cryptography==42.0.7
# via authlib
# via msoffcrypto-tool # via msoffcrypto-tool
# via pyjwt # via pyjwt
ctranslate2==4.2.1 ctranslate2==4.2.1
...@@ -395,6 +398,7 @@ pandas==2.2.2 ...@@ -395,6 +398,7 @@ pandas==2.2.2
# via open-webui # via open-webui
passlib==1.7.4 passlib==1.7.4
# via open-webui # via open-webui
# via passlib
pathspec==0.12.1 pathspec==0.12.1
# via black # via black
pcodedmp==1.2.6 pcodedmp==1.2.6
...@@ -453,6 +457,7 @@ pygments==2.18.0 ...@@ -453,6 +457,7 @@ pygments==2.18.0
# via rich # via rich
pyjwt==2.8.0 pyjwt==2.8.0
# via open-webui # via open-webui
# via pyjwt
pymysql==1.1.0 pymysql==1.1.0
# via open-webui # via open-webui
pypandoc==1.13 pypandoc==1.13
...@@ -554,9 +559,6 @@ scipy==1.13.0 ...@@ -554,9 +559,6 @@ scipy==1.13.0
# via sentence-transformers # via sentence-transformers
sentence-transformers==2.7.0 sentence-transformers==2.7.0
# via open-webui # via open-webui
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
shapely==2.0.4 shapely==2.0.4
# via rapidocr-onnxruntime # via rapidocr-onnxruntime
shellingham==1.5.4 shellingham==1.5.4
...@@ -651,6 +653,7 @@ uvicorn==0.22.0 ...@@ -651,6 +653,7 @@ uvicorn==0.22.0
# via chromadb # via chromadb
# via fastapi # via fastapi
# via open-webui # via open-webui
# via uvicorn
uvloop==0.19.0 uvloop==0.19.0
# via uvicorn # via uvicorn
validators==0.28.1 validators==0.28.1
...@@ -678,3 +681,6 @@ youtube-transcript-api==0.6.2 ...@@ -678,3 +681,6 @@ youtube-transcript-api==0.6.2
# via open-webui # via open-webui
zipp==3.18.1 zipp==3.18.1
# via importlib-metadata # via importlib-metadata
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
...@@ -31,6 +31,8 @@ asgiref==3.8.1 ...@@ -31,6 +31,8 @@ asgiref==3.8.1
# via opentelemetry-instrumentation-asgi # via opentelemetry-instrumentation-asgi
attrs==23.2.0 attrs==23.2.0
# via aiohttp # via aiohttp
authlib==1.3.0
# via open-webui
av==11.0.0 av==11.0.0
# via faster-whisper # via faster-whisper
backoff==2.2.1 backoff==2.2.1
...@@ -93,6 +95,7 @@ coloredlogs==15.0.1 ...@@ -93,6 +95,7 @@ coloredlogs==15.0.1
compressed-rtf==1.0.6 compressed-rtf==1.0.6
# via extract-msg # via extract-msg
cryptography==42.0.7 cryptography==42.0.7
# via authlib
# via msoffcrypto-tool # via msoffcrypto-tool
# via pyjwt # via pyjwt
ctranslate2==4.2.1 ctranslate2==4.2.1
...@@ -395,6 +398,7 @@ pandas==2.2.2 ...@@ -395,6 +398,7 @@ pandas==2.2.2
# via open-webui # via open-webui
passlib==1.7.4 passlib==1.7.4
# via open-webui # via open-webui
# via passlib
pathspec==0.12.1 pathspec==0.12.1
# via black # via black
pcodedmp==1.2.6 pcodedmp==1.2.6
...@@ -453,6 +457,7 @@ pygments==2.18.0 ...@@ -453,6 +457,7 @@ pygments==2.18.0
# via rich # via rich
pyjwt==2.8.0 pyjwt==2.8.0
# via open-webui # via open-webui
# via pyjwt
pymysql==1.1.0 pymysql==1.1.0
# via open-webui # via open-webui
pypandoc==1.13 pypandoc==1.13
...@@ -554,9 +559,6 @@ scipy==1.13.0 ...@@ -554,9 +559,6 @@ scipy==1.13.0
# via sentence-transformers # via sentence-transformers
sentence-transformers==2.7.0 sentence-transformers==2.7.0
# via open-webui # via open-webui
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
shapely==2.0.4 shapely==2.0.4
# via rapidocr-onnxruntime # via rapidocr-onnxruntime
shellingham==1.5.4 shellingham==1.5.4
...@@ -651,6 +653,7 @@ uvicorn==0.22.0 ...@@ -651,6 +653,7 @@ uvicorn==0.22.0
# via chromadb # via chromadb
# via fastapi # via fastapi
# via open-webui # via open-webui
# via uvicorn
uvloop==0.19.0 uvloop==0.19.0
# via uvicorn # via uvicorn
validators==0.28.1 validators==0.28.1
...@@ -678,3 +681,6 @@ youtube-transcript-api==0.6.2 ...@@ -678,3 +681,6 @@ youtube-transcript-api==0.6.2
# via open-webui # via open-webui
zipp==3.18.1 zipp==3.18.1
# via importlib-metadata # via importlib-metadata
setuptools==69.5.1
# via ctranslate2
# via opentelemetry-instrumentation
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
"Content": "الاتصال", "Content": "الاتصال",
"Context Length": "طول السياق", "Context Length": "طول السياق",
"Continue Response": "متابعة الرد", "Continue Response": "متابعة الرد",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة", "Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة",
"Copy": "نسخ", "Copy": "نسخ",
"Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة", "Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة",
...@@ -378,6 +379,7 @@ ...@@ -378,6 +379,7 @@
"Notifications": "إشعارات", "Notifications": "إشعارات",
"November": "نوفمبر", "November": "نوفمبر",
"num_thread (Ollama)": "num_thread (أولاما)", "num_thread (Ollama)": "num_thread (أولاما)",
"OAuth ID": "",
"October": "اكتوبر", "October": "اكتوبر",
"Off": "أغلاق", "Off": "أغلاق",
"Okay, Let's Go!": "حسنا دعنا نذهب!", "Okay, Let's Go!": "حسنا دعنا نذهب!",
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
"Content": "Съдържание", "Content": "Съдържание",
"Context Length": "Дължина на Контекста", "Context Length": "Дължина на Контекста",
"Continue Response": "Продължи отговора", "Continue Response": "Продължи отговора",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Копирана е връзката за чат!", "Copied shared chat URL to clipboard!": "Копирана е връзката за чат!",
"Copy": "Копирай", "Copy": "Копирай",
"Copy last code block": "Копиране на последен код блок", "Copy last code block": "Копиране на последен код блок",
...@@ -378,6 +379,7 @@ ...@@ -378,6 +379,7 @@
"Notifications": "Десктоп Известия", "Notifications": "Десктоп Известия",
"November": "Ноември", "November": "Ноември",
"num_thread (Ollama)": "num_thread (Ollama)", "num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Октомври", "October": "Октомври",
"Off": "Изкл.", "Off": "Изкл.",
"Okay, Let's Go!": "ОК, Нека започваме!", "Okay, Let's Go!": "ОК, Нека започваме!",
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
"Content": "বিষয়বস্তু", "Content": "বিষয়বস্তু",
"Context Length": "কনটেক্সটের দৈর্ঘ্য", "Context Length": "কনটেক্সটের দৈর্ঘ্য",
"Continue Response": "যাচাই করুন", "Continue Response": "যাচাই করুন",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "শেয়ারকৃত কথা-ব্যবহারের URL ক্লিপবোর্ডে কপি করা হয়েছে!", "Copied shared chat URL to clipboard!": "শেয়ারকৃত কথা-ব্যবহারের URL ক্লিপবোর্ডে কপি করা হয়েছে!",
"Copy": "অনুলিপি", "Copy": "অনুলিপি",
"Copy last code block": "সর্বশেষ কোড ব্লক কপি করুন", "Copy last code block": "সর্বশেষ কোড ব্লক কপি করুন",
...@@ -378,6 +379,7 @@ ...@@ -378,6 +379,7 @@
"Notifications": "নোটিফিকেশনসমূহ", "Notifications": "নোটিফিকেশনসমূহ",
"November": "নভেম্বর", "November": "নভেম্বর",
"num_thread (Ollama)": "num_thread (ওলামা)", "num_thread (Ollama)": "num_thread (ওলামা)",
"OAuth ID": "",
"October": "অক্টোবর", "October": "অক্টোবর",
"Off": "বন্ধ", "Off": "বন্ধ",
"Okay, Let's Go!": "ঠিক আছে, চলুন যাই!", "Okay, Let's Go!": "ঠিক আছে, চলুন যাই!",
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
"Content": "Contingut", "Content": "Contingut",
"Context Length": "Mida del context", "Context Length": "Mida del context",
"Continue Response": "Continuar la resposta", "Continue Response": "Continuar la resposta",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "S'ha copiat l'URL compartida al porta-retalls!", "Copied shared chat URL to clipboard!": "S'ha copiat l'URL compartida al porta-retalls!",
"Copy": "Copiar", "Copy": "Copiar",
"Copy last code block": "Copiar l'últim bloc de codi", "Copy last code block": "Copiar l'últim bloc de codi",
...@@ -378,6 +379,7 @@ ...@@ -378,6 +379,7 @@
"Notifications": "Notificacions", "Notifications": "Notificacions",
"November": "Novembre", "November": "Novembre",
"num_thread (Ollama)": "num_thread (Ollama)", "num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Octubre", "October": "Octubre",
"Off": "Desactivat", "Off": "Desactivat",
"Okay, Let's Go!": "D'acord, som-hi!", "Okay, Let's Go!": "D'acord, som-hi!",
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
"Content": "Kontento", "Content": "Kontento",
"Context Length": "Ang gitas-on sa konteksto", "Context Length": "Ang gitas-on sa konteksto",
"Continue Response": "", "Continue Response": "",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "", "Copied shared chat URL to clipboard!": "",
"Copy": "", "Copy": "",
"Copy last code block": "Kopyaha ang katapusang bloke sa code", "Copy last code block": "Kopyaha ang katapusang bloke sa code",
...@@ -378,6 +379,7 @@ ...@@ -378,6 +379,7 @@
"Notifications": "Mga pahibalo sa desktop", "Notifications": "Mga pahibalo sa desktop",
"November": "", "November": "",
"num_thread (Ollama)": "", "num_thread (Ollama)": "",
"OAuth ID": "",
"October": "", "October": "",
"Off": "Napuo", "Off": "Napuo",
"Okay, Let's Go!": "Okay, lakaw na!", "Okay, Let's Go!": "Okay, lakaw na!",
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
"Content": "Info", "Content": "Info",
"Context Length": "Context Length", "Context Length": "Context Length",
"Continue Response": "Antwort fortsetzen", "Continue Response": "Antwort fortsetzen",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "Geteilte Chat-URL in die Zwischenablage kopiert!", "Copied shared chat URL to clipboard!": "Geteilte Chat-URL in die Zwischenablage kopiert!",
"Copy": "Kopieren", "Copy": "Kopieren",
"Copy last code block": "Letzten Codeblock kopieren", "Copy last code block": "Letzten Codeblock kopieren",
...@@ -378,6 +379,7 @@ ...@@ -378,6 +379,7 @@
"Notifications": "Desktop-Benachrichtigungen", "Notifications": "Desktop-Benachrichtigungen",
"November": "November", "November": "November",
"num_thread (Ollama)": "num_thread (Ollama)", "num_thread (Ollama)": "num_thread (Ollama)",
"OAuth ID": "",
"October": "Oktober", "October": "Oktober",
"Off": "Aus", "Off": "Aus",
"Okay, Let's Go!": "Okay, los geht's!", "Okay, Let's Go!": "Okay, los geht's!",
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
"Content": "Content", "Content": "Content",
"Context Length": "Context Length", "Context Length": "Context Length",
"Continue Response": "", "Continue Response": "",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "", "Copied shared chat URL to clipboard!": "",
"Copy": "", "Copy": "",
"Copy last code block": "Copy last code block", "Copy last code block": "Copy last code block",
...@@ -378,6 +379,7 @@ ...@@ -378,6 +379,7 @@
"Notifications": "Notifications", "Notifications": "Notifications",
"November": "", "November": "",
"num_thread (Ollama)": "", "num_thread (Ollama)": "",
"OAuth ID": "",
"October": "", "October": "",
"Off": "Off", "Off": "Off",
"Okay, Let's Go!": "Okay, Let's Go!", "Okay, Let's Go!": "Okay, Let's Go!",
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
"Content": "", "Content": "",
"Context Length": "", "Context Length": "",
"Continue Response": "", "Continue Response": "",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "", "Copied shared chat URL to clipboard!": "",
"Copy": "", "Copy": "",
"Copy last code block": "", "Copy last code block": "",
...@@ -378,6 +379,7 @@ ...@@ -378,6 +379,7 @@
"Notifications": "", "Notifications": "",
"November": "", "November": "",
"num_thread (Ollama)": "", "num_thread (Ollama)": "",
"OAuth ID": "",
"October": "", "October": "",
"Off": "", "Off": "",
"Okay, Let's Go!": "", "Okay, Let's Go!": "",
......
...@@ -126,6 +126,7 @@ ...@@ -126,6 +126,7 @@
"Content": "", "Content": "",
"Context Length": "", "Context Length": "",
"Continue Response": "", "Continue Response": "",
"Continue with {{provider}}": "",
"Copied shared chat URL to clipboard!": "", "Copied shared chat URL to clipboard!": "",
"Copy": "", "Copy": "",
"Copy last code block": "", "Copy last code block": "",
...@@ -378,6 +379,7 @@ ...@@ -378,6 +379,7 @@
"Notifications": "", "Notifications": "",
"November": "", "November": "",
"num_thread (Ollama)": "", "num_thread (Ollama)": "",
"OAuth ID": "",
"October": "", "October": "",
"Off": "", "Off": "",
"Okay, Let's Go!": "", "Okay, Let's Go!": "",
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment