"docs/source/vscode:/vscode.git/clone" did not exist on "5c53ca5ed89b30cd7b04cbf01665388e2af9f249"
Commit 6e934c2d authored by Sergey Mihaylin's avatar Sergey Mihaylin
Browse files

Merge branch 'refs/heads/main' into custom-openid-claims

# Conflicts:
#	backend/main.py
parents 9f32e9ef 824966ad
...@@ -4,6 +4,7 @@ updates: ...@@ -4,6 +4,7 @@ updates:
directory: '/backend' directory: '/backend'
schedule: schedule:
interval: weekly interval: weekly
target-branch: 'dev'
- package-ecosystem: 'github-actions' - package-ecosystem: 'github-actions'
directory: '/' directory: '/'
schedule: schedule:
......
...@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. ...@@ -5,6 +5,23 @@ 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.3.7] - 2024-06-29
### Added
- **🌐 Enhanced Internationalization (i18n)**: Newly introduced Indonesian translation, and updated translations for Turkish, Chinese, and Catalan languages to improve user accessibility.
### Fixed
- **🕵️‍♂️ Browser Language Detection**: Corrected the issue where the application was not properly detecting and adapting to the browser's language settings.
- **🔐 OIDC Admin Role Assignment**: Fixed a bug where the admin role was not being assigned to the first user who signed up via OpenID Connect (OIDC).
- **💬 Chat/Completions Endpoint**: Resolved an issue where the chat/completions endpoint was non-functional when the stream option was set to False.
- **🚫 'WEBUI_AUTH' Configuration**: Addressed the problem where setting 'WEBUI_AUTH' to False was not being applied correctly.
### Changed
- **📦 Dependency Update**: Upgraded 'authlib' from version 1.3.0 to 1.3.1 to ensure better security and performance enhancements.
## [0.3.6] - 2024-06-27 ## [0.3.6] - 2024-06-27
### Added ### Added
......
...@@ -153,7 +153,7 @@ async def cleanup_response( ...@@ -153,7 +153,7 @@ async def cleanup_response(
await session.close() await session.close()
async def post_streaming_url(url: str, payload: str): async def post_streaming_url(url: str, payload: str, stream: bool = True):
r = None r = None
try: try:
session = aiohttp.ClientSession( session = aiohttp.ClientSession(
...@@ -162,12 +162,20 @@ async def post_streaming_url(url: str, payload: str): ...@@ -162,12 +162,20 @@ async def post_streaming_url(url: str, payload: str):
r = await session.post(url, data=payload) r = await session.post(url, data=payload)
r.raise_for_status() r.raise_for_status()
return StreamingResponse( if stream:
r.content, return StreamingResponse(
status_code=r.status, r.content,
headers=dict(r.headers), status_code=r.status,
background=BackgroundTask(cleanup_response, response=r, session=session), headers=dict(r.headers),
) background=BackgroundTask(
cleanup_response, response=r, session=session
),
)
else:
res = await r.json()
await cleanup_response(r, session)
return res
except Exception as e: except Exception as e:
error_detail = "Open WebUI: Server Connection Error" error_detail = "Open WebUI: Server Connection Error"
if r is not None: if r is not None:
...@@ -963,7 +971,11 @@ async def generate_openai_chat_completion( ...@@ -963,7 +971,11 @@ async def generate_openai_chat_completion(
url = app.state.config.OLLAMA_BASE_URLS[url_idx] url = app.state.config.OLLAMA_BASE_URLS[url_idx]
log.info(f"url: {url}") log.info(f"url: {url}")
return await post_streaming_url(f"{url}/v1/chat/completions", json.dumps(payload)) return await post_streaming_url(
f"{url}/v1/chat/completions",
json.dumps(payload),
stream=payload.get("stream", False),
)
@app.get("/v1/models") @app.get("/v1/models")
......
...@@ -686,6 +686,13 @@ ENABLE_SIGNUP = PersistentConfig( ...@@ -686,6 +686,13 @@ ENABLE_SIGNUP = PersistentConfig(
else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true" else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true"
), ),
) )
DEFAULT_LOCALE = PersistentConfig(
"DEFAULT_LOCALE",
"ui.default_locale",
os.environ.get("DEFAULT_LOCALE", ""),
)
DEFAULT_MODELS = PersistentConfig( DEFAULT_MODELS = PersistentConfig(
"DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None) "DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None)
) )
......
...@@ -101,6 +101,7 @@ from config import ( ...@@ -101,6 +101,7 @@ from config import (
UPLOAD_DIR, UPLOAD_DIR,
CACHE_DIR, CACHE_DIR,
STATIC_DIR, STATIC_DIR,
DEFAULT_LOCALE,
ENABLE_OPENAI_API, ENABLE_OPENAI_API,
ENABLE_OLLAMA_API, ENABLE_OLLAMA_API,
ENABLE_MODEL_FILTER, ENABLE_MODEL_FILTER,
...@@ -617,6 +618,8 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware): ...@@ -617,6 +618,8 @@ class ChatCompletionMiddleware(BaseHTTPMiddleware):
return StreamingResponse( return StreamingResponse(
self.ollama_stream_wrapper(response.body_iterator, data_items), self.ollama_stream_wrapper(response.body_iterator, data_items),
) )
return response
else: else:
return response return response
...@@ -1720,18 +1723,11 @@ async def update_pipeline_valves( ...@@ -1720,18 +1723,11 @@ async def update_pipeline_valves(
@app.get("/api/config") @app.get("/api/config")
async def get_app_config(): async def get_app_config():
# Checking and Handling the Absence of 'ui' in CONFIG_DATA
default_locale = "en-US"
if "ui" in CONFIG_DATA:
default_locale = CONFIG_DATA["ui"].get("default_locale", "en-US")
# The Rest of the Function Now Uses the Variables Defined Above
return { return {
"status": True, "status": True,
"name": WEBUI_NAME, "name": WEBUI_NAME,
"version": VERSION, "version": VERSION,
"default_locale": default_locale, "default_locale": str(DEFAULT_LOCALE),
"default_models": webui_app.state.config.DEFAULT_MODELS, "default_models": webui_app.state.config.DEFAULT_MODELS,
"default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS, "default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS,
"features": { "features": {
...@@ -1924,8 +1920,7 @@ async def oauth_callback(provider: str, request: Request, response: Response): ...@@ -1924,8 +1920,7 @@ async def oauth_callback(provider: str, request: Request, response: Response):
if existing_user: if existing_user:
raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
picture_claim = webui_app.state.config.OAUTH_PICTURE_CLAIM picture_url = user_data.get("picture", "")
picture_url = user_data.get(picture_claim, "")
if picture_url: if picture_url:
# Download the profile image into a base64 string # Download the profile image into a base64 string
try: try:
...@@ -1946,6 +1941,11 @@ async def oauth_callback(provider: str, request: Request, response: Response): ...@@ -1946,6 +1941,11 @@ async def oauth_callback(provider: str, request: Request, response: Response):
if not picture_url: if not picture_url:
picture_url = "/user.png" picture_url = "/user.png"
username_claim = webui_app.state.config.OAUTH_USERNAME_CLAIM username_claim = webui_app.state.config.OAUTH_USERNAME_CLAIM
role = (
"admin"
if Users.get_num_users() == 0
else webui_app.state.config.DEFAULT_USER_ROLE
)
user = Auths.insert_new_auth( user = Auths.insert_new_auth(
email=email, email=email,
password=get_password_hash( password=get_password_hash(
...@@ -1953,7 +1953,7 @@ async def oauth_callback(provider: str, request: Request, response: Response): ...@@ -1953,7 +1953,7 @@ async def oauth_callback(provider: str, request: Request, response: Response):
), # Random password, not used ), # Random password, not used
name=user_data.get(username_claim, "User"), name=user_data.get(username_claim, "User"),
profile_image_url=picture_url, profile_image_url=picture_url,
role=webui_app.state.config.DEFAULT_USER_ROLE, role=role,
oauth_sub=provider_sub, oauth_sub=provider_sub,
) )
...@@ -1980,7 +1980,7 @@ async def oauth_callback(provider: str, request: Request, response: Response): ...@@ -1980,7 +1980,7 @@ async def oauth_callback(provider: str, request: Request, response: Response):
# Set the cookie token # Set the cookie token
response.set_cookie( response.set_cookie(
key="token", key="token",
value=token, value=jwt_token,
httponly=True, # Ensures the cookie is not accessible via JavaScript httponly=True, # Ensures the cookie is not accessible via JavaScript
) )
......
...@@ -6,7 +6,7 @@ python-multipart==0.0.9 ...@@ -6,7 +6,7 @@ python-multipart==0.0.9
Flask==3.0.3 Flask==3.0.3
Flask-Cors==4.0.1 Flask-Cors==4.0.1
python-socketio==5.11.2 python-socketio==5.11.3
python-jose==3.3.0 python-jose==3.3.0
passlib[bcrypt]==1.7.4 passlib[bcrypt]==1.7.4
...@@ -32,10 +32,10 @@ google-generativeai==0.5.4 ...@@ -32,10 +32,10 @@ google-generativeai==0.5.4
langchain==0.2.0 langchain==0.2.0
langchain-community==0.2.0 langchain-community==0.2.0
langchain-chroma==0.1.1 langchain-chroma==0.1.2
fake-useragent==1.5.1 fake-useragent==1.5.1
chromadb==0.5.0 chromadb==0.5.3
sentence-transformers==2.7.0 sentence-transformers==2.7.0
pypdf==4.2.0 pypdf==4.2.0
docx2txt==0.8 docx2txt==0.8
...@@ -58,7 +58,7 @@ rank-bm25==0.2.2 ...@@ -58,7 +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 authlib==1.3.1
black==24.4.2 black==24.4.2
langfuse==2.33.0 langfuse==2.33.0
...@@ -67,4 +67,4 @@ pytube==15.0.0 ...@@ -67,4 +67,4 @@ pytube==15.0.0
extract_msg extract_msg
pydub pydub
duckduckgo-search~=6.1.5 duckduckgo-search~=6.1.7
\ No newline at end of file \ No newline at end of file
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.3.6", "version": "0.3.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "open-webui", "name": "open-webui",
"version": "0.3.6", "version": "0.3.7",
"dependencies": { "dependencies": {
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
......
{ {
"name": "open-webui", "name": "open-webui",
"version": "0.3.6", "version": "0.3.7",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run pyodide:fetch && vite dev --host", "dev": "npm run pyodide:fetch && vite dev --host",
......
...@@ -59,7 +59,7 @@ dependencies = [ ...@@ -59,7 +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", "authlib==1.3.1",
"black==24.4.2", "black==24.4.2",
"langfuse==2.33.0", "langfuse==2.33.0",
......
...@@ -27,61 +27,73 @@ ...@@ -27,61 +27,73 @@
} }
let codeEditor; let codeEditor;
let boilerplate = `from pydantic import BaseModel let boilerplate = `"""
title: Example Filter
author: open-webui
author_url: https://github.com/open-webui
funding_url: https://github.com/open-webui
version: 0.1
"""
from pydantic import BaseModel, Field
from typing import Optional from typing import Optional
class Filter: class Filter:
class Valves(BaseModel): class Valves(BaseModel):
max_turns: int = 4 priority: int = Field(
default=0, description="Priority level for the filter operations."
)
max_turns: int = Field(
default=8, description="Maximum allowable conversation turns for a user."
)
pass
class UserValves(BaseModel):
max_turns: int = Field(
default=4, description="Maximum allowable conversation turns for a user."
)
pass pass
def __init__(self): def __init__(self):
# Indicates custom file handling logic. This flag helps disengage default routines in favor of custom # Indicates custom file handling logic. This flag helps disengage default routines in favor of custom
# implementations, informing the WebUI to defer file-related operations to designated methods within this class. # implementations, informing the WebUI to defer file-related operations to designated methods within this class.
# Alternatively, you can remove the files directly from the body in from the inlet hook # Alternatively, you can remove the files directly from the body in from the inlet hook
self.file_handler = True # self.file_handler = True
# Initialize 'valves' with specific configurations. Using 'Valves' instance helps encapsulate settings, # Initialize 'valves' with specific configurations. Using 'Valves' instance helps encapsulate settings,
# which ensures settings are managed cohesively and not confused with operational flags like 'file_handler'. # which ensures settings are managed cohesively and not confused with operational flags like 'file_handler'.
self.valves = self.Valves(**{"max_turns": 2}) self.valves = self.Valves()
pass pass
def inlet(self, body: dict, user: Optional[dict] = None) -> dict: def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
# Modify the request body or validate it before processing by the chat completion API. # Modify the request body or validate it before processing by the chat completion API.
# This function is the pre-processor for the API where various checks on the input can be performed. # This function is the pre-processor for the API where various checks on the input can be performed.
# It can also modify the request before sending it to the API. # It can also modify the request before sending it to the API.
print(f"inlet:{__name__}") print(f"inlet:{__name__}")
print(f"inlet:body:{body}") print(f"inlet:body:{body}")
print(f"inlet:user:{user}") print(f"inlet:user:{__user__}")
if user.get("role", "admin") in ["user", "admin"]: if __user__.get("role", "admin") in ["user", "admin"]:
messages = body.get("messages", []) messages = body.get("messages", [])
if len(messages) > self.valves.max_turns:
max_turns = min(__user__["valves"].max_turns, self.valves.max_turns)
if len(messages) > max_turns:
raise Exception( raise Exception(
f"Conversation turn limit exceeded. Max turns: {self.valves.max_turns}" f"Conversation turn limit exceeded. Max turns: {max_turns}"
) )
return body return body
def outlet(self, body: dict, user: Optional[dict] = None) -> dict: def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
# Modify or analyze the response body after processing by the API. # Modify or analyze the response body after processing by the API.
# This function is the post-processor for the API, which can be used to modify the response # This function is the post-processor for the API, which can be used to modify the response
# or perform additional checks and analytics. # or perform additional checks and analytics.
print(f"outlet:{__name__}") print(f"outlet:{__name__}")
print(f"outlet:body:{body}") print(f"outlet:body:{body}")
print(f"outlet:user:{user}") print(f"outlet:user:{__user__}")
messages = [
{
**message,
"content": f"{message['content']} - @@Modified from Filter Outlet",
}
for message in body.get("messages", [])
]
return {"messages": messages}
return body
`; `;
const _boilerplate = `from pydantic import BaseModel const _boilerplate = `from pydantic import BaseModel
......
This diff is collapsed.
This diff is collapsed.
...@@ -63,6 +63,10 @@ ...@@ -63,6 +63,10 @@
"code": "hr-HR", "code": "hr-HR",
"title": "Croatian (Hrvatski)" "title": "Croatian (Hrvatski)"
}, },
{
"code": "id-ID",
"title": "Indonesian (Bahasa Indonesia)"
},
{ {
"code": "it-IT", "code": "it-IT",
"title": "Italian (Italiano)" "title": "Italian (Italiano)"
......
This diff is collapsed.
...@@ -261,8 +261,8 @@ ...@@ -261,8 +261,8 @@
"File": "文件", "File": "文件",
"File Mode": "文件模式", "File Mode": "文件模式",
"File not found.": "文件未找到。", "File not found.": "文件未找到。",
"Filter is now globally disabled": "", "Filter is now globally disabled": "过滤器已全局禁用",
"Filter is now globally enabled": "", "Filter is now globally enabled": "过滤器已全局启用",
"Filters": "过滤器", "Filters": "过滤器",
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "检测到指纹伪造:无法使用姓名缩写作为头像。默认使用默认个人形象。", "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "检测到指纹伪造:无法使用姓名缩写作为头像。默认使用默认个人形象。",
"Fluidly stream large external response chunks": "流畅地传输外部大型响应块数据", "Fluidly stream large external response chunks": "流畅地传输外部大型响应块数据",
...@@ -281,7 +281,7 @@ ...@@ -281,7 +281,7 @@
"Generate Image": "生成图像", "Generate Image": "生成图像",
"Generating search query": "生成搜索查询", "Generating search query": "生成搜索查询",
"Generation Info": "生成信息", "Generation Info": "生成信息",
"Global": "", "Global": "全局",
"Good Response": "点赞此回答", "Good Response": "点赞此回答",
"Google PSE API Key": "Google PSE API 密钥", "Google PSE API Key": "Google PSE API 密钥",
"Google PSE Engine Id": "Google PSE 引擎 ID", "Google PSE Engine Id": "Google PSE 引擎 ID",
......
...@@ -746,3 +746,15 @@ export const extractFrontmatter = (content) => { ...@@ -746,3 +746,15 @@ export const extractFrontmatter = (content) => {
return frontmatter; return frontmatter;
}; };
// Function to determine the best matching language
export const bestMatchingLanguage = (supportedLanguages, preferredLanguages, defaultLocale) => {
const languages = supportedLanguages.map((lang) => lang.code);
const match = preferredLanguages
.map((prefLang) => languages.find((lang) => lang.startsWith(prefLang)))
.find(Boolean);
console.log(languages, preferredLanguages, match, defaultLocale);
return match || defaultLocale;
};
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
import { WEBUI_BASE_URL, WEBUI_HOSTNAME } from '$lib/constants'; import { WEBUI_BASE_URL, WEBUI_HOSTNAME } from '$lib/constants';
import i18n, { initI18n, getLanguages } from '$lib/i18n'; import i18n, { initI18n, getLanguages } from '$lib/i18n';
import { bestMatchingLanguage } from '$lib/utils';
setContext('i18n', i18n); setContext('i18n', i18n);
...@@ -91,13 +92,17 @@ ...@@ -91,13 +92,17 @@
// Initialize i18n even if we didn't get a backend config, // Initialize i18n even if we didn't get a backend config,
// so `/error` can show something that's not `undefined`. // so `/error` can show something that's not `undefined`.
const languages = await getLanguages(); initI18n();
if (!localStorage.locale) {
const browserLanguage = navigator.languages const languages = await getLanguages();
? navigator.languages[0] const browserLanguages = navigator.languages
: navigator.language || navigator.userLanguage; ? navigator.languages
: [navigator.language || navigator.userLanguage];
initI18n(languages.includes(browserLanguage) ? browserLanguage : backendConfig?.default_locale); const lang = backendConfig.default_locale
? backendConfig.default_locale
: bestMatchingLanguage(languages, browserLanguages, 'en-US');
$i18n.changeLanguage(lang);
}
if (backendConfig) { if (backendConfig) {
// Save Backend Status to Store // Save Backend Status to Store
......
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