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/astrbot/cli/__init__.py b/astrbot/cli/__init__.py index d996423cd8..aebc801bf0 100644 --- a/astrbot/cli/__init__.py +++ b/astrbot/cli/__init__.py @@ -1 +1 @@ -__version__ = "4.23.5" +__version__ = "4.23.6" diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index cf70b41504..d96a4c92cb 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -183,10 +183,10 @@ async def _complete_with_assistant_response(self, llm_resp: LLMResponse) -> None self.stats.end_time = time.time() parts = [] - if llm_resp.reasoning_content or llm_resp.reasoning_signature: + if llm_resp.reasoning_content is not None or llm_resp.reasoning_signature: parts.append( ThinkPart( - think=llm_resp.reasoning_content, + think=llm_resp.reasoning_content or "", encrypted=llm_resp.reasoning_signature, ) ) @@ -876,10 +876,10 @@ async def step(self): # 将结果添加到上下文中 parts = [] - if llm_resp.reasoning_content or llm_resp.reasoning_signature: + if llm_resp.reasoning_content is not None or llm_resp.reasoning_signature: parts.append( ThinkPart( - think=llm_resp.reasoning_content, + think=llm_resp.reasoning_content or "", encrypted=llm_resp.reasoning_signature, ) ) @@ -1361,10 +1361,10 @@ async def _finalize_aborted_step( self.stats.end_time = time.time() parts = [] - if llm_resp.reasoning_content or llm_resp.reasoning_signature: + if llm_resp.reasoning_content is not None or llm_resp.reasoning_signature: parts.append( ThinkPart( - think=llm_resp.reasoning_content, + think=llm_resp.reasoning_content or "", encrypted=llm_resp.reasoning_signature, ) ) diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index cd1c81a888..9b27d482e9 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -5,7 +5,7 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path -VERSION = "4.23.5" +VERSION = "4.23.6" DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db") PERSONAL_WECHAT_CONFIG_METADATA = { "weixin_oc_base_url": { diff --git a/astrbot/core/provider/entities.py b/astrbot/core/provider/entities.py index d4bf8814d9..9b64196e7a 100644 --- a/astrbot/core/provider/entities.py +++ b/astrbot/core/provider/entities.py @@ -353,7 +353,7 @@ class LLMResponse: """Tool call IDs.""" tools_call_extra_content: dict[str, dict[str, Any]] = field(default_factory=dict) """Tool call extra content. tool_call_id -> extra_content dict""" - reasoning_content: str = "" + reasoning_content: str | None = None """The reasoning content extracted from the LLM, if any.""" reasoning_signature: str | None = None """The signature of the reasoning content, if any.""" @@ -404,8 +404,6 @@ def __init__( raw_completion (ChatCompletion, optional): 原始响应, OpenAI 格式. Defaults to None. """ - if reasoning_content is None: - reasoning_content = "" if tools_call_args is None: tools_call_args = [] if tools_call_name is None: diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index d2fce17ded..87e1b0284a 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -39,7 +39,7 @@ def _ensure_usable_response( stop_reason: str | None = None, ) -> None: has_text_output = bool((llm_response.completion_text or "").strip()) - has_reasoning_output = bool(llm_response.reasoning_content.strip()) + has_reasoning_output = bool((llm_response.reasoning_content or "").strip()) has_tool_output = bool(llm_response.tools_call_args) if has_text_output or has_reasoning_output or has_tool_output: return diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index 158170d516..a942c56e4a 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -462,7 +462,7 @@ def _ensure_usable_response( finish_reason: str | None = None, ) -> None: has_text_output = bool((llm_response.completion_text or "").strip()) - has_reasoning_output = bool(llm_response.reasoning_content.strip()) + has_reasoning_output = bool((llm_response.reasoning_content or "").strip()) has_tool_output = bool(llm_response.tools_call_args) if has_text_output or has_reasoning_output or has_tool_output: return diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index c1191a2dd3..512e47233a 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -671,9 +671,9 @@ async def _query_stream( reasoning = self._extract_reasoning_content(chunk) _y = False llm_response.id = chunk.id - llm_response.reasoning_content = "" + llm_response.reasoning_content = None llm_response.completion_text = "" - if reasoning: + if reasoning is not None: llm_response.reasoning_content = reasoning _y = True if delta and delta.content: @@ -701,22 +701,28 @@ async def _query_stream( def _extract_reasoning_content( self, completion: ChatCompletion | ChatCompletionChunk, - ) -> str: + ) -> str | None: """Extract reasoning content from OpenAI ChatCompletion if available.""" - reasoning_text = "" + + def _get_reasoning_attr(obj: Any) -> str | None: + fields_set = getattr(obj, "model_fields_set", None) + if isinstance(fields_set, set) and self.reasoning_key in fields_set: + attr = getattr(obj, self.reasoning_key, "") + return "" if attr is None else str(attr) + attr = getattr(obj, self.reasoning_key, None) + return None if attr is None else str(attr) + if not completion.choices: - return reasoning_text + return None if isinstance(completion, ChatCompletion): choice = completion.choices[0] - reasoning_attr = getattr(choice.message, self.reasoning_key, None) - if reasoning_attr: - reasoning_text = str(reasoning_attr) + reasoning_attr = _get_reasoning_attr(choice.message) elif isinstance(completion, ChatCompletionChunk): delta = completion.choices[0].delta - reasoning_attr = getattr(delta, self.reasoning_key, None) - if reasoning_attr: - reasoning_text = str(reasoning_attr) - return reasoning_text + reasoning_attr = _get_reasoning_attr(delta) + else: + return None + return reasoning_attr def _extract_usage(self, usage: CompletionUsage | dict) -> TokenUsage: ptd = getattr(usage, "prompt_tokens_details", None) @@ -859,7 +865,9 @@ async def _parse_openai_completion( # parse the reasoning content if any # the priority is higher than the tag extraction - llm_response.reasoning_content = self._extract_reasoning_content(completion) + reasoning_content = self._extract_reasoning_content(completion) + if reasoning_content is not None: + llm_response.reasoning_content = reasoning_content # parse tool calls if any if choice.message.tool_calls and tools is not None: @@ -906,7 +914,7 @@ async def _parse_openai_completion( "API 返回的 completion 由于内容安全过滤被拒绝(非 AstrBot)。", ) has_text_output = bool((llm_response.completion_text or "").strip()) - has_reasoning_output = bool(llm_response.reasoning_content.strip()) + has_reasoning_output = bool((llm_response.reasoning_content or "").strip()) if ( not has_text_output and not has_reasoning_output @@ -987,31 +995,34 @@ def _finally_convert_payload(self, payloads: dict) -> None: model in deepseek_reasoning_models or "api.deepseek.com" in self.client.base_url.host ) - for message in payloads.get("messages", []): if message.get("role") == "assistant" and isinstance( message.get("content"), list ): reasoning_content = "" + reasoning_content_present = False new_content = [] # not including think part for part in message["content"]: if part.get("type") == "think": + reasoning_content_present = True reasoning_content += str(part.get("think")) else: new_content.append(part) # Some providers (Grok, etc.) reject empty content lists. # When all parts were think blocks, fall back to None. message["content"] = new_content or None - if is_deepseek_v4_reasoning and not reasoning_content: - logger.info( - "Deepseek v4 model requires non-empty reasoning content, but got empty. Setting to 'none' to satisfy the requirement." - ) - # Deepseek models require the field on assistant - # history messages, even when the reasoning content is empty. - message["reasoning_content"] = "none" - elif reasoning_content: + if reasoning_content_present: message["reasoning_content"] = reasoning_content + if ( + message.get("role") == "assistant" + and is_deepseek_v4_reasoning + and "reasoning_content" not in message + ): + # DeepSeek v4 reasoning models require the field on assistant + # history messages, even when the reasoning content is empty. + message["reasoning_content"] = "" + # Gemini 的 function_response 要求 google.protobuf.Struct(即 JSON 对象), # 纯文本会触发 400 Invalid argument,需要包一层 JSON。 if is_gemini and message.get("role") == "tool": diff --git a/astrbot/dashboard/routes/stat.py b/astrbot/dashboard/routes/stat.py index 2eb3cd400e..b02091d5d4 100644 --- a/astrbot/dashboard/routes/stat.py +++ b/astrbot/dashboard/routes/stat.py @@ -243,6 +243,7 @@ async def get_provider_token_stats(self): total_by_umo: dict[str, int] = defaultdict(int) total_by_bucket: dict[int, int] = defaultdict(int) range_total_tokens = 0 + range_total_output_tokens = 0 range_total_calls = 0 range_success_calls = 0 range_ttft_total_ms = 0.0 @@ -286,6 +287,7 @@ async def get_provider_token_stats(self): record.end_time - record.start_time ) * 1000 range_duration_samples += 1 + range_total_output_tokens += record.token_output if created_at_local >= today_start_local: today_total_calls += 1 @@ -371,7 +373,8 @@ async def get_provider_token_stats(self): else 0 ), "range_avg_tpm": ( - range_total_tokens / (range_duration_total_ms / 1000 / 60) + range_total_output_tokens + / (range_duration_total_ms / 1000 / 60) if range_duration_total_ms > 0 else 0 ), diff --git a/changelogs/v4.23.6.md b/changelogs/v4.23.6.md new file mode 100644 index 0000000000..f02e7e7af7 --- /dev/null +++ b/changelogs/v4.23.6.md @@ -0,0 +1,56 @@ +- [更新日志(简体中文)](#chinese) +- [Changelog(English)](#english) + + + +## What's Changed + +### 新增 + +- 新增 `/stats` 命令,可查看当前会话的 Token 使用统计,并按总量、输入(缓存)、输入(其他)与输出拆分展示。([#7831](https://github.com/AstrBotDevs/AstrBot/pull/7831)) +- 新增 Firecrawl Web 搜索与网页提取工具,支持搜索结果处理、网页内容提取、会话管理、请求校验与相关测试。([#7764](https://github.com/AstrBotDevs/AstrBot/pull/7764)) +- 微信客服文本消息新增 15 秒内去重,减少重复消息处理。([#7788](https://github.com/AstrBotDevs/AstrBot/pull/7788)) + +### 优化 + +- 优化 Provider 配置界面性能与响应式显示,改善相关组件的字体和布局体验。([#7772](https://github.com/AstrBotDevs/AstrBot/pull/7772)) +- 优化统计页 TPM 计算逻辑,TPM 现在仅统计输出 Token,并更新相关文案。([#7827](https://github.com/AstrBotDevs/AstrBot/pull/7827)) +- 优化 OpenAI 兼容 Provider 的空 assistant 消息过滤逻辑,流式与非流式路径统一处理空字符串和空列表内容,避免严格 Provider 拒绝历史消息。([#7758](https://github.com/AstrBotDevs/AstrBot/pull/7758)) + +### 修复 + +- 修复 DeepSeek v4 与 reasoning content 相关处理,支持空字符串 reasoning 内容,并在 assistant 消息中保留 reasoning 字段。([#7823](https://github.com/AstrBotDevs/AstrBot/pull/7823), [#7830](https://github.com/AstrBotDevs/AstrBot/pull/7830)) +- 修复 OpenRouter reasoning 字段属性名不正确的问题。([#7821](https://github.com/AstrBotDevs/AstrBot/pull/7821)) +- 修复超大图片未压缩可能导致后续处理异常的问题,并复用图片最大尺寸检查工具。([#7807](https://github.com/AstrBotDevs/AstrBot/pull/7807)) +- 修复 MiniMax TTS 默认输出 MP3 导致 QQ 官方平台语音转换出现 RIFF 错误的问题,默认输出格式改为 WAV。([#7797](https://github.com/AstrBotDevs/AstrBot/pull/7797)) +- 修复 Computer 沙盒图片下载未按图片发送的问题。([#7785](https://github.com/AstrBotDevs/AstrBot/pull/7785)) +- 修复 Windows 环境下部分 HTTPS 请求证书校验失败的问题,使用 certifi SSL context 提升兼容性。([#7778](https://github.com/AstrBotDevs/AstrBot/pull/7778)) +- 修复非安全上下文或部分对话框中复制功能不可用的问题,抽取共享剪贴板工具并增加 fallback。([#7747](https://github.com/AstrBotDevs/AstrBot/pull/7747)) +- 修复文件上传可能存在路径穿越的问题,并清理上传文件名中的 NUL 字节。([#7751](https://github.com/AstrBotDevs/AstrBot/pull/7751)) + + + +## What's Changed (EN) + +### New Features + +- Added a `/stats` command to show token usage for the current conversation, including total tokens, input cached tokens, input other tokens, and output tokens. ([#7831](https://github.com/AstrBotDevs/AstrBot/pull/7831)) +- Added Firecrawl web search and web extract tools with result handling, content extraction, session management, payload validation, and tests. ([#7764](https://github.com/AstrBotDevs/AstrBot/pull/7764)) +- Added 15-second deduplication for WeChat kefu text messages to reduce duplicate message handling. ([#7788](https://github.com/AstrBotDevs/AstrBot/pull/7788)) + +### Improvements + +- Improved the Provider configuration UI performance and responsive layout, including font and component styling updates. ([#7772](https://github.com/AstrBotDevs/AstrBot/pull/7772)) +- Updated stats-page TPM calculation so TPM only counts output tokens, with matching label updates. ([#7827](https://github.com/AstrBotDevs/AstrBot/pull/7827)) +- Improved empty assistant message filtering for OpenAI-compatible providers by sharing the logic across streaming and non-streaming paths and handling empty string or empty list content. ([#7758](https://github.com/AstrBotDevs/AstrBot/pull/7758)) + +### Bug Fixes + +- Fixed DeepSeek v4 and reasoning content handling by supporting empty-string reasoning content and preserving the reasoning field in assistant messages. ([#7823](https://github.com/AstrBotDevs/AstrBot/pull/7823), [#7830](https://github.com/AstrBotDevs/AstrBot/pull/7830)) +- Fixed the reasoning field attribute used for OpenRouter. ([#7821](https://github.com/AstrBotDevs/AstrBot/pull/7821)) +- Fixed oversized image handling by downscaling large images and sharing the image max-size check helper. ([#7807](https://github.com/AstrBotDevs/AstrBot/pull/7807)) +- Fixed MiniMax TTS output for QQ Official voice conversion by changing the default output format from MP3 to WAV. ([#7797](https://github.com/AstrBotDevs/AstrBot/pull/7797)) +- Fixed Computer sandbox image downloads so they are sent as images. ([#7785](https://github.com/AstrBotDevs/AstrBot/pull/7785)) +- Fixed HTTPS certificate verification issues on Windows by using a certifi SSL context. ([#7778](https://github.com/AstrBotDevs/AstrBot/pull/7778)) +- Fixed copy actions in insecure contexts and dialogs by extracting a shared clipboard utility with fallback behavior. ([#7747](https://github.com/AstrBotDevs/AstrBot/pull/7747)) +- Fixed path traversal risks in file uploads and removed embedded NUL bytes from upload filenames. ([#7751](https://github.com/AstrBotDevs/AstrBot/pull/7751)) 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/en-US/features/stats.json b/dashboard/src/i18n/locales/en-US/features/stats.json index b6349011ee..18c2e28132 100644 --- a/dashboard/src/i18n/locales/en-US/features/stats.json +++ b/dashboard/src/i18n/locales/en-US/features/stats.json @@ -67,7 +67,7 @@ "callCount": "{count} calls", "avgTtft": "Average TTFT", "avgDuration": "Average Response Time", - "avgTpm": "Average TPM", + "avgTpm": "Average Output TPM", "successRate": "Success Rate" }, "modelRanking": { 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/ru-RU/features/stats.json b/dashboard/src/i18n/locales/ru-RU/features/stats.json index 45cc3ed6c4..572ac35859 100644 --- a/dashboard/src/i18n/locales/ru-RU/features/stats.json +++ b/dashboard/src/i18n/locales/ru-RU/features/stats.json @@ -67,7 +67,7 @@ "callCount": "{count} вызовов", "avgTtft": "Средний TTFT", "avgDuration": "Среднее время ответа", - "avgTpm": "Средний TPM", + "avgTpm": "Средний Output TPM", "successRate": "Доля успешных вызовов" }, "modelRanking": { 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": "首字时间" }, diff --git a/dashboard/src/i18n/locales/zh-CN/features/stats.json b/dashboard/src/i18n/locales/zh-CN/features/stats.json index 64c8bb84a2..6dc717511b 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/stats.json +++ b/dashboard/src/i18n/locales/zh-CN/features/stats.json @@ -67,7 +67,7 @@ "callCount": "共 {count} 次调用", "avgTtft": "平均首字延迟(TTFT)", "avgDuration": "平均响应时间", - "avgTpm": "平均每分钟词元数(TPM)", + "avgTpm": "平均每分钟输出(TPM)", "successRate": "调用成功率" }, "modelRanking": { diff --git a/pyproject.toml b/pyproject.toml index 2cdfbc44e0..8ee8ed6199 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "AstrBot" -version = "4.23.5" +version = "4.23.6" description = "Easy-to-use multi-platform LLM chatbot and development framework" readme = "README.md" license = { text = "AGPL-3.0-or-later" }