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.
- {token_tracker-0.4.0/src/token_tracker.egg-info → token_tracker-0.4.2}/PKG-INFO +14 -11
- {token_tracker-0.4.0 → token_tracker-0.4.2}/README.md +13 -10
- {token_tracker-0.4.0 → token_tracker-0.4.2}/pyproject.toml +1 -1
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/analyzer/cost.py +83 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/cli.py +19 -2
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/config.py +20 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/hooks.py +4 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/i18n.py +2 -6
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/format.py +43 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/heatmap.py +75 -32
- {token_tracker-0.4.0 → token_tracker-0.4.2/src/token_tracker.egg-info}/PKG-INFO +14 -11
- {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_cost.py +118 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_hooks.py +78 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/LICENSE +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/setup.cfg +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/__init__.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/__init__.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/claude.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/codex.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/rate_limits.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/registry.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/types.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/adapters/util.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/analyzer/__init__.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/analyzer/aggregator.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/analyzer/blocks.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/__init__.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/console.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/panels.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/status.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/tables.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/theme.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/ui/themes.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker/wizard.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker.egg-info/SOURCES.txt +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker.egg-info/dependency_links.txt +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker.egg-info/entry_points.txt +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker.egg-info/requires.txt +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/src/token_tracker.egg-info/top_level.txt +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_aggregator.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_blocks.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_cli.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_codex.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_heatmap.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_status.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_tables.py +0 -0
- {token_tracker-0.4.0 → token_tracker-0.4.2}/tests/test_theme.py +0 -0
- {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.
|
|
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
|
-
##
|
|
102
|
+
## Daily 概览和 日/周/月 数据报表分析
|
|
103
103
|
|
|
104
|
-
`tt`(无参)/ `tt
|
|
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
|
-
|
|
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
|

|
|
109
111
|
|
|
112
|
+

|
|
113
|
+
|
|
110
114
|

|
|
111
115
|
|
|
112
116
|

|
|
@@ -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
|
-
|
|
136
|
+
脚本自动选最优安装方式(uv / pipx / 私有 venv),绕开 PEP 668、不污染系统 Python。
|
|
133
137
|
|
|
134
|
-
|
|
135
|
-
|
|
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 #
|
|
143
|
-
tt
|
|
144
|
-
tt
|
|
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
|
-
##
|
|
49
|
+
## Daily 概览和 日/周/月 数据报表分析
|
|
50
50
|
|
|
51
|
-
`tt`(无参)/ `tt
|
|
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
|
-
|
|
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
|

|
|
56
58
|
|
|
59
|
+

|
|
60
|
+
|
|
57
61
|

|
|
58
62
|
|
|
59
63
|

|
|
@@ -76,19 +80,18 @@
|
|
|
76
80
|
curl -sSL https://raw.githubusercontent.com/stormzhang/token-tracker/main/install.sh | bash
|
|
77
81
|
```
|
|
78
82
|
|
|
79
|
-
|
|
83
|
+
脚本自动选最优安装方式(uv / pipx / 私有 venv),绕开 PEP 668、不污染系统 Python。
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
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 #
|
|
90
|
-
tt
|
|
91
|
-
tt
|
|
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 改排序)
|
|
@@ -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 "
|
|
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 == "
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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 暗红)+ 红分割线
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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,
|
|
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.
|
|
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
|
-
##
|
|
102
|
+
## Daily 概览和 日/周/月 数据报表分析
|
|
103
103
|
|
|
104
|
-
`tt`(无参)/ `tt
|
|
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
|
-
|
|
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
|

|
|
109
111
|
|
|
112
|
+

|
|
113
|
+
|
|
110
114
|

|
|
111
115
|
|
|
112
116
|

|
|
@@ -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
|
-
|
|
136
|
+
脚本自动选最优安装方式(uv / pipx / 私有 venv),绕开 PEP 668、不污染系统 Python。
|
|
133
137
|
|
|
134
|
-
|
|
135
|
-
|
|
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 #
|
|
143
|
-
tt
|
|
144
|
-
tt
|
|
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
|
|
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
|