token-tracker 0.4.0__tar.gz → 0.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {token_tracker-0.4.0/src/token_tracker.egg-info → token_tracker-0.4.2}/PKG-INFO +14 -11
  2. {token_tracker-0.4.0 → token_tracker-0.4.2}/README.md +13 -10
  3. {token_tracker-0.4.0 → token_tracker-0.4.2}/pyproject.toml +1 -1
  4. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/analyzer/cost.py +83 -0
  5. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/cli.py +19 -2
  6. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/config.py +20 -0
  7. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/hooks.py +4 -0
  8. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/i18n.py +2 -6
  9. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/format.py +43 -0
  10. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/heatmap.py +75 -32
  11. {token_tracker-0.4.0 → token_tracker-0.4.2/src/token_tracker.egg-info}/PKG-INFO +14 -11
  12. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_cost.py +118 -0
  13. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_hooks.py +78 -0
  14. {token_tracker-0.4.0 → token_tracker-0.4.2}/LICENSE +0 -0
  15. {token_tracker-0.4.0 → token_tracker-0.4.2}/setup.cfg +0 -0
  16. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/__init__.py +0 -0
  17. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/__init__.py +0 -0
  18. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/claude.py +0 -0
  19. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/codex.py +0 -0
  20. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/rate_limits.py +0 -0
  21. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/registry.py +0 -0
  22. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/types.py +0 -0
  23. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/util.py +0 -0
  24. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/analyzer/__init__.py +0 -0
  25. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/analyzer/aggregator.py +0 -0
  26. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/analyzer/blocks.py +0 -0
  27. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/__init__.py +0 -0
  28. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/console.py +0 -0
  29. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/panels.py +0 -0
  30. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/status.py +0 -0
  31. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/tables.py +0 -0
  32. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/theme.py +0 -0
  33. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/themes.py +0 -0
  34. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/wizard.py +0 -0
  35. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker.egg-info/SOURCES.txt +0 -0
  36. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker.egg-info/dependency_links.txt +0 -0
  37. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker.egg-info/entry_points.txt +0 -0
  38. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker.egg-info/requires.txt +0 -0
  39. {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker.egg-info/top_level.txt +0 -0
  40. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_aggregator.py +0 -0
  41. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_blocks.py +0 -0
  42. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_cli.py +0 -0
  43. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_codex.py +0 -0
  44. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_heatmap.py +0 -0
  45. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_status.py +0 -0
  46. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_tables.py +0 -0
  47. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_theme.py +0 -0
  48. {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: token-tracker
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Track token usage across local AI agents (Claude Code, Codex)
5
5
  Author-email: stormzhang <stormzhang.dev@gmail.com>
6
6
  License: MIT License
@@ -99,14 +99,18 @@ Dynamic: license-file
99
99
 
100
100
  通过 `Stop` hook 注入 `systemMessage` 实现,渲染 24-bit 真彩色、**不进模型上下文**(实测),**配色跟随当前主题**(与 CLI 报表 / CC 状态栏同源,`tt theme` 切换三者一起变)。`tt unsetup` 一并移除。
101
101
 
102
- ## Status 实时面板和 日/周/月 数据报表分析
102
+ ## Daily 概览和 日/周/月 数据报表分析
103
103
 
104
- `tt`(无参)/ `tt status`:聚焦**过去 5 小时**的实时面板——顶部多 Agent **合并**概览(Token / Cost / Sessions / Messages / Top Model),中间 **5h / 7d 订阅额度**进度条(Claude Code / Codex 分开;都没订阅额度时换成 per-agent 的 token/cost/sessions/messages 统计),底部**近期会话**列表(CC + Codex 合并、带 Agent 列、按 Cost 倒序、Cost 前三名高亮)。所有时间按**系统时区**显示,配色跟随当前主题。
104
+ `tt`(无参)/ `tt daily`:默认入口,GitHub 风格 token 贡献热力图 + 顶部**单一卡片**内拼三段概览(**Last 12 months / This Month / This Week**,粗→细)。
105
+ - Last 12 months:橙色 Tokens / Cost / Sessions / Avg/Cost / 活跃天数 + 蓝色 单日峰值 / 当前·最长连续活跃天数
106
+ - This Month / This Week:橙色 Tokens / Cost / Avg/Cost / 活跃天数,**带环比**(↑/↓ 上月 / 上周)
105
107
 
106
- ![Token Tracker Status](assets/screenshot.png)
108
+ `tt status`:聚焦**过去 5 小时**的实时面板——顶部多 Agent **合并**概览(Token / Cost / Sessions / Messages / Top Model),中间 **5h / 7d 订阅额度**进度条(Claude Code / Codex 分开;都没订阅额度时换成 per-agent 的 token/cost/sessions/messages 统计),底部**近期会话**列表(CC + Codex 合并、带 Agent 列、按 Cost 倒序、Cost 前三名高亮)。所有时间按**系统时区**显示,配色跟随当前主题。
107
109
 
108
110
  ![Token Tracker Daily](assets/screenshot-daily.png)
109
111
 
112
+ ![Token Tracker Status](assets/screenshot.png)
113
+
110
114
  ![Token Tracker Weekly](assets/screenshot-weekly.png)
111
115
 
112
116
  ![Token Tracker Monthly](assets/screenshot-monthly.png)
@@ -129,19 +133,18 @@ Dynamic: license-file
129
133
  curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/install.sh | bash
130
134
  ```
131
135
 
132
- 或者通过 pip:
136
+ 脚本自动选最优安装方式(uv / pipx / 私有 venv),绕开 PEP 668、不污染系统 Python。
133
137
 
134
- ```bash
135
- pip install --force-reinstall token-tracker && tt setup
136
- ```
138
+ > **升级**:重跑上面的命令即可(脚本幂等、自动升到最新)。
139
+ > **卸载**:先 `tt unsetup` 还原状态栏,再按装法移除(`uv tool uninstall token-tracker` / `pipx uninstall token-tracker` / 删 `~/.local/share/token-tracker` 与 `~/.local/bin/tt`)。
137
140
 
138
141
  ## 使用
139
142
 
140
143
  ```bash
141
144
  tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty 环境自动全装
142
- tt # 过去 5h 实时面板(合并概览 + 5h/7d 额度 + 近期会话,= tt status
143
- tt status # 同上(tt 无参即进 status
144
- tt daily # 过去一年 token 贡献热力图(GitHub 风格)+ 年度分析卡片
145
+ tt # 过去一年 token 热力图 + 顶部三段概览(Last 12 months / This Month / This Week,= tt daily
146
+ tt daily # 同上(tt 无参即进 daily
147
+ tt status # 过去 5h 实时面板(合并概览 + 5h/7d 额度 + 近期会话)
145
148
  tt weekly # 周报:本周分析卡片 + 每日趋势柱状图 + 周 / 项目 / 模型趋势
146
149
  tt monthly # 按月汇总(多 Agent 分组展示)
147
150
  tt sessions # 最近 20 条会话明细(按 cost 倒序展示;tt sessions <n> 改条数、--sort 改排序)
@@ -46,14 +46,18 @@
46
46
 
47
47
  通过 `Stop` hook 注入 `systemMessage` 实现,渲染 24-bit 真彩色、**不进模型上下文**(实测),**配色跟随当前主题**(与 CLI 报表 / CC 状态栏同源,`tt theme` 切换三者一起变)。`tt unsetup` 一并移除。
48
48
 
49
- ## Status 实时面板和 日/周/月 数据报表分析
49
+ ## Daily 概览和 日/周/月 数据报表分析
50
50
 
51
- `tt`(无参)/ `tt status`:聚焦**过去 5 小时**的实时面板——顶部多 Agent **合并**概览(Token / Cost / Sessions / Messages / Top Model),中间 **5h / 7d 订阅额度**进度条(Claude Code / Codex 分开;都没订阅额度时换成 per-agent 的 token/cost/sessions/messages 统计),底部**近期会话**列表(CC + Codex 合并、带 Agent 列、按 Cost 倒序、Cost 前三名高亮)。所有时间按**系统时区**显示,配色跟随当前主题。
51
+ `tt`(无参)/ `tt daily`:默认入口,GitHub 风格 token 贡献热力图 + 顶部**单一卡片**内拼三段概览(**Last 12 months / This Month / This Week**,粗→细)。
52
+ - Last 12 months:橙色 Tokens / Cost / Sessions / Avg/Cost / 活跃天数 + 蓝色 单日峰值 / 当前·最长连续活跃天数
53
+ - This Month / This Week:橙色 Tokens / Cost / Avg/Cost / 活跃天数,**带环比**(↑/↓ 上月 / 上周)
52
54
 
53
- ![Token Tracker Status](assets/screenshot.png)
55
+ `tt status`:聚焦**过去 5 小时**的实时面板——顶部多 Agent **合并**概览(Token / Cost / Sessions / Messages / Top Model),中间 **5h / 7d 订阅额度**进度条(Claude Code / Codex 分开;都没订阅额度时换成 per-agent 的 token/cost/sessions/messages 统计),底部**近期会话**列表(CC + Codex 合并、带 Agent 列、按 Cost 倒序、Cost 前三名高亮)。所有时间按**系统时区**显示,配色跟随当前主题。
54
56
 
55
57
  ![Token Tracker Daily](assets/screenshot-daily.png)
56
58
 
59
+ ![Token Tracker Status](assets/screenshot.png)
60
+
57
61
  ![Token Tracker Weekly](assets/screenshot-weekly.png)
58
62
 
59
63
  ![Token Tracker Monthly](assets/screenshot-monthly.png)
@@ -76,19 +80,18 @@
76
80
  curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/install.sh | bash
77
81
  ```
78
82
 
79
- 或者通过 pip:
83
+ 脚本自动选最优安装方式(uv / pipx / 私有 venv),绕开 PEP 668、不污染系统 Python。
80
84
 
81
- ```bash
82
- pip install --force-reinstall token-tracker && tt setup
83
- ```
85
+ > **升级**:重跑上面的命令即可(脚本幂等、自动升到最新)。
86
+ > **卸载**:先 `tt unsetup` 还原状态栏,再按装法移除(`uv tool uninstall token-tracker` / `pipx uninstall token-tracker` / 删 `~/.local/share/token-tracker` 与 `~/.local/bin/tt`)。
84
87
 
85
88
  ## 使用
86
89
 
87
90
  ```bash
88
91
  tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty 环境自动全装
89
- tt # 过去 5h 实时面板(合并概览 + 5h/7d 额度 + 近期会话,= tt status
90
- tt status # 同上(tt 无参即进 status
91
- tt daily # 过去一年 token 贡献热力图(GitHub 风格)+ 年度分析卡片
92
+ tt # 过去一年 token 热力图 + 顶部三段概览(Last 12 months / This Month / This Week,= tt daily
93
+ tt daily # 同上(tt 无参即进 daily
94
+ tt status # 过去 5h 实时面板(合并概览 + 5h/7d 额度 + 近期会话)
92
95
  tt weekly # 周报:本周分析卡片 + 每日趋势柱状图 + 周 / 项目 / 模型趋势
93
96
  tt monthly # 按月汇总(多 Agent 分组展示)
94
97
  tt sessions # 最近 20 条会话明细(按 cost 倒序展示;tt sessions <n> 改条数、--sort 改排序)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "token-tracker"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "Track token usage across local AI agents (Claude Code, Codex)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -28,6 +28,21 @@ _FAMILY_FALLBACK = (
28
28
  ("claude-haiku", "claude-haiku-4-5-20251001"),
29
29
  ("claude-fable", "claude-fable-5"),
30
30
  ("codex-", "gpt-5.5"),
31
+ # 国产模型系列兜底:出新版本(如 GLM-4.8、Kimi K3)litellm 未收录时退回该系列最新已知价
32
+ ("kimi", "kimi-k2.6"),
33
+ ("moonshot-v", "moonshot-v1-128k"),
34
+ ("glm-4", "glm-4.6"),
35
+ ("qwen3-coder", "qwen3-coder-plus"),
36
+ ("qwen3-max", "qwen-max"),
37
+ ("doubao-seed", "doubao-seed-1-6"),
38
+ ("doubao-1-5-pro", "doubao-1-5-pro-256k"),
39
+ ("deepseek", "deepseek-v4-flash"),
40
+ ("minimax", "MiniMax-M2"),
41
+ ("mimo", "mimo-v2.5"),
42
+ # Grok:退役 slug 按官方路由兜底(grok-code-* → build-0.1;grok-4-fast/4.1-fast/grok-3 等 → grok-4.3)
43
+ ("grok-code", "grok-build-0.1"),
44
+ ("grok-4", "grok-4.3"),
45
+ ("grok", "grok-4.3"),
31
46
  )
32
47
 
33
48
  # 解析不到定价的模型只提示一次,避免聚合时每条 entry 刷屏
@@ -195,6 +210,31 @@ _FABLE_PRICING = {
195
210
  "cache_read_input_token_cost": 1.0e-6,
196
211
  }
197
212
 
213
+ # 国产模型多以人民币计价,统一折 USD 入表,与 CC/Codex 同口径(2026-06 近似汇率)
214
+ _CNY_PER_USD = 7.1
215
+
216
+
217
+ def _cny(input_m: float, output_m: float, cache_read_m: float | None = None) -> dict:
218
+ """人民币「元 / 百万 tokens」→ USD per token(÷汇率 ÷1e6)。国产模型按中国站人民币价折算。"""
219
+ info = {
220
+ "input_cost_per_token": input_m / _CNY_PER_USD * 1e-6,
221
+ "output_cost_per_token": output_m / _CNY_PER_USD * 1e-6,
222
+ }
223
+ if cache_read_m is not None:
224
+ info["cache_read_input_token_cost"] = cache_read_m / _CNY_PER_USD * 1e-6
225
+ return info
226
+
227
+
228
+ def _usd(input_m: float, output_m: float, cache_read_m: float | None = None) -> dict:
229
+ """美元「$ / 百万 tokens」→ USD per token(÷1e6)。用于只有官方国际站 USD 价的模型。"""
230
+ info = {
231
+ "input_cost_per_token": input_m * 1e-6,
232
+ "output_cost_per_token": output_m * 1e-6,
233
+ }
234
+ if cache_read_m is not None:
235
+ info["cache_read_input_token_cost"] = cache_read_m * 1e-6
236
+ return info
237
+
198
238
 
199
239
  def _fallback_pricing() -> dict:
200
240
  return {
@@ -249,4 +289,47 @@ def _fallback_pricing() -> dict:
249
289
  "output_cost_per_token": 6e-6,
250
290
  "cache_read_input_token_cost": 0.375e-6,
251
291
  },
292
+ # ---- 国产模型(2026-06 官方核实)。除 GLM 用 z.ai 国际站 USD 外,其余按各家中国站
293
+ # 人民币价 ÷7.1 折算;阶梯定价模型(Qwen3-Coder / Doubao)统一取 0-32K 基础档。----
294
+ # Kimi / Moonshot(platform.kimi.com 官方人民币价;老 kimi-k2-instruct 已 EOL,靠系列兜底)
295
+ "kimi-k2.7-code": _cny(6.5, 27, 1.3),
296
+ "kimi-k2.6": _cny(6.5, 27, 1.1),
297
+ "kimi-k2.5": _cny(4, 21, 0.7),
298
+ "moonshot-v1-8k": _cny(2, 10),
299
+ "moonshot-v1-32k": _cny(5, 20),
300
+ "moonshot-v1-128k": _cny(10, 30),
301
+ # 智谱 GLM(z.ai 国际站官方 USD;中国站按量完整价含缓存无法从官方 SPA 取得,国内口径可能偏高)
302
+ "glm-4.6": _usd(0.6, 2.2, 0.11),
303
+ "glm-4.5": _usd(0.6, 2.2, 0.11),
304
+ "glm-4.5-air": _usd(0.2, 1.1, 0.03),
305
+ "glm-4.7": _usd(0.6, 2.2, 0.11),
306
+ "glm-5": _usd(1.0, 3.2, 0.2),
307
+ # 阿里 Qwen(中国站百炼人民币价,0-32K 基础档)
308
+ "qwen3-coder-plus": _cny(4, 16, 0.4),
309
+ "qwen-max": _cny(2.5, 10),
310
+ "qwen-plus": _cny(0.8, 2),
311
+ # 火山方舟 Doubao(中国站人民币价,0-32K 基础档)
312
+ "doubao-seed-1-6": _cny(0.8, 8),
313
+ "doubao-seed-code": _cny(1.2, 8),
314
+ "doubao-1-5-pro-32k": _cny(0.8, 2, 0.16),
315
+ "doubao-1-5-pro-256k": _cny(5, 9),
316
+ # DeepSeek(官方中国站人民币价;chat/reasoner 现映射 V4-Flash,2026-07-24 弃用旧名)
317
+ "deepseek-v4-flash": _cny(1, 2, 0.02),
318
+ "deepseek-v4-pro": _cny(3, 6, 0.025),
319
+ "deepseek-chat": _cny(1, 2, 0.02),
320
+ "deepseek-reasoner": _cny(1, 2, 0.02),
321
+ # MiniMax(官方 USD,与中国站÷7 自洽;M2/M2.1/M2.5 同价 legacy)
322
+ "MiniMax-M2": _usd(0.3, 1.2, 0.03),
323
+ "MiniMax-M2.1": _usd(0.3, 1.2, 0.03),
324
+ "MiniMax-M2.5": _usd(0.3, 1.2, 0.03),
325
+ "MiniMax-M2.7": _usd(0.3, 1.2, 0.06),
326
+ "MiniMax-M3": _usd(0.3, 1.2, 0.06),
327
+ # 小米 MiMo(mimo.mi.com 官方中国站人民币价;与 DeepSeek 同价,V2.5-Pro 主攻 agentic 编程)
328
+ "mimo-v2.5-pro": _cny(3, 6, 0.025),
329
+ "mimo-v2.5": _cny(1, 2, 0.02),
330
+ # xAI Grok(docs.x.ai 官方 USD)。2026-05-15 退役潮:grok-4-fast/4.1-fast/grok-3 路由到 grok-4.3,
331
+ # grok-code-fast-1 退役为 grok-build-0.1 别名(退役 slug 靠 _FAMILY_FALLBACK 接住)。grok-4.3 取 ≤200K 默认档。
332
+ "grok-4.3": _usd(1.25, 2.5, 0.2),
333
+ "grok-build-0.1": _usd(1.0, 2.0, 0.2),
334
+ "grok-code-fast-1": _usd(1.0, 2.0, 0.2),
252
335
  }
