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
+5 -5
View File
@@ -1,4 +1,4 @@
<!--
<!--
⚠️ CRITICAL CHECKS FOR CONTRIBUTORS (READ, DON'T DELETE) ⚠️
1. Target the `dev` branch. PRs targeting `main` will be automatically closed.
2. Do NOT delete the CLA section at the bottom. It is required for the bot to accept your PR.
@@ -84,13 +84,13 @@ This is to ensure large feature PRs are discussed with the community first, befo
### Contributor License Agreement
<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
<!--
🚨 DO NOT DELETE THE TEXT BELOW 🚨
Keep the "Contributor License Agreement" confirmation text intact.
Deleting it will trigger the CLA-Bot to INVALIDATE your PR.
-->
By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms.
> [!NOTE]
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
> Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
+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_HTTP_SECURE = os.environ.get("WEAVIATE_HTTP_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_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".
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.
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", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512")
+20 -18
View File
@@ -195,13 +195,23 @@ ENABLE_FORWARD_USER_INFO_HEADERS = (
)
# 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_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")
FORWARD_USER_INFO_HEADER_USER_NAME = os.environ.get(
"FORWARD_USER_INFO_HEADER_USER_NAME", "X-OpenWebUI-User-Name"
)
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)
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
ENABLE_STAR_SESSIONS_MIDDLEWARE = (
@@ -401,18 +411,14 @@ try:
REDIS_SOCKET_CONNECT_TIMEOUT = float(REDIS_SOCKET_CONNECT_TIMEOUT)
except ValueError:
REDIS_SOCKET_CONNECT_TIMEOUT = None
REDIS_RECONNECT_DELAY = os.environ.get(
"REDIS_RECONNECT_DELAY", ""
)
REDIS_RECONNECT_DELAY = os.environ.get("REDIS_RECONNECT_DELAY", "")
if REDIS_RECONNECT_DELAY == "":
REDIS_RECONNECT_DELAY = None
else:
try:
REDIS_RECONNECT_DELAY = float(
REDIS_RECONNECT_DELAY
)
REDIS_RECONNECT_DELAY = float(REDIS_RECONNECT_DELAY)
if REDIS_RECONNECT_DELAY < 0:
REDIS_RECONNECT_DELAY = None
except Exception:
@@ -580,15 +586,11 @@ LICENSE_PUBLIC_KEY = os.environ.get("LICENSE_PUBLIC_KEY", "")
pk = None
if LICENSE_PUBLIC_KEY:
pk = serialization.load_pem_public_key(
f"""
pk = serialization.load_pem_public_key(f"""
-----BEGIN PUBLIC KEY-----
{LICENSE_PUBLIC_KEY}
-----END PUBLIC KEY-----
""".encode(
"utf-8"
)
)
""".encode("utf-8"))
####################################
-1
View File
@@ -50,7 +50,6 @@ from open_webui.utils.payload import (
apply_system_prompt_to_body,
)
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -25,7 +25,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
import playhouse.postgres_ext as pw_pext
@@ -29,7 +29,6 @@ from contextlib import suppress
import peewee as pw
from peewee_migrate import Migrator
with suppress(ImportError):
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
if SAFE_MODE:
print("SAFE MODE ENABLED")
Functions.deactivate_all_functions()
@@ -575,8 +574,7 @@ class SPAStaticFiles(StaticFiles):
raise ex
print(
rf"""
print(rf"""
██████╗ ██████╗ ███████╗███╗ ██╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗██╗
██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██║ ██║██╔════╝██╔══██╗██║ ██║██║
██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ █╗ ██║█████╗ ██████╔╝██║ ██║██║
@@ -588,8 +586,7 @@ print(
v{VERSION} - building the best AI user interface.
{f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""}
https://github.com/open-webui/open-webui
"""
)
""")
@asynccontextmanager
@@ -1845,9 +1842,7 @@ async def chat_completion(
# Emit chat:active=true when task starts
event_emitter = get_event_emitter(metadata, update_db=False)
if event_emitter:
await event_emitter(
{"type": "chat:active", "data": {"active": True}}
)
await event_emitter({"type": "chat:active", "data": {"active": True}})
return {"status": True, "task_id": task_id}
else:
return await process_chat(request, form_data, user, metadata, model)
@@ -12,7 +12,6 @@ from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
# revision identifiers, used by Alembic.
revision: str = "2f1211949ecc"
down_revision: Union[str, None] = "37f288994c47"
@@ -12,7 +12,6 @@ import uuid
from alembic import op
import sqlalchemy as sa
revision: str = "374d2f66af06"
down_revision: Union[str, None] = "c440947495f3"
branch_labels: Union[str, Sequence[str], None] = None
@@ -14,7 +14,6 @@ from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "37f288994c47"
down_revision: Union[str, None] = "a5c220713937"
@@ -11,7 +11,6 @@ from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "38d63c18f30f"
down_revision: Union[str, None] = "3af16a1c9fb6"
@@ -12,7 +12,6 @@ from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
# revision identifiers, used by Alembic.
revision: str = "6283dc0e4d8d"
down_revision: Union[str, None] = "3e0e00844bb0"
@@ -11,7 +11,6 @@ import sqlalchemy as sa
from sqlalchemy.sql import table, column, select
import json
revision = "6a39f3d8e55c"
down_revision = "c0fbf31ca0db"
branch_labels = None
@@ -12,7 +12,6 @@ from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
# revision identifiers, used by Alembic.
revision: str = "81cc2ce44d79"
down_revision: Union[str, None] = "6283dc0e4d8d"
@@ -165,7 +165,9 @@ def upgrade() -> None:
log.warning(f"Failed to insert message {message_id}: {e}")
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:
@@ -12,7 +12,6 @@ from alembic import op
import sqlalchemy as sa
import open_webui.internal.db
# revision identifiers, used by Alembic.
revision: str = "90ef40d4714e"
down_revision: Union[str, None] = "b10670c03dd5"
@@ -173,12 +173,10 @@ def upgrade() -> None:
for uid, api_key in users_with_keys:
if api_key:
conn.execute(
sa.text(
"""
sa.text("""
INSERT INTO api_key (id, user_id, key, created_at, updated_at)
VALUES (:id, :user_id, :key, :created_at, :updated_at)
"""
),
"""),
{
"id": f"key_{uid}",
"user_id": uid,
@@ -12,7 +12,6 @@ import json
from sqlalchemy.sql import table, column
from sqlalchemy import String, Text, JSON, and_
revision = "c29facfe716b"
down_revision = "c69f45358db4"
branch_labels = None
@@ -11,7 +11,6 @@ from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "c440947495f3"
down_revision: Union[str, None] = "81cc2ce44d79"
@@ -98,26 +98,27 @@ def upgrade() -> None:
# Could be Python None (SQL NULL) or string "null" (JSON null)
# EXCEPTION: files with NULL are PRIVATE (owner-only), not public
is_null = (
access_control_json is None or
access_control_json == "null" or
(isinstance(access_control_json, str) and access_control_json.strip().lower() == "null")
access_control_json is None
or access_control_json == "null"
or (
isinstance(access_control_json, str)
and access_control_json.strip().lower() == "null"
)
)
if is_null:
# Files: NULL = private (no entry needed, owner has implicit access)
# Other resources: NULL = public (insert user:* for read)
if resource_type == "file":
continue # Private - no entry needed
key = (resource_type, resource_id, "user", "*", "read")
if key not in inserted:
try:
conn.execute(
sa.text(
"""
sa.text("""
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)
"""
),
"""),
{
"id": str(uuid.uuid4()),
"resource_type": resource_type,
@@ -174,12 +175,10 @@ def upgrade() -> None:
continue
try:
conn.execute(
sa.text(
"""
sa.text("""
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)
"""
),
"""),
{
"id": str(uuid.uuid4()),
"resource_type": resource_type,
@@ -200,12 +199,10 @@ def upgrade() -> None:
continue
try:
conn.execute(
sa.text(
"""
sa.text("""
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)
"""
),
"""),
{
"id": str(uuid.uuid4()),
"resource_type": resource_type,
@@ -233,9 +230,9 @@ def upgrade() -> None:
def downgrade() -> None:
import json
conn = op.get_bind()
# Resource tables mapping: (table_name, resource_type)
resource_tables = [
("knowledge", "knowledge"),
@@ -265,7 +262,7 @@ def downgrade() -> None:
FROM access_grant
WHERE resource_type = :resource_type
"""),
{"resource_type": resource_type}
{"resource_type": resource_type},
)
rows = result.fetchall()
except Exception:
@@ -287,39 +284,61 @@ def downgrade() -> None:
}
# 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
continue
# Add to appropriate list
if permission in ["read", "write"]:
if principal_type == "group":
if principal_id not in resource_grants[resource_id][permission]["group_ids"]:
resource_grants[resource_id][permission]["group_ids"].append(principal_id)
if (
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":
if principal_id not in resource_grants[resource_id][permission]["user_ids"]:
resource_grants[resource_id][permission]["user_ids"].append(principal_id)
if (
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
for resource_id, grants in resource_grants.items():
if grants["is_public"]:
# Public = NULL
access_control_value = None
elif (not grants["read"]["group_ids"] and not grants["read"]["user_ids"] and
not grants["write"]["group_ids"] and not grants["write"]["user_ids"]):
elif (
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 {}
access_control_value = json.dumps({})
else:
# Custom permissions
access_control_value = json.dumps({
"read": grants["read"],
"write": grants["write"],
})
access_control_value = json.dumps(
{
"read": grants["read"],
"write": grants["write"],
}
)
try:
conn.execute(
sa.text(f'UPDATE "{table_name}" SET access_control = :access_control WHERE id = :id'),
{"access_control": access_control_value, "id": resource_id}
sa.text(
f'UPDATE "{table_name}" SET access_control = :access_control WHERE id = :id'
),
{"access_control": access_control_value, "id": resource_id},
)
except Exception:
pass
@@ -330,15 +349,15 @@ def downgrade() -> None:
if resource_type != "file":
try:
conn.execute(
sa.text(f'''
sa.text(f"""
UPDATE "{table_name}"
SET access_control = :private_value
WHERE id NOT IN (
SELECT DISTINCT resource_id FROM access_grant WHERE resource_type = :resource_type
)
AND access_control IS NULL
'''),
{"private_value": json.dumps({}), "resource_type": resource_type}
"""),
{"private_value": json.dumps({}), "resource_type": resource_type},
)
except Exception:
pass
+11 -5
View File
@@ -22,10 +22,14 @@ class AccessGrant(Base):
__tablename__ = "access_grant"
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)
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"
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)
deduped[key] = {
"id": grant.get("id")
if isinstance(grant.get("id"), str) and grant.get("id")
else str(uuid.uuid4()),
"id": (
grant.get("id")
if isinstance(grant.get("id"), str) and grant.get("id")
else str(uuid.uuid4())
),
"principal_type": principal_type,
"principal_id": principal_id,
"permission": permission,
+4 -4
View File
@@ -263,7 +263,9 @@ class ChannelTable:
def _to_channel_model(
self, channel: Channel, db: Optional[Session] = None
) -> 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)
channel_data["access_grants"] = access_grants
return ChannelModel.model_validate(channel_data)
@@ -770,9 +772,7 @@ class ChannelTable:
.first()
)
if membership:
allowed_channels.append(
self._to_channel_model(channel, db=db)
)
allowed_channels.append(self._to_channel_model(channel, db=db))
continue
# --- Case B: standard channel => rely on ACL permissions ---
+48 -14
View File
@@ -332,7 +332,11 @@ class ChatMessageTable:
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
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))
results = query.group_by(ChatMessage.model_id).all()
@@ -362,10 +366,12 @@ class ChatMessageTable:
elif dialect == "postgresql":
# Use json_extract_path_text for PostgreSQL JSON columns
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(
func.json_extract_path_text(ChatMessage.usage, "output_tokens"), Integer
func.json_extract_path_text(ChatMessage.usage, "output_tokens"),
Integer,
)
else:
raise NotImplementedError(f"Unsupported dialect: {dialect}")
@@ -387,7 +393,11 @@ class ChatMessageTable:
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
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))
results = query.group_by(ChatMessage.model_id).all()
@@ -424,10 +434,12 @@ class ChatMessageTable:
elif dialect == "postgresql":
# Use json_extract_path_text for PostgreSQL JSON columns
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(
func.json_extract_path_text(ChatMessage.usage, "output_tokens"), Integer
func.json_extract_path_text(ChatMessage.usage, "output_tokens"),
Integer,
)
else:
raise NotImplementedError(f"Unsupported dialect: {dialect}")
@@ -481,7 +493,11 @@ class ChatMessageTable:
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
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))
results = query.group_by(ChatMessage.user_id).all()
@@ -507,7 +523,11 @@ class ChatMessageTable:
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
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))
results = query.group_by(ChatMessage.chat_id).all()
@@ -536,7 +556,11 @@ class ChatMessageTable:
if end_date:
query = query.filter(ChatMessage.created_at <= end_date)
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))
results = query.all()
@@ -544,10 +568,14 @@ class ChatMessageTable:
# Group by date -> model -> count
daily_counts: dict[str, dict[str, int]] = {}
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:
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
if start_date and end_date:
@@ -587,14 +615,20 @@ class ChatMessageTable:
# Group by hour -> model -> count
hourly_counts: dict[str, dict[str, int]] = {}
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:
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
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))
while current <= end_dt:
hour_str = current.strftime("%Y-%m-%d %H:00")
+18 -24
View File
@@ -329,7 +329,9 @@ class ChatTable:
data=message,
)
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
@@ -388,7 +390,9 @@ class ChatTable:
data=message,
)
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]
@@ -739,8 +743,10 @@ class ChatTable:
) -> list[ChatModel]:
with get_db_context(db) as db:
query = db.query(Chat).filter_by(user_id=user_id).filter(
Chat.share_id.isnot(None)
query = (
db.query(Chat)
.filter_by(user_id=user_id)
.filter(Chat.share_id.isnot(None))
)
if filter:
@@ -1110,29 +1116,23 @@ class ChatTable:
# Check if there are any tags to filter, it should have all the tags
if "none" in tag_ids:
query = query.filter(
text(
"""
query = query.filter(text("""
NOT EXISTS (
SELECT 1
FROM json_each(Chat.meta, '$.tags') AS tag
)
"""
)
)
"""))
elif tag_ids:
query = query.filter(
and_(
*[
text(
f"""
text(f"""
EXISTS (
SELECT 1
FROM json_each(Chat.meta, '$.tags') AS tag
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)
]
)
@@ -1168,29 +1168,23 @@ class ChatTable:
# Check if there are any tags to filter, it should have all the tags
if "none" in tag_ids:
query = query.filter(
text(
"""
query = query.filter(text("""
NOT EXISTS (
SELECT 1
FROM json_array_elements_text(Chat.meta->'tags') AS tag
)
"""
)
)
"""))
elif tag_ids:
query = query.filter(
and_(
*[
text(
f"""
text(f"""
EXISTS (
SELECT 1
FROM json_array_elements_text(Chat.meta->'tags') AS tag
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)
]
)
+2 -2
View File
@@ -65,7 +65,7 @@ class FileMeta(BaseModel):
"""Sanitize metadata fields to handle malformed legacy data."""
if not isinstance(data, dict):
return data
# Handle content_type that may be a list like ['application/pdf', None]
content_type = data.get("content_type")
if isinstance(content_type, list):
@@ -75,7 +75,7 @@ class FileMeta(BaseModel):
)
elif content_type is not None and not isinstance(content_type, str):
data["content_type"] = None
return data
-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
log = logging.getLogger(__name__)
-1
View File
@@ -214,7 +214,6 @@ class FunctionsTable:
except Exception:
return []
def get_functions(
self, active_only=False, include_valves=False, db: Optional[Session] = None
) -> list[FunctionModel | FunctionWithValvesModel]:
+2 -3
View File
@@ -25,7 +25,6 @@ from sqlalchemy import (
select,
)
log = logging.getLogger(__name__)
####################
@@ -182,12 +181,12 @@ class GroupTable:
if share_value:
# Groups open to anyone: data is null, config.share is null, or share is true
# Use case-insensitive string comparison to handle variations like "True", "TRUE"
# Handle potential JSON boolean to string casting issues by checking for both string 'true' and boolean equivalence if possible,
# Handle potential JSON boolean to string casting issues by checking for both string 'true' and boolean equivalence if possible,
anyone_can_share = or_(
Group.data.is_(None),
json_share_str.is_(None),
json_share_lower == "true",
json_share_lower == "1", # Handle SQLite boolean true
json_share_lower == "1", # Handle SQLite boolean true
)
if member_id:
+14 -8
View File
@@ -30,7 +30,6 @@ from sqlalchemy import (
or_,
)
log = logging.getLogger(__name__)
####################
@@ -402,9 +401,7 @@ class KnowledgeTable:
try:
with get_db_context(db) as db:
knowledge = db.query(Knowledge).filter_by(id=id).first()
return (
self._to_knowledge_model(knowledge, db=db) if knowledge else None
)
return self._to_knowledge_model(knowledge, db=db) if knowledge else None
except Exception:
return None
@@ -443,7 +440,10 @@ class KnowledgeTable:
.filter(KnowledgeFile.file_id == file_id)
.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:
return []
@@ -484,11 +484,17 @@ class KnowledgeTable:
is_asc = direction == "asc"
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":
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":
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
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 import BigInteger, Column, Text, Boolean
log = logging.getLogger(__name__)
@@ -182,7 +181,9 @@ class ModelsTable:
def get_all_models(self, db: Optional[Session] = None) -> list[ModelModel]:
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]:
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 sqlalchemy import BigInteger, Column, Text, JSON, Index
####################
# PromptHistory DB Schema
####################
+9 -9
View File
@@ -13,7 +13,6 @@ from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import BigInteger, Boolean, Column, String, Text, JSON, or_, func, cast
####################
# Prompts DB Schema
####################
@@ -146,7 +145,9 @@ class PromptsTable:
"data": form_data.data or {},
"meta": form_data.meta 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(
@@ -345,7 +346,6 @@ class PromptsTable:
return PromptListResponse(items=prompts, total=total)
def update_prompt_by_command(
self,
command: str,
form_data: PromptForm,
@@ -450,7 +450,7 @@ class PromptsTable:
prompt.content = form_data.content
prompt.data = form_data.data or prompt.data
prompt.meta = form_data.meta or prompt.meta
if form_data.tags is not None:
prompt.tags = form_data.tags
@@ -459,7 +459,7 @@ class PromptsTable:
"prompt", prompt.id, form_data.access_grants, db=db
)
current_access_grants = self._get_access_grants(prompt.id, db=db)
prompt.updated_at = int(time.time())
db.commit()
@@ -510,16 +510,16 @@ class PromptsTable:
prompt = db.query(Prompt).filter_by(id=prompt_id).first()
if not prompt:
return None
prompt.name = name
prompt.command = command
if tags is not None:
prompt.tags = tags
prompt.updated_at = int(time.time())
db.commit()
return self._to_prompt_model(prompt, db=db)
except Exception:
return None
+4 -5
View File
@@ -11,7 +11,6 @@ from open_webui.models.access_grants import AccessGrantModel, AccessGrants
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import BigInteger, Boolean, Column, String, Text, or_
log = logging.getLogger(__name__)
####################
@@ -112,7 +111,9 @@ class SkillsTable:
return AccessGrants.get_grants_by_resource("skill", skill_id, db=db)
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)
return SkillModel.model_validate(skill_data)
@@ -223,9 +224,7 @@ class SkillsTable:
from open_webui.models.users import User, UserModel
# Join with User table for user filtering
query = db.query(Skill, User).outerjoin(
User, User.id == Skill.user_id
)
query = db.query(Skill, User).outerjoin(User, User.id == Skill.user_id)
if filter:
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 sqlalchemy import BigInteger, Column, String, Text
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.utils.headers import include_user_info_headers
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)
) as session:
async with session.post(
full_url, headers=headers, json=form_data,
full_url,
headers=headers,
json=form_data,
ssl=AIOHTTP_CLIENT_SESSION_SSL,
) as r:
r.raise_for_status()
@@ -256,8 +256,7 @@ class Oracle23aiClient(VectorDBBase):
with connection.cursor() as cursor:
try:
log.info("Creating Table document_chunk")
cursor.execute(
"""
cursor.execute("""
BEGIN
EXECUTE IMMEDIATE '
CREATE TABLE IF NOT EXISTS document_chunk (
@@ -274,12 +273,10 @@ class Oracle23aiClient(VectorDBBase):
RAISE;
END IF;
END;
"""
)
""")
log.info("Creating Index document_chunk_collection_name_idx")
cursor.execute(
"""
cursor.execute("""
BEGIN
EXECUTE IMMEDIATE '
CREATE INDEX IF NOT EXISTS document_chunk_collection_name_idx
@@ -291,12 +288,10 @@ class Oracle23aiClient(VectorDBBase):
RAISE;
END IF;
END;
"""
)
""")
log.info("Creating VECTOR INDEX document_chunk_vector_ivf_idx")
cursor.execute(
"""
cursor.execute("""
BEGIN
EXECUTE IMMEDIATE '
CREATE VECTOR INDEX IF NOT EXISTS document_chunk_vector_ivf_idx
@@ -312,8 +307,7 @@ class Oracle23aiClient(VectorDBBase):
RAISE;
END IF;
END;
"""
)
""")
connection.commit()
log.info("Database initialization completed successfully.")
@@ -51,7 +51,6 @@ from open_webui.config import (
PGVECTOR_USE_HALFVEC,
)
VECTOR_LENGTH = PGVECTOR_INITIALIZE_MAX_VECTOR_LENGTH
USE_HALFVEC = PGVECTOR_USE_HALFVEC
@@ -121,34 +120,26 @@ class PgvectorClient(VectorDBBase):
# Ensure the pgvector extension is available
# Use a conditional check to avoid permission issues on Azure PostgreSQL
if PGVECTOR_CREATE_EXTENSION:
self.session.execute(
text(
"""
self.session.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'vector') THEN
CREATE EXTENSION IF NOT EXISTS vector;
END IF;
END $$;
"""
)
)
"""))
if PGVECTOR_PGCRYPTO:
# Ensure the pgcrypto extension is available for encryption
# Use a conditional check to avoid permission issues on Azure PostgreSQL
self.session.execute(
text(
"""
self.session.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pgcrypto') THEN
CREATE EXTENSION IF NOT EXISTS pgcrypto;
END IF;
END $$;
"""
)
)
"""))
if not PGVECTOR_PGCRYPTO_KEY:
raise ValueError(
@@ -216,15 +207,13 @@ class PgvectorClient(VectorDBBase):
def _ensure_vector_index(self, index_method: str, index_options: str) -> None:
index_name = "idx_document_chunk_vector"
existing_index_def = self.session.execute(
text(
"""
text("""
SELECT indexdef
FROM pg_indexes
WHERE schemaname = current_schema()
AND tablename = 'document_chunk'
AND indexname = :index_name
"""
),
"""),
{"index_name": index_name},
).scalar()
@@ -310,8 +299,7 @@ class PgvectorClient(VectorDBBase):
# Ensure metadata is converted to its JSON text representation
json_metadata = json.dumps(item["metadata"])
self.session.execute(
text(
"""
text("""
INSERT INTO document_chunk
(id, vector, collection_name, text, vmetadata)
VALUES (
@@ -320,8 +308,7 @@ class PgvectorClient(VectorDBBase):
pgp_sym_encrypt(:metadata_text, :key)
)
ON CONFLICT (id) DO NOTHING
"""
),
"""),
{
"id": item["id"],
"vector": vector,
@@ -363,8 +350,7 @@ class PgvectorClient(VectorDBBase):
vector = self.adjust_vector_length(item["vector"])
json_metadata = json.dumps(item["metadata"])
self.session.execute(
text(
"""
text("""
INSERT INTO document_chunk
(id, vector, collection_name, text, vmetadata)
VALUES (
@@ -377,8 +363,7 @@ class PgvectorClient(VectorDBBase):
collection_name = EXCLUDED.collection_name,
text = EXCLUDED.text,
vmetadata = EXCLUDED.vmetadata
"""
),
"""),
{
"id": item["id"],
"vector": vector,
@@ -33,7 +33,6 @@ from open_webui.config import (
)
from open_webui.retrieval.vector.utils import process_metadata
NO_LIMIT = 10000 # Reasonable limit to avoid overwhelming the system
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.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID
log = logging.getLogger(__name__)
@@ -3,7 +3,6 @@ from typing import Optional, List
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
log = logging.getLogger(__name__)
@@ -5,7 +5,6 @@ import requests
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
from open_webui.utils.headers import include_user_info_headers
log = logging.getLogger(__name__)
+32 -16
View File
@@ -31,14 +31,14 @@ def xml_element_contents_to_string(element: Element) -> str:
def search_yandex(
request: Request,
yandex_search_url: str,
yandex_search_api_key: str,
yandex_search_config: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
user=None,
request: Request,
yandex_search_url: str,
yandex_search_api_key: str,
yandex_search_config: str,
query: str,
count: int,
filter_list: Optional[List[str]] = None,
user=None,
) -> List[SearchResult]:
try:
headers = {
@@ -73,7 +73,11 @@ def search_yandex(
payload["groupSpec"]["docsInGroup"] = 1
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,
json=payload,
)
@@ -84,18 +88,28 @@ def search_yandex(
if "rawData" not in 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))
results = []
for group in doc_root.findall("response/results/grouping/group"):
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"),
"snippet": xml_element_contents_to_string(group.find("doc/passages/passage")),
})
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"),
"snippet": xml_element_contents_to_string(
group.find("doc/passages/passage")
),
}
)
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_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",
3,
)
+31 -28
View File
@@ -88,25 +88,29 @@ async def get_user_analytics(
token_usage = ChatMessages.get_token_usage_by_user(
start_date=start_date, end_date=end_date, db=db
)
# 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)}
users = []
for user_id in top_user_ids:
u = user_info.get(user_id)
tokens = token_usage.get(user_id, {})
users.append(UserAnalyticsEntry(
user_id=user_id,
name=u.name if u else None,
email=u.email if u else None,
count=counts[user_id],
input_tokens=tokens.get("input_tokens", 0),
output_tokens=tokens.get("output_tokens", 0),
total_tokens=tokens.get("total_tokens", 0),
))
users.append(
UserAnalyticsEntry(
user_id=user_id,
name=u.name if u else None,
email=u.email if u else None,
count=counts[user_id],
input_tokens=tokens.get("input_tokens", 0),
output_tokens=tokens.get("output_tokens", 0),
total_tokens=tokens.get("total_tokens", 0),
)
)
return UserAnalyticsResponse(users=users)
@@ -168,7 +172,7 @@ async def get_summary(
chat_counts = ChatMessages.get_message_count_by_chat(
start_date=start_date, end_date=end_date, group_id=group_id, db=db
)
return SummaryResponse(
total_messages=sum(model_counts.values()),
total_chats=len(chat_counts),
@@ -317,9 +321,7 @@ async def get_model_chats(
if isinstance(content, str):
first_message = content[:200]
elif isinstance(content, list):
text_parts = [
b.get("text", "") for b in content if isinstance(b, dict)
]
text_parts = [b.get("text", "") for b in content if isinstance(b, dict)]
first_message = " ".join(text_parts)[:200]
# Get user info
@@ -331,7 +333,6 @@ async def get_model_chats(
# Timestamps from messages
updated_at = max(m.created_at for m in messages) if messages else 0
chats_data.append(
ModelChatEntry(
chat_id=chat_id,
@@ -387,24 +388,24 @@ async def get_model_overview(
# Get feedback history per day
history_counts: dict[str, dict] = defaultdict(lambda: {"won": 0, "lost": 0})
# Calculate start date for history
now = datetime.now()
start_dt = None
if days > 0:
start_dt = now - timedelta(days=days)
for chat_id in chat_ids:
feedbacks = Feedbacks.get_feedbacks_by_chat_id(chat_id, db=db)
for fb in feedbacks:
if fb.data and "rating" in fb.data:
rating = fb.data["rating"]
fb_date = datetime.fromtimestamp(fb.created_at)
# Filter by date range
if start_dt and fb_date < start_dt:
continue
date_str = fb_date.strftime("%Y-%m-%d")
if rating == 1:
history_counts[date_str]["won"] += 1
@@ -423,15 +424,17 @@ async def get_model_overview(
current = datetime.strptime(min_date, "%Y-%m-%d")
else:
current = now
while current <= end_dt:
date_str = current.strftime("%Y-%m-%d")
counts = history_counts.get(date_str, {"won": 0, "lost": 0})
history.append(HistoryEntry(
date=date_str,
won=counts["won"],
lost=counts["lost"],
))
history.append(
HistoryEntry(
date=date_str,
won=counts["won"],
lost=counts["lost"],
)
)
current += timedelta(days=1)
# Get chat tags
-1
View File
@@ -57,7 +57,6 @@ from open_webui.env import (
ENABLE_FORWARD_USER_INFO_HEADERS,
)
router = APIRouter()
# Constants
+4 -2
View File
@@ -98,7 +98,7 @@ def create_session_response(
"""
Create JWT token and build session response for a user.
Shared helper for signin, signup, ldap_auth, add_user, and token_exchange endpoints.
Args:
request: FastAPI request object
user: User object
@@ -558,7 +558,9 @@ async def ldap_auth(
except Exception as 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:
raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED)
else:
+1 -3
View File
@@ -1633,9 +1633,7 @@ async def update_message_by_id(
if (
user.role != "admin"
and message.user_id != user.id
and not channel_has_access(
user.id, channel, permission="read", db=db
)
and not channel_has_access(user.id, channel, permission="read", db=db)
):
raise HTTPException(
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.access_control import has_permission
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 sqlalchemy.orm import Session
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
log = logging.getLogger(__name__)
router = APIRouter()
+18 -7
View File
@@ -14,7 +14,11 @@ import requests
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile
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.retrieval.web.utils import validate_url
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
set_image_model(request, form_data.IMAGE_GENERATION_MODEL)
if (
form_data.IMAGE_SIZE == "auto"
and not re.match(IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, form_data.IMAGE_GENERATION_MODEL)
if form_data.IMAGE_SIZE == "auto" and not re.match(
IMAGE_AUTO_SIZE_MODELS_REGEX_PATTERN, form_data.IMAGE_GENERATION_MODEL
):
raise HTTPException(
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"}
),
**(
@@ -912,7 +918,9 @@ async def image_edits(
form_data.image = await load_url_image(form_data.image)
elif isinstance(form_data.image, list):
# 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:
raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
@@ -947,7 +955,10 @@ async def image_edits(
**({"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"}
),
}
+2 -5
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.models.models import Models, ModelForm
log = logging.getLogger(__name__)
router = APIRouter()
@@ -358,7 +357,7 @@ async def reindex_knowledge_base_metadata_embeddings(
user=Depends(get_admin_user),
):
"""Batch embed all existing knowledge bases. Admin only.
NOTE: We intentionally do NOT use Depends(get_session) here.
This endpoint loops through ALL knowledge bases and calls embed_knowledge_base_metadata()
for each one, making N external embedding API calls. Holding a session during
@@ -540,9 +539,7 @@ async def update_knowledge_access_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
AccessGrants.set_access_grants(
"knowledge", id, form_data.access_grants, db=db
)
AccessGrants.set_access_grants("knowledge", id, form_data.access_grants, db=db)
return KnowledgeFilesResponse(
**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()
)
AccessGrants.set_access_grants(
"note", id, form_data.access_grants, db=db
)
AccessGrants.set_access_grants("note", id, form_data.access_grants, db=db)
return Notes.get_note_by_id(id, db=db)
+34 -26
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.headers import include_user_info_headers
log = logging.getLogger(__name__)
@@ -805,67 +804,77 @@ def convert_to_azure_payload(url, payload: dict, api_version: str):
def convert_to_responses_payload(payload: dict) -> dict:
"""
Convert Chat Completions payload to Responses API format.
Chat Completions: { messages: [{role, content}], ... }
Responses API: { input: [{type: "message", role, content: [...]}], instructions: "system" }
"""
messages = payload.pop("messages", [])
system_content = ""
input_items = []
for msg in messages:
role = msg.get("role", "user")
content = msg.get("content", "")
# Check for stored output items (from previous Responses API turn)
stored_output = msg.get("output")
if stored_output and isinstance(stored_output, list):
input_items.extend(stored_output)
continue
if role == "system":
if isinstance(content, str):
system_content = content
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
# Convert content format
text_type = "output_text" if role == "assistant" else "input_text"
if isinstance(content, str):
content_parts = [{"type": text_type, "text": content}]
elif isinstance(content, list):
content_parts = []
for part in content:
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":
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})
else:
content_parts = [{"type": text_type, "text": str(content)}]
input_items.append({
"type": "message",
"role": role,
"content": content_parts
})
input_items.append({"type": "message", "role": role, "content": content_parts})
responses_payload = {**payload, "input": input_items}
if system_content:
responses_payload["instructions"] = system_content
if "max_tokens" in responses_payload:
responses_payload["max_output_tokens"] = responses_payload.pop("max_tokens")
# 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)
# Convert Chat Completions tools format to Responses API format
# Chat Completions: {"type": "function", "function": {"name": ..., "description": ..., "parameters": ...}}
# Responses API: {"type": "function", "name": ..., "description": ..., "parameters": ...}
@@ -888,9 +897,8 @@ def convert_to_responses_payload(payload: dict) -> dict:
# Already in correct format or unknown format, pass through
converted_tools.append(tool)
responses_payload["tools"] = converted_tools
return responses_payload
return responses_payload
def convert_responses_result(response: dict) -> dict:
@@ -1036,7 +1044,7 @@ async def generate_chat_completion(
headers["api-key"] = key
headers["api-version"] = api_version
if is_responses:
payload = convert_to_responses_payload(payload)
request_url = f"{request_url}/responses?api-version={api_version}"
+8 -14
View File
@@ -107,7 +107,9 @@ async def get_prompt_list(
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(
items=[
@@ -313,9 +315,7 @@ async def update_prompt_by_id(
)
# Use the ID from the found prompt
updated_prompt = Prompts.update_prompt_by_id(
prompt.id, form_data, user.id, db=db
)
updated_prompt = Prompts.update_prompt_by_id(prompt.id, form_data, user.id, db=db)
if updated_prompt:
return updated_prompt
else:
@@ -464,9 +464,7 @@ async def update_prompt_access_by_id(
detail=ERROR_MESSAGES.ACCESS_PROHIBITED,
)
AccessGrants.set_access_grants(
"prompt", prompt_id, form_data.access_grants, db=db
)
AccessGrants.set_access_grants("prompt", prompt_id, form_data.access_grants, db=db)
return Prompts.get_prompt_by_id(prompt_id, db=db)
@@ -522,7 +520,7 @@ async def get_prompt_history(
):
"""Get version history for a prompt."""
PAGE_SIZE = 20
prompt = Prompts.get_prompt_by_id(prompt_id, db=db)
if not prompt:
@@ -554,9 +552,7 @@ async def get_prompt_history(
return history
@router.get(
"/id/{prompt_id}/history/{history_id}", response_model=PromptHistoryModel
)
@router.get("/id/{prompt_id}/history/{history_id}", response_model=PromptHistoryModel)
async def get_prompt_history_entry(
prompt_id: str,
history_id: str,
@@ -599,9 +595,7 @@ async def get_prompt_history_entry(
return history_entry
@router.delete(
"/id/{prompt_id}/history/{history_id}", response_model=bool
)
@router.delete("/id/{prompt_id}/history/{history_id}", response_model=bool)
async def delete_prompt_history_entry(
prompt_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.constants import ERROR_MESSAGES
log = logging.getLogger(__name__)
PAGE_ITEM_COUNT = 30
@@ -98,9 +97,7 @@ async def get_skill_list(
filter["user_id"] = user.id
result = Skills.search_skills(
user.id, filter=filter, skip=skip, limit=limit, db=db
)
result = Skills.search_skills(user.id, filter=filter, skip=skip, limit=limit, db=db)
return SkillAccessListResponse(
items=[
@@ -343,9 +340,7 @@ async def update_skill_access_by_id(
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
AccessGrants.set_access_grants(
"skill", id, form_data.access_grants, db=db
)
AccessGrants.set_access_grants("skill", id, form_data.access_grants, 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,
)
log = logging.getLogger(__name__)
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.constants import ERROR_MESSAGES
log = logging.getLogger(__name__)
@@ -555,9 +554,7 @@ async def update_tool_access_by_id(
detail=ERROR_MESSAGES.UNAUTHORIZED,
)
AccessGrants.set_access_grants(
"tool", id, form_data.access_grants, db=db
)
AccessGrants.set_access_grants("tool", id, form_data.access_grants, 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
log = logging.getLogger(__name__)
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.code_interpreter import execute_code_jupyter
log = logging.getLogger(__name__)
router = APIRouter()
-1
View File
@@ -49,7 +49,6 @@ from open_webui.env import (
GLOBAL_LOG_LEVEL,
)
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
-1
View File
@@ -34,7 +34,6 @@ from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient
from azure.core.exceptions import ResourceNotFoundError
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
log = logging.getLogger(__name__)
# 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
if CODE_INTERPRETER_BLOCKED_MODULES:
import textwrap
blocking_code = textwrap.dedent(
f"""
blocking_code = textwrap.dedent(f"""
import builtins
BLOCKED_MODULES = {CODE_INTERPRETER_BLOCKED_MODULES}
@@ -397,15 +397,20 @@ async def execute_code(
return _real_import(name, globals, locals, fromlist, level)
builtins.__import__ = restricted_import
"""
)
""")
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":
# Execute via frontend pyodide using bidirectional event call
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__(
{
@@ -413,7 +418,9 @@ async def execute_code(
"data": {
"id": str(uuid4()),
"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,
(
__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
),
(
__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
),
__request__.app.state.config.CODE_INTERPRETER_JUPYTER_TIMEOUT,
@@ -1921,7 +1930,9 @@ async def view_skill(
# Check user access
user_role = __user__.get("role", "user")
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(
user_id=user_id,
resource_type="skill",
@@ -1931,11 +1942,13 @@ async def view_skill(
):
return json.dumps({"error": "Access denied"})
return json.dumps({
"name": skill.name,
"content": skill.content,
}, ensure_ascii=False)
return json.dumps(
{
"name": skill.name,
"content": skill.content,
},
ensure_ascii=False,
)
except Exception as e:
log.exception(f"view_skill error: {e}")
return json.dumps({"error": str(e)})
+25 -14
View File
@@ -139,16 +139,23 @@ def has_access(
continue
principal_type = grant.get("principal_type")
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
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 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.
@@ -169,17 +176,21 @@ def migrate_access_control(data: dict, ac_key: str = "access_control", grants_ke
if not perm_data:
continue
for group_id in perm_data.get("group_ids", []):
grants.append({
"principal_type": "group",
"principal_id": group_id,
"permission": perm,
})
grants.append(
{
"principal_type": "group",
"principal_id": group_id,
"permission": perm,
}
)
for uid in perm_data.get("user_ids", []):
grants.append({
"principal_type": "user",
"principal_id": uid,
"permission": perm,
})
grants.append(
{
"principal_type": "user",
"principal_id": uid,
"permission": perm,
}
)
data[grants_key] = grants
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.models.users import UserModel
if TYPE_CHECKING:
from loguru import Logger
@@ -222,7 +221,9 @@ class AuditLoggingMiddleware:
# Skip logging if the request is not authenticated
# 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
# 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.security import HTTPAuthorizationCredentials, HTTPBearer
log = logging.getLogger(__name__)
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
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
@@ -341,7 +340,9 @@ async def chat_completed(request: Request, form_data: dict, user: Any):
}
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)
result, _ = await process_filter_functions(
@@ -8,7 +8,6 @@ import aiohttp
import websockets
from pydantic import BaseModel
logger = logging.getLogger(__name__)
-1
View File
@@ -86,4 +86,3 @@ async def generate_embeddings(
form_data=form_data,
user=user,
)
-1
View File
@@ -17,7 +17,6 @@ from open_webui.env import (
ENABLE_OTEL_LOGS,
)
if TYPE_CHECKING:
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.constants import TASKS
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
log = logging.getLogger(__name__)
@@ -2106,10 +2105,15 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if all_skill_ids:
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 = [
s for sid in all_skill_ids
if sid in accessible_skill_ids and (s := SkillsModel.get_skill_by_id(sid)) and s.is_active
s
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:
@@ -2238,7 +2242,9 @@ async def process_chat_payload(request, form_data, user, metadata, model):
if ENABLE_FORWARD_USER_INFO_HEADERS and user:
headers = include_user_info_headers(headers, user)
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()
await mcp_clients[server_id].connect(
@@ -2812,9 +2818,7 @@ async def non_streaming_chat_response_handler(response, ctx):
"id": output_id("msg"),
"status": "completed",
"role": "assistant",
"content": [
{"type": "output_text", "text": content}
],
"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)
if match:
try:
attr_content = (
match.group(1) if match.group(1) else ""
)
attr_content = match.group(1) if match.group(1) else ""
except:
attr_content = ""
@@ -2982,7 +2984,7 @@ async def streaming_chat_response_handler(response, ctx):
current_text = get_last_text(output)
set_last_text(
output,
current_text.replace(match.group(0) + after_tag, "")
current_text.replace(match.group(0) + after_tag, ""),
)
if before_tag:
@@ -3031,7 +3033,9 @@ async def streaming_chat_response_handler(response, ctx):
"id": output_id("msg"),
"status": "in_progress",
"role": "assistant",
"content": [{"type": "output_text", "text": ""}],
"content": [
{"type": "output_text", "text": ""}
],
"_tag_type": content_type,
"start_tag": start_tag,
"end_tag": end_tag,
@@ -3059,8 +3063,14 @@ async def streaming_chat_response_handler(response, ctx):
elif (
(last_type == "reasoning" and content_type == "reasoning")
or (last_type == "open_webui:code_interpreter" and content_type == "code_interpreter")
or (last_type == "message" and output[-1].get("_tag_type") == content_type)
or (
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]
start_tag = item.get("start_tag", "")
@@ -3178,9 +3188,7 @@ async def streaming_chat_response_handler(response, ctx):
# Clean processed content
start_tag_clean = rf"{re.escape(start_tag)}"
if start_tag.startswith("<") and start_tag.endswith(">"):
start_tag_clean = (
rf"<{re.escape(start_tag[1:-1])}(\s.*?)?>"
)
start_tag_clean = rf"<{re.escape(start_tag[1:-1])}(\s.*?)?>"
content = re.sub(
rf"{start_tag_clean}(.|\n)*?{re.escape(end_tag)}",
@@ -3231,7 +3239,6 @@ async def streaming_chat_response_handler(response, ctx):
else:
output = []
usage = None
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:
call_id = tc.get("id", "")
func = tc.get("function", {})
pending_fc_items.append({
"type": "function_call",
"id": call_id or output_id("fc"),
"call_id": call_id,
"name": func.get("name", ""),
"arguments": func.get("arguments", "{}"),
"status": "in_progress",
})
pending_fc_items.append(
{
"type": "function_call",
"id": call_id
or output_id("fc"),
"call_id": call_id,
"name": func.get("name", ""),
"arguments": func.get(
"arguments", "{}"
),
"status": "in_progress",
}
)
pending_output = output + pending_fc_items
await event_emitter(
{
@@ -3585,22 +3597,25 @@ async def streaming_chat_response_handler(response, ctx):
# Append to reasoning 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
else:
reasoning_item["content"] = [
{"type": "output_text", "text": reasoning_content}
{
"type": "output_text",
"text": reasoning_content,
}
]
data = {
"content": serialize_output(output)
}
data = {"content": serialize_output(output)}
if value:
if (
output
and output[-1].get("type")
== "reasoning"
and output[-1].get("type") == "reasoning"
and output[-1]
.get("attributes", {})
.get("type")
@@ -3620,7 +3635,12 @@ async def streaming_chat_response_handler(response, ctx):
"id": output_id("msg"),
"status": "in_progress",
"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"),
"status": "in_progress",
"role": "assistant",
"content": [{"type": "output_text", "text": ""}],
"content": [
{
"type": "output_text",
"text": "",
}
],
}
)
# Append value to last message item's text
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
else:
output[-1]["content"] = [
@@ -3664,32 +3693,26 @@ async def streaming_chat_response_handler(response, ctx):
]
if DETECT_REASONING_TAGS:
content, output, _ = (
tag_output_handler(
"reasoning",
reasoning_tags,
content,
output,
)
content, output, _ = tag_output_handler(
"reasoning",
reasoning_tags,
content,
output,
)
content, output, _ = (
tag_output_handler(
"solution",
DEFAULT_SOLUTION_TAGS,
content,
output,
)
content, output, _ = tag_output_handler(
"solution",
DEFAULT_SOLUTION_TAGS,
content,
output,
)
if DETECT_CODE_INTERPRETER:
content, output, end = (
tag_output_handler(
"code_interpreter",
DEFAULT_CODE_INTERPRETER_TAGS,
content,
output,
)
content, output, end = tag_output_handler(
"code_interpreter",
DEFAULT_CODE_INTERPRETER_TAGS,
content,
output,
)
if end:
@@ -3707,9 +3730,7 @@ async def streaming_chat_response_handler(response, ctx):
)
else:
data = {
"content": serialize_output(
output
),
"content": serialize_output(output),
}
if delta:
@@ -3750,7 +3771,9 @@ async def streaming_chat_response_handler(response, ctx):
"id": output_id("msg"),
"status": "in_progress",
"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:
call_id = tc.get("id", "")
func = tc.get("function", {})
output.append({
"type": "function_call",
"id": call_id or output_id("fc"),
"call_id": call_id,
"name": func.get("name", ""),
"arguments": func.get("arguments", "{}"),
"status": "in_progress",
})
output.append(
{
"type": "function_call",
"id": call_id or output_id("fc"),
"call_id": call_id,
"name": func.get("name", ""),
"arguments": func.get("arguments", "{}"),
"status": "in_progress",
}
)
await event_emitter(
{
@@ -3954,35 +3979,42 @@ async def streaming_chat_response_handler(response, ctx):
call_id = tc.get("id", "")
# Mark function_call as completed
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"
# Update arguments with parsed/sanitized version
item["arguments"] = tc.get("function", {}).get("arguments", "{}")
item["arguments"] = tc.get("function", {}).get(
"arguments", "{}"
)
break
for result in results:
output.append({
"type": "function_call_output",
"id": output_id("fco"),
"call_id": result.get("tool_call_id", ""),
"output": [
{
"type": "input_text",
"text": result.get("content", ""),
}
],
"status": "completed",
**(
{"files": result.get("files")}
if result.get("files")
else {}
),
**(
{"embeds": result.get("embeds")}
if result.get("embeds")
else {}
),
})
output.append(
{
"type": "function_call_output",
"id": output_id("fco"),
"call_id": result.get("tool_call_id", ""),
"output": [
{
"type": "input_text",
"text": result.get("content", ""),
}
],
"status": "completed",
**(
{"files": result.get("files")}
if result.get("files")
else {}
),
**(
{"embeds": result.get("embeds")}
if result.get("embeds")
else {}
),
}
)
# Append a new empty message item for the next response
output.append(
@@ -4079,8 +4111,7 @@ async def streaming_chat_response_handler(response, ctx):
code = sanitize_code(code)
if CODE_INTERPRETER_BLOCKED_MODULES:
blocking_code = textwrap.dedent(
f"""
blocking_code = textwrap.dedent(f"""
import builtins
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)
builtins.__import__ = restricted_import
"""
)
""")
code = blocking_code + "\n" + code
if (
+26 -20
View File
@@ -151,11 +151,15 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
def flush_pending():
nonlocal pending_content, pending_tool_calls
if pending_content or pending_tool_calls:
messages.append({
"role": "assistant",
"content": "\n".join(pending_content) if pending_content else "",
**({"tool_calls": pending_tool_calls} if pending_tool_calls else {}),
})
messages.append(
{
"role": "assistant",
"content": "\n".join(pending_content) if pending_content else "",
**(
{"tool_calls": pending_tool_calls} if pending_tool_calls else {}
),
}
)
pending_content = []
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
if not isinstance(arguments, str):
arguments = json.dumps(arguments)
pending_tool_calls.append({
"id": item.get("call_id", ""),
"type": "function",
"function": {
"name": item.get("name", ""),
"arguments": arguments,
pending_tool_calls.append(
{
"id": item.get("call_id", ""),
"type": "function",
"function": {
"name": item.get("name", ""),
"arguments": arguments,
},
}
})
)
elif item_type == "function_call_output":
# 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":
content += part.get("text", "")
messages.append({
"role": "tool",
"tool_call_id": item.get("call_id", ""),
"content": content,
})
messages.append(
{
"role": "tool",
"tool_call_id": item.get("call_id", ""),
"content": content,
}
)
elif item_type == "reasoning":
if raw:
@@ -218,9 +226,7 @@ def convert_output_to_messages(output: list, raw: bool = False) -> list[dict]:
if reasoning_text:
start_tag = item.get("start_tag", "<think>")
end_tag = item.get("end_tag", "</think>")
pending_content.append(
f"{start_tag}{reasoning_text}{end_tag}"
)
pending_content.append(f"{start_tag}{reasoning_text}{end_tag}")
# else: skip reasoning blocks for normal LLM messages
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.models.users import UserModel
logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL)
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)
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:
openai_payload["metadata"] = dict(metadata)
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]
return ollama_payload
-1
View File
@@ -151,7 +151,6 @@ def resolve_valves_schema_options(
return schema
def extract_frontmatter(content):
"""
Extract frontmatter as a dictionary from the provided content string.
+1 -1
View File
@@ -68,7 +68,7 @@ def convert_ollama_usage_to_openai(data: dict) -> dict:
input_tokens = int(data.get("prompt_eval_count", 0))
output_tokens = int(data.get("eval_count", 0))
total_tokens = input_tokens + output_tokens
return {
# Standardized fields
"input_tokens": input_tokens,
+4 -3
View File
@@ -2,7 +2,9 @@ import re
# ANSI escape code pattern - matches all common ANSI 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:
@@ -18,7 +20,7 @@ def strip_ansi_codes(text: str) -> str:
- Reset codes: \x1b[0m, \x1b[39m
- 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:
@@ -55,4 +57,3 @@ def sanitize_code(code: str) -> str:
code = strip_ansi_codes(code)
code = strip_markdown_code_fences(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
log = logging.getLogger(__name__)
@@ -29,7 +29,6 @@ from fastapi import status
from open_webui.utils.telemetry.constants import SPAN_REDIS_TYPE, SpanAttributes
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)
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
"""
@@ -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.
try:
db = ""
if hasattr(instance, 'nodes_manager'):
if hasattr(instance, "nodes_manager"):
default_node = instance.nodes_manager.default_node
if not default_node:
return
+11 -6
View File
@@ -352,7 +352,9 @@ async def get_tools(
headers = include_user_info_headers(headers, user)
metadata = extra_params.get("__metadata__", {})
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(
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)
def get_model_capability(name: str, default: bool = True) -> bool:
return (
(model.get("info", {}).get("meta", {}).get("capabilities") or {})
.get(name, default)
return (model.get("info", {}).get("meta", {}).get("capabilities") or {}).get(
name, default
)
# 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)
# 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(
[search_notes, view_note, write_note, replace_note_content]
)
# 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(
[
search_channels,
-1
View File
@@ -36,4 +36,3 @@ def validate_profile_image_url(url: str) -> str:
raise ValueError(
"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';
export const getModelAnalytics = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
groupId: string | null = null
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
groupId: string | null = null
) => {
let error = null;
let error = null;
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId);
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
if (error) {
throw error;
}
return res;
return res;
};
export const getUserAnalytics = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
limit: number = 50,
groupId: string | null = null
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
limit: number = 50,
groupId: string | null = null
) => {
let error = null;
let error = null;
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (limit) searchParams.append('limit', limit.toString());
if (groupId) searchParams.append('group_id', groupId);
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (limit) searchParams.append('limit', limit.toString());
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/users?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/users?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
if (error) {
throw error;
}
return res;
return res;
};
export const getMessages = async (
token: string = '',
modelId: string | null = null,
userId: string | null = null,
chatId: string | null = null,
startDate: number | null = null,
endDate: number | null = null,
skip: number = 0,
limit: number = 50
token: string = '',
modelId: string | null = null,
userId: string | null = null,
chatId: string | null = null,
startDate: number | null = null,
endDate: number | null = null,
skip: number = 0,
limit: number = 50
) => {
let error = null;
let error = null;
const searchParams = new URLSearchParams();
if (modelId) searchParams.append('model_id', modelId);
if (userId) searchParams.append('user_id', userId);
if (chatId) searchParams.append('chat_id', chatId);
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (skip) searchParams.append('skip', skip.toString());
if (limit) searchParams.append('limit', limit.toString());
const searchParams = new URLSearchParams();
if (modelId) searchParams.append('model_id', modelId);
if (userId) searchParams.append('user_id', userId);
if (chatId) searchParams.append('chat_id', chatId);
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (skip) searchParams.append('skip', skip.toString());
if (limit) searchParams.append('limit', limit.toString());
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/messages?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/messages?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
if (error) {
throw error;
}
return res;
return res;
};
export const getSummary = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
groupId: string | null = null
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
groupId: string | null = null
) => {
let error = null;
let error = null;
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId);
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/summary?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/summary?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
if (error) {
throw error;
}
return res;
return res;
};
export const getDailyStats = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
granularity: 'hourly' | 'daily' = 'daily',
groupId: string | null = null
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
granularity: 'hourly' | 'daily' = 'daily',
groupId: string | null = null
) => {
let error = null;
let error = null;
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
searchParams.append('granularity', granularity);
if (groupId) searchParams.append('group_id', groupId);
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
searchParams.append('granularity', granularity);
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/daily?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/daily?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
if (error) {
throw error;
}
return res;
return res;
};
export const getTokenUsage = async (
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
groupId: string | null = null
token: string = '',
startDate: number | null = null,
endDate: number | null = null,
groupId: string | null = null
) => {
let error = null;
let error = null;
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId);
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (groupId) searchParams.append('group_id', groupId);
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/tokens?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/tokens?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
if (error) {
throw error;
}
return res;
return res;
};
export const getModelChats = async (
token: string = '',
modelId: string,
startDate: number | null = null,
endDate: number | null = null,
skip: number = 0,
limit: number = 50
token: string = '',
modelId: string,
startDate: number | null = null,
endDate: number | null = null,
skip: number = 0,
limit: number = 50
) => {
let error = null;
let error = null;
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (skip) searchParams.append('skip', skip.toString());
if (limit) searchParams.append('limit', limit.toString());
const searchParams = new URLSearchParams();
if (startDate) searchParams.append('start_date', startDate.toString());
if (endDate) searchParams.append('end_date', endDate.toString());
if (skip) searchParams.append('skip', skip.toString());
if (limit) searchParams.append('limit', limit.toString());
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models/${encodeURIComponent(modelId)}/chats?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
const res = await fetch(
`${WEBUI_API_BASE_URL}/analytics/models/${encodeURIComponent(modelId)}/chats?${searchParams.toString()}`,
{
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
}
)
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
if (error) {
throw error;
}
return res;
return res;
};
export const getModelOverview = async (
token: string = '',
modelId: string,
days: number = 30
) => {
let error = null;
export const getModelOverview = async (token: string = '', modelId: string, days: number = 30) => {
let error = null;
const searchParams = new URLSearchParams();
searchParams.append('days', days.toString());
const searchParams = new URLSearchParams();
searchParams.append('days', days.toString());
const res = await fetch(`${WEBUI_API_BASE_URL}/analytics/models/${encodeURIComponent(modelId)}/overview?${searchParams.toString()}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
const res = await fetch(
`${WEBUI_API_BASE_URL}/analytics/models/${encodeURIComponent(modelId)}/overview?${searchParams.toString()}`,
{
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
}
)
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
if (error) {
throw error;
}
return res;
return res;
};
+1 -5
View File
@@ -255,11 +255,7 @@ export const getArchivedChatList = async (
}));
};
export const getSharedChatList = async (
token: string = '',
page: number = 1,
filter?: object
) => {
export const getSharedChatList = async (token: string = '', page: number = 1, filter?: object) => {
let error = null;
const searchParams = new URLSearchParams();
+1 -1
View File
@@ -1675,7 +1675,7 @@ export interface ModelMeta {
profile_image_url?: string;
}
export interface ModelParams { }
export interface ModelParams {}
export type GlobalModelConfig = ModelConfig[];

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