sprag-cli 3.58.0 → 3.58.1
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.
- package/README.ko.md +15 -10
- package/README.md +17 -11
- package/package.json +1 -1
- package/src/agents.js +11 -4
- package/src/cost.js +130 -20
- package/src/parser.js +15 -0
- package/src/route-scan.js +1 -0
- package/src/session-cache.js +5 -1
- package/src/stats.js +11 -0
- package/src/subagent-records.js +12 -0
package/README.ko.md
CHANGED
|
@@ -46,7 +46,7 @@ npm i -g sprag-cli # 예전 이름 claude-token-saver 와 같은 패키지입
|
|
|
46
46
|
사용량 한도에 문제가 생기면 경고 칩이 줄 맨 앞에 나타납니다.
|
|
47
47
|
|
|
48
48
|
```
|
|
49
|
-
🔀 Routing saved $
|
|
49
|
+
🔀 Routing saved $188 | opus→sonnet 67× $93.4 · fable→sonnet 26× $83.7 · opus→haiku 25× $7.41 · fable→haiku 6× $3.73
|
|
50
50
|
🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 🅷 5/5 · 📦 Ctx 47% of 1M
|
|
51
51
|
```
|
|
52
52
|
|
|
@@ -74,7 +74,7 @@ Sprag 는 사용자가 지정한 모델을 바꾸지 않습니다. 이미 실행
|
|
|
74
74
|
| 공개 벤치마크 정확도 | **59.1%** · 단일 최고 모델 57.9% | `▰▰▰▰▰▰▰▰▰▰▰▰` | [벤치마크](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) |
|
|
75
75
|
| 같은 문항 총비용 | **$268** · gpt-5 $388 · gemini-2.5-pro $734 | `▰▰▰▰▰▱▱▱▱▱▱▱` | [벤치마크](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) |
|
|
76
76
|
| 같은 문서들의 토큰 | **−95.9%** · 2,011,178 → 82,209 (12건) | `▰▱▱▱▱▱▱▱▱▱▱▱` | [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) |
|
|
77
|
-
| 위임 절감액 (원장) | **$
|
|
77
|
+
| 위임 절감액 (원장) | **$188.27** · 위임 124건 | `▰▰▰▰▰▰▰▰▰▰▰▱` | `sprag route-scan savings` |
|
|
78
78
|
| 같은 프롬프트를 저가 등급에 | **비용 24%** · 여덟 건 중 한 건이 더 틀림 | `▰▰▰▱▱▱▱▱▱▱▱▱` | [A/B 실측](#같은-프롬프트를-저가-등급에-돌리면) |
|
|
79
79
|
|
|
80
80
|
앞의 두 줄은 다음 반박에 답하려고 있습니다. 저가 모델에 일을 내려보내면 정확도가 떨어질 것이라는 반박입니다. 공개 데이터에서는 떨어지지 않았습니다. 같은 판정 기준을 라우터로 만들어 11,696건에 적용했더니 정확도가 단일 최고 모델보다 1.2%p 높고 비용은 3분의 1이었습니다. 정확도가 오른 까닭은 간단합니다. 모든 유형에서 1등인 모델은 없기 때문입니다.
|
|
@@ -83,15 +83,20 @@ LLMRouterBench(Findings of ACL 2026)에서 오프라인으로 측정했습니다
|
|
|
83
83
|
|
|
84
84
|
다만 이렇게 확인한 것은 판정 기준이고, sprag 가 실제로 쓰는 모델 조합은 아닙니다. 데이터셋에 Claude 계열이 없어서 deepseek-v3 와 qwen3-235b, gpt-5 가 각각 저가·중간·상위 등급을 대신했습니다.
|
|
85
85
|
|
|
86
|
-
뒤의 세 줄은 이 머신에서 실제로 쌓인 기록이라 시간이 지나면 값이 커집니다.
|
|
86
|
+
뒤의 세 줄은 이 머신에서 실제로 쌓인 기록이라 시간이 지나면 값이 커집니다. 위임 수치는 2026-10-09 에, 문서 변환 수치는 2026-09-18 에 뽑았습니다. 모델 이름은 각 실행이 실제로 쓴 모델입니다.
|
|
87
87
|
|
|
88
88
|
### 지금 이 머신의 최근 30일
|
|
89
89
|
|
|
90
90
|
```
|
|
91
|
-
🔀 라우팅 절감 누적 $
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
opus-5
|
|
91
|
+
🔀 라우팅 절감 누적 $188.27 (최근 7일 $13.31 · 30일 $150.64)
|
|
92
|
+
|
|
93
|
+
fable-5 → sonnet-5 24회 $81.19 ▰▰▰▰▰▰▰▰▰▰▰▰
|
|
94
|
+
opus-5 → sonnet-5 54회 $68.85 ▰▰▰▰▰▰▰▰▰▰▱▱
|
|
95
|
+
opus-5 → sonnet-5.5 13회 $24.58 ▰▰▰▰▱▱▱▱▱▱▱▱
|
|
96
|
+
opus-5 → haiku-4.5 25회 $7.41 ▰▱▱▱▱▱▱▱▱▱▱▱
|
|
97
|
+
fable-5 → haiku-4.5 5회 $3.22 ▰▱▱▱▱▱▱▱▱▱▱▱
|
|
98
|
+
fable-5 → sonnet-5.5 2회 $2.51 ▰▱▱▱▱▱▱▱▱▱▱▱
|
|
99
|
+
fable-5 → haiku-5.5 1회 $0.51 ▰▱▱▱▱▱▱▱▱▱▱▱
|
|
95
100
|
```
|
|
96
101
|
|
|
97
102
|
| 함께 측정한 값 | 30일 기준 | |
|
|
@@ -122,8 +127,8 @@ LLMRouterBench(Findings of ACL 2026)에서 오프라인으로 측정했습니다
|
|
|
122
127
|
## statusline 한 줄로 보는 상태
|
|
123
128
|
|
|
124
129
|
```
|
|
125
|
-
🔀 라우팅 절감 $
|
|
126
|
-
🚨 5H ▰▰▰▰▰▰▰▰▰▰▰▱ 94% 🔄 12:36 · 🅷 5/5 · 🤖 Opus 5 · 🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 📅 weekly ▰▰▰▰▰▱▱▱▱▱▱▱ 38% · 💵 Sep $42 · 📦 Ctx 47% of 1M
|
|
130
|
+
🔀 라우팅 절감 $188 | opus→sonnet 67회 $93.4 · fable→sonnet 26회 $83.7 · opus→haiku 25회 $7.41 · fable→haiku 6회 $3.73
|
|
131
|
+
🚨 5H ▰▰▰▰▰▰▰▰▰▰▰▱ 94% 🔄 12:36 · 🅷 5/5 · 🤖 Opus 5.5 · 🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 📅 weekly ▰▰▰▰▰▱▱▱▱▱▱▱ 38% · 💵 Sep $42 · 📦 Ctx 47% of 1M
|
|
127
132
|
```
|
|
128
133
|
|
|
129
134
|
문제가 생기면 경고 칩이 줄 맨 앞으로 옵니다. `🚨 5H/7D NN%`, `⚠ Ctx 500k+`, `⚠ Cache miss`, `⚠ Input spike`, `⚠ Output heavy`, `⚠ Call surge`, `⚠ Rebuild churn`, `⚠ 5m TTL` 여덟 가지입니다. 칩 문구를 Claude 에게 그대로 붙여 넣으면 Skill 이 원인 코드와 조치 순서를 꺼내 줍니다. 세그먼트마다 무엇을 뜻하는지는 [statusline 문서](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.ko.md)에 정리했습니다.
|
|
@@ -151,7 +156,7 @@ JetBrains IDE 내장 터미널에서는 이모지 대신 한 칸 글리프로
|
|
|
151
156
|
| 측정 항목 | 값 | 표본 | 기간 |
|
|
152
157
|
|---|---|---|---|
|
|
153
158
|
| 문서 변환 | 2,011,178 → 82,209 토큰 | 12건 (xlsx·pptx·pdf) | 2026-09-18 까지 누적 |
|
|
154
|
-
| 위임 실행 | $
|
|
159
|
+
| 위임 실행 | $188.27 절감 · 건당 $1.52 | 124건 | 2026-10-09 까지 누적 |
|
|
155
160
|
| 래칫 규칙 누적 | 37건 (글로벌 30 · 프로젝트 7) | 저장소 4곳 | 2026-05-08 ~ 09-16 |
|
|
156
161
|
|
|
157
162
|
래칫 규칙과 위임 규칙은 늘어나는 방식이 다릅니다. 래칫 규칙은 사람이 저지른 실수에서 나오므로 계속 늘어납니다. 위임 규칙은 멈춥니다. 기준이 알아보는 작업 유형이 정해져 있어서, 유형마다 규칙이 하나 생기면 더 승격할 것이 없습니다.
|
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ npm i -g sprag-cli # Claude Code, plus Codex when it is installed
|
|
|
60
60
|
needs attention:
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
🔀 Routing saved $
|
|
63
|
+
🔀 Routing saved $188 | opus→sonnet 67× $93.4 · fable→sonnet 26× $83.7 · opus→haiku 25× $7.41 · fable→haiku 6× $3.73
|
|
64
64
|
🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 🅷 5/5 · 📦 Ctx 47% of 1M
|
|
65
65
|
```
|
|
66
66
|
|
|
@@ -95,7 +95,7 @@ holding.
|
|
|
95
95
|
| Public benchmark accuracy | **59.1%** vs 57.9% best single model | `▰▰▰▰▰▰▰▰▰▰▰▰` | [benchmark](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) |
|
|
96
96
|
| Cost for the same queries | **$268** vs gpt-5 $388, gemini-2.5-pro $734 | `▰▰▰▰▰▱▱▱▱▱▱▱` | [benchmark](https://github.com/rootstudioyaml/sprag/blob/main/docs/BENCHMARK.md) |
|
|
97
97
|
| Tokens for the same documents | **−95.9%**, 2,011,178 → 82,209 over 12 files | `▰▱▱▱▱▱▱▱▱▱▱▱` | [doc2md](https://github.com/rootstudioyaml/sprag/blob/main/docs/DOC2MD.md) |
|
|
98
|
-
| Delegation savings, from the ledger | **$
|
|
98
|
+
| Delegation savings, from the ledger | **$188.27** over 124 delegated runs | `▰▰▰▰▰▰▰▰▰▰▰▱` | `sprag route-scan savings` |
|
|
99
99
|
| Cheap tier on the same prompts | **24% of the cost**, one answer worse out of eight | `▰▰▰▱▱▱▱▱▱▱▱▱` | [A/B run](#the-cheap-tier-on-the-same-prompts) |
|
|
100
100
|
|
|
101
101
|
The first two rows answer the objection that comes up first: that moving work to
|
|
@@ -111,16 +111,22 @@ it again and you get the same figures. It validates the criteria, not sprag's ow
|
|
|
111
111
|
routing: the dataset has no Claude models, so deepseek-v3, qwen3-235b and gpt-5
|
|
112
112
|
stood in for cheap, mid and flagship.
|
|
113
113
|
|
|
114
|
-
The last three rows are ledgers on this machine. They grow as it runs.
|
|
115
|
-
read on 2026-09-18.
|
|
114
|
+
The last three rows are ledgers on this machine. They grow as it runs. The
|
|
115
|
+
routing figures were read on 2026-10-09, the document figures on 2026-09-18.
|
|
116
|
+
Model names are the ones each run actually used.
|
|
116
117
|
|
|
117
118
|
### Current, on one machine, last 30 days
|
|
118
119
|
|
|
119
120
|
```
|
|
120
|
-
🔀 Routing saved $
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
opus-5
|
|
121
|
+
🔀 Routing saved $188.27 total (last 7d $13.31 · last 30d $150.64)
|
|
122
|
+
|
|
123
|
+
fable-5 → sonnet-5 24 runs $81.19 ▰▰▰▰▰▰▰▰▰▰▰▰
|
|
124
|
+
opus-5 → sonnet-5 54 runs $68.85 ▰▰▰▰▰▰▰▰▰▰▱▱
|
|
125
|
+
opus-5 → sonnet-5.5 13 runs $24.58 ▰▰▰▰▱▱▱▱▱▱▱▱
|
|
126
|
+
opus-5 → haiku-4.5 25 runs $7.41 ▰▱▱▱▱▱▱▱▱▱▱▱
|
|
127
|
+
fable-5 → haiku-4.5 5 runs $3.22 ▰▱▱▱▱▱▱▱▱▱▱▱
|
|
128
|
+
fable-5 → sonnet-5.5 2 runs $2.51 ▰▱▱▱▱▱▱▱▱▱▱▱
|
|
129
|
+
fable-5 → haiku-5.5 1 run $0.51 ▰▱▱▱▱▱▱▱▱▱▱▱
|
|
124
130
|
```
|
|
125
131
|
|
|
126
132
|
| Also measured | Over 30 days | |
|
|
@@ -155,8 +161,8 @@ arrives as fewer round-trips rather than as cheaper ones.
|
|
|
155
161
|
## The statusline in one line
|
|
156
162
|
|
|
157
163
|
```
|
|
158
|
-
🔀 Routing saved $
|
|
159
|
-
🚨 5H ▰▰▰▰▰▰▰▰▰▰▰▱ 94% 🔄 12:36 · 🅷 5/5 · 🤖 Opus 5 · 🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 📅 weekly ▰▰▰▰▰▱▱▱▱▱▱▱ 38% · 💵 Sep $42 · 📦 Ctx 47% of 1M
|
|
164
|
+
🔀 Routing saved $188 | opus→sonnet 67× $93.4 · fable→sonnet 26× $83.7 · opus→haiku 25× $7.41 · fable→haiku 6× $3.73
|
|
165
|
+
🚨 5H ▰▰▰▰▰▰▰▰▰▰▰▱ 94% 🔄 12:36 · 🅷 5/5 · 🤖 Opus 5.5 · 🧠 Cache hit 98.8% · ⏳ Cache expires 59:46 · 📅 weekly ▰▰▰▰▰▱▱▱▱▱▱▱ 38% · 💵 Sep $42 · 📦 Ctx 47% of 1M
|
|
160
166
|
```
|
|
161
167
|
|
|
162
168
|
When something is wrong the warning chip leads the line: `🚨 5H/7D NN%`, `⚠ Ctx 500k+`, `⚠ Cache miss`, `⚠ Input spike`, `⚠ Output heavy`, `⚠ Call surge`, `⚠ Rebuild churn`, `⚠ 5m TTL`. Paste the chip text into Claude and the Skill names the root-cause code and the fix. Every segment is explained in [the statusline reference](https://github.com/rootstudioyaml/sprag/blob/main/docs/STATUSLINE.md).
|
|
@@ -194,7 +200,7 @@ surfaces, and never send design or diagnosis.
|
|
|
194
200
|
| Measured | Value | Sample | Window |
|
|
195
201
|
|---|---|---|---|
|
|
196
202
|
| Documents converted | 2,011,178 → 82,209 tokens | 12 files (xlsx, pptx, pdf) | to 2026-09-18 |
|
|
197
|
-
| Delegated runs | $
|
|
203
|
+
| Delegated runs | $188.27 saved, $1.52 per run | 124 runs | to 2026-10-09 |
|
|
198
204
|
| Ratchet rules accumulated | 37 (30 global, 7 per-project) | 4 repositories | 2026-05-08 to 09-16 |
|
|
199
205
|
|
|
200
206
|
The two counts grow differently. Ratchet rules come from mistakes a person made,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprag-cli",
|
|
3
|
-
"version": "3.58.
|
|
3
|
+
"version": "3.58.1",
|
|
4
4
|
"description": "Lower your Claude Code bill without changing how you work: simple tasks go to cheaper models, documents go in as compact Markdown, and the statusline shows spend and savings live. Works with Codex too.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/agents.js
CHANGED
|
@@ -45,22 +45,29 @@ export function agentExists(name, root = process.cwd()) {
|
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Korean phrase for a delegation target, e.g.
|
|
48
|
-
* agent present → `haiku-explore(model
|
|
48
|
+
* agent present → `haiku-explore(model 인자 생략)`
|
|
49
49
|
* agent absent → `model: haiku`
|
|
50
|
+
*
|
|
51
|
+
* A named agent carries its model in its own definition, and the Agent tool's
|
|
52
|
+
* `model` argument overrides that definition. The old phrase
|
|
53
|
+
* `haiku-explore(model: haiku)` read as "pass model: haiku", and Claude Code
|
|
54
|
+
* resolves the bare `haiku` alias to an older Haiku than a definition pinned to
|
|
55
|
+
* a full id (claude-haiku-5-5), so following the rule undid the pin. The phrase
|
|
56
|
+
* now says to leave the argument out.
|
|
50
57
|
*/
|
|
51
58
|
export function agentPhrase(name, { model, root } = {}) {
|
|
52
59
|
const tier = model || AGENT_MODEL[name] || 'haiku';
|
|
53
|
-
return agentExists(name, root) ? `${name}(model
|
|
60
|
+
return agentExists(name, root) ? `${name}(model 인자 생략)` : `model: ${tier}`;
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
/**
|
|
57
64
|
* English phrase, including the article, e.g.
|
|
58
|
-
* agent present → `the haiku-explore (model
|
|
65
|
+
* agent present → `the haiku-explore subagent (omit the model argument)`
|
|
59
66
|
* agent absent → `a model: haiku subagent`
|
|
60
67
|
*/
|
|
61
68
|
export function agentPhraseEn(name, { model, root } = {}) {
|
|
62
69
|
const tier = model || AGENT_MODEL[name] || 'haiku';
|
|
63
70
|
return agentExists(name, root)
|
|
64
|
-
? `the ${name} (model
|
|
71
|
+
? `the ${name} subagent (omit the model argument)`
|
|
65
72
|
: `a model: ${tier} subagent`;
|
|
66
73
|
}
|
package/src/cost.js
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cost impact estimation based on Anthropic pricing.
|
|
3
|
-
* Source: https://
|
|
4
|
-
* Prices per million tokens (USD). Updated 2026-
|
|
3
|
+
* Source: https://platform.claude.com/docs/en/about-claude/pricing
|
|
4
|
+
* Prices per million tokens (USD). Updated 2026-10 for Fable 5.1, Opus 5.5,
|
|
5
|
+
* Sonnet 5 / 5.5 and Haiku 5.5 (checked against the pricing page 2026-10-09).
|
|
5
6
|
*
|
|
6
7
|
* Note: Opus 4.5/4.6/4.7 use reduced pricing ($5/$25) vs. older Opus 4/4.1 ($15/$75).
|
|
7
8
|
* Cache writes are now tracked separately for 5m and 1h TTLs, each with their own rate.
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
// Haiku 5.5 charges a request whose prompt (input + cache writes + cache
|
|
12
|
+
// reads) exceeds this many tokens at the higher 'claude-haiku-5-5-long' rates.
|
|
13
|
+
// Each request is priced on its own, so callers accumulate such requests into
|
|
14
|
+
// `totals.long` (see parser.js) rather than relying on session-wide sums.
|
|
15
|
+
export const LONG_PROMPT_THRESHOLD = 100_000;
|
|
16
|
+
|
|
10
17
|
const PRICING = {
|
|
18
|
+
// Fable 5.1 / Mythos 5.1 — same prices as Fable 5 except cache reads, which
|
|
19
|
+
// drop to $0.25.
|
|
20
|
+
'claude-fable-5-1': {
|
|
21
|
+
input: 10.0,
|
|
22
|
+
cacheWrite5m: 12.5,
|
|
23
|
+
cacheWrite1h: 20.0,
|
|
24
|
+
cacheRead: 0.25,
|
|
25
|
+
output: 50.0,
|
|
26
|
+
},
|
|
11
27
|
// Fable 5 / Mythos 5 — premium tier above Opus ($10/$50). Cache write
|
|
12
28
|
// rates follow the standard multipliers (1.25x input for 5m, 2x for 1h),
|
|
13
29
|
// cache read is 0.1x input.
|
|
@@ -18,6 +34,14 @@ const PRICING = {
|
|
|
18
34
|
cacheRead: 1.0,
|
|
19
35
|
output: 50.0,
|
|
20
36
|
},
|
|
37
|
+
// Opus 5.5 — cheaper than the Opus 4.5-5 tier below.
|
|
38
|
+
'claude-opus-5-5': {
|
|
39
|
+
input: 4.0,
|
|
40
|
+
cacheWrite5m: 5.0,
|
|
41
|
+
cacheWrite1h: 8.0,
|
|
42
|
+
cacheRead: 0.2,
|
|
43
|
+
output: 20.0,
|
|
44
|
+
},
|
|
21
45
|
// Opus 4.5+ (new pricing tier — includes 4.5, 4.6, 4.7, 4.8, and future)
|
|
22
46
|
'claude-opus-new': {
|
|
23
47
|
input: 5.0,
|
|
@@ -34,6 +58,23 @@ const PRICING = {
|
|
|
34
58
|
cacheRead: 1.5,
|
|
35
59
|
output: 75.0,
|
|
36
60
|
},
|
|
61
|
+
// Sonnet 5 — standard price is $2/$10. The rise to $3/$15 planned for
|
|
62
|
+
// 2026-09-01 was cancelled, so this is not an introductory rate.
|
|
63
|
+
'claude-sonnet-5': {
|
|
64
|
+
input: 2.0,
|
|
65
|
+
cacheWrite5m: 2.5,
|
|
66
|
+
cacheWrite1h: 4.0,
|
|
67
|
+
cacheRead: 0.2,
|
|
68
|
+
output: 10.0,
|
|
69
|
+
},
|
|
70
|
+
// Sonnet 5.5 — same as Sonnet 5 except cache reads at $0.10.
|
|
71
|
+
'claude-sonnet-5-5': {
|
|
72
|
+
input: 2.0,
|
|
73
|
+
cacheWrite5m: 2.5,
|
|
74
|
+
cacheWrite1h: 4.0,
|
|
75
|
+
cacheRead: 0.1,
|
|
76
|
+
output: 10.0,
|
|
77
|
+
},
|
|
37
78
|
// Sonnet 4 / 4.5 / 4.6 / 3.7
|
|
38
79
|
'claude-sonnet': {
|
|
39
80
|
input: 3.0,
|
|
@@ -42,6 +83,23 @@ const PRICING = {
|
|
|
42
83
|
cacheRead: 0.3,
|
|
43
84
|
output: 15.0,
|
|
44
85
|
},
|
|
86
|
+
// Haiku 5.5, prompt <= LONG_PROMPT_THRESHOLD tokens.
|
|
87
|
+
'claude-haiku-5-5': {
|
|
88
|
+
input: 0.1,
|
|
89
|
+
cacheWrite5m: 0.125,
|
|
90
|
+
cacheWrite1h: 0.2,
|
|
91
|
+
cacheRead: 0.01,
|
|
92
|
+
output: 0.5,
|
|
93
|
+
},
|
|
94
|
+
// Haiku 5.5, prompt > LONG_PROMPT_THRESHOLD tokens. Not returned by
|
|
95
|
+
// detectPricingTier: estimateCost applies it to `totals.long` only.
|
|
96
|
+
'claude-haiku-5-5-long': {
|
|
97
|
+
input: 0.5,
|
|
98
|
+
cacheWrite5m: 0.625,
|
|
99
|
+
cacheWrite1h: 1.0,
|
|
100
|
+
cacheRead: 0.05,
|
|
101
|
+
output: 2.5,
|
|
102
|
+
},
|
|
45
103
|
// Haiku 4.5
|
|
46
104
|
'claude-haiku-4-5': {
|
|
47
105
|
input: 1.0,
|
|
@@ -72,16 +130,23 @@ const PRICING = {
|
|
|
72
130
|
* Detect pricing tier from Claude model identifier.
|
|
73
131
|
* Examples: 'claude-opus-4-7', 'claude-sonnet-4-5', 'claude-haiku-4-5'.
|
|
74
132
|
*/
|
|
75
|
-
function detectPricingTier(model) {
|
|
133
|
+
export function detectPricingTier(model) {
|
|
76
134
|
if (!model) return 'claude-sonnet';
|
|
77
135
|
const m = model.toLowerCase();
|
|
78
136
|
|
|
79
137
|
// Fable 5 / Mythos 5 — must be checked before the generic fallback:
|
|
80
138
|
// without this, 'claude-fable-5' fell through to the Sonnet tier and
|
|
81
139
|
// under-estimated costs ~3x ($3/$15 vs the real $10/$50).
|
|
82
|
-
if (m.includes('fable') || m.includes('mythos'))
|
|
140
|
+
if (m.includes('fable') || m.includes('mythos')) {
|
|
141
|
+
// `(?!\d)` keeps a date suffix (`fable-5-20261001`) from reading as 5.1.
|
|
142
|
+
if (/(fable|mythos)[-_.]?5[-_.]1(?!\d)/.test(m)) return 'claude-fable-5-1';
|
|
143
|
+
return 'claude-fable-5';
|
|
144
|
+
}
|
|
83
145
|
|
|
84
146
|
if (m.includes('opus')) {
|
|
147
|
+
// 5.5 first: `opus-5-5` would otherwise match the generic 5+ pattern, and
|
|
148
|
+
// `(?!\d)` keeps `opus-5-20260101` (Opus 5 with a date) out of 5.5.
|
|
149
|
+
if (/opus[-_.]?5[-_.]5(?!\d)/.test(m)) return 'claude-opus-5-5';
|
|
85
150
|
// Opus 4.5, 4.6, 4.7, and future 5+ use the new reduced pricing.
|
|
86
151
|
if (/opus[-_.]?4[-_.]?[5-9]\b/.test(m)) return 'claude-opus-new';
|
|
87
152
|
if (/opus[-_.]?[5-9]/.test(m)) return 'claude-opus-new';
|
|
@@ -90,6 +155,7 @@ function detectPricingTier(model) {
|
|
|
90
155
|
}
|
|
91
156
|
|
|
92
157
|
if (m.includes('haiku')) {
|
|
158
|
+
if (/haiku[-_.]?5[-_.]5(?!\d)/.test(m)) return 'claude-haiku-5-5';
|
|
93
159
|
if (/haiku[-_.]?4[-_.]?5/.test(m)) return 'claude-haiku-4-5';
|
|
94
160
|
// The 3.x ids put the version first (`claude-3-5-haiku-20241022`,
|
|
95
161
|
// `claude-3-haiku-20240307`), so both orders are matched. With only the
|
|
@@ -99,6 +165,13 @@ function detectPricingTier(model) {
|
|
|
99
165
|
return 'claude-haiku-4-5';
|
|
100
166
|
}
|
|
101
167
|
|
|
168
|
+
if (m.includes('sonnet')) {
|
|
169
|
+
if (/sonnet[-_.]?5[-_.]5(?!\d)/.test(m)) return 'claude-sonnet-5-5';
|
|
170
|
+
// Sonnet 5 incl. date-suffixed ids; the 3.x/4.x ids never put a 5 right
|
|
171
|
+
// after the family name (`claude-3-5-sonnet-2024…` has it before).
|
|
172
|
+
if (/sonnet[-_.]?5(?!\d)/.test(m)) return 'claude-sonnet-5';
|
|
173
|
+
}
|
|
174
|
+
|
|
102
175
|
// Sonnet (default fallback): 3.7, 4, 4.5, 4.6 all share the same pricing.
|
|
103
176
|
return 'claude-sonnet';
|
|
104
177
|
}
|
|
@@ -112,9 +185,14 @@ function detectPricingTier(model) {
|
|
|
112
185
|
*/
|
|
113
186
|
const TIER_RANK = {
|
|
114
187
|
'claude-fable-5': 3,
|
|
188
|
+
'claude-fable-5-1': 3,
|
|
189
|
+
'claude-opus-5-5': 2,
|
|
115
190
|
'claude-opus-legacy': 2,
|
|
116
191
|
'claude-opus-new': 2,
|
|
117
192
|
'claude-sonnet': 1,
|
|
193
|
+
'claude-sonnet-5': 1,
|
|
194
|
+
'claude-sonnet-5-5': 1,
|
|
195
|
+
'claude-haiku-5-5': 0,
|
|
118
196
|
'claude-haiku-4-5': 0,
|
|
119
197
|
'claude-haiku-3-5': 0,
|
|
120
198
|
'claude-haiku-3': 0,
|
|
@@ -203,8 +281,54 @@ function tokensToMillions(n) {
|
|
|
203
281
|
export function estimateCost(totals, model, { price } = {}) {
|
|
204
282
|
if (price) return estimateCostAtPrice(totals, price);
|
|
205
283
|
const tier = detectPricingTier(model);
|
|
206
|
-
const p = PRICING[tier];
|
|
207
284
|
|
|
285
|
+
// Haiku 5.5 prices each request by its own prompt length, so the long-prompt
|
|
286
|
+
// part (`totals.long`, same shape) is priced at the long rates and the rest
|
|
287
|
+
// at the short rates. Every other tier ignores `long`.
|
|
288
|
+
let c;
|
|
289
|
+
if (tier === 'claude-haiku-5-5' && totals.long) {
|
|
290
|
+
const short = subtractTotals(totals, totals.long);
|
|
291
|
+
const a = priceTotals(short, PRICING[tier]);
|
|
292
|
+
const b = priceTotals(totals.long, PRICING['claude-haiku-5-5-long']);
|
|
293
|
+
c = {
|
|
294
|
+
actual: a.actual + b.actual,
|
|
295
|
+
noCacheCost: a.noCacheCost + b.noCacheCost,
|
|
296
|
+
scenario5mCost: a.scenario5mCost + b.scenario5mCost,
|
|
297
|
+
write1h: a.write1h + b.write1h,
|
|
298
|
+
};
|
|
299
|
+
} else {
|
|
300
|
+
c = priceTotals(totals, PRICING[tier]);
|
|
301
|
+
}
|
|
302
|
+
const { actual, noCacheCost, scenario5mCost, write1h } = c;
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
tier,
|
|
306
|
+
actual: round(actual),
|
|
307
|
+
noCacheCost: round(noCacheCost),
|
|
308
|
+
savings: round(noCacheCost - actual),
|
|
309
|
+
savingsRate: noCacheCost > 0 ? (noCacheCost - actual) / noCacheCost : 0,
|
|
310
|
+
scenario5mCost: round(scenario5mCost),
|
|
311
|
+
extraCostIf5m: round(scenario5mCost - actual),
|
|
312
|
+
// The row asks a counterfactual: what would dropping to 5m-only cost you?
|
|
313
|
+
// With no 1h writes there is nothing to lose, and the arithmetically
|
|
314
|
+
// honest `+$0` it printed read as "5m-only is free" — the opposite of the
|
|
315
|
+
// truth for a gateway user already confined to 5m. The display layer has
|
|
316
|
+
// to change the question rather than the number.
|
|
317
|
+
extraCostIf5mApplicable: write1h > 0,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const TOTAL_FIELDS = ['input', 'cacheCreation', 'cacheRead', 'ephemeral5m', 'ephemeral1h', 'output'];
|
|
322
|
+
|
|
323
|
+
/** totals - part, field by field (clamped at 0), dropping the `long` key. */
|
|
324
|
+
function subtractTotals(totals, part) {
|
|
325
|
+
const out = {};
|
|
326
|
+
for (const f of TOTAL_FIELDS) out[f] = Math.max(0, (totals[f] ?? 0) - (part[f] ?? 0));
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Unrounded cost figures for one set of totals at one rate card. */
|
|
331
|
+
function priceTotals(totals, p) {
|
|
208
332
|
// Prefer explicit 5m/1h split when available; fall back to cacheCreation at 5m rate
|
|
209
333
|
// (conservative — 5m is cheaper than 1h).
|
|
210
334
|
const write5m = totals.ephemeral5m ?? 0;
|
|
@@ -235,21 +359,7 @@ export function estimateCost(totals, model, { price } = {}) {
|
|
|
235
359
|
tokensToMillions(Math.max(0, totals.cacheRead - extra5mCreation)) * p.cacheRead +
|
|
236
360
|
tokensToMillions(totals.output) * p.output;
|
|
237
361
|
|
|
238
|
-
return {
|
|
239
|
-
tier,
|
|
240
|
-
actual: round(actual),
|
|
241
|
-
noCacheCost: round(noCacheCost),
|
|
242
|
-
savings: round(noCacheCost - actual),
|
|
243
|
-
savingsRate: noCacheCost > 0 ? (noCacheCost - actual) / noCacheCost : 0,
|
|
244
|
-
scenario5mCost: round(scenario5mCost),
|
|
245
|
-
extraCostIf5m: round(scenario5mCost - actual),
|
|
246
|
-
// The row asks a counterfactual: what would dropping to 5m-only cost you?
|
|
247
|
-
// With no 1h writes there is nothing to lose, and the arithmetically
|
|
248
|
-
// honest `+$0` it printed read as "5m-only is free" — the opposite of the
|
|
249
|
-
// truth for a gateway user already confined to 5m. The display layer has
|
|
250
|
-
// to change the question rather than the number.
|
|
251
|
-
extraCostIf5mApplicable: write1h > 0,
|
|
252
|
-
};
|
|
362
|
+
return { actual, noCacheCost, scenario5mCost, write1h };
|
|
253
363
|
}
|
|
254
364
|
|
|
255
365
|
/**
|
package/src/parser.js
CHANGED
|
@@ -5,6 +5,7 @@ import { join, isAbsolute, basename } from 'node:path';
|
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { loadCache, getCached, putCached, saveCache, getCachedSince, putCachedSince } from './session-cache.js';
|
|
7
7
|
import { resolveModelAlias, isGatewayModelId } from './model-alias.js';
|
|
8
|
+
import { LONG_PROMPT_THRESHOLD } from './cost.js';
|
|
8
9
|
|
|
9
10
|
const CLAUDE_DIR = join(homedir(), '.claude', 'projects');
|
|
10
11
|
|
|
@@ -135,6 +136,20 @@ function addRequest(acc, r) {
|
|
|
135
136
|
acc.ephemeral5m += r.ephemeral5mTokens;
|
|
136
137
|
acc.ephemeral1h += r.ephemeral1hTokens;
|
|
137
138
|
acc.output += r.outputTokens;
|
|
139
|
+
// Haiku 5.5 bills a request whose prompt exceeds LONG_PROMPT_THRESHOLD at
|
|
140
|
+
// higher rates, per request. The sums above lose that, so such requests are
|
|
141
|
+
// also accumulated into `long` (same shape). The key only appears once a long
|
|
142
|
+
// request exists, which keeps the usual totals shape unchanged.
|
|
143
|
+
if (r.inputTokens + r.cacheCreationTokens + r.cacheReadTokens > LONG_PROMPT_THRESHOLD) {
|
|
144
|
+
if (!acc.long) acc.long = emptyTotals();
|
|
145
|
+
const l = acc.long;
|
|
146
|
+
l.input += r.inputTokens;
|
|
147
|
+
l.cacheCreation += r.cacheCreationTokens;
|
|
148
|
+
l.cacheRead += r.cacheReadTokens;
|
|
149
|
+
l.ephemeral5m += r.ephemeral5mTokens;
|
|
150
|
+
l.ephemeral1h += r.ephemeral1hTokens;
|
|
151
|
+
l.output += r.outputTokens;
|
|
152
|
+
}
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
/**
|
package/src/route-scan.js
CHANGED
|
@@ -366,6 +366,7 @@ export function runSaving(run, mainModel) {
|
|
|
366
366
|
ephemeral5m: run.ephemeral5m,
|
|
367
367
|
ephemeral1h: run.ephemeral1h,
|
|
368
368
|
output: run.out,
|
|
369
|
+
...(run.long ? { long: run.long } : {}),
|
|
369
370
|
};
|
|
370
371
|
const actual = sessionCost(totals, run.model);
|
|
371
372
|
const counterfactual = sessionCost(totals, mainModel);
|
package/src/session-cache.js
CHANGED
|
@@ -35,7 +35,11 @@ const CACHE_PATH = join(userDataDir(), 'session-cache.json');
|
|
|
35
35
|
// zero-token `<synthetic>` stubs no longer count as requests. A v3 entry has
|
|
36
36
|
// neither, so a mixed-model session cached before the bump would go on being
|
|
37
37
|
// priced at one rate until its transcript changed.
|
|
38
|
-
|
|
38
|
+
// 5: totals (and each modelTotals entry) may carry `long`, the part of the
|
|
39
|
+
// tokens from requests whose prompt exceeds 100k, which Haiku 5.5 prices at a
|
|
40
|
+
// higher rate. A v4 entry lacks it, so a Haiku 5.5 session cached before the
|
|
41
|
+
// bump would keep being priced at the short rate until its transcript changed.
|
|
42
|
+
const CACHE_VERSION = 5;
|
|
39
43
|
// Entries for transcripts this old are pruned on write. Keeps the file
|
|
40
44
|
// bounded without an existence check per entry (which would cost the syscalls
|
|
41
45
|
// the cache exists to avoid).
|
package/src/stats.js
CHANGED
|
@@ -10,6 +10,15 @@ function dateKey(date) {
|
|
|
10
10
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// Sum a session's optional `long` sub-totals (requests whose prompt exceeds
|
|
14
|
+
// 100k tokens; see parser.js) into an aggregate, creating it on first use.
|
|
15
|
+
function addLong(acc, totals) {
|
|
16
|
+
const l = totals.long;
|
|
17
|
+
if (!l) return;
|
|
18
|
+
const a = (acc.long ||= { input: 0, cacheCreation: 0, cacheRead: 0, ephemeral5m: 0, ephemeral1h: 0, output: 0 });
|
|
19
|
+
for (const k of Object.keys(a)) a[k] += l[k] || 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
function hitRate(read, creation, input) {
|
|
14
23
|
const total = read + creation + input;
|
|
15
24
|
return total > 0 ? read / total : 0;
|
|
@@ -43,6 +52,7 @@ export function dailyTrend(sessions) {
|
|
|
43
52
|
d.cacheRead += s.totals.cacheRead;
|
|
44
53
|
d.ephemeral5m += s.totals.ephemeral5m;
|
|
45
54
|
d.ephemeral1h += s.totals.ephemeral1h;
|
|
55
|
+
addLong(d, s.totals);
|
|
46
56
|
d.output += s.totals.output;
|
|
47
57
|
d.apiCalls += s.requestCount;
|
|
48
58
|
d.sessions += 1;
|
|
@@ -377,6 +387,7 @@ export function summary(sessions) {
|
|
|
377
387
|
acc.cacheRead += s.totals.cacheRead;
|
|
378
388
|
acc.ephemeral5m += s.totals.ephemeral5m;
|
|
379
389
|
acc.ephemeral1h += s.totals.ephemeral1h;
|
|
390
|
+
addLong(acc, s.totals);
|
|
380
391
|
acc.output += s.totals.output;
|
|
381
392
|
acc.apiCalls += s.requestCount;
|
|
382
393
|
acc.sessions += 1;
|
package/src/subagent-records.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
31
31
|
import { join, dirname, basename } from 'node:path';
|
|
32
32
|
import { collectSessionRecords, normalizeModelId } from './session-records.js';
|
|
33
|
+
import { LONG_PROMPT_THRESHOLD } from './cost.js';
|
|
33
34
|
|
|
34
35
|
/** Directory holding a session's subagent transcripts (may not exist). */
|
|
35
36
|
export function subagentDirFor(sessionPath) {
|
|
@@ -98,6 +99,17 @@ export async function collectSubagentRun(jsonlPath) {
|
|
|
98
99
|
run.cacheRead += r.cache_read_tokens || 0;
|
|
99
100
|
run.ephemeral5m += r.ephemeral5m || 0;
|
|
100
101
|
run.ephemeral1h += r.ephemeral1h || 0;
|
|
102
|
+
// Haiku 5.5 long-prompt split (see cost.js LONG_PROMPT_THRESHOLD); only
|
|
103
|
+
// present once a request this large exists.
|
|
104
|
+
if ((r.prompt_tokens || 0) > LONG_PROMPT_THRESHOLD) {
|
|
105
|
+
const l = (run.long ||= { input: 0, cacheCreation: 0, cacheRead: 0, ephemeral5m: 0, ephemeral1h: 0, output: 0 });
|
|
106
|
+
l.input += r.input_tokens || 0;
|
|
107
|
+
l.cacheCreation += r.cache_creation_tokens || 0;
|
|
108
|
+
l.cacheRead += r.cache_read_tokens || 0;
|
|
109
|
+
l.ephemeral5m += r.ephemeral5m || 0;
|
|
110
|
+
l.ephemeral1h += r.ephemeral1h || 0;
|
|
111
|
+
l.output += r.completion_tokens || 0;
|
|
112
|
+
}
|
|
101
113
|
run.toolErrors += r.toolErrors || 0;
|
|
102
114
|
const model = normalizeModelId(r.model);
|
|
103
115
|
byModelOut.set(model, (byModelOut.get(model) || 0) + (r.completion_tokens || 0));
|