token-tracker 0.4.1__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.1/src/token_tracker.egg-info → token_tracker-0.4.2}/PKG-INFO +1 -1
  2. {token_tracker-0.4.1 → token_tracker-0.4.2}/pyproject.toml +1 -1
  3. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/analyzer/cost.py +83 -0
  4. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/cli.py +4 -7
  5. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/config.py +4 -3
  6. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/i18n.py +0 -2
  7. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/format.py +43 -0
  8. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/heatmap.py +25 -26
  9. {token_tracker-0.4.1 → token_tracker-0.4.2/src/token_tracker.egg-info}/PKG-INFO +1 -1
  10. {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_cost.py +118 -0
  11. {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_hooks.py +13 -38
  12. {token_tracker-0.4.1 → token_tracker-0.4.2}/LICENSE +0 -0
  13. {token_tracker-0.4.1 → token_tracker-0.4.2}/README.md +0 -0
  14. {token_tracker-0.4.1 → token_tracker-0.4.2}/setup.cfg +0 -0
  15. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/__init__.py +0 -0
  16. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/__init__.py +0 -0
  17. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/claude.py +0 -0
  18. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/codex.py +0 -0
  19. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/rate_limits.py +0 -0
  20. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/registry.py +0 -0
  21. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/types.py +0 -0
  22. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/util.py +0 -0
  23. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/analyzer/__init__.py +0 -0
  24. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/analyzer/aggregator.py +0 -0
  25. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/analyzer/blocks.py +0 -0
  26. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/hooks.py +0 -0
  27. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/__init__.py +0 -0
  28. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/console.py +0 -0
  29. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/panels.py +0 -0
  30. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/status.py +0 -0
  31. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/tables.py +0 -0
  32. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/theme.py +0 -0
  33. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/themes.py +0 -0
  34. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/wizard.py +0 -0
  35. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker.egg-info/SOURCES.txt +0 -0
  36. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker.egg-info/dependency_links.txt +0 -0
  37. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker.egg-info/entry_points.txt +0 -0
  38. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker.egg-info/requires.txt +0 -0
  39. {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker.egg-info/top_level.txt +0 -0
  40. {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_aggregator.py +0 -0
  41. {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_blocks.py +0 -0
  42. {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_cli.py +0 -0
  43. {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_codex.py +0 -0
  44. {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_heatmap.py +0 -0
  45. {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_status.py +0 -0
  46. {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_tables.py +0 -0
  47. {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_theme.py +0 -0
  48. {token_tracker-0.4.1 → 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.1
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "token-tracker"
7
- version = "0.4.1"
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
  }
@@ -386,18 +386,15 @@ def main():
386
386
  update_hook()
387
387
 
388
388
  # 升级感知:新版若新增了值得重配的选项(SETUP_VERSION bump),老用户跑任意命令时
389
- # 真终端直接弹一次 wizard;非 tty / 会话内只打印一行提示、不打断。
390
- # wizard 内部最终会调 setup() save_setup_version(),下次启动不再触发。
389
+ # 自动走一遍 setup——_run_setup_flow 内部分流:真终端弹 wizard、会话内 / 非 tty 静默
390
+ # _auto_setup 用默认值全装(语言跟随系统 / mocha / 组件全开)。两者最终都 save_setup_version()
391
+ # 下次启动 setup_version 已是最新、不再触发。
391
392
  if (
392
393
  command not in ("setup", "unsetup")
393
394
  and is_setup()
394
395
  and config.setup_version() < config.SETUP_VERSION
395
396
  ):
396
- if _should_run_wizard():
397
- from .wizard import run_wizard
398
- run_wizard()
399
- else:
400
- get_console().print(f"[dim]{t('setup_outdated_hint')}[/dim]")
397
+ _run_setup_flow()
401
398
 
402
399
  if command == "setup":
403
400
  _run_setup_flow()
@@ -18,10 +18,11 @@ 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
- # 引导版本:每次新增"值得让老用户重新走一遍 wizard"的配置项时手动 +1
22
- # 老用户 config 里没这字段 读出 0 → 触发重新引导(真终端弹 wizard、非 tty 仅提示)。
21
+ # 引导版本:每次新增"值得让老用户重新走一遍 setup"的配置项时手动 +1(只能整数、一次 +1)。
22
+ # 老用户 config 里没这字段 / 旧版本号 < 当前 → 触发重新引导(真终端弹 wizard、非 tty 静默 _auto_setup)。
23
23
  # 跟 SCHEMA_VERSION 解耦:那是数据格式版本,这是用户引导版本,bump 节奏完全不同。
24
- SETUP_VERSION = 1
24
+ # 2(0.4.2):强制所有现存用户(0.3.8/0.4.0=无字段=0、0.4.1=1,全 < 2)升级后重走一遍 setup。
25
+ SETUP_VERSION = 2
25
26
 
26
27
  # 旧位置(独立 theme.json / lang.json),老用户首次读 config.json 不存在时自动合并迁移
27
28
  _LEGACY_THEME_PATH = os.path.join(CONFIG_DIR, "theme.json")
@@ -102,7 +102,6 @@ _STRINGS = {
102
102
  "no_agent_install": "未检测到 Claude Code 或 Codex,请先安装其中之一",
103
103
  "auto_setup_hint": "非交互环境,已按默认(语言跟随系统 / 主题 mocha / 组件全开)配置;如需自定义请在终端运行 tt setup",
104
104
  "first_setup": "首次使用,正在配置状态栏...",
105
- "setup_outdated_hint": "检测到新版本新增了配置项;请在终端运行 tt setup 体验",
106
105
  "cc_not_found": "未检测到 Claude Code,跳过",
107
106
  "codex_not_found": "未检测到 Codex,跳过",
108
107
  "sl_backup_replace": "检测到已有 statusLine,备份后替换",
@@ -220,7 +219,6 @@ _STRINGS = {
220
219
  "no_agent_install": "Claude Code or Codex not detected, please install one first",
221
220
  "auto_setup_hint": "Non-interactive env — configured with defaults (language follows system / theme mocha / all components on); run tt setup in a terminal to customize",
222
221
  "first_setup": "First run, configuring status bar...",
223
- "setup_outdated_hint": "New configuration options added in this release — run tt setup in a terminal to try them",
224
222
  "cc_not_found": "Claude Code not detected, skipping",
225
223
  "codex_not_found": "Codex not detected, skipping",
226
224
  "sl_backup_replace": "Existing statusLine detected, backing up and replacing",
@@ -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
 
@@ -101,19 +101,8 @@ def _render_summary(stats: list[DailyStats], agents: list[str] | None,
101
101
  cur_m_avg = cur_m.cost_usd / max(1, elapsed_m)
102
102
  prev_m_avg = prev_m.cost_usd / max(1, _month_span(prev_m.month)[0]) if prev_m else None
103
103
  active_m = len({s.date for s in stats if s.date.startswith(cur_m.month)})
104
- body_m = Text()
105
- body_m.append("This Month", style=f"bold {_S.good}")
106
- body_m.append(f" {cur_m.month}", style=f"dim {_S.good}")
107
- body_m.append("\n")
108
- append_metric(body_m, "Tokens", _fmt_tokens(cur_m.total_tokens), _S.peach,
109
- cur_m.total_tokens, prev_m.total_tokens if prev_m else None)
110
- body_m.append(" ")
111
- append_metric(body_m, "Cost", _fmt_cost(cur_m.cost_usd), _S.peach,
112
- cur_m.cost_usd, prev_m.cost_usd if prev_m else None)
113
- body_m.append(" ")
114
- append_metric(body_m, "Avg/Cost", _fmt_cost(cur_m_avg), _S.peach, cur_m_avg, prev_m_avg)
115
- body_m.append(" ")
116
- append_metric(body_m, t("active_days"), f"{active_m}/{days_in_m}", _S.peach, active_m, None)
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}")
117
106
  parts.extend([Rule(style=_S.dim), body_m])
118
107
 
119
108
  # --- Section 3:This Week(Tokens / Cost / Avg/Cost 带环比 + 活跃天数 X/7) ---
@@ -126,19 +115,8 @@ def _render_summary(stats: list[DailyStats], agents: list[str] | None,
126
115
  cur_w_avg = cur_w.cost_usd / days_w
127
116
  prev_w_avg = prev_w.cost_usd / 7 if prev_w else None
128
117
  active_w = len({s.date for s in stats if s.date >= cur_w.week})
129
- body_w = Text()
130
- body_w.append("This Week", style=f"bold {_S.good}")
131
- body_w.append(f" {cur_w.week_start} ~ {cur_w.week_end}", style=f"dim {_S.good}")
132
- body_w.append("\n")
133
- append_metric(body_w, "Tokens", _fmt_tokens(cur_w.total_tokens), _S.peach,
134
- cur_w.total_tokens, prev_w.total_tokens if prev_w else None)
135
- body_w.append(" ")
136
- append_metric(body_w, "Cost", _fmt_cost(cur_w.cost_usd), _S.peach,
137
- cur_w.cost_usd, prev_w.cost_usd if prev_w else None)
138
- body_w.append(" ")
139
- append_metric(body_w, "Avg/Cost", _fmt_cost(cur_w_avg), _S.peach, cur_w_avg, prev_w_avg)
140
- body_w.append(" ")
141
- append_metric(body_w, t("active_days"), f"{active_w}/7", _S.peach, active_w, None)
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")
142
120
  parts.extend([Rule(style=_S.dim), body_w])
143
121
 
144
122
  get_console().print(Padding(Panel(Group(*parts),
@@ -147,6 +125,27 @@ def _render_summary(stats: list[DailyStats], agents: list[str] | None,
147
125
  get_console().print()
148
126
 
149
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
+
150
149
  def _display_weeks() -> int:
151
150
  """要显示的周数,右对齐只保留最近若干周。宽度交给 Rich console 判定(它依次读 tty
152
151
  尺寸、`COLUMNS`,都拿不到才回落 80);装不下整年时砍掉最左(最老)的周、不折行。
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: token-tracker
3
- Version: 0.4.1
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
@@ -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}"
@@ -410,63 +410,38 @@ def test_setup_writes_setup_version(tmp_path, monkeypatch):
410
410
  assert config.setup_version() == config.SETUP_VERSION # setup 完成后被打上当前版本
411
411
 
412
412
 
413
- def test_cli_outdated_setup_triggers_wizard_in_tty(monkeypatch, tmp_path):
414
- # 老用户 is_setup=True 且 setup_version < SETUP_VERSION 且双 tty 非会话内 直接弹 wizard。
415
- from token_tracker import cli, config, wizard
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
416
417
  _isolate_config(monkeypatch, tmp_path)
417
418
  calls: dict = {}
418
419
 
419
- def fake_wizard():
420
- calls["wizard"] = True
420
+ def fake_flow():
421
+ calls["flow"] = True
421
422
  raise SystemExit(0) # 短路 cli.main 后续数据命令逻辑
422
423
 
423
- monkeypatch.setattr(wizard, "run_wizard", fake_wizard)
424
+ monkeypatch.setattr(cli, "_run_setup_flow", fake_flow)
424
425
  monkeypatch.setattr(cli, "is_setup", lambda: True)
425
426
  monkeypatch.setattr(cli, "needs_update", lambda: False)
426
- monkeypatch.setattr(cli, "_should_run_wizard", lambda: True)
427
427
  # setup_version 字段缺失 → 读出 0 < SETUP_VERSION
428
- monkeypatch.setattr(config, "SETUP_VERSION", 1)
428
+ monkeypatch.setattr(config, "SETUP_VERSION", 2)
429
429
  monkeypatch.setattr("sys.argv", ["tt", "status"])
430
430
 
431
431
  with pytest.raises(SystemExit):
432
432
  cli.main()
433
- assert calls == {"wizard": True}
433
+ assert calls == {"flow": True}
434
434
 
435
435
 
436
- def test_cli_outdated_setup_non_tty_prints_hint_only(monkeypatch, tmp_path, capsys):
437
- # 老用户但非 tty / 会话内 不进 wizard,只打印一行 setup_outdated_hint 提示。
438
- from token_tracker import cli, config, wizard
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
439
  _isolate_config(monkeypatch, tmp_path)
440
440
  calls: dict = {}
441
441
 
442
- monkeypatch.setattr(wizard, "run_wizard", lambda: calls.__setitem__("wizard", True))
442
+ monkeypatch.setattr(cli, "_run_setup_flow", lambda: calls.__setitem__("flow", True))
443
443
  monkeypatch.setattr(cli, "is_setup", lambda: True)
444
444
  monkeypatch.setattr(cli, "needs_update", lambda: False)
445
- monkeypatch.setattr(cli, "_should_run_wizard", lambda: False)
446
- monkeypatch.setattr(cli, "_build_status_data", lambda _agents: {}) # 走 "no data" 早返回
447
- from types import SimpleNamespace
448
- monkeypatch.setattr(cli, "detect_agents",
449
- lambda: [SimpleNamespace(name="Claude Code", id="claude-code")])
450
- monkeypatch.setattr(config, "SETUP_VERSION", 1)
451
- monkeypatch.setattr("sys.argv", ["tt", "status"])
452
-
453
- cli.main()
454
- assert calls == {} # wizard 没被调
455
- out = capsys.readouterr().out
456
- # 中英任一命中即可(取决于运行环境系统语言)
457
- assert "tt setup" in out
458
-
459
-
460
- def test_cli_setup_up_to_date_skips_wizard(monkeypatch, tmp_path):
461
- # setup_version 已是当前 → 不触发 wizard 也不打印提示,正常往下跑。
462
- from token_tracker import cli, config, wizard
463
- _isolate_config(monkeypatch, tmp_path)
464
- calls: dict = {}
465
-
466
- monkeypatch.setattr(wizard, "run_wizard", lambda: calls.__setitem__("wizard", True))
467
- monkeypatch.setattr(cli, "is_setup", lambda: True)
468
- monkeypatch.setattr(cli, "needs_update", lambda: False)
469
- monkeypatch.setattr(cli, "_should_run_wizard", lambda: True)
470
445
  monkeypatch.setattr(cli, "_build_status_data", lambda _agents: {})
471
446
  from types import SimpleNamespace
472
447
  monkeypatch.setattr(cli, "detect_agents",
File without changes
File without changes
File without changes