token-tracker 0.3.7__tar.gz → 0.3.8__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.3.7/token_tracker.egg-info → token_tracker-0.3.8}/PKG-INFO +1 -1
- {token_tracker-0.3.7 → token_tracker-0.3.8}/pyproject.toml +1 -1
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/analyzer/cost.py +25 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/ui/format.py +2 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/tests/test_cost.py +42 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8/token_tracker.egg-info}/PKG-INFO +1 -1
- {token_tracker-0.3.7 → token_tracker-0.3.8}/LICENSE +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/README.md +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/setup.cfg +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/__init__.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/adapters/__init__.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/adapters/claude.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/adapters/codex.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/adapters/rate_limits.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/adapters/registry.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/adapters/types.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/adapters/util.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/analyzer/__init__.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/analyzer/aggregator.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/analyzer/blocks.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/cli.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/hooks.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/i18n.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/ui/__init__.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/ui/console.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/ui/panels.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/ui/tables.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/ui/theme.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/src/ui/widgets.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/tests/test_aggregator.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/tests/test_blocks.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/tests/test_cli.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/tests/test_codex.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/tests/test_hooks.py +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/token_tracker.egg-info/SOURCES.txt +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/token_tracker.egg-info/dependency_links.txt +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/token_tracker.egg-info/entry_points.txt +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/token_tracker.egg-info/requires.txt +0 -0
- {token_tracker-0.3.7 → token_tracker-0.3.8}/token_tracker.egg-info/top_level.txt +0 -0
|
@@ -25,8 +25,12 @@ _FAMILY_FALLBACK = (
|
|
|
25
25
|
("claude-opus", "claude-opus-4-8"),
|
|
26
26
|
("claude-sonnet", "claude-sonnet-4-6"),
|
|
27
27
|
("claude-haiku", "claude-haiku-4-5-20251001"),
|
|
28
|
+
("claude-fable", "claude-fable-5"),
|
|
28
29
|
)
|
|
29
30
|
|
|
31
|
+
# 解析不到定价的模型只提示一次,避免聚合时每条 entry 刷屏
|
|
32
|
+
_warned_unknown_models: set[str] = set()
|
|
33
|
+
|
|
30
34
|
|
|
31
35
|
def get_pricing() -> dict:
|
|
32
36
|
global _pricing
|
|
@@ -44,6 +48,7 @@ def calculate_cost(entry: UsageEntry) -> float:
|
|
|
44
48
|
pricing = get_pricing()
|
|
45
49
|
model_key = _resolve_model_key(entry.model, pricing)
|
|
46
50
|
if model_key is None:
|
|
51
|
+
_warn_unknown_model_once(entry.model)
|
|
47
52
|
return 0.0
|
|
48
53
|
|
|
49
54
|
info = pricing[model_key]
|
|
@@ -85,6 +90,17 @@ def _resolve_model_key(model: str, pricing: dict) -> str | None:
|
|
|
85
90
|
return None
|
|
86
91
|
|
|
87
92
|
|
|
93
|
+
def _warn_unknown_model_once(model: str) -> None:
|
|
94
|
+
# 全新系列(非 opus/sonnet/haiku/fable)连系列兜底都接不住,成本会按 $0 计;显形以免静默少算
|
|
95
|
+
if model and model not in _warned_unknown_models:
|
|
96
|
+
_warned_unknown_models.add(model)
|
|
97
|
+
print(
|
|
98
|
+
f"token-tracker: 未知模型 {model!r} 缺少定价,本次成本按 $0 计;"
|
|
99
|
+
"litellm 收录后自动恢复,或在 cost.py 的 _fallback_pricing 补内置价",
|
|
100
|
+
file=sys.stderr,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
88
104
|
def _load_pricing() -> dict:
|
|
89
105
|
cached = _read_cache()
|
|
90
106
|
if cached is not None and not _cache_stale():
|
|
@@ -169,9 +185,18 @@ _OPUS_PRICING = {
|
|
|
169
185
|
"cache_read_input_token_cost": 0.5e-6,
|
|
170
186
|
}
|
|
171
187
|
|
|
188
|
+
# Fable 5 / Mythos 5 同价,是 Opus 档的 2 倍($10/$50 每百万 token)
|
|
189
|
+
_FABLE_PRICING = {
|
|
190
|
+
"input_cost_per_token": 10e-6,
|
|
191
|
+
"output_cost_per_token": 50e-6,
|
|
192
|
+
"cache_creation_input_token_cost": 12.5e-6,
|
|
193
|
+
"cache_read_input_token_cost": 1.0e-6,
|
|
194
|
+
}
|
|
195
|
+
|
|
172
196
|
|
|
173
197
|
def _fallback_pricing() -> dict:
|
|
174
198
|
return {
|
|
199
|
+
"claude-fable-5": _FABLE_PRICING,
|
|
175
200
|
"claude-opus-4-8": _OPUS_PRICING,
|
|
176
201
|
"claude-opus-4-7": _OPUS_PRICING,
|
|
177
202
|
"claude-opus-4-6": _OPUS_PRICING,
|
|
@@ -8,8 +8,10 @@ AGENT_SHORT = {"claude-code": "CC", "codex": "Codex"}
|
|
|
8
8
|
AGENT_LABEL = {"claude-code": "Claude Code", "codex": "Codex"}
|
|
9
9
|
|
|
10
10
|
MODEL_SHORT = {
|
|
11
|
+
"claude-fable-5": "Fable 5",
|
|
11
12
|
"claude-opus-4-6": "Opus 4.6",
|
|
12
13
|
"claude-opus-4-7": "Opus 4.7",
|
|
14
|
+
"claude-opus-4-8": "Opus 4.8",
|
|
13
15
|
"claude-sonnet-4-6": "Sonnet 4.6",
|
|
14
16
|
"claude-sonnet": "Sonnet",
|
|
15
17
|
"claude-haiku-4-5-20251001": "Haiku 4.5",
|
|
@@ -114,6 +114,48 @@ def test_fallback_pricing_includes_openai_models():
|
|
|
114
114
|
assert pricing[k].get("input_cost_per_token", 0) > 0
|
|
115
115
|
|
|
116
116
|
|
|
117
|
+
def test_fallback_pricing_includes_fable():
|
|
118
|
+
# Fable 5 是全新系列,必须有专属兜底价(不能退回 Opus,价格差一倍)
|
|
119
|
+
info = cost._fallback_pricing()["claude-fable-5"]
|
|
120
|
+
assert info["input_cost_per_token"] == pytest.approx(10e-6)
|
|
121
|
+
assert info["output_cost_per_token"] == pytest.approx(50e-6)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_fable_cost_is_double_opus(monkeypatch):
|
|
125
|
+
monkeypatch.setattr(cost, "_pricing", cost._fallback_pricing())
|
|
126
|
+
entry = make_entry(
|
|
127
|
+
model="claude-fable-5",
|
|
128
|
+
input_tokens=1_000_000,
|
|
129
|
+
output_tokens=1_000_000,
|
|
130
|
+
cache_creation_tokens=1_000_000,
|
|
131
|
+
cache_read_tokens=1_000_000,
|
|
132
|
+
)
|
|
133
|
+
# 10 + 50 + 12.5 + 1.0
|
|
134
|
+
assert cost.calculate_cost(entry) == pytest.approx(73.5)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_fable_dated_variant_resolves_via_prefix(monkeypatch):
|
|
138
|
+
monkeypatch.setattr(cost, "_pricing", cost._fallback_pricing())
|
|
139
|
+
entry = make_entry(model="claude-fable-5-20260601", input_tokens=1_000_000)
|
|
140
|
+
assert cost.calculate_cost(entry) == pytest.approx(10.0)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_unknown_fable_variant_falls_back_to_family(monkeypatch):
|
|
144
|
+
# 未来的 fable-6 即便 litellm 未收录,也按系列退回 fable-5,不归零
|
|
145
|
+
monkeypatch.setattr(cost, "_pricing", cost._fallback_pricing())
|
|
146
|
+
entry = make_entry(model="claude-fable-6-20270101", input_tokens=1_000_000)
|
|
147
|
+
assert cost.calculate_cost(entry) == pytest.approx(10.0)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_unknown_model_warns_once(fixed_pricing, monkeypatch, capsys):
|
|
151
|
+
# 全新系列接不住时按 $0 计,但必须显形提醒;同一模型只提示一次
|
|
152
|
+
monkeypatch.setattr(cost, "_warned_unknown_models", set())
|
|
153
|
+
assert cost.calculate_cost(make_entry(model="claude-quartz-1", input_tokens=1_000_000)) == 0.0
|
|
154
|
+
assert cost.calculate_cost(make_entry(model="claude-quartz-1", input_tokens=500_000)) == 0.0
|
|
155
|
+
err = capsys.readouterr().err
|
|
156
|
+
assert err.count("claude-quartz-1") == 1
|
|
157
|
+
|
|
158
|
+
|
|
117
159
|
def test_fresh_cache_is_used_without_fetching(tmp_path, monkeypatch):
|
|
118
160
|
cache = tmp_path / "pricing_cache.json"
|
|
119
161
|
cache.write_text('{"gpt-5": {"input_cost_per_token": 1e-6}}', encoding="utf-8")
|
|
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
|