Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions astrbot/builtin_stars/builtin_commands/commands/conversation.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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))
5 changes: 5 additions & 0 deletions astrbot/builtin_stars/builtin_commands/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
15 changes: 14 additions & 1 deletion dashboard/src/components/chat/ChatMessageList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,15 @@
/>
</template>
<v-card class="stats-card" elevation="4">
<div
v-if="cachedInputTokens(messageContent(msg).agentStats) > 0"
class="stats-row"
>
<span>{{ tm("stats.cachedTokens") }}</span>
<strong>{{
cachedInputTokens(messageContent(msg).agentStats)
}}</strong>
</div>
<div class="stats-row">
<span>{{ tm("stats.inputTokens") }}</span>
<strong>{{ inputTokens(messageContent(msg).agentStats) }}</strong>
Expand Down Expand Up @@ -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",
Expand Down
15 changes: 14 additions & 1 deletion dashboard/src/components/chat/MessageList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@
/>
</template>
<v-card class="stats-card" elevation="4">
<div
v-if="cachedInputTokens(messageContent(msg).agentStats) > 0"
class="stats-row"
>
<span>{{ tm("stats.cachedTokens") }}</span>
<strong>{{
cachedInputTokens(messageContent(msg).agentStats)
}}</strong>
</div>
<div class="stats-row">
<span>{{ tm("stats.inputTokens") }}</span>
<strong>{{
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions dashboard/src/i18n/locales/en-US/features/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
4 changes: 2 additions & 2 deletions dashboard/src/i18n/locales/ru-RU/features/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@
},
"stats": {
"tokens": "Токены",
"inputTokens": "Входящие",
"inputTokens": "Входящие (прочие)",
"outputTokens": "Исходящие",
"cachedTokens": "Кэшированные",
"cachedTokens": "Входящие (кэш)",
"duration": "Время",
"ttft": "Время до первого токена"
},
Expand Down
4 changes: 2 additions & 2 deletions dashboard/src/i18n/locales/zh-CN/features/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@
},
"stats": {
"tokens": "Token",
"inputTokens": "输入 Token",
"inputTokens": "输入(其他)",
"outputTokens": "输出 Token",
"cachedTokens": "缓存 Token",
"cachedTokens": "输入(缓存)",
"duration": "耗时",
"ttft": "首字时间"
},
Expand Down
Loading