背景
目前 /model 命令已经支持在会话中切换模型了,但 token 统计那一层完全没有跟上——所有模型的 usage 都混在一个桶里,导致退出时的 summary 和状态栏展示的数据都有问题。
具体问题
1. SessionEntry.usage 是「一锅粥」
所有 API 调用返回的 usage(prompt_tokens / completion_tokens / cached_tokens / total_tokens)通过 accumulateUsage 递归 deep-merge 到一起,不区分模型。
一个 session 里先用 deepseek-v4-pro 聊了几轮,然后切到 deepseek-v4-flash 继续——最后 usage 全部混在一起,根本没法分清哪个模型消耗了多少。
2. Exit Summary 展示的模型名可能不对
exitSummary.ts 里拿的是 resolveCurrentSettings().model(当前配置的模型),但 usage 数据是所有模型的累计。如果用户中途切换过模型,退出的表格会显示「Model Usage: deepseek-v4-flash」但数字里包含了之前的 pro 消耗。
3. activeTokens 语义混乱
SessionEntry.activeTokens 存的是最后一次 API 调用的 total_tokens,不是会话级累计,也不是真正的「当前活跃 token 量」。但它被用在:
- 状态栏
buildStatusLine 的展示,用户以为这是累计消耗
- Compaction 阈值比较 (
getCompactPromptTokenThreshold(model)),但阈值取的又是当前模型而不是历史模型
如果从 V4(512K 窗口)切到非 V4(128K 窗口),阈值骤降,但 activeTokens 可能因为刚切模型第一轮调用很短而偏小——compaction 根本不会触发,实际上下文可能已经爆了。
4. 成本完全没法算
不同模型价格不一样,也没有做 per-model 的 caching token 拆分,用户想知道「这个 session 大概花了多少钱」完全不可能。
大致修复思路
核心是把 SessionEntry.usage 从单值改成 per-model 的映射:
// 现状
usage: unknown | null;
// 改成
usage: Record<string, ModelUsage> | null; // key = model name
accumulateUsage 增加 model 参数,调用处把 createOpenAIClient() 返回的 model 传进去。Exit summary 的表格改成多行(每个模型一行),最下面加个汇总行。
activeTokens 的逻辑也要重新想——要么直接砍掉改为基于实际消息量的估算,要么也拆成 per-model。
Compaction 那块,getCompactPromptTokenThreshold 不应该用「当前激活的模型」,应该基于实际上下文消息量来判断。
建议 PR 拆分
PR1 — 数据结构改造
SessionEntry.usage 从 unknown → Record<string, ModelUsage>
accumulateUsage 签名加上 model 参数
- 所有调用处传 model(activateSession / compactSession)
PR2 — 前端的展示修复
- Exit Summary 表格支持多行 per-model 展示 + 汇总行
buildStatusLine 的 token 展示修正(展示累计 total_tokens 而非最后一次的值)
- 确保模型切换消息(
isModelChange: true)里也带当时的模型名
PR3 — activeTokens 和 compaction 修正
- 重新定义
activeTokens 的语义(改为真正的会话级估算,或基于消息内容估算)
getCompactPromptTokenThreshold 基于实际会话模型历史判断,或保守取最小值
PR4(可选)— 成本估算
- 如果后续有价格数据源,可以基于 per-model usage 做成本展示
讨论点
@qorzj 想确认几个事情:
usage 的 per-model 拆分方向你觉得 OK 吗?还是有更简单的中间方案?
activeTokens 你更倾向于「砍掉 + 用消息量估算替代」还是「保留 + 改成 per-model 累计」?
- PR 拆分粒度合不合适?要不要 PR1+PR2 一起做?
分析来自 session.ts / exitSummary.ts / App.tsx / settings.ts 的 token 统计链路。
背景
目前
/model命令已经支持在会话中切换模型了,但 token 统计那一层完全没有跟上——所有模型的 usage 都混在一个桶里,导致退出时的 summary 和状态栏展示的数据都有问题。具体问题
1. SessionEntry.usage 是「一锅粥」
所有 API 调用返回的
usage(prompt_tokens / completion_tokens / cached_tokens / total_tokens)通过accumulateUsage递归 deep-merge 到一起,不区分模型。一个 session 里先用
deepseek-v4-pro聊了几轮,然后切到deepseek-v4-flash继续——最后 usage 全部混在一起,根本没法分清哪个模型消耗了多少。2. Exit Summary 展示的模型名可能不对
exitSummary.ts里拿的是resolveCurrentSettings().model(当前配置的模型),但 usage 数据是所有模型的累计。如果用户中途切换过模型,退出的表格会显示「Model Usage: deepseek-v4-flash」但数字里包含了之前的 pro 消耗。3. activeTokens 语义混乱
SessionEntry.activeTokens存的是最后一次 API 调用的total_tokens,不是会话级累计,也不是真正的「当前活跃 token 量」。但它被用在:buildStatusLine的展示,用户以为这是累计消耗getCompactPromptTokenThreshold(model)),但阈值取的又是当前模型而不是历史模型如果从 V4(512K 窗口)切到非 V4(128K 窗口),阈值骤降,但 activeTokens 可能因为刚切模型第一轮调用很短而偏小——compaction 根本不会触发,实际上下文可能已经爆了。
4. 成本完全没法算
不同模型价格不一样,也没有做 per-model 的 caching token 拆分,用户想知道「这个 session 大概花了多少钱」完全不可能。
大致修复思路
核心是把
SessionEntry.usage从单值改成 per-model 的映射:accumulateUsage增加 model 参数,调用处把createOpenAIClient()返回的 model 传进去。Exit summary 的表格改成多行(每个模型一行),最下面加个汇总行。activeTokens的逻辑也要重新想——要么直接砍掉改为基于实际消息量的估算,要么也拆成 per-model。Compaction 那块,
getCompactPromptTokenThreshold不应该用「当前激活的模型」,应该基于实际上下文消息量来判断。建议 PR 拆分
PR1 — 数据结构改造
SessionEntry.usage从unknown→Record<string, ModelUsage>accumulateUsage签名加上 model 参数PR2 — 前端的展示修复
buildStatusLine的 token 展示修正(展示累计 total_tokens 而非最后一次的值)isModelChange: true)里也带当时的模型名PR3 — activeTokens 和 compaction 修正
activeTokens的语义(改为真正的会话级估算,或基于消息内容估算)getCompactPromptTokenThreshold基于实际会话模型历史判断,或保守取最小值PR4(可选)— 成本估算
讨论点
@qorzj 想确认几个事情:
usage的 per-model 拆分方向你觉得 OK 吗?还是有更简单的中间方案?activeTokens你更倾向于「砍掉 + 用消息量估算替代」还是「保留 + 改成 per-model 累计」?分析来自 session.ts / exitSummary.ts / App.tsx / settings.ts 的 token 统计链路。