chore: format

This commit is contained in:
Timothy Jaeryang Baek
2026-02-11 16:24:11 -06:00
parent 89fddcc741
commit f376d4f378
202 changed files with 8328 additions and 2046 deletions
+9 -3
View File
@@ -2287,7 +2287,9 @@ WEAVIATE_GRPC_PORT = int(os.environ.get("WEAVIATE_GRPC_PORT", "50051"))
WEAVIATE_API_KEY = os.environ.get("WEAVIATE_API_KEY") WEAVIATE_API_KEY = os.environ.get("WEAVIATE_API_KEY")
WEAVIATE_HTTP_SECURE = os.environ.get("WEAVIATE_HTTP_SECURE", "false").lower() == "true" WEAVIATE_HTTP_SECURE = os.environ.get("WEAVIATE_HTTP_SECURE", "false").lower() == "true"
WEAVIATE_GRPC_SECURE = os.environ.get("WEAVIATE_GRPC_SECURE", "false").lower() == "true" WEAVIATE_GRPC_SECURE = os.environ.get("WEAVIATE_GRPC_SECURE", "false").lower() == "true"
WEAVIATE_SKIP_INIT_CHECKS = os.environ.get("WEAVIATE_SKIP_INIT_CHECKS", "false").lower() == "true" WEAVIATE_SKIP_INIT_CHECKS = (
os.environ.get("WEAVIATE_SKIP_INIT_CHECKS", "false").lower() == "true"
)
# OpenSearch # OpenSearch
OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200") OPENSEARCH_URI = os.environ.get("OPENSEARCH_URI", "https://localhost:9200")
@@ -3486,10 +3488,14 @@ IMAGE_GENERATION_MODEL = PersistentConfig(
) )
# Regex pattern for models that support IMAGE_SIZE = "auto". # Regex pattern for models that support IMAGE_SIZE = "auto".
IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN = os.getenv("IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN", "^gpt-image") IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN = os.getenv(
"IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN", "^gpt-image"
)
# Regex pattern for models that return URLs instead of base64 data. # Regex pattern for models that return URLs instead of base64 data.
IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN = os.getenv("IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN", "^gpt-image") IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN = os.getenv(
"IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN", "^gpt-image"
)
IMAGE_SIZE = PersistentConfig( IMAGE_SIZE = PersistentConfig(
"IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512") "IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
+19 -17
View File
@@ -195,13 +195,23 @@ ENABLE_FORWARD_USER_INFO_HEADERS = (
) )
# Header names for user info forwarding (customizable via environment variables) # Header names for user info forwarding (customizable via environment variables)
FORWARD_USER_INFO_HEADER_USER_NAME = os.environ.get("FORWARD_USER_INFO_HEADER_USER_NAME", "X-OpenWebUI-User-Name") FORWARD_USER_INFO_HEADER_USER_NAME = os.environ.get(
FORWARD_USER_INFO_HEADER_USER_ID = os.environ.get("FORWARD_USER_INFO_HEADER_USER_ID", "X-OpenWebUI-User-Id") "FORWARD_USER_INFO_HEADER_USER_NAME", "X-OpenWebUI-User-Name"
FORWARD_USER_INFO_HEADER_USER_EMAIL = os.environ.get("FORWARD_USER_INFO_HEADER_USER_EMAIL", "X-OpenWebUI-User-Email") )
FORWARD_USER_INFO_HEADER_USER_ROLE = os.environ.get("FORWARD_USER_INFO_HEADER_USER_ROLE", "X-OpenWebUI-User-Role") FORWARD_USER_INFO_HEADER_USER_ID = os.environ.get(
"FORWARD_USER_INFO_HEADER_USER_ID", "X-OpenWebUI-User-Id"
)
FORWARD_USER_INFO_HEADER_USER_EMAIL = os.environ.get(
"FORWARD_USER_INFO_HEADER_USER_EMAIL", "X-OpenWebUI-User-Email"
)
FORWARD_USER_INFO_HEADER_USER_ROLE = os.environ.get(
"FORWARD_USER_INFO_HEADER_USER_ROLE", "X-OpenWebUI-User-Role"
)
# Header name for chat ID forwarding (customizable via environment variable) # Header name for chat ID forwarding (customizable via environment variable)
FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.environ.get("FORWARD_SESSION_INFO_HEADER_CHAT_ID", "X-OpenWebUI-Chat-Id") FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.environ.get(
"FORWARD_SESSION_INFO_HEADER_CHAT_ID", "X-OpenWebUI-Chat-Id"
)
# Experimental feature, may be removed in future # Experimental feature, may be removed in future
ENABLE_STAR_SESSIONS_MIDDLEWARE = ( ENABLE_STAR_SESSIONS_MIDDLEWARE = (
@@ -402,17 +412,13 @@ try:
except ValueError: except ValueError:
REDIS_SOCKET_CONNECT_TIMEOUT = None REDIS_SOCKET_CONNECT_TIMEOUT = None
REDIS_RECONNECT_DELAY = os.environ.get( REDIS_RECONNECT_DELAY = os.environ.get("REDIS_RECONNECT_DELAY", "")
"REDIS_RECONNECT_DELAY", ""
)
if REDIS_RECONNECT_DELAY == "": if REDIS_RECONNECT_DELAY == "":
REDIS_RECONNECT_DELAY = None REDIS_RECONNECT_DELAY = None
else: else:
try: try:
REDIS_RECONNECT_DELAY = float( REDIS_RECONNECT_DELAY = float(REDIS_RECONNECT_DELAY)
REDIS_RECONNECT_DELAY
)
if REDIS_RECONNECT_DELAY < 0: if REDIS_RECONNECT_DELAY < 0:
REDIS_RECONNECT_DELAY = None REDIS_RECONNECT_DELAY = None
except Exception: except Exception:
@@ -580,15 +586,11 @@ LICENSE_PUBLIC_KEY = os.environ.get("LICENSE_PUBLIC_KEY", "")
pk = None pk = None
if LICENSE_PUBLIC_KEY: if LICENSE_PUBLIC_KEY:
pk = serialization.load_pem_public_key( pk = serialization.load_pem_public_key(f"""
f"""
-----BEGIN PUBLIC KEY----- -----BEGIN PUBLIC KEY-----
{LICENSE_PUBLIC_KEY} {LICENSE_PUBLIC_KEY}
-----END PUBLIC KEY----- -----END PUBLIC KEY-----
""".encode( """.encode("utf-8"))
"utf-8"
)
)
#################################### ####################################
-1
View File
@@ -50,7 +50,6 @@ from open_webui.utils.payload import (
apply_system_prompt_to_body, apply_system_prompt_to_body,
) )
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -25,7 +25,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw import peewee as pw
from peewee_migrate import Migrator from peewee_migrate import Migrator
with suppress(ImportError): with suppress(ImportError):
import playhouse.postgres_ext as pw_pext import playhouse.postgres_ext as pw_pext
+3 -8
View File
@@ -551,7 +551,6 @@ from open_webui.utils.redis import get_sentinels_from_env
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
if SAFE_MODE: if SAFE_MODE:
print("SAFE MODE ENABLED") print("SAFE MODE ENABLED")
Functions.deactivate_all_functions() Functions.deactivate_all_functions()
@@ -575,8 +574,7 @@ class SPAStaticFiles(StaticFiles):
raise ex raise ex
print( print(rf"""
rf"""
██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ ██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗
██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║ ██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║
██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║ ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║
@@ -588,8 +586,7 @@ print(
v{VERSION} - building the best AI user interface. v{VERSION} - building the best AI user interface.
{f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""} {f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""}
https://github.com/open-webui/open-webui https://github.com/open-webui/open-webui
""" """)
)
@asynccontextmanager @asynccontextmanager
@@ -1845,9 +1842,7 @@ async def chat_completion(
# Emit chat:active=true when task starts # Emit chat:active=true when task starts
event_emitter = get_event_emitter(metadata, update_db=False) event_emitter = get_event_emitter(metadata, update_db=False)
if event_emitter: if event_emitter:
await event_emitter( await event_emitter({"type": "chat:active", "data": {"active": True}})
{"type": "chat:active", "data": {"active": True}}
)
return {"status": True, "task_id": task_id} return {"status": True, "task_id": task_id}
else: else:
return await process_chat(request, form_data, user, metadata, model) return await process_chat(request, form_data, user, metadata, model)
@@ -12,7 +12,6 @@ from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
import open_webui.internal.db import open_webui.internal.db
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "2f1211949ecc" revision: str = "2f1211949ecc"
down_revision: Union[str, None] = "37f288994c47" down_revision: Union[str, None] = "37f288994c47"
@@ -12,7 +12,6 @@ import uuid
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
revision: str = "374d2f66af06" revision: str = "374d2f66af06"
down_revision: Union[str, None] = "c440947495f3" down_revision: Union[str, None] = "c440947495f3"
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
@@ -14,7 +14,6 @@ from typing import Sequence, Union
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "37f288994c47" revision: str = "37f288994c47"
down_revision: Union[str, None] = "a5c220713937" down_revision: Union[str, None] = "a5c220713937"
@@ -11,7 +11,6 @@ from typing import Sequence, Union
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "38d63c18f30f" revision: str = "38d63c18f30f"
down_revision: Union[str, None] = "3af16a1c9fb6" down_revision: Union[str, None] = "3af16a1c9fb6"
@@ -12,7 +12,6 @@ from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
import open_webui.internal.db import open_webui.internal.db
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "6283dc0e4d8d" revision: str = "6283dc0e4d8d"
down_revision: Union[str, None] = "3e0e00844bb0" down_revision: Union[str, None] = "3e0e00844bb0"
@@ -11,7 +11,6 @@ import sqlalchemy as sa
from sqlalchemy.sql import table, column, select from sqlalchemy.sql import table, column, select
import json import json
revision = "6a39f3d8e55c" revision = "6a39f3d8e55c"
down_revision = "c0fbf31ca0db" down_revision = "c0fbf31ca0db"
branch_labels = None branch_labels = None
@@ -12,7 +12,6 @@ from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
import open_webui.internal.db import open_webui.internal.db
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "81cc2ce44d79" revision: str = "81cc2ce44d79"
down_revision: Union[str, None] = "6283dc0e4d8d" down_revision: Union[str, None] = "6283dc0e4d8d"
@@ -165,7 +165,9 @@ def upgrade() -> None:
log.warning(f"Failed to insert message {message_id}: {e}") log.warning(f"Failed to insert message {message_id}: {e}")
continue continue
log.info(f"Backfilled {messages_inserted} messages into chat_message table ({messages_failed} failed)") log.info(
f"Backfilled {messages_inserted} messages into chat_message table ({messages_failed} failed)"
)
def downgrade() -> None: def downgrade() -> None:
@@ -12,7 +12,6 @@ from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
import open_webui.internal.db import open_webui.internal.db
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "90ef40d4714e" revision: str = "90ef40d4714e"
down_revision: Union[str, None] = "b10670c03dd5" down_revision: Union[str, None] = "b10670c03dd5"
@@ -173,12 +173,10 @@ def upgrade() -> None:
for uid, api_key in users_with_keys: for uid, api_key in users_with_keys:
if api_key: if api_key:
conn.execute( conn.execute(
sa.text( sa.text("""
"""
INSERT INTO api_key (id, user_id, key, created_at, updated_at) INSERT INTO api_key (id, user_id, key, created_at, updated_at)
VALUES (:id, :user_id, :key, :created_at, :updated_at) VALUES (:id, :user_id, :key, :created_at, :updated_at)
""" """),
),
{ {
"id": f"key_{uid}", "id": f"key_{uid}",
"user_id": uid, "user_id": uid,
@@ -12,7 +12,6 @@ import json
from sqlalchemy.sql import table, column from sqlalchemy.sql import table, column
from sqlalchemy import String, Text, JSON, and_ from sqlalchemy import String, Text, JSON, and_
revision = "c29facfe716b" revision = "c29facfe716b"
down_revision = "c69f45358db4" down_revision = "c69f45358db4"
branch_labels = None branch_labels = None
@@ -11,7 +11,6 @@ from typing import Sequence, Union
from alembic import op from alembic import op
import sqlalchemy as sa import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = "c440947495f3" revision: str = "c440947495f3"
down_revision: Union[str, None] = "81cc2ce44d79" down_revision: Union[str, None] = "81cc2ce44d79"
@@ -98,9 +98,12 @@ def upgrade() -> None:
# Could be Python None (SQL NULL) or string "null" (JSON null) # Could be Python None (SQL NULL) or string "null" (JSON null)
# EXCEPTION: files with NULL are PRIVATE (owner-only), not public # EXCEPTION: files with NULL are PRIVATE (owner-only), not public
is_null = ( is_null = (
access_control_json is None or access_control_json is None
access_control_json == "null" or or access_control_json == "null"
(isinstance(access_control_json, str) and access_control_json.strip().lower() == "null") or (
isinstance(access_control_json, str)
and access_control_json.strip().lower() == "null"
)
) )
if is_null: if is_null:
# Files: NULL = private (no entry needed, owner has implicit access) # Files: NULL = private (no entry needed, owner has implicit access)
@@ -112,12 +115,10 @@ def upgrade() -> None:
if key not in inserted: if key not in inserted:
try: try:
conn.execute( conn.execute(
sa.text( sa.text("""
"""
INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at) INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at)
VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at) VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at)
""" """),
),
{ {
"id": str(uuid.uuid4()), "id": str(uuid.uuid4()),
"resource_type": resource_type, "resource_type": resource_type,
@@ -174,12 +175,10 @@ def upgrade() -> None:
continue continue
try: try:
conn.execute( conn.execute(
sa.text( sa.text("""
"""
INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at) INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at)
VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at) VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at)
""" """),
),
{ {
"id": str(uuid.uuid4()), "id": str(uuid.uuid4()),
"resource_type": resource_type, "resource_type": resource_type,
@@ -200,12 +199,10 @@ def upgrade() -> None:
continue continue
try: try:
conn.execute( conn.execute(
sa.text( sa.text("""
"""
INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at) INSERT INTO access_grant (id, resource_type, resource_id, principal_type, principal_id, permission, created_at)
VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at) VALUES (:id, :resource_type, :resource_id, :principal_type, :principal_id, :permission, :created_at)
""" """),
),
{ {
"id": str(uuid.uuid4()), "id": str(uuid.uuid4()),
"resource_type": resource_type, "resource_type": resource_type,
@@ -265,7 +262,7 @@ def downgrade() -> None:
FROM access_grant FROM access_grant
WHERE resource_type = :resource_type WHERE resource_type = :resource_type
"""), """),
{"resource_type": resource_type} {"resource_type": resource_type},
) )
rows = result.fetchall() rows = result.fetchall()
except Exception: except Exception:
@@ -287,39 +284,61 @@ def downgrade() -> None:
} }
# Handle public access (user:* for read) # Handle public access (user:* for read)
if principal_type == "user" and principal_id == "*" and permission == "read": if (
principal_type == "user"
and principal_id == "*"
and permission == "read"
):
resource_grants[resource_id]["is_public"] = True resource_grants[resource_id]["is_public"] = True
continue continue
# Add to appropriate list # Add to appropriate list
if permission in ["read", "write"]: if permission in ["read", "write"]:
if principal_type == "group": if principal_type == "group":
if principal_id not in resource_grants[resource_id][permission]["group_ids"]: if (
resource_grants[resource_id][permission]["group_ids"].append(principal_id) principal_id
not in resource_grants[resource_id][permission]["group_ids"]
):
resource_grants[resource_id][permission]["group_ids"].append(
principal_id
)
elif principal_type == "user": elif principal_type == "user":
if principal_id not in resource_grants[resource_id][permission]["user_ids"]: if (
resource_grants[resource_id][permission]["user_ids"].append(principal_id) principal_id
not in resource_grants[resource_id][permission]["user_ids"]
):
resource_grants[resource_id][permission]["user_ids"].append(
principal_id
)
# Step 3: Update each resource with reconstructed JSON # Step 3: Update each resource with reconstructed JSON
for resource_id, grants in resource_grants.items(): for resource_id, grants in resource_grants.items():
if grants["is_public"]: if grants["is_public"]:
# Public = NULL # Public = NULL
access_control_value = None access_control_value = None
elif (not grants["read"]["group_ids"] and not grants["read"]["user_ids"] and elif (
not grants["write"]["group_ids"] and not grants["write"]["user_ids"]): not grants["read"]["group_ids"]
and not grants["read"]["user_ids"]
and not grants["write"]["group_ids"]
and not grants["write"]["user_ids"]
):
# No grants = should not happen (would mean no entries), default to {} # No grants = should not happen (would mean no entries), default to {}
access_control_value = json.dumps({}) access_control_value = json.dumps({})
else: else:
# Custom permissions # Custom permissions
access_control_value = json.dumps({ access_control_value = json.dumps(
"read": grants["read"], {
"write": grants["write"], "read": grants["read"],
}) "write": grants["write"],
}
)
try: try:
conn.execute( conn.execute(
sa.text(f'UPDATE "{table_name}" SET access_control = :access_control WHERE id = :id'), sa.text(
{"access_control": access_control_value, "id": resource_id} f'UPDATE "{table_name}" SET access_control = :access_control WHERE id = :id'
),
{"access_control": access_control_value, "id": resource_id},
) )
except Exception: except Exception:
pass pass
@@ -330,15 +349,15 @@ def downgrade() -> None:
if resource_type != "file": if resource_type != "file":
try: try:
conn.execute( conn.execute(
sa.text(f''' sa.text(f"""
UPDATE "{table_name}" UPDATE "{table_name}"
SET access_control = :private_value SET access_control = :private_value
WHERE id NOT IN ( WHERE id NOT IN (
SELECT DISTINCT resource_id FROM access_grant WHERE resource_type = :resource_type SELECT DISTINCT resource_id FROM access_grant WHERE resource_type = :resource_type
) )
AND access_control IS NULL AND access_control IS NULL
'''), """),
{"private_value": json.dumps({}), "resource_type": resource_type} {"private_value": json.dumps({}), "resource_type": resource_type},
) )
except Exception: except Exception:
pass pass
+11 -5
View File
@@ -22,10 +22,14 @@ class AccessGrant(Base):
__tablename__ = "access_grant" __tablename__ = "access_grant"
id = Column(Text, primary_key=True) id = Column(Text, primary_key=True)
resource_type = Column(Text, nullable=False) # "knowledge", "model", "prompt", "tool", "note", "channel", "file" resource_type = Column(
Text, nullable=False
) # "knowledge", "model", "prompt", "tool", "note", "channel", "file"
resource_id = Column(Text, nullable=False) resource_id = Column(Text, nullable=False)
principal_type = Column(Text, nullable=False) # "user" or "group" principal_type = Column(Text, nullable=False) # "user" or "group"
principal_id = Column(Text, nullable=False) # user_id, group_id, or "*" (wildcard for public) principal_id = Column(
Text, nullable=False
) # user_id, group_id, or "*" (wildcard for public)
permission = Column(Text, nullable=False) # "read" or "write" permission = Column(Text, nullable=False) # "read" or "write"
created_at = Column(BigInteger, nullable=False) created_at = Column(BigInteger, nullable=False)
@@ -173,9 +177,11 @@ def normalize_access_grants(access_grants: Optional[list]) -> list[dict]:
key = (principal_type, principal_id, permission) key = (principal_type, principal_id, permission)
deduped[key] = { deduped[key] = {
"id": grant.get("id") "id": (
if isinstance(grant.get("id"), str) and grant.get("id") grant.get("id")
else str(uuid.uuid4()), if isinstance(grant.get("id"), str) and grant.get("id")
else str(uuid.uuid4())
),
"principal_type": principal_type, "principal_type": principal_type,
"principal_id": principal_id, "principal_id": principal_id,
"permission": permission, "permission": permission,
+4 -4
View File
@@ -263,7 +263,9 @@ class ChannelTable:
def _to_channel_model( def _to_channel_model(
self, channel: Channel, db: Optional[Session] = None self, channel: Channel, db: Optional[Session] = None
) -> ChannelModel: ) -> ChannelModel:
channel_data = ChannelModel.model_validate(channel).model_dump(exclude={"access_grants"}) channel_data = ChannelModel.model_validate(channel).model_dump(
exclude={"access_grants"}
)
access_grants = self._get_access_grants(channel_data["id"], db=db) access_grants = self._get_access_grants(channel_data["id"], db=db)
channel_data["access_grants"] = access_grants channel_data["access_grants"] = access_grants
return ChannelModel.model_validate(channel_data) return ChannelModel.model_validate(channel_data)
@@ -770,9 +772,7 @@ class ChannelTable:
.first() .first()
) )
if membership: if membership:
allowed_channels.append( allowed_channels.append(self._to_channel_model(channel, db=db))
self._to_channel_model(channel, db=db)
)
continue continue
# --- Case B: standard channel => rely on ACL permissions --- # --- Case B: standard channel => rely on ACL permissions ---
+48 -14
View File
@@ -332,7 +332,11 @@ class ChatMessageTable:
if end_date: if end_date:
query = query.filter(ChatMessage.created_at <= end_date) query = query.filter(ChatMessage.created_at <= end_date)
if group_id: if group_id:
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery() group_users = (
db.query(GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.subquery()
)
query = query.filter(ChatMessage.user_id.in_(group_users)) query = query.filter(ChatMessage.user_id.in_(group_users))
results = query.group_by(ChatMessage.model_id).all() results = query.group_by(ChatMessage.model_id).all()
@@ -362,10 +366,12 @@ class ChatMessageTable:
elif dialect == "postgresql": elif dialect == "postgresql":
# Use json_extract_path_text for PostgreSQL JSON columns # Use json_extract_path_text for PostgreSQL JSON columns
input_tokens = cast( input_tokens = cast(
func.json_extract_path_text(ChatMessage.usage, "input_tokens"), Integer func.json_extract_path_text(ChatMessage.usage, "input_tokens"),
Integer,
) )
output_tokens = cast( output_tokens = cast(
func.json_extract_path_text(ChatMessage.usage, "output_tokens"), Integer func.json_extract_path_text(ChatMessage.usage, "output_tokens"),
Integer,
) )
else: else:
raise NotImplementedError(f"Unsupported dialect: {dialect}") raise NotImplementedError(f"Unsupported dialect: {dialect}")
@@ -387,7 +393,11 @@ class ChatMessageTable:
if end_date: if end_date:
query = query.filter(ChatMessage.created_at <= end_date) query = query.filter(ChatMessage.created_at <= end_date)
if group_id: if group_id:
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery() group_users = (
db.query(GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.subquery()
)
query = query.filter(ChatMessage.user_id.in_(group_users)) query = query.filter(ChatMessage.user_id.in_(group_users))
results = query.group_by(ChatMessage.model_id).all() results = query.group_by(ChatMessage.model_id).all()
@@ -424,10 +434,12 @@ class ChatMessageTable:
elif dialect == "postgresql": elif dialect == "postgresql":
# Use json_extract_path_text for PostgreSQL JSON columns # Use json_extract_path_text for PostgreSQL JSON columns
input_tokens = cast( input_tokens = cast(
func.json_extract_path_text(ChatMessage.usage, "input_tokens"), Integer func.json_extract_path_text(ChatMessage.usage, "input_tokens"),
Integer,
) )
output_tokens = cast( output_tokens = cast(
func.json_extract_path_text(ChatMessage.usage, "output_tokens"), Integer func.json_extract_path_text(ChatMessage.usage, "output_tokens"),
Integer,
) )
else: else:
raise NotImplementedError(f"Unsupported dialect: {dialect}") raise NotImplementedError(f"Unsupported dialect: {dialect}")
@@ -481,7 +493,11 @@ class ChatMessageTable:
if end_date: if end_date:
query = query.filter(ChatMessage.created_at <= end_date) query = query.filter(ChatMessage.created_at <= end_date)
if group_id: if group_id:
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery() group_users = (
db.query(GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.subquery()
)
query = query.filter(ChatMessage.user_id.in_(group_users)) query = query.filter(ChatMessage.user_id.in_(group_users))
results = query.group_by(ChatMessage.user_id).all() results = query.group_by(ChatMessage.user_id).all()
@@ -507,7 +523,11 @@ class ChatMessageTable:
if end_date: if end_date:
query = query.filter(ChatMessage.created_at <= end_date) query = query.filter(ChatMessage.created_at <= end_date)
if group_id: if group_id:
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery() group_users = (
db.query(GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.subquery()
)
query = query.filter(ChatMessage.user_id.in_(group_users)) query = query.filter(ChatMessage.user_id.in_(group_users))
results = query.group_by(ChatMessage.chat_id).all() results = query.group_by(ChatMessage.chat_id).all()
@@ -536,7 +556,11 @@ class ChatMessageTable:
if end_date: if end_date:
query = query.filter(ChatMessage.created_at <= end_date) query = query.filter(ChatMessage.created_at <= end_date)
if group_id: if group_id:
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery() group_users = (
db.query(GroupMember.user_id)
.filter(GroupMember.group_id == group_id)
.subquery()
)
query = query.filter(ChatMessage.user_id.in_(group_users)) query = query.filter(ChatMessage.user_id.in_(group_users))
results = query.all() results = query.all()
@@ -544,10 +568,14 @@ class ChatMessageTable:
# Group by date -> model -> count # Group by date -> model -> count
daily_counts: dict[str, dict[str, int]] = {} daily_counts: dict[str, dict[str, int]] = {}
for timestamp, model_id in results: for timestamp, model_id in results:
date_str = datetime.fromtimestamp(_normalize_timestamp(timestamp)).strftime("%Y-%m-%d") date_str = datetime.fromtimestamp(
_normalize_timestamp(timestamp)
).strftime("%Y-%m-%d")
if date_str not in daily_counts: if date_str not in daily_counts:
daily_counts[date_str] = {} daily_counts[date_str] = {}
daily_counts[date_str][model_id] = daily_counts[date_str].get(model_id, 0) + 1 daily_counts[date_str][model_id] = (
daily_counts[date_str].get(model_id, 0) + 1
)
# Fill in missing days # Fill in missing days
if start_date and end_date: if start_date and end_date:
@@ -587,14 +615,20 @@ class ChatMessageTable:
# Group by hour -> model -> count # Group by hour -> model -> count
hourly_counts: dict[str, dict[str, int]] = {} hourly_counts: dict[str, dict[str, int]] = {}
for timestamp, model_id in results: for timestamp, model_id in results:
hour_str = datetime.fromtimestamp(_normalize_timestamp(timestamp)).strftime("%Y-%m-%d %H:00") hour_str = datetime.fromtimestamp(
_normalize_timestamp(timestamp)
).strftime("%Y-%m-%d %H:00")
if hour_str not in hourly_counts: if hour_str not in hourly_counts:
hourly_counts[hour_str] = {} hourly_counts[hour_str] = {}
hourly_counts[hour_str][model_id] = hourly_counts[hour_str].get(model_id, 0) + 1 hourly_counts[hour_str][model_id] = (
hourly_counts[hour_str].get(model_id, 0) + 1
)
# Fill in missing hours # Fill in missing hours
if start_date and end_date: if start_date and end_date:
current = datetime.fromtimestamp(_normalize_timestamp(start_date)).replace(minute=0, second=0, microsecond=0) current = datetime.fromtimestamp(
_normalize_timestamp(start_date)
).replace(minute=0, second=0, microsecond=0)
end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date)) end_dt = datetime.fromtimestamp(_normalize_timestamp(end_date))
while current <= end_dt: while current <= end_dt:
hour_str = current.strftime("%Y-%m-%d %H:00") hour_str = current.strftime("%Y-%m-%d %H:00")
+18 -24
View File
@@ -329,7 +329,9 @@ class ChatTable:
data=message, data=message,
) )
except Exception as e: except Exception as e:
log.warning(f"Failed to write initial messages to chat_message table: {e}") log.warning(
f"Failed to write initial messages to chat_message table: {e}"
)
return ChatModel.model_validate(chat_item) if chat_item else None return ChatModel.model_validate(chat_item) if chat_item else None
@@ -388,7 +390,9 @@ class ChatTable:
data=message, data=message,
) )
except Exception as e: except Exception as e:
log.warning(f"Failed to write imported messages to chat_message table: {e}") log.warning(
f"Failed to write imported messages to chat_message table: {e}"
)
return [ChatModel.model_validate(chat) for chat in chats] return [ChatModel.model_validate(chat) for chat in chats]
@@ -739,8 +743,10 @@ class ChatTable:
) -> list[ChatModel]: ) -> list[ChatModel]:
with get_db_context(db) as db: with get_db_context(db) as db:
query = db.query(Chat).filter_by(user_id=user_id).filter( query = (
Chat.share_id.isnot(None) db.query(Chat)
.filter_by(user_id=user_id)
.filter(Chat.share_id.isnot(None))
) )
if filter: if filter:
@@ -1110,29 +1116,23 @@ class ChatTable:
# Check if there are any tags to filter, it should have all the tags # Check if there are any tags to filter, it should have all the tags
if "none" in tag_ids: if "none" in tag_ids:
query = query.filter( query = query.filter(text("""
text(
"""
NOT EXISTS ( NOT EXISTS (
SELECT 1 SELECT 1
FROM json_each(Chat.meta, '$.tags') AS tag FROM json_each(Chat.meta, '$.tags') AS tag
) )
""" """))
)
)
elif tag_ids: elif tag_ids:
query = query.filter( query = query.filter(
and_( and_(
*[ *[
text( text(f"""
f"""
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM json_each(Chat.meta, '$.tags') AS tag FROM json_each(Chat.meta, '$.tags') AS tag
WHERE tag.value = :tag_id_{tag_idx} WHERE tag.value = :tag_id_{tag_idx}
) )
""" """).params(**{f"tag_id_{tag_idx}": tag_id})
).params(**{f"tag_id_{tag_idx}": tag_id})
for tag_idx, tag_id in enumerate(tag_ids) for tag_idx, tag_id in enumerate(tag_ids)
] ]
) )
@@ -1168,29 +1168,23 @@ class ChatTable:
# Check if there are any tags to filter, it should have all the tags # Check if there are any tags to filter, it should have all the tags
if "none" in tag_ids: if "none" in tag_ids:
query = query.filter( query = query.filter(text("""
text(
"""
NOT EXISTS ( NOT EXISTS (
SELECT 1 SELECT 1
FROM json_array_elements_text(Chat.meta->'tags') AS tag FROM json_array_elements_text(Chat.meta->'tags') AS tag
) )
""" """))
)
)
elif tag_ids: elif tag_ids:
query = query.filter( query = query.filter(
and_( and_(
*[ *[
text( text(f"""
f"""
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM json_array_elements_text(Chat.meta->'tags') AS tag FROM json_array_elements_text(Chat.meta->'tags') AS tag
WHERE tag = :tag_id_{tag_idx} WHERE tag = :tag_id_{tag_idx}
) )
""" """).params(**{f"tag_id_{tag_idx}": tag_id})
).params(**{f"tag_id_{tag_idx}": tag_id})
for tag_idx, tag_id in enumerate(tag_ids) for tag_idx, tag_id in enumerate(tag_ids)
] ]
) )
-1
View File
@@ -11,7 +11,6 @@ from sqlalchemy.orm import Session
from open_webui.internal.db import Base, JSONField, get_db, get_db_context from open_webui.internal.db import Base, JSONField, get_db, get_db_context
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
-1
View File
@@ -214,7 +214,6 @@ class FunctionsTable:
except Exception: except Exception:
return [] return []
def get_functions( def get_functions(
self, active_only=False, include_valves=False, db: Optional[Session] = None self, active_only=False, include_valves=False, db: Optional[Session] = None
) -> list[FunctionModel | FunctionWithValvesModel]: ) -> list[FunctionModel | FunctionWithValvesModel]:
+1 -2
View File
@@ -25,7 +25,6 @@ from sqlalchemy import (
select, select,
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
#################### ####################
@@ -187,7 +186,7 @@ class GroupTable:
Group.data.is_(None), Group.data.is_(None),
json_share_str.is_(None), json_share_str.is_(None),
json_share_lower == "true", json_share_lower == "true",
json_share_lower == "1", # Handle SQLite boolean true json_share_lower == "1", # Handle SQLite boolean true
) )
if member_id: if member_id:
+14 -8
View File
@@ -30,7 +30,6 @@ from sqlalchemy import (
or_, or_,
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
#################### ####################
@@ -402,9 +401,7 @@ class KnowledgeTable:
try: try:
with get_db_context(db) as db: with get_db_context(db) as db:
knowledge = db.query(Knowledge).filter_by(id=id).first() knowledge = db.query(Knowledge).filter_by(id=id).first()
return ( return self._to_knowledge_model(knowledge, db=db) if knowledge else None
self._to_knowledge_model(knowledge, db=db) if knowledge else None
)
except Exception: except Exception:
return None return None
@@ -443,7 +440,10 @@ class KnowledgeTable:
.filter(KnowledgeFile.file_id == file_id) .filter(KnowledgeFile.file_id == file_id)
.all() .all()
) )
return [self._to_knowledge_model(knowledge, db=db) for knowledge in knowledges] return [
self._to_knowledge_model(knowledge, db=db)
for knowledge in knowledges
]
except Exception: except Exception:
return [] return []
@@ -484,11 +484,17 @@ class KnowledgeTable:
is_asc = direction == "asc" is_asc = direction == "asc"
if order_by == "name": if order_by == "name":
primary_sort = File.filename.asc() if is_asc else File.filename.desc() primary_sort = (
File.filename.asc() if is_asc else File.filename.desc()
)
elif order_by == "created_at": elif order_by == "created_at":
primary_sort = File.created_at.asc() if is_asc else File.created_at.desc() primary_sort = (
File.created_at.asc() if is_asc else File.created_at.desc()
)
elif order_by == "updated_at": elif order_by == "updated_at":
primary_sort = File.updated_at.asc() if is_asc else File.updated_at.desc() primary_sort = (
File.updated_at.asc() if is_asc else File.updated_at.desc()
)
# Apply sort with secondary key for deterministic pagination # Apply sort with secondary key for deterministic pagination
query = query.order_by(primary_sort, File.id.asc()) query = query.order_by(primary_sort, File.id.asc())
+3 -2
View File
@@ -18,7 +18,6 @@ from sqlalchemy.dialects import postgresql, sqlite
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy import BigInteger, Column, Text, Boolean from sqlalchemy import BigInteger, Column, Text, Boolean
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -182,7 +181,9 @@ class ModelsTable:
def get_all_models(self, db: Optional[Session] = None) -> list[ModelModel]: def get_all_models(self, db: Optional[Session] = None) -> list[ModelModel]:
with get_db_context(db) as db: with get_db_context(db) as db:
return [self._to_model_model(model, db=db) for model in db.query(Model).all()] return [
self._to_model_model(model, db=db) for model in db.query(Model).all()
]
def get_models(self, db: Optional[Session] = None) -> list[ModelUserResponse]: def get_models(self, db: Optional[Session] = None) -> list[ModelUserResponse]:
with get_db_context(db) as db: with get_db_context(db) as db:
@@ -13,7 +13,6 @@ from open_webui.models.users import Users, UserResponse
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from sqlalchemy import BigInteger, Column, Text, JSON, Index from sqlalchemy import BigInteger, Column, Text, JSON, Index
#################### ####################
# PromptHistory DB Schema # PromptHistory DB Schema
#################### ####################
+3 -3
View File
@@ -13,7 +13,6 @@ from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, or_, func, cast from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, or_, func, cast
#################### ####################
# Prompts DB Schema # Prompts DB Schema
#################### ####################
@@ -146,7 +145,9 @@ class PromptsTable:
"data": form_data.data or {}, "data": form_data.data or {},
"meta": form_data.meta or {}, "meta": form_data.meta or {},
"tags": form_data.tags or [], "tags": form_data.tags or [],
"access_grants": [grant.model_dump() for grant in current_access_grants], "access_grants": [
grant.model_dump() for grant in current_access_grants
],
} }
history_entry = PromptHistories.create_history_entry( history_entry = PromptHistories.create_history_entry(
@@ -345,7 +346,6 @@ class PromptsTable:
return PromptListResponse(items=prompts, total=total) return PromptListResponse(items=prompts, total=total)
def update_prompt_by_command( def update_prompt_by_command(
self, self,
command: str, command: str,
form_data: PromptForm, form_data: PromptForm,
+4 -5
View File
@@ -11,7 +11,6 @@ from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import BigInteger, Boolean, Column, String, Text, or_ from sqlalchemy import BigInteger, Boolean, Column, String, Text, or_
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
#################### ####################
@@ -112,7 +111,9 @@ class SkillsTable:
return AccessGrants.get_grants_by_resource("skill", skill_id, db=db) return AccessGrants.get_grants_by_resource("skill", skill_id, db=db)
def _to_skill_model(self, skill: Skill, db: Optional[Session] = None) -> SkillModel: def _to_skill_model(self, skill: Skill, db: Optional[Session] = None) -> SkillModel:
skill_data = SkillModel.model_validate(skill).model_dump(exclude={"access_grants"}) skill_data = SkillModel.model_validate(skill).model_dump(
exclude={"access_grants"}
)
skill_data["access_grants"] = self._get_access_grants(skill_data["id"], db=db) skill_data["access_grants"] = self._get_access_grants(skill_data["id"], db=db)
return SkillModel.model_validate(skill_data) return SkillModel.model_validate(skill_data)
@@ -223,9 +224,7 @@ class SkillsTable:
from open_webui.models.users import User, UserModel from open_webui.models.users import User, UserModel
# Join with User table for user filtering # Join with User table for user filtering
query = db.query(Skill, User).outerjoin( query = db.query(Skill, User).outerjoin(User, User.id == Skill.user_id)
User, User.id == Skill.user_id
)
if filter: if filter:
query_key = filter.get("query") query_key = filter.get("query")
-1
View File
@@ -11,7 +11,6 @@ from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import BigInteger, Column, String, Text from sqlalchemy import BigInteger, Column, String, Text
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
#################### ####################
@@ -8,7 +8,6 @@ from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS, REQUESTS_VERIFY
from open_webui.retrieval.models.base_reranker import BaseReranker from open_webui.retrieval.models.base_reranker import BaseReranker
from open_webui.utils.headers import include_user_info_headers from open_webui.utils.headers import include_user_info_headers
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
+3 -1
View File
@@ -695,7 +695,9 @@ async def agenerate_azure_openai_batch_embeddings(
trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT)
) as session: ) as session:
async with session.post( async with session.post(
full_url, headers=headers, json=form_data, full_url,
headers=headers,
json=form_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL, ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r: ) as r:
r.raise_for_status() r.raise_for_status()
@@ -256,8 +256,7 @@ class Oracle23aiClient(VectorDBBase):
with connection.cursor() as cursor: with connection.cursor() as cursor:
try: try:
log.info("Creating Table document_chunk") log.info("Creating Table document_chunk")
cursor.execute( cursor.execute("""
"""
BEGIN BEGIN
EXECUTE IMMEDIATE ' EXECUTE IMMEDIATE '
CREATE TABLE IF NOT EXISTS document_chunk ( CREATE TABLE IF NOT EXISTS document_chunk (
@@ -274,12 +273,10 @@ class Oracle23aiClient(VectorDBBase):
RAISE; RAISE;
END IF; END IF;
END; END;
""" """)
)
log.info("Creating Index document_chunk_collection_name_idx") log.info("Creating Index document_chunk_collection_name_idx")
cursor.execute( cursor.execute("""
"""
BEGIN BEGIN
EXECUTE IMMEDIATE ' EXECUTE IMMEDIATE '
CREATE INDEX IF NOT EXISTS document_chunk_collection_name_idx CREATE INDEX IF NOT EXISTS document_chunk_collection_name_idx
@@ -291,12 +288,10 @@ class Oracle23aiClient(VectorDBBase):
RAISE; RAISE;
END IF; END IF;
END; END;
""" """)
)
log.info("Creating VECTOR INDEX document_chunk_vector_ivf_idx") log.info("Creating VECTOR INDEX document_chunk_vector_ivf_idx")
cursor.execute( cursor.execute("""
"""
BEGIN BEGIN
EXECUTE IMMEDIATE ' EXECUTE IMMEDIATE '
CREATE VECTOR INDEX IF NOT EXISTS document_chunk_vector_ivf_idx CREATE VECTOR INDEX IF NOT EXISTS document_chunk_vector_ivf_idx
@@ -312,8 +307,7 @@ class Oracle23aiClient(VectorDBBase):
RAISE; RAISE;
END IF; END IF;
END; END;
""" """)
)
connection.commit() connection.commit()
log.info("Database initialization completed successfully.") log.info("Database initialization completed successfully.")
@@ -51,7 +51,6 @@ from open_webui.config import (
PGVECTOR_USE_HALFVEC, PGVECTOR_USE_HALFVEC,
) )
VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
USE_HALFVEC = PGVECTOR_USE_HALFVEC USE_HALFVEC = PGVECTOR_USE_HALFVEC
@@ -121,34 +120,26 @@ class PgvectorClient(VectorDBBase):
# Ensure the pgvector extension is available # Ensure the pgvector extension is available
# Use a conditional check to avoid permission issues on Azure PostgreSQL # Use a conditional check to avoid permission issues on Azure PostgreSQL
if PGVECTOR_CREATE_EXTENSION: if PGVECTOR_CREATE_EXTENSION:
self.session.execute( self.session.execute(text("""
text(
"""
DO $$ DO $$
BEGIN BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN
CREATE EXTENSION IF NOT EXISTS vector; CREATE EXTENSION IF NOT EXISTS vector;
END IF; END IF;
END $$; END $$;
""" """))
)
)
if PGVECTOR_PGCRYPTO: if PGVECTOR_PGCRYPTO:
# Ensure the pgcrypto extension is available for encryption # Ensure the pgcrypto extension is available for encryption
# Use a conditional check to avoid permission issues on Azure PostgreSQL # Use a conditional check to avoid permission issues on Azure PostgreSQL
self.session.execute( self.session.execute(text("""
text(
"""
DO $$ DO $$
BEGIN BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto') THEN IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto') THEN
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pgcrypto;
END IF; END IF;
END $$; END $$;
""" """))
)
)
if not PGVECTOR_PGCRYPTO_KEY: if not PGVECTOR_PGCRYPTO_KEY:
raise ValueError( raise ValueError(
@@ -216,15 +207,13 @@ class PgvectorClient(VectorDBBase):
def _ensure_vector_index(self, index_method: str, index_options: str) -> None: def _ensure_vector_index(self, index_method: str, index_options: str) -> None:
index_name = "idx_document_chunk_vector" index_name = "idx_document_chunk_vector"
existing_index_def = self.session.execute( existing_index_def = self.session.execute(
text( text("""
"""
SELECT indexdef SELECT indexdef
FROM pg_indexes FROM pg_indexes
WHERE schemaname = current_schema() WHERE schemaname = current_schema()
AND tablename = 'document_chunk' AND tablename = 'document_chunk'
AND indexname = :index_name AND indexname = :index_name
""" """),
),
{"index_name": index_name}, {"index_name": index_name},
).scalar() ).scalar()
@@ -310,8 +299,7 @@ class PgvectorClient(VectorDBBase):
# Ensure metadata is converted to its JSON text representation # Ensure metadata is converted to its JSON text representation
json_metadata = json.dumps(item["metadata"]) json_metadata = json.dumps(item["metadata"])
self.session.execute( self.session.execute(
text( text("""
"""
INSERT INTO document_chunk INSERT INTO document_chunk
(id, vector, collection_name, text, vmetadata) (id, vector, collection_name, text, vmetadata)
VALUES ( VALUES (
@@ -320,8 +308,7 @@ class PgvectorClient(VectorDBBase):
pgp_sym_encrypt(:metadata_text, :key) pgp_sym_encrypt(:metadata_text, :key)
) )
ON CONFLICT (id) DO NOTHING ON CONFLICT (id) DO NOTHING
""" """),
),
{ {
"id": item["id"], "id": item["id"],
"vector": vector, "vector": vector,
@@ -363,8 +350,7 @@ class PgvectorClient(VectorDBBase):
vector = self.adjust_vector_length(item["vector"]) vector = self.adjust_vector_length(item["vector"])
json_metadata = json.dumps(item["metadata"]) json_metadata = json.dumps(item["metadata"])
self.session.execute( self.session.execute(
text( text("""
"""
INSERT INTO document_chunk INSERT INTO document_chunk
(id, vector, collection_name, text, vmetadata) (id, vector, collection_name, text, vmetadata)
VALUES ( VALUES (
@@ -377,8 +363,7 @@ class PgvectorClient(VectorDBBase):
collection_name = EXCLUDED.collection_name, collection_name = EXCLUDED.collection_name,
text = EXCLUDED.text, text = EXCLUDED.text,
vmetadata = EXCLUDED.vmetadata vmetadata = EXCLUDED.vmetadata
""" """),
),
{ {
"id": item["id"], "id": item["id"],
"vector": vector, "vector": vector,
@@ -33,7 +33,6 @@ from open_webui.config import (
) )
from open_webui.retrieval.vector.utils import process_metadata from open_webui.retrieval.vector.utils import process_metadata
NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system
BATCH_SIZE = 100 # Recommended batch size for Pinecone operations BATCH_SIZE = 100 # Recommended batch size for Pinecone operations
@@ -10,7 +10,6 @@ from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.utils.headers import include_user_info_headers from open_webui.utils.headers import include_user_info_headers
from open_webui.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID from open_webui.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -3,7 +3,6 @@ from typing import Optional, List
from open_webui.retrieval.web.main import SearchResult, get_filtered_results from open_webui.retrieval.web.main import SearchResult, get_filtered_results
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -5,7 +5,6 @@ import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.utils.headers import include_user_info_headers from open_webui.utils.headers import include_user_info_headers
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
+32 -16
View File
@@ -31,14 +31,14 @@ def xml_element_contents_to_string(element: Element) -> str:
def search_yandex( def search_yandex(
request: Request, request: Request,
yandex_search_url: str, yandex_search_url: str,
yandex_search_api_key: str, yandex_search_api_key: str,
yandex_search_config: str, yandex_search_config: str,
query: str, query: str,
count: int, count: int,
filter_list: Optional[List[str]] = None, filter_list: Optional[List[str]] = None,
user=None, user=None,
) -> List[SearchResult]: ) -> List[SearchResult]:
try: try:
headers = { headers = {
@@ -73,7 +73,11 @@ def search_yandex(
payload["groupSpec"]["docsInGroup"] = 1 payload["groupSpec"]["docsInGroup"] = 1
response = requests.post( response = requests.post(
"https://searchapi.api.cloud.yandex.net/v2/web/search" if yandex_search_url == "" else yandex_search_url, (
"https://searchapi.api.cloud.yandex.net/v2/web/search"
if yandex_search_url == ""
else yandex_search_url
),
headers=headers, headers=headers,
json=payload, json=payload,
) )
@@ -84,18 +88,28 @@ def search_yandex(
if "rawData" not in response_body: if "rawData" not in response_body:
raise Exception(f"No `rawData` in response body: {response_body}") raise Exception(f"No `rawData` in response body: {response_body}")
search_result_body_bytes = base64.decodebytes(bytes(response_body["rawData"], "utf-8")) search_result_body_bytes = base64.decodebytes(
bytes(response_body["rawData"], "utf-8")
)
doc_root = ET.parse(io.BytesIO(search_result_body_bytes)) doc_root = ET.parse(io.BytesIO(search_result_body_bytes))
results = [] results = []
for group in doc_root.findall("response/results/grouping/group"): for group in doc_root.findall("response/results/grouping/group"):
results.append({ results.append(
"url": xml_element_contents_to_string(group.find("doc/url")).strip("\n"), {
"title": xml_element_contents_to_string(group.find("doc/title")).strip("\n"), "url": xml_element_contents_to_string(group.find("doc/url")).strip(
"snippet": xml_element_contents_to_string(group.find("doc/passages/passage")), "\n"
}) ),
"title": xml_element_contents_to_string(
group.find("doc/title")
).strip("\n"),
"snippet": xml_element_contents_to_string(
group.find("doc/passages/passage")
),
}
)
results = get_filtered_results(results, filter_list) results = get_filtered_results(results, filter_list)
@@ -140,7 +154,9 @@ if __name__ == "__main__":
), ),
os.environ.get("YANDEX_WEB_SEARCH_URL", ""), os.environ.get("YANDEX_WEB_SEARCH_URL", ""),
os.environ.get("YANDEX_WEB_SEARCH_API_KEY", ""), os.environ.get("YANDEX_WEB_SEARCH_API_KEY", ""),
os.environ.get("YANDEX_WEB_SEARCH_CONFIG", "{\"query\": {\"searchType\": \"SEARCH_TYPE_COM\"}}"), os.environ.get(
"YANDEX_WEB_SEARCH_CONFIG", '{"query": {"searchType": "SEARCH_TYPE_COM"}}'
),
"TOP movies of the past year", "TOP movies of the past year",
3, 3,
) )
+22 -19
View File
@@ -90,22 +90,26 @@ async def get_user_analytics(
) )
# Get user info for top users # Get user info for top users
top_user_ids = [uid for uid, _ in sorted(counts.items(), key=lambda x: -x[1])[:limit]] top_user_ids = [
uid for uid, _ in sorted(counts.items(), key=lambda x: -x[1])[:limit]
]
user_info = {u.id: u for u in Users.get_users_by_user_ids(top_user_ids, db=db)} user_info = {u.id: u for u in Users.get_users_by_user_ids(top_user_ids, db=db)}
users = [] users = []
for user_id in top_user_ids: for user_id in top_user_ids:
u = user_info.get(user_id) u = user_info.get(user_id)
tokens = token_usage.get(user_id, {}) tokens = token_usage.get(user_id, {})
users.append(UserAnalyticsEntry( users.append(
user_id=user_id, UserAnalyticsEntry(
name=u.name if u else None, user_id=user_id,
email=u.email if u else None, name=u.name if u else None,
count=counts[user_id], email=u.email if u else None,
input_tokens=tokens.get("input_tokens", 0), count=counts[user_id],
output_tokens=tokens.get("output_tokens", 0), input_tokens=tokens.get("input_tokens", 0),
total_tokens=tokens.get("total_tokens", 0), output_tokens=tokens.get("output_tokens", 0),
)) total_tokens=tokens.get("total_tokens", 0),
)
)
return UserAnalyticsResponse(users=users) return UserAnalyticsResponse(users=users)
@@ -317,9 +321,7 @@ async def get_model_chats(
if isinstance(content, str): if isinstance(content, str):
first_message = content[:200] first_message = content[:200]
elif isinstance(content, list): elif isinstance(content, list):
text_parts = [ text_parts = [b.get("text", "") for b in content if isinstance(b, dict)]
b.get("text", "") for b in content if isinstance(b, dict)
]
first_message = " ".join(text_parts)[:200] first_message = " ".join(text_parts)[:200]
# Get user info # Get user info
@@ -331,7 +333,6 @@ async def get_model_chats(
# Timestamps from messages # Timestamps from messages
updated_at = max(m.created_at for m in messages) if messages else 0 updated_at = max(m.created_at for m in messages) if messages else 0
chats_data.append( chats_data.append(
ModelChatEntry( ModelChatEntry(
chat_id=chat_id, chat_id=chat_id,
@@ -427,11 +428,13 @@ async def get_model_overview(
while current <= end_dt: while current <= end_dt:
date_str = current.strftime("%Y-%m-%d") date_str = current.strftime("%Y-%m-%d")
counts = history_counts.get(date_str, {"won": 0, "lost": 0}) counts = history_counts.get(date_str, {"won": 0, "lost": 0})
history.append(HistoryEntry( history.append(
date=date_str, HistoryEntry(
won=counts["won"], date=date_str,
lost=counts["lost"], won=counts["won"],
)) lost=counts["lost"],
)
)
current += timedelta(days=1) current += timedelta(days=1)
# Get chat tags # Get chat tags
-1
View File
@@ -57,7 +57,6 @@ from open_webui.env import (
ENABLE_FORWARD_USER_INFO_HEADERS, ENABLE_FORWARD_USER_INFO_HEADERS,
) )
router = APIRouter() router = APIRouter()
# Constants # Constants
+3 -1
View File
@@ -558,7 +558,9 @@ async def ldap_auth(
except Exception as e: except Exception as e:
log.error(f"Failed to sync groups for user {user.id}: {e}") log.error(f"Failed to sync groups for user {user.id}: {e}")
return create_session_response(request, user, db, response, set_cookie=True) return create_session_response(
request, user, db, response, set_cookie=True
)
else: else:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
else: else:
+1 -3
View File
@@ -1633,9 +1633,7 @@ async def update_message_by_id(
if ( if (
user.role != "admin" user.role != "admin"
and message.user_id != user.id and message.user_id != user.id
and not channel_has_access( and not channel_has_access(user.id, channel, permission="read", db=db)
user.id, channel, permission="read", db=db
)
): ):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
-1
View File
@@ -33,7 +33,6 @@ from fastapi.responses import FileResponse, StreamingResponse
from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.access_control import has_permission from open_webui.utils.access_control import has_permission
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
-1
View File
@@ -29,7 +29,6 @@ from pydantic import BaseModel, HttpUrl
from open_webui.internal.db import get_session from open_webui.internal.db import get_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
-1
View File
@@ -22,7 +22,6 @@ from sqlalchemy.orm import Session
from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.auth import get_admin_user, get_verified_user
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
+18 -7
View File
@@ -14,7 +14,11 @@ import requests
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from open_webui.config import CACHE_DIR, IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN from open_webui.config import (
CACHE_DIR,
IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN,
IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN,
)
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
from open_webui.retrieval.web.utils import validate_url from open_webui.retrieval.web.utils import validate_url
from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS from open_webui.env import ENABLE_FORWARD_USER_INFO_HEADERS
@@ -199,9 +203,8 @@ async def update_config(
request.app.state.config.IMAGE_GENERATION_ENGINE = form_data.IMAGE_GENERATION_ENGINE request.app.state.config.IMAGE_GENERATION_ENGINE = form_data.IMAGE_GENERATION_ENGINE
set_image_model(request, form_data.IMAGE_GENERATION_MODEL) set_image_model(request, form_data.IMAGE_GENERATION_MODEL)
if ( if form_data.IMAGE_SIZE == "auto" and not re.match(
form_data.IMAGE_SIZE == "auto" IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, form_data.IMAGE_GENERATION_MODEL
and not re.match(IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, form_data.IMAGE_GENERATION_MODEL)
): ):
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
@@ -610,7 +613,10 @@ async def image_generations(
), ),
**( **(
{} {}
if re.match(IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN, request.app.state.config.IMAGE_GENERATION_MODEL) if re.match(
IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN,
request.app.state.config.IMAGE_GENERATION_MODEL,
)
else {"response_format": "b64_json"} else {"response_format": "b64_json"}
), ),
**( **(
@@ -912,7 +918,9 @@ async def image_edits(
form_data.image = await load_url_image(form_data.image) form_data.image = await load_url_image(form_data.image)
elif isinstance(form_data.image, list): elif isinstance(form_data.image, list):
# Load all images in parallel for better performance # Load all images in parallel for better performance
form_data.image = list(await asyncio.gather(*[load_url_image(img) for img in form_data.image])) form_data.image = list(
await asyncio.gather(*[load_url_image(img) for img in form_data.image])
)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
@@ -947,7 +955,10 @@ async def image_edits(
**({"size": size} if size else {}), **({"size": size} if size else {}),
**( **(
{} {}
if re.match(IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN, request.app.state.config.IMAGE_EDIT_MODEL) if re.match(
IMAGE_URL_RESPONSE_MODELS_REGEX_PATTERN,
request.app.state.config.IMAGE_EDIT_MODEL,
)
else {"response_format": "b64_json"} else {"response_format": "b64_json"}
), ),
} }
+1 -4
View File
@@ -36,7 +36,6 @@ from open_webui.models.access_grants import AccessGrants, has_public_read_access
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
from open_webui.models.models import Models, ModelForm from open_webui.models.models import Models, ModelForm
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
@@ -540,9 +539,7 @@ async def update_knowledge_access_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
) )
AccessGrants.set_access_grants( AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db)
"knowledge", id, form_data.access_grants, db=db
)
return KnowledgeFilesResponse( return KnowledgeFilesResponse(
**Knowledges.get_knowledge_by_id(id=id, db=db).model_dump(), **Knowledges.get_knowledge_by_id(id=id, db=db).model_dump(),
+1 -3
View File
@@ -345,9 +345,7 @@ async def update_note_access_by_id(
status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT() status_code=status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.DEFAULT()
) )
AccessGrants.set_access_grants( AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db)
"note", id, form_data.access_grants, db=db
)
return Notes.get_note_by_id(id, db=db) return Notes.get_note_by_id(id, db=db)
+19 -11
View File
@@ -54,7 +54,6 @@ from open_webui.utils.misc import (
from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.headers import include_user_info_headers from open_webui.utils.headers import include_user_info_headers
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -828,7 +827,9 @@ def convert_to_responses_payload(payload: dict) -> dict:
if isinstance(content, str): if isinstance(content, str):
system_content = content system_content = content
elif isinstance(content, list): elif isinstance(content, list):
system_content = "\n".join(p.get("text", "") for p in content if p.get("type") == "text") system_content = "\n".join(
p.get("text", "") for p in content if p.get("type") == "text"
)
continue continue
# Convert content format # Convert content format
@@ -840,19 +841,21 @@ def convert_to_responses_payload(payload: dict) -> dict:
content_parts = [] content_parts = []
for part in content: for part in content:
if part.get("type") == "text": if part.get("type") == "text":
content_parts.append({"type": text_type, "text": part.get("text", "")}) content_parts.append(
{"type": text_type, "text": part.get("text", "")}
)
elif part.get("type") == "image_url": elif part.get("type") == "image_url":
url_data = part.get("image_url", {}) url_data = part.get("image_url", {})
url = url_data.get("url", "") if isinstance(url_data, dict) else url_data url = (
url_data.get("url", "")
if isinstance(url_data, dict)
else url_data
)
content_parts.append({"type": "input_image", "image_url": url}) content_parts.append({"type": "input_image", "image_url": url})
else: else:
content_parts = [{"type": text_type, "text": str(content)}] content_parts = [{"type": text_type, "text": str(content)}]
input_items.append({ input_items.append({"type": "message", "role": role, "content": content_parts})
"type": "message",
"role": role,
"content": content_parts
})
responses_payload = {**payload, "input": input_items} responses_payload = {**payload, "input": input_items}
@@ -863,7 +866,13 @@ def convert_to_responses_payload(payload: dict) -> dict:
responses_payload["max_output_tokens"] = responses_payload.pop("max_tokens") responses_payload["max_output_tokens"] = responses_payload.pop("max_tokens")
# Remove Chat Completions-only parameters not supported by the Responses API # Remove Chat Completions-only parameters not supported by the Responses API
for unsupported_key in ("stream_options", "logit_bias", "frequency_penalty", "presence_penalty", "stop"): for unsupported_key in (
"stream_options",
"logit_bias",
"frequency_penalty",
"presence_penalty",
"stop",
):
responses_payload.pop(unsupported_key, None) responses_payload.pop(unsupported_key, None)
# Convert Chat Completions tools format to Responses API format # Convert Chat Completions tools format to Responses API format
@@ -892,7 +901,6 @@ def convert_to_responses_payload(payload: dict) -> dict:
return responses_payload return responses_payload
def convert_responses_result(response: dict) -> dict: def convert_responses_result(response: dict) -> dict:
""" """
Convert non-streaming Responses API result. Convert non-streaming Responses API result.
+7 -13
View File
@@ -107,7 +107,9 @@ async def get_prompt_list(
filter["user_id"] = user.id filter["user_id"] = user.id
result = Prompts.search_prompts(user.id, filter=filter, skip=skip, limit=limit, db=db) result = Prompts.search_prompts(
user.id, filter=filter, skip=skip, limit=limit, db=db
)
return PromptAccessListResponse( return PromptAccessListResponse(
items=[ items=[
@@ -313,9 +315,7 @@ async def update_prompt_by_id(
) )
# Use the ID from the found prompt # Use the ID from the found prompt
updated_prompt = Prompts.update_prompt_by_id( updated_prompt = Prompts.update_prompt_by_id(prompt.id, form_data, user.id, db=db)
prompt.id, form_data, user.id, db=db
)
if updated_prompt: if updated_prompt:
return updated_prompt return updated_prompt
else: else:
@@ -464,9 +464,7 @@ async def update_prompt_access_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED, detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
) )
AccessGrants.set_access_grants( AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db)
"prompt", prompt_id, form_data.access_grants, db=db
)
return Prompts.get_prompt_by_id(prompt_id, db=db) return Prompts.get_prompt_by_id(prompt_id, db=db)
@@ -554,9 +552,7 @@ async def get_prompt_history(
return history return history
@router.get( @router.get("/id/{prompt_id}/history/{history_id}", response_model=PromptHistoryModel)
"/id/{prompt_id}/history/{history_id}", response_model=PromptHistoryModel
)
async def get_prompt_history_entry( async def get_prompt_history_entry(
prompt_id: str, prompt_id: str,
history_id: str, history_id: str,
@@ -599,9 +595,7 @@ async def get_prompt_history_entry(
return history_entry return history_entry
@router.delete( @router.delete("/id/{prompt_id}/history/{history_id}", response_model=bool)
"/id/{prompt_id}/history/{history_id}", response_model=bool
)
async def delete_prompt_history_entry( async def delete_prompt_history_entry(
prompt_id: str, prompt_id: str,
history_id: str, history_id: str,
+2 -7
View File
@@ -24,7 +24,6 @@ from open_webui.utils.access_control import has_access, has_permission
from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
PAGE_ITEM_COUNT = 30 PAGE_ITEM_COUNT = 30
@@ -98,9 +97,7 @@ async def get_skill_list(
filter["user_id"] = user.id filter["user_id"] = user.id
result = Skills.search_skills( result = Skills.search_skills(user.id, filter=filter, skip=skip, limit=limit, db=db)
user.id, filter=filter, skip=skip, limit=limit, db=db
)
return SkillAccessListResponse( return SkillAccessListResponse(
items=[ items=[
@@ -343,9 +340,7 @@ async def update_skill_access_by_id(
detail=ERROR_MESSAGES.UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED,
) )
AccessGrants.set_access_grants( AccessGrants.set_access_grants("skill", id, form_data.access_grants, db=db)
"skill", id, form_data.access_grants, db=db
)
return Skills.get_skill_by_id(id, db=db) return Skills.get_skill_by_id(id, db=db)
-1
View File
@@ -36,7 +36,6 @@ from open_webui.config import (
DEFAULT_VOICE_MODE_PROMPT_TEMPLATE, DEFAULT_VOICE_MODE_PROMPT_TEMPLATE,
) )
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
+1 -4
View File
@@ -36,7 +36,6 @@ from open_webui.utils.tools import get_tool_servers
from open_webui.config import CACHE_DIR, BYPASS_ADMIN_ACCESS_CONTROL from open_webui.config import CACHE_DIR, BYPASS_ADMIN_ACCESS_CONTROL
from open_webui.constants import ERROR_MESSAGES from open_webui.constants import ERROR_MESSAGES
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -555,9 +554,7 @@ async def update_tool_access_by_id(
detail=ERROR_MESSAGES.UNAUTHORIZED, detail=ERROR_MESSAGES.UNAUTHORIZED,
) )
AccessGrants.set_access_grants( AccessGrants.set_access_grants("tool", id, form_data.access_grants, db=db)
"tool", id, form_data.access_grants, db=db
)
return Tools.get_tool_by_id(id, db=db) return Tools.get_tool_by_id(id, db=db)
-1
View File
@@ -41,7 +41,6 @@ from open_webui.utils.auth import (
) )
from open_webui.utils.access_control import get_permissions, has_permission from open_webui.utils.access_control import get_permissions, has_permission
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
-1
View File
@@ -15,7 +15,6 @@ from open_webui.utils.pdf_generator import PDFGenerator
from open_webui.utils.auth import get_admin_user, get_verified_user from open_webui.utils.auth import get_admin_user, get_verified_user
from open_webui.utils.code_interpreter import execute_code_jupyter from open_webui.utils.code_interpreter import execute_code_jupyter
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
-1
View File
@@ -49,7 +49,6 @@ from open_webui.env import (
GLOBAL_LOG_LEVEL, GLOBAL_LOG_LEVEL,
) )
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
-1
View File
@@ -34,7 +34,6 @@ from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient from azure.storage.blob import BlobServiceClient
from azure.core.exceptions import ResourceNotFoundError from azure.core.exceptions import ResourceNotFoundError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
-1
View File
@@ -10,7 +10,6 @@ from typing import Dict, List, Optional
from open_webui.env import REDIS_KEY_PREFIX from open_webui.env import REDIS_KEY_PREFIX
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# A dictionary to keep track of active tasks # A dictionary to keep track of active tasks
+28 -15
View File
@@ -380,8 +380,8 @@ async def execute_code(
# Add import blocking code if there are blocked modules # Add import blocking code if there are blocked modules
if CODE_INTERPRETER_BLOCKED_MODULES: if CODE_INTERPRETER_BLOCKED_MODULES:
import textwrap import textwrap
blocking_code = textwrap.dedent(
f""" blocking_code = textwrap.dedent(f"""
import builtins import builtins
BLOCKED_MODULES = {CODE_INTERPRETER_BLOCKED_MODULES} BLOCKED_MODULES = {CODE_INTERPRETER_BLOCKED_MODULES}
@@ -397,15 +397,20 @@ async def execute_code(
return _real_import(name, globals, locals, fromlist, level) return _real_import(name, globals, locals, fromlist, level)
builtins.__import__ = restricted_import builtins.__import__ = restricted_import
""" """)
)
code = blocking_code + "\n" + code code = blocking_code + "\n" + code
engine = getattr(__request__.app.state.config, "CODE_INTERPRETER_ENGINE", "pyodide") engine = getattr(
__request__.app.state.config, "CODE_INTERPRETER_ENGINE", "pyodide"
)
if engine == "pyodide": if engine == "pyodide":
# Execute via frontend pyodide using bidirectional event call # Execute via frontend pyodide using bidirectional event call
if __event_call__ is None: if __event_call__ is None:
return json.dumps({"error": "Event call not available. WebSocket connection required for pyodide execution."}) return json.dumps(
{
"error": "Event call not available. WebSocket connection required for pyodide execution."
}
)
output = await __event_call__( output = await __event_call__(
{ {
@@ -413,7 +418,9 @@ async def execute_code(
"data": { "data": {
"id": str(uuid4()), "id": str(uuid4()),
"code": code, "code": code,
"session_id": __metadata__.get("session_id") if __metadata__ else None, "session_id": (
__metadata__.get("session_id") if __metadata__ else None
),
}, },
} }
) )
@@ -436,12 +443,14 @@ async def execute_code(
code, code,
( (
__request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN __request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_TOKEN
if __request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH == "token" if __request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH
== "token"
else None else None
), ),
( (
__request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD __request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH_PASSWORD
if __request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH == "password" if __request__.app.state.config.CODE_INTERPRETER_JUPYTER_AUTH
== "password"
else None else None
), ),
__request__.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT, __request__.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
@@ -1921,7 +1930,9 @@ async def view_skill(
# Check user access # Check user access
user_role = __user__.get("role", "user") user_role = __user__.get("role", "user")
if user_role != "admin" and skill.user_id != user_id: if user_role != "admin" and skill.user_id != user_id:
user_group_ids = [group.id for group in Groups.get_groups_by_member_id(user_id)] user_group_ids = [
group.id for group in Groups.get_groups_by_member_id(user_id)
]
if not AccessGrants.has_access( if not AccessGrants.has_access(
user_id=user_id, user_id=user_id,
resource_type="skill", resource_type="skill",
@@ -1931,11 +1942,13 @@ async def view_skill(
): ):
return json.dumps({"error": "Access denied"}) return json.dumps({"error": "Access denied"})
return json.dumps({ return json.dumps(
"name": skill.name, {
"content": skill.content, "name": skill.name,
}, ensure_ascii=False) "content": skill.content,
},
ensure_ascii=False,
)
except Exception as e: except Exception as e:
log.exception(f"view_skill error: {e}") log.exception(f"view_skill error: {e}")
return json.dumps({"error": str(e)}) return json.dumps({"error": str(e)})
+25 -14
View File
@@ -139,16 +139,23 @@ def has_access(
continue continue
principal_type = grant.get("principal_type") principal_type = grant.get("principal_type")
principal_id = grant.get("principal_id") principal_id = grant.get("principal_id")
if principal_type == "user" and (principal_id == "*" or principal_id == user_id): if principal_type == "user" and (
principal_id == "*" or principal_id == user_id
):
return True return True
if principal_type == "group" and user_group_ids and principal_id in user_group_ids: if (
principal_type == "group"
and user_group_ids
and principal_id in user_group_ids
):
return True return True
return False return False
def migrate_access_control(data: dict, ac_key: str = "access_control", grants_key: str = "access_grants") -> None: def migrate_access_control(
data: dict, ac_key: str = "access_control", grants_key: str = "access_grants"
) -> None:
""" """
Auto-migrate a config dict in-place from legacy access_control dict to access_grants list. Auto-migrate a config dict in-place from legacy access_control dict to access_grants list.
@@ -169,17 +176,21 @@ def migrate_access_control(data: dict, ac_key: str = "access_control", grants_ke
if not perm_data: if not perm_data:
continue continue
for group_id in perm_data.get("group_ids", []): for group_id in perm_data.get("group_ids", []):
grants.append({ grants.append(
"principal_type": "group", {
"principal_id": group_id, "principal_type": "group",
"permission": perm, "principal_id": group_id,
}) "permission": perm,
}
)
for uid in perm_data.get("user_ids", []): for uid in perm_data.get("user_ids", []):
grants.append({ grants.append(
"principal_type": "user", {
"principal_id": uid, "principal_type": "user",
"permission": perm, "principal_id": uid,
}) "permission": perm,
}
)
data[grants_key] = grants data[grants_key] = grants
data.pop(ac_key, None) data.pop(ac_key, None)
+3 -2
View File
@@ -28,7 +28,6 @@ from open_webui.env import AUDIT_LOG_LEVEL, MAX_BODY_LOG_SIZE
from open_webui.utils.auth import get_current_user, get_http_authorization_cred from open_webui.utils.auth import get_current_user, get_http_authorization_cred
from open_webui.models.users import UserModel from open_webui.models.users import UserModel
if TYPE_CHECKING: if TYPE_CHECKING:
from loguru import Logger from loguru import Logger
@@ -222,7 +221,9 @@ class AuditLoggingMiddleware:
# Skip logging if the request is not authenticated # Skip logging if the request is not authenticated
# Check both Authorization header (API keys) and token cookie (browser sessions) # Check both Authorization header (API keys) and token cookie (browser sessions)
if not request.headers.get("authorization") and not request.cookies.get("token"): if not request.headers.get("authorization") and not request.cookies.get(
"token"
):
return True return True
# match either /api/<resource>/...(for the endpoint /api/chat case) or /api/v1/<resource>/... # match either /api/<resource>/...(for the endpoint /api/chat case) or /api/v1/<resource>/...
-1
View File
@@ -46,7 +46,6 @@ from open_webui.env import (
from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status from fastapi import BackgroundTasks, Depends, HTTPException, Request, Response, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
SESSION_SECRET = WEBUI_SECRET_KEY SESSION_SECRET = WEBUI_SECRET_KEY
+3 -2
View File
@@ -57,7 +57,6 @@ from open_webui.utils.filter import (
from open_webui.env import GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL from open_webui.env import GLOBAL_LOG_LEVEL, BYPASS_MODEL_ACCESS_CONTROL
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -341,7 +340,9 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
} }
try: try:
filter_ids = get_sorted_filter_ids(request, model, metadata.get("filter_ids", [])) filter_ids = get_sorted_filter_ids(
request, model, metadata.get("filter_ids", [])
)
filter_functions = Functions.get_functions_by_ids(filter_ids) filter_functions = Functions.get_functions_by_ids(filter_ids)
result, _ = await process_filter_functions( result, _ = await process_filter_functions(
@@ -8,7 +8,6 @@ import aiohttp
import websockets import websockets
from pydantic import BaseModel from pydantic import BaseModel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
-1
View File
@@ -86,4 +86,3 @@ async def generate_embeddings(
form_data=form_data, form_data=form_data,
user=user, user=user,
) )
-1
View File
@@ -17,7 +17,6 @@ from open_webui.env import (
ENABLE_OTEL_LOGS, ENABLE_OTEL_LOGS,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from loguru import Record from loguru import Record
+128 -98
View File
@@ -133,7 +133,6 @@ from open_webui.env import (
from open_webui.utils.headers import include_user_info_headers from open_webui.utils.headers import include_user_info_headers
from open_webui.constants import TASKS from open_webui.constants import TASKS
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -2106,10 +2105,15 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if all_skill_ids: if all_skill_ids:
from open_webui.models.skills import Skills as SkillsModel from open_webui.models.skills import Skills as SkillsModel
accessible_skill_ids = {s.id for s in SkillsModel.get_skills_by_user_id(user.id, "read")} accessible_skill_ids = {
s.id for s in SkillsModel.get_skills_by_user_id(user.id, "read")
}
available_skills = [ available_skills = [
s for sid in all_skill_ids s
if sid in accessible_skill_ids and (s := SkillsModel.get_skill_by_id(sid)) and s.is_active for sid in all_skill_ids
if sid in accessible_skill_ids
and (s := SkillsModel.get_skill_by_id(sid))
and s.is_active
] ]
if available_skills: if available_skills:
@@ -2238,7 +2242,9 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if ENABLE_FORWARD_USER_INFO_HEADERS and user: if ENABLE_FORWARD_USER_INFO_HEADERS and user:
headers = include_user_info_headers(headers, user) headers = include_user_info_headers(headers, user)
if metadata and metadata.get("chat_id"): if metadata and metadata.get("chat_id"):
headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get("chat_id") headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get(
"chat_id"
)
mcp_clients[server_id] = MCPClient() mcp_clients[server_id] = MCPClient()
await mcp_clients[server_id].connect( await mcp_clients[server_id].connect(
@@ -2812,9 +2818,7 @@ async def non_streaming_chat_response_handler(response, ctx):
"id": output_id("msg"), "id": output_id("msg"),
"status": "completed", "status": "completed",
"role": "assistant", "role": "assistant",
"content": [ "content": [{"type": "output_text", "text": content}],
{"type": "output_text", "text": content}
],
} }
] ]
@@ -2967,9 +2971,7 @@ async def streaming_chat_response_handler(response, ctx):
match = re.search(start_tag_pattern, content) match = re.search(start_tag_pattern, content)
if match: if match:
try: try:
attr_content = ( attr_content = match.group(1) if match.group(1) else ""
match.group(1) if match.group(1) else ""
)
except: except:
attr_content = "" attr_content = ""
@@ -2982,7 +2984,7 @@ async def streaming_chat_response_handler(response, ctx):
current_text = get_last_text(output) current_text = get_last_text(output)
set_last_text( set_last_text(
output, output,
current_text.replace(match.group(0) + after_tag, "") current_text.replace(match.group(0) + after_tag, ""),
) )
if before_tag: if before_tag:
@@ -3031,7 +3033,9 @@ async def streaming_chat_response_handler(response, ctx):
"id": output_id("msg"), "id": output_id("msg"),
"status": "in_progress", "status": "in_progress",
"role": "assistant", "role": "assistant",
"content": [{"type": "output_text", "text": ""}], "content": [
{"type": "output_text", "text": ""}
],
"_tag_type": content_type, "_tag_type": content_type,
"start_tag": start_tag, "start_tag": start_tag,
"end_tag": end_tag, "end_tag": end_tag,
@@ -3059,8 +3063,14 @@ async def streaming_chat_response_handler(response, ctx):
elif ( elif (
(last_type == "reasoning" and content_type == "reasoning") (last_type == "reasoning" and content_type == "reasoning")
or (last_type == "open_webui:code_interpreter" and content_type == "code_interpreter") or (
or (last_type == "message" and output[-1].get("_tag_type") == content_type) last_type == "open_webui:code_interpreter"
and content_type == "code_interpreter"
)
or (
last_type == "message"
and output[-1].get("_tag_type") == content_type
)
): ):
item = output[-1] item = output[-1]
start_tag = item.get("start_tag", "") start_tag = item.get("start_tag", "")
@@ -3178,9 +3188,7 @@ async def streaming_chat_response_handler(response, ctx):
# Clean processed content # Clean processed content
start_tag_clean = rf"{re.escape(start_tag)}" start_tag_clean = rf"{re.escape(start_tag)}"
if start_tag.startswith("<") and start_tag.endswith(">"): if start_tag.startswith("<") and start_tag.endswith(">"):
start_tag_clean = ( start_tag_clean = rf"<{re.escape(start_tag[1:-1])}(\s.*?)?>"
rf"<{re.escape(start_tag[1:-1])}(\s.*?)?>"
)
content = re.sub( content = re.sub(
rf"{start_tag_clean}(.|\n)*?{re.escape(end_tag)}", rf"{start_tag_clean}(.|\n)*?{re.escape(end_tag)}",
@@ -3231,7 +3239,6 @@ async def streaming_chat_response_handler(response, ctx):
else: else:
output = [] output = []
usage = None usage = None
reasoning_tags_param = metadata.get("params", {}).get("reasoning_tags") reasoning_tags_param = metadata.get("params", {}).get("reasoning_tags")
@@ -3514,14 +3521,19 @@ async def streaming_chat_response_handler(response, ctx):
for tc in response_tool_calls: for tc in response_tool_calls:
call_id = tc.get("id", "") call_id = tc.get("id", "")
func = tc.get("function", {}) func = tc.get("function", {})
pending_fc_items.append({ pending_fc_items.append(
"type": "function_call", {
"id": call_id or output_id("fc"), "type": "function_call",
"call_id": call_id, "id": call_id
"name": func.get("name", ""), or output_id("fc"),
"arguments": func.get("arguments", "{}"), "call_id": call_id,
"status": "in_progress", "name": func.get("name", ""),
}) "arguments": func.get(
"arguments", "{}"
),
"status": "in_progress",
}
)
pending_output = output + pending_fc_items pending_output = output + pending_fc_items
await event_emitter( await event_emitter(
{ {
@@ -3585,22 +3597,25 @@ async def streaming_chat_response_handler(response, ctx):
# Append to reasoning content # Append to reasoning content
parts = reasoning_item.get("content", []) parts = reasoning_item.get("content", [])
if parts and parts[-1].get("type") == "output_text": if (
parts
and parts[-1].get("type") == "output_text"
):
parts[-1]["text"] += reasoning_content parts[-1]["text"] += reasoning_content
else: else:
reasoning_item["content"] = [ reasoning_item["content"] = [
{"type": "output_text", "text": reasoning_content} {
"type": "output_text",
"text": reasoning_content,
}
] ]
data = { data = {"content": serialize_output(output)}
"content": serialize_output(output)
}
if value: if value:
if ( if (
output output
and output[-1].get("type") and output[-1].get("type") == "reasoning"
== "reasoning"
and output[-1] and output[-1]
.get("attributes", {}) .get("attributes", {})
.get("type") .get("type")
@@ -3620,7 +3635,12 @@ async def streaming_chat_response_handler(response, ctx):
"id": output_id("msg"), "id": output_id("msg"),
"status": "in_progress", "status": "in_progress",
"role": "assistant", "role": "assistant",
"content": [{"type": "output_text", "text": ""}], "content": [
{
"type": "output_text",
"text": "",
}
],
} }
) )
@@ -3650,13 +3670,22 @@ async def streaming_chat_response_handler(response, ctx):
"id": output_id("msg"), "id": output_id("msg"),
"status": "in_progress", "status": "in_progress",
"role": "assistant", "role": "assistant",
"content": [{"type": "output_text", "text": ""}], "content": [
{
"type": "output_text",
"text": "",
}
],
} }
) )
# Append value to last message item's text # Append value to last message item's text
msg_parts = output[-1].get("content", []) msg_parts = output[-1].get("content", [])
if msg_parts and msg_parts[-1].get("type") == "output_text": if (
msg_parts
and msg_parts[-1].get("type")
== "output_text"
):
msg_parts[-1]["text"] += value msg_parts[-1]["text"] += value
else: else:
output[-1]["content"] = [ output[-1]["content"] = [
@@ -3664,32 +3693,26 @@ async def streaming_chat_response_handler(response, ctx):
] ]
if DETECT_REASONING_TAGS: if DETECT_REASONING_TAGS:
content, output, _ = ( content, output, _ = tag_output_handler(
tag_output_handler( "reasoning",
"reasoning", reasoning_tags,
reasoning_tags, content,
content, output,
output,
)
) )
content, output, _ = ( content, output, _ = tag_output_handler(
tag_output_handler( "solution",
"solution", DEFAULT_SOLUTION_TAGS,
DEFAULT_SOLUTION_TAGS, content,
content, output,
output,
)
) )
if DETECT_CODE_INTERPRETER: if DETECT_CODE_INTERPRETER:
content, output, end = ( content, output, end = tag_output_handler(
tag_output_handler( "code_interpreter",
"code_interpreter", DEFAULT_CODE_INTERPRETER_TAGS,
DEFAULT_CODE_INTERPRETER_TAGS, content,
content, output,
output,
)
) )
if end: if end:
@@ -3707,9 +3730,7 @@ async def streaming_chat_response_handler(response, ctx):
) )
else: else:
data = { data = {
"content": serialize_output( "content": serialize_output(output),
output
),
} }
if delta: if delta:
@@ -3750,7 +3771,9 @@ async def streaming_chat_response_handler(response, ctx):
"id": output_id("msg"), "id": output_id("msg"),
"status": "in_progress", "status": "in_progress",
"role": "assistant", "role": "assistant",
"content": [{"type": "output_text", "text": ""}], "content": [
{"type": "output_text", "text": ""}
],
} }
) )
@@ -3788,14 +3811,16 @@ async def streaming_chat_response_handler(response, ctx):
for tc in response_tool_calls: for tc in response_tool_calls:
call_id = tc.get("id", "") call_id = tc.get("id", "")
func = tc.get("function", {}) func = tc.get("function", {})
output.append({ output.append(
"type": "function_call", {
"id": call_id or output_id("fc"), "type": "function_call",
"call_id": call_id, "id": call_id or output_id("fc"),
"name": func.get("name", ""), "call_id": call_id,
"arguments": func.get("arguments", "{}"), "name": func.get("name", ""),
"status": "in_progress", "arguments": func.get("arguments", "{}"),
}) "status": "in_progress",
}
)
await event_emitter( await event_emitter(
{ {
@@ -3954,35 +3979,42 @@ async def streaming_chat_response_handler(response, ctx):
call_id = tc.get("id", "") call_id = tc.get("id", "")
# Mark function_call as completed # Mark function_call as completed
for item in output: for item in output:
if item.get("type") == "function_call" and item.get("call_id") == call_id: if (
item.get("type") == "function_call"
and item.get("call_id") == call_id
):
item["status"] = "completed" item["status"] = "completed"
# Update arguments with parsed/sanitized version # Update arguments with parsed/sanitized version
item["arguments"] = tc.get("function", {}).get("arguments", "{}") item["arguments"] = tc.get("function", {}).get(
"arguments", "{}"
)
break break
for result in results: for result in results:
output.append({ output.append(
"type": "function_call_output", {
"id": output_id("fco"), "type": "function_call_output",
"call_id": result.get("tool_call_id", ""), "id": output_id("fco"),
"output": [ "call_id": result.get("tool_call_id", ""),
{ "output": [
"type": "input_text", {
"text": result.get("content", ""), "type": "input_text",
} "text": result.get("content", ""),
], }
"status": "completed", ],
**( "status": "completed",
{"files": result.get("files")} **(
if result.get("files") {"files": result.get("files")}
else {} if result.get("files")
), else {}
**( ),
{"embeds": result.get("embeds")} **(
if result.get("embeds") {"embeds": result.get("embeds")}
else {} if result.get("embeds")
), else {}
}) ),
}
)
# Append a new empty message item for the next response # Append a new empty message item for the next response
output.append( output.append(
@@ -4079,8 +4111,7 @@ async def streaming_chat_response_handler(response, ctx):
code = sanitize_code(code) code = sanitize_code(code)
if CODE_INTERPRETER_BLOCKED_MODULES: if CODE_INTERPRETER_BLOCKED_MODULES:
blocking_code = textwrap.dedent( blocking_code = textwrap.dedent(f"""
f"""
import builtins import builtins
BLOCKED_MODULES = {CODE_INTERPRETER_BLOCKED_MODULES} BLOCKED_MODULES = {CODE_INTERPRETER_BLOCKED_MODULES}
@@ -4096,8 +4127,7 @@ async def streaming_chat_response_handler(response, ctx):
return _real_import(name, globals, locals, fromlist, level) return _real_import(name, globals, locals, fromlist, level)
builtins.__import__ = restricted_import builtins.__import__ = restricted_import
""" """)
)
code = blocking_code + "\n" + code code = blocking_code + "\n" + code
if ( if (
+26 -20
View File
@@ -151,11 +151,15 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
def flush_pending(): def flush_pending():
nonlocal pending_content, pending_tool_calls nonlocal pending_content, pending_tool_calls
if pending_content or pending_tool_calls: if pending_content or pending_tool_calls:
messages.append({ messages.append(
"role": "assistant", {
"content": "\n".join(pending_content) if pending_content else "", "role": "assistant",
**({"tool_calls": pending_tool_calls} if pending_tool_calls else {}), "content": "\n".join(pending_content) if pending_content else "",
}) **(
{"tool_calls": pending_tool_calls} if pending_tool_calls else {}
),
}
)
pending_content = [] pending_content = []
pending_tool_calls = [] pending_tool_calls = []
@@ -178,14 +182,16 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
# Ensure arguments is always a JSON string # Ensure arguments is always a JSON string
if not isinstance(arguments, str): if not isinstance(arguments, str):
arguments = json.dumps(arguments) arguments = json.dumps(arguments)
pending_tool_calls.append({ pending_tool_calls.append(
"id": item.get("call_id", ""), {
"type": "function", "id": item.get("call_id", ""),
"function": { "type": "function",
"name": item.get("name", ""), "function": {
"arguments": arguments, "name": item.get("name", ""),
"arguments": arguments,
},
} }
}) )
elif item_type == "function_call_output": elif item_type == "function_call_output":
# Flush any pending content/tool_calls before adding tool result # Flush any pending content/tool_calls before adding tool result
@@ -198,11 +204,13 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
if part.get("type") == "input_text": if part.get("type") == "input_text":
content += part.get("text", "") content += part.get("text", "")
messages.append({ messages.append(
"role": "tool", {
"tool_call_id": item.get("call_id", ""), "role": "tool",
"content": content, "tool_call_id": item.get("call_id", ""),
}) "content": content,
}
)
elif item_type == "reasoning": elif item_type == "reasoning":
if raw: if raw:
@@ -218,9 +226,7 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
if reasoning_text: if reasoning_text:
start_tag = item.get("start_tag", "<think>") start_tag = item.get("start_tag", "<think>")
end_tag = item.get("end_tag", "</think>") end_tag = item.get("end_tag", "</think>")
pending_content.append( pending_content.append(f"{start_tag}{reasoning_text}{end_tag}")
f"{start_tag}{reasoning_text}{end_tag}"
)
# else: skip reasoning blocks for normal LLM messages # else: skip reasoning blocks for normal LLM messages
elif item_type == "open_webui:code_interpreter": elif item_type == "open_webui:code_interpreter":
-1
View File
@@ -32,7 +32,6 @@ from open_webui.config import (
from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL
from open_webui.models.users import UserModel from open_webui.models.users import UserModel
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
+3 -2
View File
@@ -289,7 +289,9 @@ def convert_payload_openai_to_ollama(openai_payload: dict) -> dict:
""" """
# Shallow copy metadata separately (may contain non-picklable objects) # Shallow copy metadata separately (may contain non-picklable objects)
metadata = openai_payload.get("metadata") metadata = openai_payload.get("metadata")
openai_payload = copy.deepcopy({k: v for k, v in openai_payload.items() if k != "metadata"}) openai_payload = copy.deepcopy(
{k: v for k, v in openai_payload.items() if k != "metadata"}
)
if metadata is not None: if metadata is not None:
openai_payload["metadata"] = dict(metadata) openai_payload["metadata"] = dict(metadata)
ollama_payload = {} ollama_payload = {}
@@ -421,4 +423,3 @@ def convert_embed_payload_openai_to_ollama(openai_payload: dict) -> dict:
ollama_payload[optional_key] = openai_payload[optional_key] ollama_payload[optional_key] = openai_payload[optional_key]
return ollama_payload return ollama_payload
-1
View File
@@ -151,7 +151,6 @@ def resolve_valves_schema_options(
return schema return schema
def extract_frontmatter(content): def extract_frontmatter(content):
""" """
Extract frontmatter as a dictionary from the provided content string. Extract frontmatter as a dictionary from the provided content string.
+4 -3
View File
@@ -2,7 +2,9 @@ import re
# ANSI escape code pattern - matches all common ANSI sequences # ANSI escape code pattern - matches all common ANSI sequences
# This includes color codes, cursor movement, and other terminal control sequences # This includes color codes, cursor movement, and other terminal control sequences
ANSI_ESCAPE_PATTERN = re.compile(r'\x1b\[[0-9;]*[A-Za-z]|\x1b\([AB]|\x1b[PX^_].*?\x1b\\|\x1b\].*?(?:\x07|\x1b\\)') ANSI_ESCAPE_PATTERN = re.compile(
r"\x1b\[[0-9;]*[A-Za-z]|\x1b\([AB]|\x1b[PX^_].*?\x1b\\|\x1b\].*?(?:\x07|\x1b\\)"
)
def strip_ansi_codes(text: str) -> str: def strip_ansi_codes(text: str) -> str:
@@ -18,7 +20,7 @@ def strip_ansi_codes(text: str) -> str:
- Reset codes: \x1b[0m, \x1b[39m - Reset codes: \x1b[0m, \x1b[39m
- Cursor movement: \x1b[1A, \x1b[2J, etc. - Cursor movement: \x1b[1A, \x1b[2J, etc.
""" """
return ANSI_ESCAPE_PATTERN.sub('', text) return ANSI_ESCAPE_PATTERN.sub("", text)
def strip_markdown_code_fences(code: str) -> str: def strip_markdown_code_fences(code: str) -> str:
@@ -55,4 +57,3 @@ def sanitize_code(code: str) -> str:
code = strip_ansi_codes(code) code = strip_ansi_codes(code)
code = strip_markdown_code_fences(code) code = strip_markdown_code_fences(code)
return code return code
-1
View File
@@ -10,7 +10,6 @@ from open_webui.utils.misc import get_last_user_message, get_messages_content
from open_webui.config import DEFAULT_RAG_TEMPLATE from open_webui.config import DEFAULT_RAG_TEMPLATE
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -29,7 +29,6 @@ from fastapi import status
from open_webui.utils.telemetry.constants import SPAN_REDIS_TYPE, SpanAttributes from open_webui.utils.telemetry.constants import SPAN_REDIS_TYPE, SpanAttributes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -60,7 +59,7 @@ def response_hook(span: Span, request: PreparedRequest, response: Response):
span.set_status(StatusCode.ERROR if response.status_code >= 400 else StatusCode.OK) span.set_status(StatusCode.ERROR if response.status_code >= 400 else StatusCode.OK)
def redis_request_hook(span: Span, instance: Union[Redis|RedisCluster], args, kwargs): def redis_request_hook(span: Span, instance: Union[Redis | RedisCluster], args, kwargs):
""" """
Redis Request Hook Redis Request Hook
""" """
@@ -71,7 +70,7 @@ def redis_request_hook(span: Span, instance: Union[Redis|RedisCluster], args, kw
# Instead of checking the type, we check if the instance has a nodes_manager attribute. # Instead of checking the type, we check if the instance has a nodes_manager attribute.
try: try:
db = "" db = ""
if hasattr(instance, 'nodes_manager'): if hasattr(instance, "nodes_manager"):
default_node = instance.nodes_manager.default_node default_node = instance.nodes_manager.default_node
if not default_node: if not default_node:
return return
+11 -6
View File
@@ -352,7 +352,9 @@ async def get_tools(
headers = include_user_info_headers(headers, user) headers = include_user_info_headers(headers, user)
metadata = extra_params.get("__metadata__", {}) metadata = extra_params.get("__metadata__", {})
if metadata and metadata.get("chat_id"): if metadata and metadata.get("chat_id"):
headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = metadata.get("chat_id") headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = (
metadata.get("chat_id")
)
def make_tool_function( def make_tool_function(
function_name, tool_server_data, headers function_name, tool_server_data, headers
@@ -416,9 +418,8 @@ def get_builtin_tools(
# Helper to get model capabilities (defaults to True if not specified) # Helper to get model capabilities (defaults to True if not specified)
def get_model_capability(name: str, default: bool = True) -> bool: def get_model_capability(name: str, default: bool = True) -> bool:
return ( return (model.get("info", {}).get("meta", {}).get("capabilities") or {}).get(
(model.get("info", {}).get("meta", {}).get("capabilities") or {}) name, default
.get(name, default)
) )
# Helper to check if a builtin tool category is enabled via meta.builtinTools # Helper to check if a builtin tool category is enabled via meta.builtinTools
@@ -491,13 +492,17 @@ def get_builtin_tools(
builtin_functions.append(execute_code) builtin_functions.append(execute_code)
# Notes tools - search, view, create, and update user's notes (if builtin category enabled AND notes enabled globally) # Notes tools - search, view, create, and update user's notes (if builtin category enabled AND notes enabled globally)
if is_builtin_tool_enabled("notes") and getattr(request.app.state.config, "ENABLE_NOTES", False): if is_builtin_tool_enabled("notes") and getattr(
request.app.state.config, "ENABLE_NOTES", False
):
builtin_functions.extend( builtin_functions.extend(
[search_notes, view_note, write_note, replace_note_content] [search_notes, view_note, write_note, replace_note_content]
) )
# Channels tools - search channels and messages (if builtin category enabled AND channels enabled globally) # Channels tools - search channels and messages (if builtin category enabled AND channels enabled globally)
if is_builtin_tool_enabled("channels") and getattr(request.app.state.config, "ENABLE_CHANNELS", False): if is_builtin_tool_enabled("channels") and getattr(
request.app.state.config, "ENABLE_CHANNELS", False
):
builtin_functions.extend( builtin_functions.extend(
[ [
search_channels, search_channels,
-1
View File
@@ -36,4 +36,3 @@ def validate_profile_image_url(url: str) -> str:
raise ValueError( raise ValueError(
"Invalid profile image URL: only data URIs and default avatars are allowed." "Invalid profile image URL: only data URIs and default avatars are allowed."
) )
+256 -254
View File
@@ -1,317 +1,319 @@
import { WEBUI_API_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL } from '$lib/constants';
export const getModelAnalytics = async ( export const getModelAnalytics = async (
token: string = '', token: string = '',
startDate: number | null = null, startDate: number | null = null,
endDate: number | null = null, endDate: number | null = null,
groupId: string | null = null groupId: string | null = null
) => { ) => {
let error = null; let error = null;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString()); if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId); if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models?${searchParams.toString()}`, { const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models?${searchParams.toString()}`, {
method: 'GET', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
authorization: `Bearer ${token}` authorization: `Bearer ${token}`
} }
}) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.error(err); console.error(err);
return null; return null;
}); });
if (error) { if (error) {
throw error; throw error;
} }
return res; return res;
}; };
export const getUserAnalytics = async ( export const getUserAnalytics = async (
token: string = '', token: string = '',
startDate: number | null = null, startDate: number | null = null,
endDate: number | null = null, endDate: number | null = null,
limit: number = 50, limit: number = 50,
groupId: string | null = null groupId: string | null = null
) => { ) => {
let error = null; let error = null;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString()); if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString());
if (limit) searchParams.append('limit', limit.toString()); if (limit) searchParams.append('limit', limit.toString());
if (groupId) searchParams.append('group_id', groupId); if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/users?${searchParams.toString()}`, { const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/users?${searchParams.toString()}`, {
method: 'GET', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
authorization: `Bearer ${token}` authorization: `Bearer ${token}`
} }
}) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.error(err); console.error(err);
return null; return null;
}); });
if (error) { if (error) {
throw error; throw error;
} }
return res; return res;
}; };
export const getMessages = async ( export const getMessages = async (
token: string = '', token: string = '',
modelId: string | null = null, modelId: string | null = null,
userId: string | null = null, userId: string | null = null,
chatId: string | null = null, chatId: string | null = null,
startDate: number | null = null, startDate: number | null = null,
endDate: number | null = null, endDate: number | null = null,
skip: number = 0, skip: number = 0,
limit: number = 50 limit: number = 50
) => { ) => {
let error = null; let error = null;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
if (modelId) searchParams.append('model_id', modelId); if (modelId) searchParams.append('model_id', modelId);
if (userId) searchParams.append('user_id', userId); if (userId) searchParams.append('user_id', userId);
if (chatId) searchParams.append('chat_id', chatId); if (chatId) searchParams.append('chat_id', chatId);
if (startDate) searchParams.append('start_date', startDate.toString()); if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString());
if (skip) searchParams.append('skip', skip.toString()); if (skip) searchParams.append('skip', skip.toString());
if (limit) searchParams.append('limit', limit.toString()); if (limit) searchParams.append('limit', limit.toString());
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/messages?${searchParams.toString()}`, { const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/messages?${searchParams.toString()}`, {
method: 'GET', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
authorization: `Bearer ${token}` authorization: `Bearer ${token}`
} }
}) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.error(err); console.error(err);
return null; return null;
}); });
if (error) { if (error) {
throw error; throw error;
} }
return res; return res;
}; };
export const getSummary = async ( export const getSummary = async (
token: string = '', token: string = '',
startDate: number | null = null, startDate: number | null = null,
endDate: number | null = null, endDate: number | null = null,
groupId: string | null = null groupId: string | null = null
) => { ) => {
let error = null; let error = null;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString()); if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId); if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/summary?${searchParams.toString()}`, { const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/summary?${searchParams.toString()}`, {
method: 'GET', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
authorization: `Bearer ${token}` authorization: `Bearer ${token}`
} }
}) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.error(err); console.error(err);
return null; return null;
}); });
if (error) { if (error) {
throw error; throw error;
} }
return res; return res;
}; };
export const getDailyStats = async ( export const getDailyStats = async (
token: string = '', token: string = '',
startDate: number | null = null, startDate: number | null = null,
endDate: number | null = null, endDate: number | null = null,
granularity: 'hourly' | 'daily' = 'daily', granularity: 'hourly' | 'daily' = 'daily',
groupId: string | null = null groupId: string | null = null
) => { ) => {
let error = null; let error = null;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString()); if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString());
searchParams.append('granularity', granularity); searchParams.append('granularity', granularity);
if (groupId) searchParams.append('group_id', groupId); if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/daily?${searchParams.toString()}`, { const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/daily?${searchParams.toString()}`, {
method: 'GET', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
authorization: `Bearer ${token}` authorization: `Bearer ${token}`
} }
}) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.error(err); console.error(err);
return null; return null;
}); });
if (error) { if (error) {
throw error; throw error;
} }
return res; return res;
}; };
export const getTokenUsage = async ( export const getTokenUsage = async (
token: string = '', token: string = '',
startDate: number | null = null, startDate: number | null = null,
endDate: number | null = null, endDate: number | null = null,
groupId: string | null = null groupId: string | null = null
) => { ) => {
let error = null; let error = null;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString()); if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId); if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/tokens?${searchParams.toString()}`, { const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/tokens?${searchParams.toString()}`, {
method: 'GET', method: 'GET',
headers: { headers: {
Accept: 'application/json', Accept: 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
authorization: `Bearer ${token}` authorization: `Bearer ${token}`
} }
}) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();
}) })
.catch((err) => { .catch((err) => {
error = err.detail; error = err.detail;
console.error(err); console.error(err);
return null; return null;
}); });
if (error) { if (error) {
throw error; throw error;
} }
return res; return res;
}; };
export const getModelChats = async ( export const getModelChats = async (
token: string = '', token: string = '',
modelId: string, modelId: string,
startDate: number | null = null, startDate: number | null = null,
endDate: number | null = null, endDate: number | null = null,
skip: number = 0, skip: number = 0,
limit: number = 50 limit: number = 50
) => { ) => {
let error = null; let error = null;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString()); if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString()); if (endDate) searchParams.append('end_date', endDate.toString());
if (skip) searchParams.append('skip', skip.toString()); if (skip) searchParams.append('skip', skip.toString());
if (limit) searchParams.append('limit', limit.toString()); if (limit) searchParams.append('limit', limit.toString());
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models/${encodeURIComponent(modelId)}/chats?${searchParams.toString()}`, { const res = await fetch(
method: 'GET', `${WEBUI_API_BASE_URL}/analytics/models/${encodeURIComponent(modelId)}/chats?${searchParams.toString()}`,
headers: { {
Accept: 'application/json', method: 'GET',
'Content-Type': 'application/json', headers: {
authorization: `Bearer ${token}` Accept: 'application/json',
} 'Content-Type': 'application/json',
}) authorization: `Bearer ${token}`
.then(async (res) => { }
if (!res.ok) throw await res.json(); }
return res.json(); )
}) .then(async (res) => {
.catch((err) => { if (!res.ok) throw await res.json();
error = err.detail; return res.json();
console.error(err); })
return null; .catch((err) => {
}); error = err.detail;
console.error(err);
return null;
});
if (error) { if (error) {
throw error; throw error;
} }
return res; return res;
}; };
export const getModelOverview = async ( export const getModelOverview = async (token: string = '', modelId: string, days: number = 30) => {
token: string = '', let error = null;
modelId: string,
days: number = 30
) => {
let error = null;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
searchParams.append('days', days.toString()); searchParams.append('days', days.toString());
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models/${encodeURIComponent(modelId)}/overview?${searchParams.toString()}`, { const res = await fetch(
method: 'GET', `${WEBUI_API_BASE_URL}/analytics/models/${encodeURIComponent(modelId)}/overview?${searchParams.toString()}`,
headers: { {
Accept: 'application/json', method: 'GET',
'Content-Type': 'application/json', headers: {
authorization: `Bearer ${token}` Accept: 'application/json',
} 'Content-Type': 'application/json',
}) authorization: `Bearer ${token}`
.then(async (res) => { }
if (!res.ok) throw await res.json(); }
return res.json(); )
}) .then(async (res) => {
.catch((err) => { if (!res.ok) throw await res.json();
error = err.detail; return res.json();
console.error(err); })
return null; .catch((err) => {
}); error = err.detail;
console.error(err);
return null;
});
if (error) { if (error) {
throw error; throw error;
} }
return res; return res;
}; };
+1 -5
View File
@@ -255,11 +255,7 @@ export const getArchivedChatList = async (
})); }));
}; };
export const getSharedChatList = async ( export const getSharedChatList = async (token: string = '', page: number = 1, filter?: object) => {
token: string = '',
page: number = 1,
filter?: object
) => {
let error = null; let error = null;
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
+1 -1
View File
@@ -1675,7 +1675,7 @@ export interface ModelMeta {
profile_image_url?: string; profile_image_url?: string;
} }
export interface ModelParams { } export interface ModelParams {}
export type GlobalModelConfig = ModelConfig[]; export type GlobalModelConfig = ModelConfig[];
+1 -5
View File
@@ -281,11 +281,7 @@ export const updateModelById = async (token: string, id: string, model: object)
return res; return res;
}; };
export const updateModelAccessGrants = async ( export const updateModelAccessGrants = async (token: string, id: string, accessGrants: any[]) => {
token: string,
id: string,
accessGrants: any[]
) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/models/model/access/update`, { const res = await fetch(`${WEBUI_API_BASE_URL}/models/model/access/update`, {
+1 -5
View File
@@ -253,11 +253,7 @@ export const updateNoteById = async (token: string, id: string, note: NoteItem)
return res; return res;
}; };
export const updateNoteAccessGrants = async ( export const updateNoteAccessGrants = async (token: string, id: string, accessGrants: any[]) => {
token: string,
id: string,
accessGrants: any[]
) => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/access/update`, { const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/access/update`, {
+26 -36
View File
@@ -1,16 +1,16 @@
import { WEBUI_API_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL } from '$lib/constants';
type PromptItem = { type PromptItem = {
id?: string; // Prompt ID id?: string; // Prompt ID
command: string; command: string;
name: string; // Changed from title name: string; // Changed from title
content: string; content: string;
data?: object | null; data?: object | null;
meta?: object | null; meta?: object | null;
access_grants?: object[]; access_grants?: object[];
version_id?: string | null; // Active version version_id?: string | null; // Active version
commit_message?: string | null; // For history tracking commit_message?: string | null; // For history tracking
is_production?: boolean; // Whether to set new version as production is_production?: boolean; // Whether to set new version as production
}; };
type PromptHistoryItem = { type PromptHistoryItem = {
@@ -196,7 +196,6 @@ export const getPromptItems = async (
}; };
export const getPromptList = async (token: string = '') => { export const getPromptList = async (token: string = '') => {
let error = null; let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/list`, { const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/list`, {
@@ -472,17 +471,14 @@ export const getPromptHistory = async (
): Promise<PromptHistoryItem[]> => { ): Promise<PromptHistoryItem[]> => {
let error = null; let error = null;
const res = await fetch( const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history?page=${page}`, {
`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history?page=${page}`, method: 'GET',
{ headers: {
method: 'GET', Accept: 'application/json',
headers: { 'Content-Type': 'application/json',
Accept: 'application/json', authorization: `Bearer ${token}`
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
} }
) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();
@@ -507,17 +503,14 @@ export const deletePromptHistoryVersion = async (
): Promise<boolean> => { ): Promise<boolean> => {
let error = null; let error = null;
const res = await fetch( const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}`, {
`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}`, method: 'DELETE',
{ headers: {
method: 'DELETE', Accept: 'application/json',
headers: { 'Content-Type': 'application/json',
Accept: 'application/json', authorization: `Bearer ${token}`
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
} }
) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();
@@ -542,17 +535,14 @@ export const getPromptHistoryEntry = async (
): Promise<PromptHistoryItem> => { ): Promise<PromptHistoryItem> => {
let error = null; let error = null;
const res = await fetch( const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}`, {
`${WEBUI_API_BASE_URL}/prompts/id/${promptId}/history/${historyId}`, method: 'GET',
{ headers: {
method: 'GET', Accept: 'application/json',
headers: { 'Content-Type': 'application/json',
Accept: 'application/json', authorization: `Bearer ${token}`
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
} }
) })
.then(async (res) => { .then(async (res) => {
if (!res.ok) throw await res.json(); if (!res.ok) throw await res.json();
return res.json(); return res.json();

Some files were not shown because too many files have changed in this diff Show More