Skip to content

Commit 0623339

Browse files
Merge remote-tracking branch 'upstream/dev' into dev
2 parents 62f490c + 527d48e commit 0623339

21 files changed

Lines changed: 2153 additions & 63 deletions

File tree

backend/open_webui/main.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
get_models_in_use,
6969
)
7070
from open_webui.routers import (
71+
analytics,
7172
audio,
7273
images,
7374
ollama,
@@ -1455,6 +1456,9 @@ async def inspect_websocket(request: Request, call_next):
14551456
app.include_router(
14561457
evaluations.router, prefix="/api/v1/evaluations", tags=["evaluations"]
14571458
)
1459+
app.include_router(
1460+
analytics.router, prefix="/api/v1/analytics", tags=["analytics"]
1461+
)
14581462
app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
14591463

14601464
# SCIM 2.0 API for identity management
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Add chat_message table
2+
3+
Revision ID: 8452d01d26d7
4+
Revises: 374d2f66af06
5+
Create Date: 2026-02-01 04:00:00.000000
6+
7+
"""
8+
9+
import time
10+
import json
11+
import logging
12+
from typing import Sequence, Union
13+
14+
from alembic import op
15+
import sqlalchemy as sa
16+
17+
log = logging.getLogger(__name__)
18+
19+
revision: str = "8452d01d26d7"
20+
down_revision: Union[str, None] = "374d2f66af06"
21+
branch_labels: Union[str, Sequence[str], None] = None
22+
depends_on: Union[str, Sequence[str], None] = None
23+
24+
25+
def upgrade() -> None:
26+
# Step 1: Create table
27+
op.create_table(
28+
"chat_message",
29+
sa.Column("id", sa.Text(), primary_key=True),
30+
sa.Column("chat_id", sa.Text(), nullable=False, index=True),
31+
sa.Column("user_id", sa.Text(), index=True),
32+
sa.Column("role", sa.Text(), nullable=False),
33+
sa.Column("parent_id", sa.Text(), nullable=True),
34+
sa.Column("content", sa.JSON(), nullable=True),
35+
sa.Column("output", sa.JSON(), nullable=True),
36+
sa.Column("model_id", sa.Text(), nullable=True, index=True),
37+
sa.Column("files", sa.JSON(), nullable=True),
38+
sa.Column("sources", sa.JSON(), nullable=True),
39+
sa.Column("embeds", sa.JSON(), nullable=True),
40+
sa.Column("done", sa.Boolean(), default=True),
41+
sa.Column("status_history", sa.JSON(), nullable=True),
42+
sa.Column("error", sa.JSON(), nullable=True),
43+
sa.Column("usage", sa.JSON(), nullable=True),
44+
sa.Column("created_at", sa.BigInteger(), index=True),
45+
sa.Column("updated_at", sa.BigInteger()),
46+
sa.ForeignKeyConstraint(["chat_id"], ["chat.id"], ondelete="CASCADE"),
47+
)
48+
49+
# Create composite indexes
50+
op.create_index(
51+
"chat_message_chat_parent_idx", "chat_message", ["chat_id", "parent_id"]
52+
)
53+
op.create_index(
54+
"chat_message_model_created_idx", "chat_message", ["model_id", "created_at"]
55+
)
56+
op.create_index(
57+
"chat_message_user_created_idx", "chat_message", ["user_id", "created_at"]
58+
)
59+
60+
# Step 2: Backfill from existing chats
61+
conn = op.get_bind()
62+
63+
chat_table = sa.table(
64+
"chat",
65+
sa.column("id", sa.Text()),
66+
sa.column("user_id", sa.Text()),
67+
sa.column("chat", sa.JSON()),
68+
)
69+
70+
chat_message_table = sa.table(
71+
"chat_message",
72+
sa.column("id", sa.Text()),
73+
sa.column("chat_id", sa.Text()),
74+
sa.column("user_id", sa.Text()),
75+
sa.column("role", sa.Text()),
76+
sa.column("parent_id", sa.Text()),
77+
sa.column("content", sa.JSON()),
78+
sa.column("output", sa.JSON()),
79+
sa.column("model_id", sa.Text()),
80+
sa.column("files", sa.JSON()),
81+
sa.column("sources", sa.JSON()),
82+
sa.column("embeds", sa.JSON()),
83+
sa.column("done", sa.Boolean()),
84+
sa.column("status_history", sa.JSON()),
85+
sa.column("error", sa.JSON()),
86+
sa.column("usage", sa.JSON()),
87+
sa.column("created_at", sa.BigInteger()),
88+
sa.column("updated_at", sa.BigInteger()),
89+
)
90+
91+
# Fetch all chats
92+
chats = conn.execute(
93+
sa.select(chat_table.c.id, chat_table.c.user_id, chat_table.c.chat)
94+
).fetchall()
95+
96+
now = int(time.time())
97+
messages_inserted = 0
98+
messages_failed = 0
99+
100+
for chat_row in chats:
101+
chat_id = chat_row[0]
102+
user_id = chat_row[1]
103+
chat_data = chat_row[2]
104+
105+
if not chat_data:
106+
continue
107+
108+
# Handle both string and dict chat data
109+
if isinstance(chat_data, str):
110+
try:
111+
chat_data = json.loads(chat_data)
112+
except Exception:
113+
continue
114+
115+
history = chat_data.get("history", {})
116+
messages = history.get("messages", {})
117+
118+
for message_id, message in messages.items():
119+
if not isinstance(message, dict):
120+
continue
121+
122+
role = message.get("role")
123+
if not role:
124+
continue
125+
126+
timestamp = message.get("timestamp", now)
127+
128+
# Normalize timestamp: convert ms to seconds, validate range
129+
if timestamp > 10_000_000_000:
130+
timestamp = timestamp // 1000
131+
# Must be after 2020 and not too far in the future
132+
if timestamp < 1577836800 or timestamp > now + 86400:
133+
timestamp = now
134+
135+
# Use savepoint to allow individual insert failures without aborting transaction
136+
savepoint = conn.begin_nested()
137+
try:
138+
conn.execute(
139+
sa.insert(chat_message_table).values(
140+
id=f"{chat_id}-{message_id}",
141+
chat_id=chat_id,
142+
user_id=user_id,
143+
role=role,
144+
parent_id=message.get("parentId"),
145+
content=message.get("content"),
146+
output=message.get("output"),
147+
model_id=message.get("model"),
148+
files=message.get("files"),
149+
sources=message.get("sources"),
150+
embeds=message.get("embeds"),
151+
done=message.get("done", True),
152+
status_history=message.get("statusHistory"),
153+
error=message.get("error"),
154+
created_at=timestamp,
155+
updated_at=timestamp,
156+
)
157+
)
158+
savepoint.commit()
159+
messages_inserted += 1
160+
except Exception as e:
161+
savepoint.rollback()
162+
messages_failed += 1
163+
log.warning(f"Failed to insert message {message_id}: {e}")
164+
continue
165+
166+
log.info(f"Backfilled {messages_inserted} messages into chat_message table ({messages_failed} failed)")
167+
168+
169+
def downgrade() -> None:
170+
op.drop_index("chat_message_user_created_idx", table_name="chat_message")
171+
op.drop_index("chat_message_model_created_idx", table_name="chat_message")
172+
op.drop_index("chat_message_chat_parent_idx", table_name="chat_message")
173+
op.drop_table("chat_message")

0 commit comments

Comments
 (0)