@@ -368,7 +368,7 @@ def main():
368
368
  get_console().print(f"[dim]{t('theme_options', names=', '.join(themes.THEME_NAMES))}[/dim]")
369
369
  sys.exit(1)
370
370
  theme.set_active_theme(theme_override)
371
- command = args[0] if args else "status"
371
+ command = args[0] if args else "daily"
372
372
 
373
373
  # 版本查询不该触发任何文件读写,放在 auto-update 之前短路返回
374
374
  if command in ("--version", "-v", "-V"):
@@ -385,6 +385,17 @@ def main():
385
385
  if command not in ("setup", "unsetup") and is_setup() and needs_update():
386
386
  update_hook()
387
387
 
388
+ # 升级感知:新版若新增了值得重配的选项(SETUP_VERSION bump),老用户跑任意命令时
389
+ # 自动走一遍 setup——_run_setup_flow 内部分流:真终端弹 wizard、会话内 / 非 tty 静默
390
+ # _auto_setup 用默认值全装(语言跟随系统 / mocha / 组件全开)。两者最终都 save_setup_version(),
391
+ # 下次启动 setup_version 已是最新、不再触发。
392
+ if (
393
+ command not in ("setup", "unsetup")
394
+ and is_setup()
395
+ and config.setup_version() < config.SETUP_VERSION
396
+ ):
397
+ _run_setup_flow()
398
+
388
399
  if command == "setup":
