diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index ddc5e99aaf..9dcf369096 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -1,3 +1,6 @@ +from sqlalchemy import case, func, select +from sqlmodel import col + from astrbot.api import sp, star from astrbot.api.event import AstrMessageEvent, MessageEventResult from astrbot.core import logger @@ -7,6 +10,7 @@ DEERFLOW_THREAD_ID_KEY, ) from astrbot.core.agent.runners.deerflow.deerflow_api_client import DeerFlowAPIClient +from astrbot.core.db.po import ProviderStat from astrbot.core.utils.active_event_registry import active_event_registry from .utils.rst_scene import RstScene @@ -246,3 +250,62 @@ async def new_conv(self, message: AstrMessageEvent) -> None: f"✅ Switched to new conversation: {cid[:4]}。" ), ) + + async def stats(self, message: AstrMessageEvent) -> None: + """Show token usage statistics for the current conversation.""" + umo = message.unified_msg_origin + cid = await self.context.conversation_manager.get_curr_conversation_id(umo) + + if not cid: + message.set_result( + MessageEventResult().message( + "❌ You are not in a conversation. Use /new to create one." + ), + ) + return + + db = self.context.get_db() + async with db.get_db() as session: + result = await session.execute( + select( + func.count(case((col(ProviderStat.id).is_not(None), 1))).label( + "record_count", + ), + func.coalesce(func.sum(ProviderStat.token_input_other), 0).label( + "total_input_other", + ), + func.coalesce(func.sum(ProviderStat.token_input_cached), 0).label( + "total_input_cached", + ), + func.coalesce(func.sum(ProviderStat.token_output), 0).label( + "total_output", + ), + ).where( + col(ProviderStat.agent_type) == "internal", + col(ProviderStat.conversation_id) == cid, + ) + ) + stats = result.one() + + if stats.record_count == 0: + message.set_result( + MessageEventResult().message( + "📊 No stats available for this conversation yet." + ), + ) + return + + total_input_other = stats.total_input_other + total_input_cached = stats.total_input_cached + total_output = stats.total_output + total_tokens = total_input_other + total_input_cached + total_output + + ret = ( + f"📊 Conversation Token usage (ID: {cid[:8]}...)\n" + f"Total: {total_tokens:,}\n" + f"Input (cached): {total_input_cached:,}\n" + f"Input (other): {total_input_other:,}\n" + f"Output: {total_output:,}\n" + ) + + message.set_result(MessageEventResult().message(ret)) diff --git a/astrbot/builtin_stars/builtin_commands/main.py b/astrbot/builtin_stars/builtin_commands/main.py index f2e5e26d5e..4a0e78f81a 100644 --- a/astrbot/builtin_stars/builtin_commands/main.py +++ b/astrbot/builtin_stars/builtin_commands/main.py @@ -47,6 +47,11 @@ async def new_conv(self, message: AstrMessageEvent) -> None: """Create new conversation""" await self.conversation_c.new_conv(message) + @filter.command("stats") + async def stats(self, message: AstrMessageEvent) -> None: + """Show token usage statistics for the current conversation""" + await self.conversation_c.stats(message) + @filter.permission_type(filter.PermissionType.ADMIN) @filter.command("provider") async def provider( diff --git a/dashboard/src/components/chat/ChatMessageList.vue b/dashboard/src/components/chat/ChatMessageList.vue index 630539a5c7..f19e5d6375 100644 --- a/dashboard/src/components/chat/ChatMessageList.vue +++ b/dashboard/src/components/chat/ChatMessageList.vue @@ -293,6 +293,15 @@ /> +
+ {{ tm("stats.cachedTokens") }} + {{ + cachedInputTokens(messageContent(msg).agentStats) + }} +
{{ tm("stats.inputTokens") }} {{ inputTokens(messageContent(msg).agentStats) }} @@ -850,13 +859,17 @@ function formatTime(value: string) { function inputTokens(stats: any) { const usage = stats?.token_usage || {}; - return (usage.input_other || 0) + (usage.input_cached || 0); + return usage.input_other || 0; } function outputTokens(stats: any) { return stats?.token_usage?.output || 0; } +function cachedInputTokens(stats: any) { + return stats?.token_usage?.input_cached || 0; +} + function agentDuration(stats: any) { const directDuration = readPositiveNumber(stats, [ "duration", diff --git a/dashboard/src/components/chat/MessageList.vue b/dashboard/src/components/chat/MessageList.vue index dbafab20f8..4f44bf166f 100644 --- a/dashboard/src/components/chat/MessageList.vue +++ b/dashboard/src/components/chat/MessageList.vue @@ -185,6 +185,15 @@ /> +
+ {{ tm("stats.cachedTokens") }} + {{ + cachedInputTokens(messageContent(msg).agentStats) + }} +
{{ tm("stats.inputTokens") }} {{ @@ -512,13 +521,17 @@ function formatTime(value: string) { function inputTokens(stats: any) { const usage = stats?.token_usage || {}; - return (usage.input_other || 0) + (usage.input_cached || 0); + return usage.input_other || 0; } function outputTokens(stats: any) { return stats?.token_usage?.output || 0; } +function cachedInputTokens(stats: any) { + return stats?.token_usage?.input_cached || 0; +} + function agentDuration(stats: any) { const directDuration = readPositiveNumber(stats, [ "duration", diff --git a/dashboard/src/i18n/locales/en-US/features/chat.json b/dashboard/src/i18n/locales/en-US/features/chat.json index d7d319a6bf..a026532c51 100644 --- a/dashboard/src/i18n/locales/en-US/features/chat.json +++ b/dashboard/src/i18n/locales/en-US/features/chat.json @@ -137,9 +137,9 @@ }, "stats": { "tokens": "Tokens", - "inputTokens": "Input Tokens", + "inputTokens": "Input (other)", "outputTokens": "Output Tokens", - "cachedTokens": "Cached Tokens", + "cachedTokens": "Input (cached)", "duration": "Duration", "ttft": "Time to First Token" }, diff --git a/dashboard/src/i18n/locales/ru-RU/features/chat.json b/dashboard/src/i18n/locales/ru-RU/features/chat.json index e2a72a437e..b033cedaec 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/chat.json +++ b/dashboard/src/i18n/locales/ru-RU/features/chat.json @@ -137,9 +137,9 @@ }, "stats": { "tokens": "Токены", - "inputTokens": "Входящие", + "inputTokens": "Входящие (прочие)", "outputTokens": "Исходящие", - "cachedTokens": "Кэшированные", + "cachedTokens": "Входящие (кэш)", "duration": "Время", "ttft": "Время до первого токена" }, diff --git a/dashboard/src/i18n/locales/zh-CN/features/chat.json b/dashboard/src/i18n/locales/zh-CN/features/chat.json index 67328f0ee8..9ce2c6b81f 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/chat.json +++ b/dashboard/src/i18n/locales/zh-CN/features/chat.json @@ -137,9 +137,9 @@ }, "stats": { "tokens": "Token", - "inputTokens": "输入 Token", + "inputTokens": "输入(其他)", "outputTokens": "输出 Token", - "cachedTokens": "缓存 Token", + "cachedTokens": "输入(缓存)", "duration": "耗时", "ttft": "首字时间" },