Skip to content

Commit 18e0e60

Browse files
Merge remote-tracking branch 'upstream/dev' into dev
2 parents 0adf406 + a214ec4 commit 18e0e60

32 files changed

Lines changed: 1868 additions & 569 deletions

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ RUN pip3 install --no-cache-dir uv && \
143143
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \
144144
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
145145
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
146+
python -c "import nltk; nltk.download('punkt_tab')"; \
146147
else \
147148
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \
148149
uv pip install --system -r requirements.txt --no-cache-dir && \
@@ -151,6 +152,7 @@ RUN pip3 install --no-cache-dir uv && \
151152
python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ.get('AUXILIARY_EMBEDDING_MODEL', 'TaylorAI/bge-micro-v2'), device='cpu')" && \
152153
python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])"; \
153154
python -c "import os; import tiktoken; tiktoken.get_encoding(os.environ['TIKTOKEN_ENCODING_NAME'])"; \
155+
python -c "import nltk; nltk.download('punkt_tab')"; \
154156
fi; \
155157
fi; \
156158
mkdir -p /app/backend/data && chown -R $UID:$GID /app/backend/data/ && \

backend/open_webui/env.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,15 @@ def parse_section(section):
194194
os.environ.get("ENABLE_FORWARD_USER_INFO_HEADERS", "False").lower() == "true"
195195
)
196196

197+
# Header names for user info forwarding (customizable via environment variables)
198+
FORWARD_USER_INFO_HEADER_USER_NAME = os.environ.get("FORWARD_USER_INFO_HEADER_USER_NAME", "X-OpenWebUI-User-Name")
199+
FORWARD_USER_INFO_HEADER_USER_ID = os.environ.get("FORWARD_USER_INFO_HEADER_USER_ID", "X-OpenWebUI-User-Id")
200+
FORWARD_USER_INFO_HEADER_USER_EMAIL = os.environ.get("FORWARD_USER_INFO_HEADER_USER_EMAIL", "X-OpenWebUI-User-Email")
201+
FORWARD_USER_INFO_HEADER_USER_ROLE = os.environ.get("FORWARD_USER_INFO_HEADER_USER_ROLE", "X-OpenWebUI-User-Role")
202+
203+
# Header name for chat ID forwarding (customizable via environment variable)
204+
FORWARD_SESSION_INFO_HEADER_CHAT_ID = os.environ.get("FORWARD_SESSION_INFO_HEADER_CHAT_ID", "X-OpenWebUI-Chat-Id")
205+
197206
# Experimental feature, may be removed in future
198207
ENABLE_STAR_SESSIONS_MIDDLEWARE = (
199208
os.environ.get("ENABLE_STAR_SESSIONS_MIDDLEWARE", "False").lower() == "true"

backend/open_webui/main.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,11 @@
511511
chat_action as chat_action_handler,
512512
)
513513
from open_webui.utils.embeddings import generate_embeddings
514-
from open_webui.utils.middleware import process_chat_payload, process_chat_response
514+
from open_webui.utils.middleware import (
515+
build_chat_response_context,
516+
process_chat_payload,
517+
process_chat_response,
518+
)
515519
from open_webui.utils.access_control import has_access
516520