389
400
  _run_setup_flow()
390
401
  return
@@ -441,7 +452,13 @@ def main():
441
452
  return
442
453
 
443
454
  _apply_sort(stats, sort_key, sort_desc, default_attr, default_reverse)
444
- if command == "weekly":
455
+ if command == "daily":
456
+ # daily 顶部三卡片:Last 12 months + This Month + This Week,跟 weekly/monthly 同款样式、
457
+ # 复用 _render_month_summary / _render_week_summary。多算两份 weekly/monthly 聚合传进去。
458
+ render_fn(stats, agents=agent_names, # type: ignore[operator]
459
+ weekly=_aggregate_per_agent(report_agents, aggregate_weekly),
460
+ monthly=_aggregate_per_agent(report_agents, aggregate_monthly))
461
+ elif command == "weekly":
445
462
  render_weekly(stats, agents=agent_names, daily=_aggregate_per_agent(report_agents, aggregate_daily))
446
463
  elif command == "monthly":
447
464
  render_monthly(stats, agents=agent_names,
@@ -18,6 +18,12 @@ CONFIG_DIR = os.path.expanduser("~/.config/token-tracker")
18
18
  CONFIG_PATH = os.path.join(CONFIG_DIR, "config.json")
19
19
  SCHEMA_VERSION = 1
20
20
 
21
+ # 引导版本:每次新增"值得让老用户重新走一遍 setup"的配置项时手动 +1(只能整数、一次 +1)。
22
+ # 老用户 config 里没这字段 / 旧版本号 < 当前 → 触发重新引导(真终端弹 wizard、非 tty 静默 _auto_setup)。
23
+ # 跟 SCHEMA_VERSION 解耦:那是数据格式版本,这是用户引导版本,bump 节奏完全不同。
24
+ # 2(0.4.2):强制所有现存用户(0.3.8/0.4.0=无字段=0、0.4.1=1,全 < 2)升级后重走一遍 setup。
25
+ SETUP_VERSION = 2
26
+
21
27
  # 旧位置(独立 theme.json / lang.json),老用户首次读 config.json 不存在时自动合并迁移
22
28
  _LEGACY_THEME_PATH = os.path.join(CONFIG_DIR, "theme.json")
23
29
  _LEGACY_LANG_PATH = os.path.join(CONFIG_DIR, "lang.json")
@@ -149,3 +155,17 @@ def codex_faux_statusline_intent() -> bool | None:
149
155
  """读用户对 Codex 伪 statusline 的意图。严格 bool;非 bool / 缺字段 → None(视为没表达)。"""
150
156
  val = load_config().get("codex_faux_statusline")
151
157
  return val if isinstance(val, bool) else None
158
+
159
+
160
+ # --- setup 引导版本(老用户升级后重新引导一次的判定依据) ---
161
+
162
+
163
+ def save_setup_version(v: int = SETUP_VERSION) -> None:
164
+ """setup 真正完成后写入当前 SETUP_VERSION,标记"已按当前版本引导过"。"""
165
+ _save_field("setup_version", int(v))
166
+
167
+
168
+ def setup_version() -> int:
169
+ """读已落地的 setup_version;老用户 / 字段缺失 / 非 int → 0(触发重新引导)。"""
170
+ val = load_config().get("setup_version")
171
+ return val if isinstance(val, int) else 0
@@ -857,6 +857,10 @@ def setup(auto: bool = False, components: SetupComponents | None = None, quiet:
857
857
  if not auto:
858
858
  p(f"[dim]{t('codex_not_found')}[/dim]")
859
859
 
860
+ # setup 真正落地了,写入当前引导版本——后续启动 cli 不再触发"老用户重新引导"。
861
+ # early-return 分支(无 agent)不会到这,符合语义。
862
+ config.save_setup_version()
863
+
860
864
 
861
865
  def _migrate_cc_legacy_backup(settings: dict) -> None:
862
866
  """老用户的 statusLine 备份藏在 settings.json 的 `tokenTracker.previousStatusLine` 子字段——
@@ -70,8 +70,6 @@ _STRINGS = {
70
70
  "daily_peak": "峰值",
71
71
  "daily_streak": "连续/最长",
72
72
  "active_days": "活跃天数",
73
- "daily_busiest": "最忙",
74
- "weekday_full": "周一,周二,周三,周四,周五,周六,周日", # busiest 值(Mon 开头)
75
73
  "weekday_grid": "周日,周一,周二,周三,周四,周五,周六", # 热力图左侧行标签(周日开头)
76
74
  "month_short": "1月,2月,3月,4月,5月,6月,7月,8月,9月,10月,11月,12月", # 热力图月份表头
77
75
  "unit_day": "天", # 时长 / 连续天数单位
@@ -110,7 +108,7 @@ _STRINGS = {
110
108
  "cc_configured": "Claude Code statusLine 已配置",
111
109
  "restart_cc": "重启 Claude Code 后生效",
112
110
  "codex_configured": "Codex 已配置",
113
- "codex_statusline_hint": "已启用伪 statusline(每次回答后追加一行 5h/7d/Ctx,tt unsetup 可移除)",
111
+ "codex_statusline_hint": "已启用伪 statusline(每次回答后追加一行 5h/7d/Ctx",
114
112
  "restart_codex": "重启 Codex 后生效",
115
113
  "no_agent_detected": "未检测到 Claude Code 或 Codex",
116
114
  "deleted_file": "已删除: {path}",
@@ -189,8 +187,6 @@ _STRINGS = {
189
187
  "daily_peak": "Peak",
190
188
  "daily_streak": "Current/Longest Streak",
191
189
  "active_days": "Active Days",
192
- "daily_busiest": "Busiest",
193
- "weekday_full": "Mon,Tue,Wed,Thu,Fri,Sat,Sun", # busiest 值(Mon 开头)
194
190
  "weekday_grid": "Sun,Mon,Tue,Wed,Thu,Fri,Sat", # 热力图左侧行标签(Sun 开头)
195
191
  "month_short": "Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec", # 热力图月份表头
196
192
  "unit_day": "d", # 时长 / 连续天数单位
@@ -229,7 +225,7 @@ _STRINGS = {
229
225
  "cc_configured": "Claude Code statusLine configured",
230
226
  "restart_cc": "Restart Claude Code to take effect",
231
227
  "codex_configured": "Codex configured",
232
- "codex_statusline_hint": "Faux statusline enabled (appends 5h/7d/Ctx line after each turn, remove via tt unsetup)",
228
+ "codex_statusline_hint": "Faux statusline enabled (appends 5h/7d/Ctx line after each turn)",
233
229
  "restart_codex": "Restart Codex to take effect",
234
230
  "no_agent_detected": "Claude Code or Codex not detected",
235
231
  "deleted_file": "Deleted: {path}",
@@ -21,6 +21,49 @@ MODEL_SHORT = {
21
21
  "claude-sonnet": "Sonnet",
22
22
  "claude-haiku-4-5-20251001": "Haiku 4.5",
23
23
  "claude-haiku": "Haiku",
24
+ # 国产模型短名(与 cost.py 内置定价 key 一一对应)
25
+ "kimi-k2.7-code": "Kimi K2.7",
26
+ "kimi-k2.6": "Kimi K2.6",
27
+ "kimi-k2.5": "Kimi K2.5",
28
+ "moonshot-v1-8k": "Moonshot 8k",
29
+ "moonshot-v1-32k": "Moonshot 32k",
30
+ "moonshot-v1-128k": "Moonshot 128k",
31
+ "glm-4.6": "GLM-4.6",
32
+ "glm-4.5": "GLM-4.5",
33
+ "glm-4.5-air": "GLM-4.5 Air",
34
+ "glm-4.7": "GLM-4.7",
35
+ "glm-5": "GLM-5",
36
+ "qwen3-coder-plus": "Qwen3 Coder",
37
+ "qwen-max": "Qwen Max",
38
+ "qwen-plus": "Qwen Plus",
39
+ "doubao-seed-1-6": "Doubao 1.6",
40
+ "doubao-seed-code": "Doubao Code",
41
+ "doubao-1-5-pro-32k": "Doubao Pro 32k",
42
+ "doubao-1-5-pro-256k": "Doubao Pro 256k",
43
+ "deepseek-v4-flash": "DeepSeek V4F",
44
+ "deepseek-v4-pro": "DeepSeek V4P",
45
+ "deepseek-chat": "DeepSeek Chat",
46
+ "deepseek-reasoner": "DeepSeek Rsnr",
47
+ "MiniMax-M2": "MiniMax M2",
48
+ "MiniMax-M2.1": "MiniMax M2.1",
49
+ "MiniMax-M2.5": "MiniMax M2.5",
50
+ "MiniMax-M2.7": "MiniMax M2.7",
51
+ "MiniMax-M3": "MiniMax M3",
52
+ "mimo-v2.5-pro": "MiMo V2.5P",
53
+ "mimo-v2.5": "MiMo V2.5",
54
+ # Gemini(litellm 在线表已有正确定价,这里只补短名让报表显示品牌名、不入 cost.py 兜底)
55
+ "gemini-2.5-pro": "Gemini 2.5 Pro",
56
+ "gemini-2.5-flash": "Gemini 2.5 Flash",
57
+ "gemini-3-pro-preview": "Gemini 3 Pro",
58
+ "gemini-3-pro": "Gemini 3 Pro",
59
+ "gemini-3-flash-preview": "Gemini 3 Flash",
60
+ "gemini-3.1-pro-preview": "Gemini 3.1 Pro",
61
+ "gemini-3.5-flash": "Gemini 3.5 Flash",
62
+ "gemini-2.0-flash": "Gemini 2.0 Flash",
63
+ # xAI Grok
64
+ "grok-4.3": "Grok 4.3",
65
+ "grok-build-0.1": "Grok Build",
66
+ "grok-code-fast-1": "Grok Code",
24
67
  }
25
68
 
26
69
 
@@ -6,7 +6,6 @@
6
6
  总览自己渲染(不复用 dashboard 的宽 header),紧凑单行、半屏不折。
7
7
  """
8
8
 
9
- from collections import defaultdict
10
9
  from datetime import UTC, date, datetime, timedelta
11
10
 
12
11
  from rich.cells import cell_len
@@ -16,10 +15,11 @@ from rich.panel import Panel
16
15
  from rich.rule import Rule
17
16
  from rich.text import Text
18
17
 
19
- from ..adapters.types import DailyStats
18
+ from ..adapters.types import DailyStats, MonthlyStats, WeeklyStats
20
19
  from ..i18n import t
21
20
  from .console import forced_color_console, get_console
22
- from .format import _fmt_cost, _fmt_tokens, _model_short, brand_line, emit_metrics
21
+ from .format import _fmt_cost, _fmt_tokens, append_metric, brand_line, emit_metrics
22
+ from .tables import _merge_months, _merge_weeks, _month_span
23
23
  from .theme import _S, _heat_level, _heat_thresholds, heat_greens
24
24
 
25
25
  _WEEKS = 53
@@ -29,7 +29,9 @@ _LABEL_COL = 6 # 星期标签列显示宽(含间隔),容纳「周日」
29
29
  _BLOCK_X = _INDENT + _LABEL_COL # 方块起始列偏移(月份表头 / 图例对齐它)
30
30
 
31
31
 
32
- def render_daily_heatmap(stats: list[DailyStats], agents: list[str] | None = None) -> None:
32
+ def render_daily_heatmap(stats: list[DailyStats], agents: list[str] | None = None,
33
+ weekly: list[WeeklyStats] | None = None,
34
+ monthly: list[MonthlyStats] | None = None) -> None:
33
35
  if not stats:
34
36
  get_console().print(f"[{_S.warn}]{t('no_data')}[/{_S.warn}]")
35
37
  return
@@ -40,35 +42,40 @@ def render_daily_heatmap(stats: list[DailyStats], agents: list[str] | None = Non
40
42
  tokens_by_date[s.date] = tokens_by_date.get(s.date, 0) + s.total_tokens
41
43
 
42
44
  with forced_color_console():
43
- _render_summary(stats, agents)
45
+ # 顶部单一 Panel 内拼三段:Last 12 months / This Month / This Week(粗→细),
46
+ # 共享一个品牌行 + 红 Rule;段间用 dim Rule 区分;This Month / This Week 各只一行 Tokens/Cost/Avg/Cost
47
+ _render_summary(stats, agents, weekly, monthly)
44
48
  _render_grid(tokens_by_date)
45
49
  _render_legend()
46
50
 
47
51
 
48
- def _render_summary(stats: list[DailyStats], agents: list[str] | None) -> None:
52
+ def _render_summary(stats: list[DailyStats], agents: list[str] | None,
53
+ weekly: list[WeeklyStats] | None,
54
+ monthly: list[MonthlyStats] | None) -> None:
49
55
  today = datetime.now(UTC).date()
50
56
  year_ago = (today - timedelta(days=365)).isoformat()
51
57
  rows = [s for s in stats if s.date >= year_ago] # 过去一年(与热力图范围一致)
52
58
 
53
- # 品牌行(Token Tracker + agent 暗红)+ 红分割线 + 过去一年汇总(同 weekly 顶部样式)
59
+ # 品牌行(Token Tracker + agent 暗红)+ 红分割线
54
60
  brand = brand_line(agents or ["Claude Code"])
55
-
56
61
  avail = max(40, get_console().width - 6) # 卡片可用内容宽 = 终端 - 缩进2 - 边框2 - padding2
57
- body = Text()
58
- body.append("Last 12 months", style=f"bold {_S.good}")
59
- body.append(f" {year_ago} ~ {today.isoformat()}", style=f"dim {_S.good}")
60
- body.append("\n")
62
+
63
+ # --- Section 1:Last 12 months(保留原 3 行指标) ---
64
+ body_year = Text()
65
+ body_year.append("Last 12 months", style=f"bold {_S.good}")
66
+ body_year.append(f" {year_ago} ~ {today.isoformat()}", style=f"dim {_S.good}")
67
+ body_year.append("\n")
61
68
  days = len({s.date for s in rows})
62
69
  total_cost = sum(s.cost_usd for s in rows)
63
70
  # 第一行(橙):Tokens / Cost / Sessions / Avg/Cost / Active Days(Avg/Cost = 总成本 ÷ 活跃天数)
64
- emit_metrics(body, [
71
+ emit_metrics(body_year, [
65
72
  ("Tokens", _fmt_tokens(sum(s.total_tokens for s in rows))),
66
73
  ("Cost", _fmt_cost(total_cost)),
67
74
  ("Sessions", str(sum(s.session_count for s in rows))),
68
75
  ("Avg/Cost", _fmt_cost(total_cost / days if days else 0)),
69
76
  (t("active_days"), str(days)),
70
77
  ], _S.peach, avail)
71
- body.append("\n")
78
+ body_year.append("\n")
72
79
  # 第二行(蓝):单日峰值 token / 当前·最长连续活跃天数
73
80
  peak = max(rows, key=lambda s: s.total_tokens)
74
81
  dts = sorted(date.fromisoformat(d) for d in {s.date for s in rows})
@@ -78,31 +85,67 @@ def _render_summary(stats: list[DailyStats], agents: list[str] | None) -> None:
78
85
  longest_streak = max(longest_streak, cur_streak)
79
86
  if dts and (today - dts[-1]).days > 1: # 最近活跃日距今 > 1 天,当前连续已断
80
87
  cur_streak = 0
81
- emit_metrics(body, [
88
+ emit_metrics(body_year, [
82
89
  (t("daily_peak"), f"{peak.date[5:]} ({_fmt_tokens(peak.total_tokens)})"),
83
90
  (t("daily_streak"), f"{cur_streak}/{longest_streak}{t('unit_day')}"),
84
91
  ], _S.blue, avail)
85
- body.append("\n")
86
- # 第三行(粉):最忙星期几 / Top Model
87
- wd_tokens: dict[int, int] = defaultdict(int)
88
- model_tokens: dict[str, int] = defaultdict(int)
89
- for s in rows:
90
- wd_tokens[date.fromisoformat(s.date).weekday()] += s.total_tokens
91
- for m, tk in s.models.items():
92
- model_tokens[m] += tk
93
- weekdays = t("weekday_full").split(",") # Mon 开头,跟随语言
94
- busiest = weekdays[max(wd_tokens.items(), key=lambda x: x[1])[0]] if wd_tokens else "-"
95
- top_model = _model_short(max(model_tokens.items(), key=lambda x: x[1])[0]) if model_tokens else "-"
96
- emit_metrics(body, [
97
- (t("daily_busiest"), busiest), ("Top Model", top_model),
98
- ], _S.pink, avail)
99
-
100
- get_console().print(Padding(Panel(Group(brand, Rule(style=f"bold {_S.red}"), body),
92
+
93
+ parts: list = [brand, Rule(style=f"bold {_S.red}"), body_year]
94
+
95
+ # --- Section 2:This Month(Tokens / Cost / Avg/Cost 带环比 + 活跃天数 X/月总天数) ---
96
+ if monthly:
97
+ months = _merge_months(monthly)
98
+ cur_m = months[-1]
99
+ prev_m = months[-2] if len(months) >= 2 else None
100
+ days_in_m, elapsed_m = _month_span(cur_m.month)
101
+ cur_m_avg = cur_m.cost_usd / max(1, elapsed_m)
102
+ prev_m_avg = prev_m.cost_usd / max(1, _month_span(prev_m.month)[0]) if prev_m else None
103
+ active_m = len({s.date for s in stats if s.date.startswith(cur_m.month)})
104
+ body_m = _period_section("This Month", cur_m.month, cur_m, prev_m,
105
+ cur_m_avg, prev_m_avg, f"{active_m}/{days_in_m}")
106
+ parts.extend([Rule(style=_S.dim), body_m])
107
+
108
+ # --- Section 3:This Week(Tokens / Cost / Avg/Cost 带环比 + 活跃天数 X/7) ---
109
+ if weekly:
110
+ weeks_list = _merge_weeks(weekly)
111
+ cur_w = weeks_list[-1]
112
+ prev_w = weeks_list[-2] if len(weeks_list) >= 2 else None
113
+ this_monday = datetime.fromisoformat(cur_w.week).date()
114
+ days_w = max(1, min(7, (today - this_monday).days + 1))
115
+ cur_w_avg = cur_w.cost_usd / days_w
116
+ prev_w_avg = prev_w.cost_usd / 7 if prev_w else None
117
+ active_w = len({s.date for s in stats if s.date >= cur_w.week})
118
+ body_w = _period_section("This Week", f"{cur_w.week_start} ~ {cur_w.week_end}",
119
+ cur_w, prev_w, cur_w_avg, prev_w_avg, f"{active_w}/7")
120
+ parts.extend([Rule(style=_S.dim), body_w])
121
+
122
+ get_console().print(Padding(Panel(Group(*parts),
101
123
  expand=False, border_style=_S.blue, padding=(0, 1)),
102
- (0, 0, 0, 2), expand=False))
124
+ (0, 0, 0, _INDENT), expand=False))
103
125
  get_console().print()
104
126
 
105
127
 
128
+ def _period_section(title: str, subtitle: str,
129
+ cur: WeeklyStats | MonthlyStats, prev: WeeklyStats | MonthlyStats | None,
130
+ cur_avg: float, prev_avg: float | None, active_str: str) -> Text:
131
+ """This Month / This Week 段:标题 + 区间,单行橙色 Tokens/Cost/Avg/Cost(带环比)+ 活跃天数(不带环比)。
132
+ cur/prev 鸭子类型——Monthly/WeeklyStats 都有 total_tokens / cost_usd。"""
133
+ body = Text()
134
+ body.append(title, style=f"bold {_S.good}")
135
+ body.append(f" {subtitle}", style=f"dim {_S.good}")
136
+ body.append("\n")
137
+ append_metric(body, "Tokens", _fmt_tokens(cur.total_tokens), _S.peach,
138
+ cur.total_tokens, prev.total_tokens if prev else None)
139
+ body.append(" ")
140
+ append_metric(body, "Cost", _fmt_cost(cur.cost_usd), _S.peach,
141
+ cur.cost_usd, prev.cost_usd if prev else None)
142
+ body.append(" ")
143
+ append_metric(body, "Avg/Cost", _fmt_cost(cur_avg), _S.peach, cur_avg, prev_avg)
144
+ body.append(" ")
145
+ append_metric(body, t("active_days"), active_str, _S.peach)
146
+ return body
147
+
148
+
106
149
  def _display_weeks() -> int:
107
150
  """要显示的周数,右对齐只保留最近若干周。宽度交给 Rich console 判定(它依次读 tty
108
151
  尺寸、`COLUMNS`,都拿不到才回落 80);装不下整年时砍掉最左(最老)的周、不折行。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: token-tracker
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Track token usage across local AI agents (Claude Code, Codex)
5
5
  Author-email: stormzhang <stormzhang.dev@gmail.com>
6
6
  License: MIT License
@@ -99,14 +99,18 @@ Dynamic: license-file
99
99
 
100
100
  通过 `Stop` hook 注入 `systemMessage` 实现,渲染 24-bit 真彩色、**不进模型上下文**(实测),**配色跟随当前主题**(与 CLI 报表 / CC 状态栏同源,`tt theme` 切换三者一起变)。`tt unsetup` 一并移除。
101
101
 
102
- ## Status 实时面板和 日/周/月 数据报表分析
102
+ ## Daily 概览和 日/周/月 数据报表分析
103
103
 
104
- `tt`(无参)/ `tt status`:聚焦**过去 5 小时**的实时面板——顶部多 Agent **合并**概览(Token / Cost / Sessions / Messages / Top Model),中间 **5h / 7d 订阅额度**进度条(Claude Code / Codex 分开;都没订阅额度时换成 per-agent 的 token/cost/sessions/messages 统计),底部**近期会话**列表(CC + Codex 合并、带 Agent 列、按 Cost 倒序、Cost 前三名高亮)。所有时间按**系统时区**显示,配色跟随当前主题。
104
+ `tt`(无参)/ `tt daily`:默认入口,GitHub 风格 token 贡献热力图 + 顶部**单一卡片**内拼三段概览(**Last 12 months / This Month / This Week**,粗→细)。
105
+ - Last 12 months:橙色 Tokens / Cost / Sessions / Avg/Cost / 活跃天数 + 蓝色 单日峰值 / 当前·最长连续活跃天数
106
+ - This Month / This Week:橙色 Tokens / Cost / Avg/Cost / 活跃天数,**带环比**(↑/↓ 上月 / 上周)
105
107
 
106
- ![Token Tracker Status](assets/screenshot.png)
108
+ `tt status`:聚焦**过去 5 小时**的实时面板——顶部多 Agent **合并**概览(Token / Cost / Sessions / Messages / Top Model),中间 **5h / 7d 订阅额度**进度条(Claude Code / Codex 分开;都没订阅额度时换成 per-agent 的 token/cost/sessions/messages 统计),底部**近期会话**列表(CC + Codex 合并、带 Agent 列、按 Cost 倒序、Cost 前三名高亮)。所有时间按**系统时区**显示,配色跟随当前主题。
107
109
 
108
110
  ![Token Tracker Daily](assets/screenshot-daily.png)
109
111
 
112
+ ![Token Tracker Status](assets/screenshot.png)
113
+
110
114
  ![Token Tracker Weekly](assets/screenshot-weekly.png)
111
115
 
112
116
  ![Token Tracker Monthly](assets/screenshot-monthly.png)
@@ -129,19 +133,18 @@ Dynamic: license-file
129
133
  curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/install.sh | bash
130
134
  ```
131
135
 
132
- 或者通过 pip:
136
+ 脚本自动选最优安装方式(uv / pipx / 私有 venv),绕开 PEP 668、不污染系统 Python。
133
137
 
134
- ```bash
135
- pip install --force-reinstall token-tracker && tt setup
136
- ```
138
+ > **升级**:重跑上面的命令即可(脚本幂等、自动升到最新)。
139
+ > **卸载**:先 `tt unsetup` 还原状态栏,再按装法移除(`uv tool uninstall token-tracker` / `pipx uninstall token-tracker` / 删 `~/.local/share/token-tracker` 与 `~/.local/bin/tt`)。
137
140
 
138
141
  ## 使用
139
142
 
140
143
  ```bash
141
144
  tt setup # 交互配置向导(终端:上下键选语言 / 主题 / 各组件);非 tty 环境自动全装
142
- tt # 过去 5h 实时面板(合并概览 + 5h/7d 额度 + 近期会话,= tt status
143
- tt status # 同上(tt 无参即进 status
144
- tt daily # 过去一年 token 贡献热力图(GitHub 风格)+ 年度分析卡片
145
+ tt # 过去一年 token 热力图 + 顶部三段概览(Last 12 months / This Month / This Week,= tt daily
146
+ tt daily # 同上(tt 无参即进 daily
147
+ tt status # 过去 5h 实时面板(合并概览 + 5h/7d 额度 + 近期会话)
145
148
  tt weekly # 周报:本周分析卡片 + 每日趋势柱状图 + 周 / 项目 / 模型趋势
146
149
  tt monthly # 按月汇总(多 Agent 分组展示)
147
150
  tt sessions # 最近 20 条会话明细(按 cost 倒序展示;tt sessions <n> 改条数、--sort 改排序)
@@ -222,3 +222,121 @@ def test_stale_cache_survives_middownload_errors(tmp_path, monkeypatch, exc):
222
222
 
223
223
  monkeypatch.setattr(cost, "_fetch_and_cache", boom)
224
224
  assert cost._load_pricing() == {"gpt-5": {"input_cost_per_token": 7e-6}}
225
+
226
+
227
+ # ---- 国产模型定价(2026-06 官方核实,详见 cost.py 注释)----
228
+
229
+
230
+ def test_fallback_pricing_includes_chinese_models():
231
+ # 六家国产主力 model id 都要有内置价,不能因 litellm 未收录 bare key 而归零
232
+ pricing = cost._fallback_pricing()
233
+ for k in (
234
+ "kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5", "moonshot-v1-128k",
235
+ "glm-4.6", "glm-4.5-air", "glm-5",
236
+ "qwen3-coder-plus", "qwen-max", "qwen-plus",
237
+ "doubao-seed-1-6", "doubao-seed-code", "doubao-1-5-pro-32k", "doubao-1-5-pro-256k",
238
+ "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat", "deepseek-reasoner",
239
+ "MiniMax-M2", "MiniMax-M2.7", "MiniMax-M3",
240
+ "mimo-v2.5-pro", "mimo-v2.5",
241
+ ):
242
+ assert k in pricing, f"fallback pricing missing {k}"
243
+ assert pricing[k].get("input_cost_per_token", 0) > 0
244
+
245
+
246
+ def test_glm_uses_intl_usd_pricing(monkeypatch):
247
+ # GLM 口径例外:用 z.ai 国际站官方 USD($0.6/$2.2/$0.11),不折汇率
248
+ monkeypatch.setattr(cost, "_pricing", cost._fallback_pricing())
249
+ entry = make_entry(
250
+ model="glm-4.6", input_tokens=1_000_000, output_tokens=1_000_000, cache_read_tokens=1_000_000
251
+ )
252
+ assert cost.calculate_cost(entry) == pytest.approx(0.6 + 2.2 + 0.11)
253
+
254
+
255
+ def test_kimi_cny_converted_to_usd(monkeypatch):
256
+ # Kimi K2.7 Code 中国站 ¥6.5/¥27/¥1.3 按 7.1 折 USD
257
+ monkeypatch.setattr(cost, "_pricing", cost._fallback_pricing())
258
+ entry = make_entry(
259
+ model="kimi-k2.7-code", input_tokens=1_000_000, output_tokens=1_000_000, cache_read_tokens=1_000_000
260
+ )
261
+ assert cost.calculate_cost(entry) == pytest.approx((6.5 + 27 + 1.3) / 7.1)
262
+
263
+
264
+ def test_deepseek_and_qwen_cny_base_tier(monkeypatch):
265
+ # DeepSeek V4-Flash ¥1/¥2;Qwen3-Coder 取 0-32K 档 ¥4/¥16,均 ÷7.1
266
+ monkeypatch.setattr(cost, "_pricing", cost._fallback_pricing())
267
+ ds = make_entry(model="deepseek-v4-flash", input_tokens=1_000_000, output_tokens=1_000_000)
268
+ assert cost.calculate_cost(ds) == pytest.approx((1 + 2) / 7.1)
269
+ qw = make_entry(model="qwen3-coder-plus", input_tokens=1_000_000, output_tokens=1_000_000)
270
+ assert cost.calculate_cost(qw) == pytest.approx((4 + 16) / 7.1)
271
+
272
+
273
+ def test_minimax_m2_usd_pricing(monkeypatch):
274
+ # MiniMax M2 官方 USD $0.3/$1.2(与中国站÷7 自洽)
275
+ monkeypatch.setattr(cost, "_pricing", cost._fallback_pricing())
276
+ entry = make_entry(model="MiniMax-M2", input_tokens=1_000_000, output_tokens=1_000_000)
277
+ assert cost.calculate_cost(entry) == pytest.approx(0.3 + 1.2)
278
+
279
+
280
+ def test_mimo_cny_pricing(monkeypatch):
281
+ # 小米 MiMo 官方中国站人民币价(mimo.mi.com):Pro ¥3/¥6、标准 ¥1/¥2,÷7.1;未来版本系列兜底
282
+ monkeypatch.setattr(cost, "_pricing", cost._fallback_pricing())
283
+ pro = make_entry(model="mimo-v2.5-pro", input_tokens=1_000_000, output_tokens=1_000_000)
284
+ assert cost.calculate_cost(pro) == pytest.approx((3 + 6) / 7.1)
285
+ std = make_entry(model="mimo-v2.5", input_tokens=1_000_000, output_tokens=1_000_000)
286
+ assert cost.calculate_cost(std) == pytest.approx((1 + 2) / 7.1)
287
+ # 未来 mimo-v3 → mimo-v2.5 系列兜底(¥1 input ÷7.1)
288
+ assert cost.calculate_cost(make_entry(model="mimo-v3", input_tokens=1_000_000)) == pytest.approx(1 / 7.1)
289
+
290
+
291
+ def test_chinese_model_family_fallback(monkeypatch):
292
+ # 未知新版本 / 已下线旧 id 按系列兜底,不归零
293
+ monkeypatch.setattr(cost, "_pricing", cost._fallback_pricing())
294
+ # 未来 kimi-k3 → kimi-k2.6(¥6.5 input ÷7.1)
295
+ assert cost.calculate_cost(make_entry(model="kimi-k3-preview", input_tokens=1_000_000)) == pytest.approx(6.5 / 7.1)
296
+ # 已 EOL 的 kimi-k2-instruct → kimi 系列兜底,不归零
297
+ assert cost.calculate_cost(make_entry(model="kimi-k2-instruct", input_tokens=1_000_000)) == pytest.approx(6.5 / 7.1)
298
+ # 未来 glm-4.8 → glm-4.6($0.6 input,不折汇率)
299
+ assert cost.calculate_cost(make_entry(model="glm-4.8", input_tokens=1_000_000)) == pytest.approx(0.6)
300
+ # 未来 minimax-m4 → MiniMax-M2($0.3 input)
301
+ assert cost.calculate_cost(make_entry(model="minimax-m4", input_tokens=1_000_000)) == pytest.approx(0.3)
302
+
303
+
304
+ def test_chinese_models_have_short_names():
305
+ # cost.py 内置的国产 key 都应在 MODEL_SHORT 有短名(报表 / 状态栏可读)
306
+ from token_tracker.ui.format import MODEL_SHORT
307
+ for k in (
308
+ "kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5",
309
+ "moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k",
310
+ "glm-4.6", "glm-4.5", "glm-4.5-air", "glm-4.7", "glm-5",
311
+ "qwen3-coder-plus", "qwen-max", "qwen-plus",
312
+ "doubao-seed-1-6", "doubao-seed-code", "doubao-1-5-pro-32k", "doubao-1-5-pro-256k",
313
+ "deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat", "deepseek-reasoner",
314
+ "MiniMax-M2", "MiniMax-M2.1", "MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3",
315
+ "mimo-v2.5-pro", "mimo-v2.5",
316
+ ):
317
+ assert k in MODEL_SHORT, f"MODEL_SHORT missing {k}"
318
+
319
+
320
+ def test_grok_pricing_and_retirement_routing(monkeypatch):
321
+ # xAI Grok 官方 USD(docs.x.ai);2026-05-15 退役 slug 按官方路由到 grok-4.3 / grok-build-0.1
322
+ monkeypatch.setattr(cost, "_pricing", cost._fallback_pricing())
323
+ flagship = make_entry(model="grok-4.3", input_tokens=1_000_000, output_tokens=1_000_000)
324
+ assert cost.calculate_cost(flagship) == pytest.approx(1.25 + 2.5)
325
+ coding = make_entry(model="grok-build-0.1", input_tokens=1_000_000, output_tokens=1_000_000)
326
+ assert cost.calculate_cost(coding) == pytest.approx(1.0 + 2.0)
327
+ # 退役别名 grok-code-fast-1 → build-0.1 价(¥ 无关,纯 USD)
328
+ alias = make_entry(model="grok-code-fast-1", input_tokens=1_000_000, output_tokens=1_000_000)
329
+ assert cost.calculate_cost(alias) == pytest.approx(3.0)
330
+ # 退役 slug grok-4-fast / grok-3 → grok-4.3 价(官方就这么路由)
331
+ assert cost.calculate_cost(make_entry(model="grok-4-fast", input_tokens=1_000_000)) == pytest.approx(1.25)
332
+ assert cost.calculate_cost(make_entry(model="grok-3", input_tokens=1_000_000)) == pytest.approx(1.25)
333
+
334
+
335
+ def test_gemini_and_grok_short_names():
336
+ # Gemini 不入 cost.py(litellm 价已对),只验短名在 MODEL_SHORT;Grok 短名同验
337
+ from token_tracker.ui.format import MODEL_SHORT
338
+ for k in (
339
+ "gemini-2.5-pro", "gemini-3-pro-preview", "gemini-3.5-flash",
340
+ "grok-4.3", "grok-build-0.1", "grok-code-fast-1",
341
+ ):
342
+ assert k in MODEL_SHORT, f"MODEL_SHORT missing {k}"
@@ -373,3 +373,81 @@ def test_detect_system_lang_non_darwin_falls_back_to_env(monkeypatch):
373
373
  assert i18n._detect_system_lang() == "zh"
374
374
  monkeypatch.setenv("LANG", "en_US.UTF-8")
375
375
  assert i18n._detect_system_lang() == "en"
376
+
377
+
378
+ # --- SETUP_VERSION 引导版本(老用户升级后重新引导) ---
379
+
380
+
381
+ def _isolate_config(monkeypatch, tmp_path):
382
+ """把 config.py 的所有路径常量切到 tmp_path,避免污染主人真实 ~/.config/token-tracker。"""
383
+ from token_tracker import config
384
+ monkeypatch.setattr(config, "CONFIG_DIR", str(tmp_path))
385
+ monkeypatch.setattr(config, "CONFIG_PATH", str(tmp_path / "config.json"))
386
+ monkeypatch.setattr(config, "_LEGACY_THEME_PATH", str(tmp_path / "theme.json"))
387
+ monkeypatch.setattr(config, "_LEGACY_LANG_PATH", str(tmp_path / "lang.json"))
388
+
389
+
390
+ def test_setup_writes_setup_version(tmp_path, monkeypatch):
391
+ # setup() 真正落地后必须写入 setup_version=当前 SETUP_VERSION——
392
+ # 这是引导机制收口:所有路径(新用户 / wizard / _auto_setup / 手动 tt setup)都经此。
393
+ from token_tracker import config
394
+ home = tmp_path / "home"
395
+ (home / ".claude").mkdir(parents=True)
396
+ (home / ".codex").mkdir(parents=True)
397
+ settings_path = home / ".claude" / "settings.json"
398
+ settings_path.write_text("{}", encoding="utf-8")
399
+ codex_config = home / ".codex" / "config.toml"
400
+ codex_config.write_text("", encoding="utf-8")
401
+ monkeypatch.setattr(hooks, "CLAUDE_SETTINGS", str(settings_path))
402
+ monkeypatch.setattr(hooks, "HOOK_SCRIPT_PATH", str(home / ".claude" / "tt-statusline.py"))
403
+ monkeypatch.setattr(hooks, "CODEX_DIR", str(home / ".codex"))
404
+ monkeypatch.setattr(hooks, "CODEX_CONFIG", str(codex_config))
405
+ monkeypatch.setattr(hooks, "CODEX_STATUSLINE_HOOK_PATH", str(home / ".codex" / "tt-statusline.py"))
406
+ _isolate_config(monkeypatch, tmp_path / "cfg")
407
+
408
+ assert config.setup_version() == 0 # 老用户初始 0
409
+ hooks.setup(quiet=True)
410
+ assert config.setup_version() == config.SETUP_VERSION # setup 完成后被打上当前版本
411
+
412
+
413
+ def test_cli_outdated_setup_triggers_setup_flow(monkeypatch, tmp_path):
414
+ # 老用户 is_setup=True 且 setup_version < SETUP_VERSION → 自动走 _run_setup_flow
415
+ # (内部分流真终端 wizard / 会话内 _auto_setup,这里只验触发、不管分流)。
416
+ from token_tracker import cli, config
417
+ _isolate_config(monkeypatch, tmp_path)
418
+ calls: dict = {}
419
+
420
+ def fake_flow():
421
+ calls["flow"] = True
422
+ raise SystemExit(0) # 短路 cli.main 后续数据命令逻辑
423
+
424
+ monkeypatch.setattr(cli, "_run_setup_flow", fake_flow)
425
+ monkeypatch.setattr(cli, "is_setup", lambda: True)
426
+ monkeypatch.setattr(cli, "needs_update", lambda: False)
427
+ # setup_version 字段缺失 → 读出 0 < SETUP_VERSION
428
+ monkeypatch.setattr(config, "SETUP_VERSION", 2)
429
+ monkeypatch.setattr("sys.argv", ["tt", "status"])
430
+
431
+ with pytest.raises(SystemExit):
432
+ cli.main()
433
+ assert calls == {"flow": True}
434
+
435
+
436
+ def test_cli_setup_up_to_date_skips_flow(monkeypatch, tmp_path):
437
+ # setup_version 已是当前 → 不触发 _run_setup_flow,正常往下跑。
438
+ from token_tracker import cli, config
439
+ _isolate_config(monkeypatch, tmp_path)
440
+ calls: dict = {}
441
+
442
+ monkeypatch.setattr(cli, "_run_setup_flow", lambda: calls.__setitem__("flow", True))
443
+ monkeypatch.setattr(cli, "is_setup", lambda: True)
444
+ monkeypatch.setattr(cli, "needs_update", lambda: False)
445
+ monkeypatch.setattr(cli, "_build_status_data", lambda _agents: {})
446
+ from types import SimpleNamespace
447
+ monkeypatch.setattr(cli, "detect_agents",
448
+ lambda: [SimpleNamespace(name="Claude Code", id="claude-code")])
449
+ config.save_setup_version(config.SETUP_VERSION) # 已是最新
450
+ monkeypatch.setattr("sys.argv", ["tt", "status"])
451
+
452
+ cli.main()
453
+ assert calls == {}
File without changes
File without changes