diff --git a/backend/open_webui/models/chat_messages.py b/backend/open_webui/models/chat_messages.py index f3058f4ff..371e90db3 100644 --- a/backend/open_webui/models/chat_messages.py +++ b/backend/open_webui/models/chat_messages.py @@ -331,6 +331,63 @@ class ChatMessageTable: for row in results } + def get_token_usage_by_user( + self, + start_date: Optional[int] = None, + end_date: Optional[int] = None, + db: Optional[Session] = None, + ) -> dict[str, dict]: + """Aggregate token usage by user using database-level aggregation.""" + with get_db_context(db) as db: + from sqlalchemy import func, cast, Integer + + dialect = db.bind.dialect.name + + if dialect == "sqlite": + input_tokens = cast( + func.json_extract(ChatMessage.usage, "$.input_tokens"), Integer + ) + output_tokens = cast( + func.json_extract(ChatMessage.usage, "$.output_tokens"), Integer + ) + elif dialect == "postgresql": + input_tokens = cast( + ChatMessage.usage["input_tokens"].astext, Integer + ) + output_tokens = cast( + ChatMessage.usage["output_tokens"].astext, Integer + ) + else: + raise NotImplementedError(f"Unsupported dialect: {dialect}") + + query = db.query( + ChatMessage.user_id, + func.coalesce(func.sum(input_tokens), 0).label("input_tokens"), + func.coalesce(func.sum(output_tokens), 0).label("output_tokens"), + func.count(ChatMessage.id).label("message_count"), + ).filter( + ChatMessage.role == "assistant", + ChatMessage.user_id.isnot(None), + ChatMessage.usage.isnot(None), + ) + + if start_date: + query = query.filter(ChatMessage.created_at >= start_date) + if end_date: + query = query.filter(ChatMessage.created_at <= end_date) + + results = query.group_by(ChatMessage.user_id).all() + + return { + row.user_id: { + "input_tokens": row.input_tokens, + "output_tokens": row.output_tokens, + "total_tokens": row.input_tokens + row.output_tokens, + "message_count": row.message_count, + } + for row in results + } + def get_message_count_by_user( self, start_date: Optional[int] = None, diff --git a/backend/open_webui/routers/analytics.py b/backend/open_webui/routers/analytics.py index db66e9943..d8f6928ff 100644 --- a/backend/open_webui/routers/analytics.py +++ b/backend/open_webui/routers/analytics.py @@ -33,6 +33,9 @@ class UserAnalyticsEntry(BaseModel): name: Optional[str] = None email: Optional[str] = None count: int + input_tokens: int = 0 + output_tokens: int = 0 + total_tokens: int = 0 class UserAnalyticsResponse(BaseModel): @@ -70,12 +73,15 @@ async def get_user_analytics( user=Depends(get_admin_user), db: Session = Depends(get_session), ): - """Get message counts per user with user info.""" + """Get message counts and token usage per user with user info.""" from open_webui.models.users import Users counts = ChatMessages.get_message_count_by_user( start_date=start_date, end_date=end_date, db=db ) + 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]] @@ -84,11 +90,15 @@ async def get_user_analytics( 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] + 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) diff --git a/src/lib/components/admin/Analytics/Dashboard.svelte b/src/lib/components/admin/Analytics/Dashboard.svelte index e49275690..82eb5b581 100644 --- a/src/lib/components/admin/Analytics/Dashboard.svelte +++ b/src/lib/components/admin/Analytics/Dashboard.svelte @@ -310,6 +310,7 @@ {/if} +