517521
from open_webui.utils.auth import (
@@ -1379,9 +1383,9 @@ async def check_url(request: Request, call_next):
13791383
# Fallback to cookie token for browser sessions
13801384
if request.state.token is None and request.cookies.get("token"):
13811385
from fastapi.security import HTTPAuthorizationCredentials
1386+
13821387
request.state.token = HTTPAuthorizationCredentials(
1383-
scheme="Bearer",
1384-
credentials=request.cookies.get("token")
1388+
scheme="Bearer", credentials=request.cookies.get("token")
13851389
)
13861390

13871391
request.state.enable_api_keys = app.state.config.ENABLE_API_KEYS
@@ -1456,9 +1460,7 @@ async def inspect_websocket(request: Request, call_next):
14561460
app.include_router(
14571461
evaluations.router, prefix="/api/v1/evaluations", tags=["evaluations"]
14581462
)
1459-
app.include_router(
1460-
analytics.router, prefix="/api/v1/analytics", tags=["analytics"]
1461-
)
1463+
app.include_router(analytics.router, prefix="/api/v1/analytics", tags=["analytics"])
14621464
app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
14631465

14641466
# SCIM 2.0 API for identity management
@@ -1746,9 +1748,11 @@ async def process_chat(request, form_data, user, metadata, model):
17461748
except:
17471749
pass
17481750

1749-
return await process_chat_response(
1750-
request, response, form_data, user, metadata, model, events, tasks
1751+
ctx = build_chat_response_context(
1752+
request, form_data, user, model, metadata, tasks, events
17511753
)
1754+
1755+
return await process_chat_response(response, ctx)
17521756
except asyncio.CancelledError:
17531757
log.info("Chat processing was cancelled")
17541758
try:

backend/open_webui/migrations/versions/8452d01d26d7_add_chat_message_table.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,11 @@ def upgrade() -> None:
8888
sa.column("updated_at", sa.BigInteger()),
8989
)
9090

91-
# Fetch all chats
91+
# Fetch all chats (excluding shared chats which have user_id starting with 'shared-')
9292
chats = conn.execute(
93-
sa.select(chat_table.c.id, chat_table.c.user_id, chat_table.c.chat)
93+
sa.select(chat_table.c.id, chat_table.c.user_id, chat_table.c.chat).where(
94+
~chat_table.c.user_id.like("shared-%")
95+
)
9496
).fetchall()
9597

9698
now = int(time.time())

backend/open_webui/models/chat_messages.py

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class ChatMessageModel(BaseModel):
111111
embeds: Optional[list] = None
112112
done: bool = True
113113
status_history: Optional[list] = None
114-
error: Optional[dict] = None
114+
error: Optional[dict | str] = None
115115
usage: Optional[dict] = None
116116
created_at: int
117117
updated_at: int
@@ -269,6 +269,36 @@ def get_messages_by_model_id(
269269
)
270270
return [ChatMessageModel.model_validate(message) for message in messages]
271271

272+
def get_chat_ids_by_model_id(
273+
self,
274+
model_id: str,
275+
start_date: Optional[int] = None,
276+
end_date: Optional[int] = None,
277+
skip: int = 0,
278+
limit: int = 50,
279+
db: Optional[Session] = None,
280+
) -> list[str]:
281+
"""Get distinct chat_ids that used a specific model."""
282+
from sqlalchemy import distinct
283+
284+
with get_db_context(db) as db:
285+
query = db.query(distinct(ChatMessage.chat_id)).filter(
286+
ChatMessage.model_id == model_id
287+
)
288+
if start_date:
289+
query = query.filter(ChatMessage.created_at >= start_date)
290+
if end_date:
291+
query = query.filter(ChatMessage.created_at <= end_date)
292+
293+
# Order by most recent message in each chat
294+
chat_ids = (
295+
query.order_by(ChatMessage.created_at.desc())
296+
.offset(skip)
297+
.limit(limit)
298+
.all()
299+
)
300+
return [chat_id for (chat_id,) in chat_ids]
301+
272302
def delete_messages_by_chat_id(
273303
self, chat_id: str, db: Optional[Session] = None
274304
) -> bool:
@@ -282,19 +312,28 @@ def get_message_count_by_model(
282312
self,
283313
start_date: Optional[int] = None,
284314
end_date: Optional[int] = None,
315+
group_id: Optional[str] = None,
285316
db: Optional[Session] = None,
286317
) -> dict[str, int]:
287318
with get_db_context(db) as db:
288319
from sqlalchemy import func
320+
from open_webui.models.groups import GroupMember
289321

290322
query = db.query(
291323
ChatMessage.model_id, func.count(ChatMessage.id).label("count")
292-
).filter(ChatMessage.role == "assistant", ChatMessage.model_id.isnot(None))
324+
).filter(
325+
ChatMessage.role == "assistant",
326+
ChatMessage.model_id.isnot(None),
327+
~ChatMessage.user_id.like("shared-%"),
328+
)
293329

