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.
- {token_tracker-0.4.1/src/token_tracker.egg-info → token_tracker-0.4.2}/PKG-INFO +1 -1
- {token_tracker-0.4.1 → token_tracker-0.4.2}/pyproject.toml +1 -1
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/analyzer/cost.py +83 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/cli.py +4 -7
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/config.py +4 -3
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/i18n.py +0 -2
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/format.py +43 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/heatmap.py +25 -26
- {token_tracker-0.4.1 → token_tracker-0.4.2/src/token_tracker.egg-info}/PKG-INFO +1 -1
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_cost.py +118 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_hooks.py +13 -38
- {token_tracker-0.4.1 → token_tracker-0.4.2}/LICENSE +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/README.md +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/setup.cfg +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/__init__.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/__init__.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/claude.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/codex.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/rate_limits.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/registry.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/types.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/adapters/util.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/analyzer/__init__.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/analyzer/aggregator.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/analyzer/blocks.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/hooks.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/__init__.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/console.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/panels.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/status.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/tables.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/theme.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/ui/themes.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker/wizard.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker.egg-info/SOURCES.txt +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker.egg-info/dependency_links.txt +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker.egg-info/entry_points.txt +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker.egg-info/requires.txt +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/src/token_tracker.egg-info/top_level.txt +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_aggregator.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_blocks.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_cli.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_codex.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_heatmap.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_status.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_tables.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_theme.py +0 -0
- {token_tracker-0.4.1 → token_tracker-0.4.2}/tests/test_util.py +0 -0
|
@@ -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
|
-
#
|
|
390
|
-
#
|
|
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
|
-
|
|
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
|
-
# 引导版本:每次新增"值得让老用户重新走一遍
|
|
22
|
-
# 老用户 config 里没这字段
|
|
21
|
+
# 引导版本:每次新增"值得让老用户重新走一遍 setup"的配置项时手动 +1(只能整数、一次 +1)。
|
|
22
|
+
# 老用户 config 里没这字段 / 旧版本号 < 当前 → 触发重新引导(真终端弹 wizard、非 tty 静默 _auto_setup)。
|
|
23
23
|
# 跟 SCHEMA_VERSION 解耦:那是数据格式版本,这是用户引导版本,bump 节奏完全不同。
|
|
24
|
-
|
|
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 =
|
|
105
|
-
|
|
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 =
|
|
130
|
-
|
|
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);装不下整年时砍掉最左(最老)的周、不折行。
|
|
@@ -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
|
|
414
|
-
# 老用户 is_setup=True 且 setup_version < SETUP_VERSION
|
|
415
|
-
|
|
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
|
|
420
|
-
calls["
|
|
420
|
+
def fake_flow():
|
|
421
|
+
calls["flow"] = True
|
|
421
422
|
raise SystemExit(0) # 短路 cli.main 后续数据命令逻辑
|
|
422
423
|
|
|
423
|
-
monkeypatch.setattr(
|
|
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",
|
|
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 == {"
|
|
433
|
+
assert calls == {"flow": True}
|
|
434
434
|
|
|
435
435
|
|
|
436
|
-
def
|
|
437
|
-
#
|
|
438
|
-
from token_tracker import cli, config
|
|
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(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|