From f02444146d0d8b9dde05076de14519e0844a6817 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 11:52:10 +0800 Subject: [PATCH 1/6] feat: add /stats command to view conversation token usage - Add stats() method to ConversationCommands that queries ProviderStat records by conversation_id and aggregates token breakdowns - Register /stats command in main.py --- .../builtin_commands/commands/conversation.py | 55 +++++++++++++++++++ .../builtin_stars/builtin_commands/main.py | 5 ++ 2 files changed, 60 insertions(+) diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index ddc5e99aaf..67726ee4c9 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -1,3 +1,5 @@ +from sqlalchemy import select + from astrbot.api import sp, star from astrbot.api.event import AstrMessageEvent, MessageEventResult from astrbot.core import logger @@ -7,6 +9,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 +249,55 @@ 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(ProviderStat).where( + ProviderStat.agent_type == "internal", + ProviderStat.conversation_id == cid, + ) + ) + records = result.scalars().all() + + if not records: + message.set_result( + MessageEventResult().message( + "📊 No stats available for this conversation yet." + ), + ) + return + + total_calls = len(records) + total_input_other = sum(r.token_input_other for r in records) + total_input_cached = sum(r.token_input_cached for r in records) + total_output = sum(r.token_output for r in records) + total_tokens = total_input_other + total_input_cached + total_output + success_count = sum(1 for r in records if r.status != "error") + + ret = ( + f"📊 Conversation Stats (ID: {cid[:8]}...)\n" + f"───\n" + f"Total Calls: {total_calls}\n" + f"Successful: {success_count}\n" + f"───\n" + f"Input Tokens (other): {total_input_other:,}\n" + f"Input Tokens (cached): {total_input_cached:,}\n" + f"Output Tokens: {total_output:,}\n" + f"Total Tokens: {total_tokens:,}" + ) + + 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( From 6ba01a477530ea825e7281a624c4d8484d67edc7 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 12:00:12 +0800 Subject: [PATCH 2/6] feat: reorder conversation stats output for better readability Co-authored-by: Copilot --- .../builtin_commands/commands/conversation.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index 67726ee4c9..751458c37c 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -1,4 +1,5 @@ from sqlalchemy import select +from sqlmodel import col from astrbot.api import sp, star from astrbot.api.event import AstrMessageEvent, MessageEventResult @@ -267,8 +268,8 @@ async def stats(self, message: AstrMessageEvent) -> None: async with db.get_db() as session: result = await session.execute( select(ProviderStat).where( - ProviderStat.agent_type == "internal", - ProviderStat.conversation_id == cid, + col(ProviderStat.agent_type) == "internal", + col(ProviderStat.conversation_id) == cid, ) ) records = result.scalars().all() @@ -281,23 +282,17 @@ async def stats(self, message: AstrMessageEvent) -> None: ) return - total_calls = len(records) total_input_other = sum(r.token_input_other for r in records) total_input_cached = sum(r.token_input_cached for r in records) total_output = sum(r.token_output for r in records) total_tokens = total_input_other + total_input_cached + total_output - success_count = sum(1 for r in records if r.status != "error") ret = ( - f"📊 Conversation Stats (ID: {cid[:8]}...)\n" - f"───\n" - f"Total Calls: {total_calls}\n" - f"Successful: {success_count}\n" - f"───\n" - f"Input Tokens (other): {total_input_other:,}\n" - f"Input Tokens (cached): {total_input_cached:,}\n" - f"Output Tokens: {total_output:,}\n" - f"Total Tokens: {total_tokens:,}" + f"📊 Token usage (ID: {cid[:8]}...)\n" + f"Total: {total_tokens:,}\n" + f"Input (other): {total_input_other:,}\n" + f"Input (cached): {total_input_cached:,}\n" + f"Output: {total_output:,}\n" ) message.set_result(MessageEventResult().message(ret)) From 8ca823117613496378cc63a26c071d1dfa4399ec Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 12:04:28 +0800 Subject: [PATCH 3/6] feat: reorder token usage output for improved clarity --- astrbot/builtin_stars/builtin_commands/commands/conversation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index 751458c37c..b9408191e4 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -290,8 +290,8 @@ async def stats(self, message: AstrMessageEvent) -> None: ret = ( f"📊 Token usage (ID: {cid[:8]}...)\n" f"Total: {total_tokens:,}\n" - f"Input (other): {total_input_other:,}\n" f"Input (cached): {total_input_cached:,}\n" + f"Input (other): {total_input_other:,}\n" f"Output: {total_output:,}\n" ) From 2ce6b1b885087b5a720ae40140decf06a41744c9 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 12:53:46 +0800 Subject: [PATCH 4/6] feat: enhance stats command to aggregate conversation token usage --- .../builtin_commands/commands/conversation.py | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index b9408191e4..9489743038 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -1,4 +1,4 @@ -from sqlalchemy import select +from sqlalchemy import case, func, select from sqlmodel import col from astrbot.api import sp, star @@ -267,14 +267,27 @@ async def stats(self, message: AstrMessageEvent) -> None: db = self.context.get_db() async with db.get_db() as session: result = await session.execute( - select(ProviderStat).where( + 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, ) ) - records = result.scalars().all() + stats = result.one() - if not records: + if stats.record_count == 0: message.set_result( MessageEventResult().message( "📊 No stats available for this conversation yet." @@ -282,9 +295,9 @@ async def stats(self, message: AstrMessageEvent) -> None: ) return - total_input_other = sum(r.token_input_other for r in records) - total_input_cached = sum(r.token_input_cached for r in records) - total_output = sum(r.token_output for r in records) + 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 = ( From 071f7b570175c60d4421e97fc935fe693c7b8ecc Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 12:58:19 +0800 Subject: [PATCH 5/6] feat: add cached input tokens display and update translations for clarity --- dashboard/src/components/chat/ChatMessageList.vue | 15 ++++++++++++++- dashboard/src/components/chat/MessageList.vue | 15 ++++++++++++++- .../src/i18n/locales/en-US/features/chat.json | 4 ++-- .../src/i18n/locales/ru-RU/features/chat.json | 4 ++-- .../src/i18n/locales/zh-CN/features/chat.json | 4 ++-- 5 files changed, 34 insertions(+), 8 deletions(-) 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": "首字时间" }, From 7d3a09f3dbb7ee123fb9ef3cbe649f91b2acffb0 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 12:59:04 +0800 Subject: [PATCH 6/6] feat: update stats command to clarify conversation token usage display --- astrbot/builtin_stars/builtin_commands/commands/conversation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/builtin_stars/builtin_commands/commands/conversation.py b/astrbot/builtin_stars/builtin_commands/commands/conversation.py index 9489743038..9dcf369096 100644 --- a/astrbot/builtin_stars/builtin_commands/commands/conversation.py +++ b/astrbot/builtin_stars/builtin_commands/commands/conversation.py @@ -301,7 +301,7 @@ async def stats(self, message: AstrMessageEvent) -> None: total_tokens = total_input_other + total_input_cached + total_output ret = ( - f"📊 Token usage (ID: {cid[:8]}...)\n" + 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"