294330
if start_date:
295331
query = query.filter(ChatMessage.created_at >= start_date)
296332
if end_date:
297333
query = query.filter(ChatMessage.created_at <= end_date)
334+
if group_id:
335+
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
336+
query = query.filter(ChatMessage.user_id.in_(group_users))
298337

299338
results = query.group_by(ChatMessage.model_id).all()
300339
return {row.model_id: row.count for row in results}
@@ -303,11 +342,13 @@ def get_token_usage_by_model(
303342
self,
304343
start_date: Optional[int] = None,
305344
end_date: Optional[int] = None,
345+
group_id: Optional[str] = None,
306346
db: Optional[Session] = None,
307347
) -> dict[str, dict]:
308348
"""Aggregate token usage by model using database-level aggregation."""
309349
with get_db_context(db) as db:
310350
from sqlalchemy import func, cast, Integer
351+
from open_webui.models.groups import GroupMember
311352

312353
dialect = db.bind.dialect.name
313354

@@ -338,12 +379,16 @@ def get_token_usage_by_model(
338379
ChatMessage.role == "assistant",
339380
ChatMessage.model_id.isnot(None),
340381
ChatMessage.usage.isnot(None),
382+
~ChatMessage.user_id.like("shared-%"),
341383
)
342384

343385
if start_date:
344386
query = query.filter(ChatMessage.created_at >= start_date)
345387
if end_date:
346388
query = query.filter(ChatMessage.created_at <= end_date)
389+
if group_id:
390+
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
391+
query = query.filter(ChatMessage.user_id.in_(group_users))
347392

348393
results = query.group_by(ChatMessage.model_id).all()
349394

@@ -396,6 +441,7 @@ def get_token_usage_by_user(
396441
ChatMessage.role == "assistant",
397442
ChatMessage.user_id.isnot(None),
398443
ChatMessage.usage.isnot(None),
444+
~ChatMessage.user_id.like("shared-%"),
399445
)
400446

401447
if start_date:
@@ -419,19 +465,24 @@ def get_message_count_by_user(
419465
self,
420466
start_date: Optional[int] = None,
421467
end_date: Optional[int] = None,
468+
group_id: Optional[str] = None,
422469
db: Optional[Session] = None,
423470
) -> dict[str, int]:
424471
with get_db_context(db) as db:
425472
from sqlalchemy import func
473+
from open_webui.models.groups import GroupMember
426474

427475
query = db.query(
428476
ChatMessage.user_id, func.count(ChatMessage.id).label("count")
429-
)
477+
).filter(~ChatMessage.user_id.like("shared-%"))
430478

431479
if start_date:
432480
query = query.filter(ChatMessage.created_at >= start_date)
433481
if end_date:
434482
query = query.filter(ChatMessage.created_at <= end_date)
483+
if group_id:
484+
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
485+
query = query.filter(ChatMessage.user_id.in_(group_users))
435486

436487
results = query.group_by(ChatMessage.user_id).all()
437488
return {row.user_id: row.count for row in results}
@@ -440,19 +491,24 @@ def get_message_count_by_chat(
440491
self,
441492
start_date: Optional[int] = None,
442493
end_date: Optional[int] = None,
494+
group_id: Optional[str] = None,
443495
db: Optional[Session] = None,
444496
) -> dict[str, int]:
445497
with get_db_context(db) as db:
446498
from sqlalchemy import func
499+
from open_webui.models.groups import GroupMember
447500

448501
query = db.query(
449502
ChatMessage.chat_id, func.count(ChatMessage.id).label("count")
450-
)
503+
).filter(~ChatMessage.user_id.like("shared-%"))
451504

452505
if start_date:
453506
query = query.filter(ChatMessage.created_at >= start_date)
454507
if end_date:
455508
query = query.filter(ChatMessage.created_at <= end_date)
509+
if group_id:
510+
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
511+
query = query.filter(ChatMessage.user_id.in_(group_users))
456512

457513
results = query.group_by(ChatMessage.chat_id).all()
458514
return {row.chat_id: row.count for row in results}
@@ -461,21 +517,27 @@ def get_daily_message_counts_by_model(
461517
self,
462518
start_date: Optional[int] = None,
463519
end_date: Optional[int] = None,
520+
group_id: Optional[str] = None,
464521
db: Optional[Session] = None,
465522
) -> dict[str, dict[str, int]]:
466523
"""Get message counts grouped by day and model."""
467524
with get_db_context(db) as db:
468525
from datetime import datetime, timedelta
526+
from open_webui.models.groups import GroupMember
469527

470528
query = db.query(ChatMessage.created_at, ChatMessage.model_id).filter(
471529
ChatMessage.role == "assistant",
472-
ChatMessage.model_id.isnot(None)
530+
ChatMessage.model_id.isnot(None),
531+
~ChatMessage.user_id.like("shared-%"),
473532
)
474533

475534
if start_date:
476535
query = query.filter(ChatMessage.created_at >= start_date)
477536
if end_date:
478537
query = query.filter(ChatMessage.created_at <= end_date)
538+
if group_id:
539+
group_users = db.query(GroupMember.user_id).filter(GroupMember.group_id == group_id).subquery()
540+
query = query.filter(ChatMessage.user_id.in_(group_users))
479541

480542
results = query.all()
481543

@@ -511,7 +573,8 @@ def get_hourly_message_counts_by_model(
511573

512574
query = db.query(ChatMessage.created_at, ChatMessage.model_id).filter(
513575
ChatMessage.role == "assistant",
514-
ChatMessage.model_id.isnot(None)
576+
ChatMessage.model_id.isnot(None),
577+
~ChatMessage.user_id.like("shared-%"),
515578
)
516579

517580
if start_date:

backend/open_webui/models/feedbacks.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,23 @@ def get_feedback_by_id_and_user_id(
191191
except Exception:
192192
return None
193193

194+
def get_feedbacks_by_chat_id(
195+
self, chat_id: str, db: Optional[Session] = None
196+
) -> list[FeedbackModel]:
197+
"""Get all feedbacks for a specific chat."""
198+
try:
199+
with get_db_context(db) as db:
200+
# meta.chat_id stores the chat reference
201+
feedbacks = (
202+
db.query(Feedback)
203+
.filter(Feedback.meta["chat_id"].as_string() == chat_id)
204+
.order_by(Feedback.created_at.desc())
205+
.all()
206+
)
207+
return [FeedbackModel.model_validate(fb) for fb in feedbacks]
208+
except Exception:
209+
return []
210+
194211
def get_feedback_items(
195212
self,
196213
filter: dict = {},

backend/open_webui/retrieval/web/external.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
1010
from open_webui.utils.headers import include_user_info_headers
11+
from open_webui.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID
1112

1213

1314
log = logging.getLogger(__name__)
@@ -31,7 +32,7 @@ def search_external(
3132

3233
chat_id = getattr(request.state, "chat_id", None)
3334
if chat_id:
34-
headers["X-OpenWebUI-Chat-Id"] = str(chat_id)
35+
headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = str(chat_id)
3536

3637
response = requests.post(
3738
external_url,

backend/open_webui/retrieval/web/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ async def _safe_process_url(self, url: str) -> bool:
174174

175175
def _safe_process_url_sync(self, url: str) -> bool:
176176
"""Synchronous version of safety checks."""
177-
if self.verify_ssl and not self._verify_ssl_cert(url):
177+
if self.verify_ssl and not verify_ssl_cert(url):
178178
raise ValueError(f"SSL certificate verification failed for {url}")
179179
self._sync_wait_for_rate_limit()
180180
return True

backend/open_webui/retrieval/web/yandex.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
1313
from open_webui.utils.headers import include_user_info_headers
14+
from open_webui.env import FORWARD_SESSION_INFO_HEADER_CHAT_ID
1415

1516
from xml.etree import ElementTree as ET
1617
from xml.etree.ElementTree import Element
@@ -50,7 +51,7 @@ def search_yandex(
5051

5152
chat_id = getattr(request.state, "chat_id", None)
5253
if chat_id:
53-
headers["X-OpenWebUI-Chat-Id"] = str(chat_id)
54+
headers[FORWARD_SESSION_INFO_HEADER_CHAT_ID] = str(chat_id)
5455

5556
payload = {} if yandex_search_config == "" else json.loads(yandex_search_config)
5657

0 commit comments

Comments
 (0)