claude-usage-limits 1.1.1 → 1.1.3
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/.claude-plugin/marketplace.json +7 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +59 -14
- package/package.json +2 -2
- package/skills/usage-limits/SKILL.md +11 -2
- package/skills/usage-limits/references/how-it-works.md +15 -3
- package/skills/usage-limits/scripts/brief.js +78 -33
- package/skills/usage-limits/scripts/usage.js +45 -6
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
{
|
|
10
10
|
"name": "usage-limits",
|
|
11
11
|
"source": "./",
|
|
12
|
-
"description": "
|
|
12
|
+
"description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
|
|
13
13
|
"category": "productivity",
|
|
14
|
-
"tags": [
|
|
14
|
+
"tags": [
|
|
15
|
+
"usage",
|
|
16
|
+
"limits",
|
|
17
|
+
"cost",
|
|
18
|
+
"planning"
|
|
19
|
+
],
|
|
15
20
|
"license": "MIT"
|
|
16
21
|
}
|
|
17
22
|
]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.1.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "1.1.3",
|
|
5
|
+
"description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Ridelink",
|
|
8
8
|
"url": "https://github.com/ridelink0"
|
package/README.md
CHANGED
|
@@ -2,14 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/claude-usage-limits)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Most usage tools for Claude Code show **you** the numbers. This one shows them
|
|
6
|
+
to **Claude**, before every prompt, and changes what it does about them.
|
|
7
7
|
|
|
8
|
-
Claude Code already knows how much of your 5-hour and weekly limit
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
Claude Code already knows how much of your 5-hour and weekly limit is gone. It
|
|
9
|
+
caches those numbers locally and will show them if you ask. What it does not do
|
|
10
|
+
is notice that the job in front of it is larger than the budget behind it. So
|
|
11
|
+
it starts anyway, and stops halfway through an edit.
|
|
12
|
+
|
|
13
|
+
This puts the budget in front of Claude before your prompt lands, so the reply
|
|
14
|
+
opens with the answer instead:
|
|
15
|
+
|
|
16
|
+
> The weekly window has about 22 turns left. That covers the parser change and
|
|
17
|
+
> its tests, but not the migration or the docs pass, so I will do the first two
|
|
18
|
+
> and leave the rest for after the reset at 09:00.
|
|
19
|
+
|
|
20
|
+
Nobody read a chart to get that. The numbers reached the model, not you.
|
|
21
|
+
|
|
22
|
+
## How this differs from a usage dashboard
|
|
23
|
+
|
|
24
|
+
There are a lot of good tools that read the same local files this does and draw
|
|
25
|
+
you a picture: status lines, menu bar apps, terminal dashboards. They are worth
|
|
26
|
+
having, and this is not trying to replace them. The difference is who the
|
|
27
|
+
output is for.
|
|
28
|
+
|
|
29
|
+
| A usage dashboard | This |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| Renders numbers for a person to read | Puts numbers in the model's context |
|
|
32
|
+
| You notice, then you interrupt | Claude notices, and adjusts on its own |
|
|
33
|
+
| Tells you 78 percent is gone | Tells you 22 turns are left, and whether what you asked for fits in them |
|
|
34
|
+
| Runs beside Claude Code | Runs inside it, as a skill and a hook |
|
|
35
|
+
| Shows what already happened | Says what to do now, and what to drop |
|
|
36
|
+
|
|
37
|
+
A percentage is a fact about the past. The useful question is whether the thing
|
|
38
|
+
you just asked for is going to finish, and answering that needs the request and
|
|
39
|
+
the budget in the same place. That place is the model's context, which is where
|
|
40
|
+
this puts them.
|
|
41
|
+
|
|
42
|
+
So: if you want to watch your usage, install a status line. This ships one too.
|
|
43
|
+
If you want the thing spending the budget to know it is spending the budget,
|
|
44
|
+
that is what this is for.
|
|
13
45
|
|
|
14
46
|
## What it prints
|
|
15
47
|
|
|
@@ -206,13 +238,19 @@ Installed as a plugin, a hook measures the budget before each prompt and puts
|
|
|
206
238
|
one line into Claude's context:
|
|
207
239
|
|
|
208
240
|
```
|
|
209
|
-
[usage-limits] 5-hour 47
|
|
210
|
-
3h 52m
|
|
241
|
+
[usage-limits] binding window is 5-hour 47% used, about 75 turns of headroom,
|
|
242
|
+
resets in 3h 52m. Other windows: weekly 16%. This session: 229 turns, $64.16.
|
|
211
243
|
```
|
|
212
244
|
|
|
245
|
+
It names the window that will stop the work first and hangs the figures off
|
|
246
|
+
that one. Two windows run at once and they are rarely in the same place, so
|
|
247
|
+
"weekly 16%" sitting next to "75 turns" would read as far more room than
|
|
248
|
+
exists.
|
|
249
|
+
|
|
213
250
|
Claude opens with it. When there is room that is a single line and it moves on:
|
|
214
251
|
|
|
215
|
-
>
|
|
252
|
+
> The 5-hour window is the binding one: 47% used, about 75 turns of headroom.
|
|
253
|
+
> This fits easily.
|
|
216
254
|
|
|
217
255
|
When there is not, the line becomes a plan rather than a status:
|
|
218
256
|
|
|
@@ -349,6 +387,9 @@ Good enough to plan with, not a bill. The honest caveats:
|
|
|
349
387
|
a percentage of the limit. On a subscription plan you are not billed them.
|
|
350
388
|
- Turns left assumes the next turns look like the last hour's. A debugging
|
|
351
389
|
spiral breaks that assumption immediately.
|
|
390
|
+
- A model released after this table was written is priced at its family's
|
|
391
|
+
average rate, and the report marks those rows with an asterisk rather than
|
|
392
|
+
passing the guess off as a published price.
|
|
352
393
|
- The cache only refreshes when Claude Code talks to the API, so after an idle
|
|
353
394
|
spell a window can sit past its own reset time. When that happens the report
|
|
354
395
|
says `stale` and the status line says `rolling` rather than reporting a
|
|
@@ -362,9 +403,13 @@ names, the formulas, and the rest of it.
|
|
|
362
403
|
```
|
|
363
404
|
.claude-plugin/plugin.json plugin manifest
|
|
364
405
|
.claude-plugin/marketplace.json lets the repo serve itself
|
|
365
|
-
skills/usage-limits/SKILL.md
|
|
366
|
-
skills/usage-limits/scripts/
|
|
367
|
-
skills/usage-limits/references/
|
|
406
|
+
skills/usage-limits/SKILL.md what Claude reads
|
|
407
|
+
skills/usage-limits/scripts/ usage.js, lowpower.js, brief.js
|
|
408
|
+
skills/usage-limits/references/ the longer notes
|
|
409
|
+
hooks/hooks.json runs brief.js before each prompt
|
|
410
|
+
commands/check.md the /usage-limits:check command
|
|
411
|
+
bin/cli.js the npx entry point
|
|
412
|
+
tools/sync-version.js keeps the manifest version in step
|
|
368
413
|
test/ node --test, no dependencies
|
|
369
414
|
```
|
|
370
415
|
|
|
@@ -374,7 +419,7 @@ test/ node --test, no dependencies
|
|
|
374
419
|
node --test
|
|
375
420
|
```
|
|
376
421
|
|
|
377
|
-
|
|
422
|
+
119 tests over the pricing, the window arithmetic, plan and credit detection,
|
|
378
423
|
the status line, the before-prompt line, job forecasting, per-project
|
|
379
424
|
attribution, the CLI, packaging, and the settings save/restore.
|
|
380
425
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.3",
|
|
4
|
+
"description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
7
7
|
"claude-code",
|
|
@@ -100,7 +100,8 @@ there is nothing to go and look up.
|
|
|
100
100
|
|
|
101
101
|
When there is room, one line, then get on with it:
|
|
102
102
|
|
|
103
|
-
>
|
|
103
|
+
> The 5-hour window is the binding one: 47% used, about 75 turns of headroom.
|
|
104
|
+
> This fits easily.
|
|
104
105
|
|
|
105
106
|
When it does not fit, that line becomes the plan:
|
|
106
107
|
|
|
@@ -116,6 +117,13 @@ Keep it to one line unless the work genuinely does not fit. The budget note is
|
|
|
116
117
|
a header, not a section, and it must never push the actual answer down the
|
|
117
118
|
page.
|
|
118
119
|
|
|
120
|
+
One thing to get right: **quote the binding window, not the roomiest one.**
|
|
121
|
+
Two windows run at once and they are rarely in the same place. The turns of
|
|
122
|
+
headroom and the reset time belong to whichever runs out first. Putting the
|
|
123
|
+
other window's percentage next to those figures claims far more room than
|
|
124
|
+
exists, and that is how a session ends mid-edit while the weekly number still
|
|
125
|
+
looks comfortable.
|
|
126
|
+
|
|
119
127
|
### Pricing the job before starting it
|
|
120
128
|
|
|
121
129
|
Size the work in turns using the table above, then price it:
|
|
@@ -198,7 +206,8 @@ a turn, which is the thing it is trying to save.
|
|
|
198
206
|
|
|
199
207
|
| Path | What it is |
|
|
200
208
|
| --- | --- |
|
|
201
|
-
| `scripts/usage.js` | The report. `--json` for raw fields, `--status` for a one-line
|
|
209
|
+
| `scripts/usage.js` | The report. `--json` for raw fields, `--status` for a one-line readout that skips the transcript scan, `--forecast N` for what an N turn job would cost. |
|
|
210
|
+
| `scripts/brief.js` | What the hook runs before each prompt. Not meant to be called by hand. |
|
|
202
211
|
| `scripts/lowpower.js` | `status`, `on`, `off`. Restores what it replaced. |
|
|
203
212
|
| `references/tactics.md` | Every lever that lowers cost, and why it works. |
|
|
204
213
|
| `references/how-it-works.md` | Where the numbers come from and where they are soft. |
|
|
@@ -131,6 +131,18 @@ immediately. Re-run the report if the shape of the work changes.
|
|
|
131
131
|
## Keeping it accurate
|
|
132
132
|
|
|
133
133
|
The rate table in `scripts/usage.js` is a plain object at the top of the file.
|
|
134
|
-
When new models ship, add a row.
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
When new models ship, add a row.
|
|
135
|
+
|
|
136
|
+
Until someone does, a model this table has not seen is priced at the average of
|
|
137
|
+
the family its name contains: an unreleased `claude-opus-5-2` is charged at the
|
|
138
|
+
mean of every Opus rate on record. Averaging assumes nothing about which
|
|
139
|
+
direction prices moved, which is why it beats pinning to whichever release
|
|
140
|
+
happened to be newest when the table was written.
|
|
141
|
+
|
|
142
|
+
A name with no recognisable family falls back to Opus rates. That is a
|
|
143
|
+
deliberate choice rather than a neutral one: over-estimating cost understates
|
|
144
|
+
your headroom, and being told you have less room than you do is the safe way to
|
|
145
|
+
be wrong about a budget.
|
|
146
|
+
|
|
147
|
+
Rows priced this way are marked with an asterisk in the report, so an assumed
|
|
148
|
+
rate never quietly passes for a published one.
|
|
@@ -38,18 +38,41 @@ function cacheFile() {
|
|
|
38
38
|
return path.join(configDir(), 'usage-limits-brief.json');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// One slot per session. A single shared slot meant that alternating between
|
|
42
|
+
// two Claude Code windows invalidated the cache on every prompt, so neither
|
|
43
|
+
// ever got a hit and both paid for a full scan each time.
|
|
44
|
+
const KEEP_SESSIONS = 5;
|
|
45
|
+
|
|
41
46
|
function readCache() {
|
|
42
47
|
try {
|
|
43
|
-
|
|
48
|
+
const parsed = JSON.parse(fs.readFileSync(cacheFile(), 'utf8'));
|
|
49
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
44
50
|
} catch (err) {
|
|
45
|
-
return
|
|
51
|
+
return {};
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
function
|
|
55
|
+
function pickCached(all, sessionId, now, ttlMs) {
|
|
56
|
+
const entry = all ? all[sessionId || '_'] : null;
|
|
57
|
+
if (!entry || !Number.isFinite(entry.at)) return null;
|
|
58
|
+
return now - entry.at < ttlMs ? entry : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Keep the newest few so a machine with many sessions does not grow the file
|
|
62
|
+
// without bound.
|
|
63
|
+
function mergeCache(all, sessionId, entry, keep) {
|
|
64
|
+
const next = Object.assign({}, all || {});
|
|
65
|
+
next[sessionId || '_'] = entry;
|
|
66
|
+
const ordered = Object.keys(next).sort((a, b) => (next[b].at || 0) - (next[a].at || 0));
|
|
67
|
+
const trimmed = {};
|
|
68
|
+
for (const key of ordered.slice(0, keep || KEEP_SESSIONS)) trimmed[key] = next[key];
|
|
69
|
+
return trimmed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeCache(all) {
|
|
50
73
|
try {
|
|
51
74
|
fs.mkdirSync(path.dirname(cacheFile()), { recursive: true });
|
|
52
|
-
fs.writeFileSync(cacheFile(), JSON.stringify(
|
|
75
|
+
fs.writeFileSync(cacheFile(), JSON.stringify(all), 'utf8');
|
|
53
76
|
} catch (err) {
|
|
54
77
|
// A cache miss costs a scan. A crash costs the prompt. Prefer the scan.
|
|
55
78
|
}
|
|
@@ -135,33 +158,47 @@ function sessionSpend(events, sessionId) {
|
|
|
135
158
|
return turns ? { turns, cost } : null;
|
|
136
159
|
}
|
|
137
160
|
|
|
138
|
-
function
|
|
161
|
+
function describeWindow(window) {
|
|
162
|
+
if (!window) return null;
|
|
163
|
+
return window.stale
|
|
164
|
+
? window.label + ' rolling over'
|
|
165
|
+
: window.label + ' ' + window.percentUsed + '%';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Everything except the window that will actually stop the work.
|
|
169
|
+
function summariseOthers(windows, bindingKey) {
|
|
139
170
|
return windows
|
|
140
|
-
.filter((window) => window.percentUsed !== null)
|
|
141
|
-
.map(
|
|
142
|
-
window.stale
|
|
143
|
-
? window.label + ' rolling over'
|
|
144
|
-
: window.label + ' ' + window.percentUsed + '%'
|
|
145
|
-
)
|
|
171
|
+
.filter((window) => window.percentUsed !== null && window.key !== bindingKey)
|
|
172
|
+
.map(describeWindow)
|
|
146
173
|
.join(', ');
|
|
147
174
|
}
|
|
148
175
|
|
|
149
176
|
function briefText(parts) {
|
|
150
|
-
|
|
151
|
-
|
|
177
|
+
// The turns and the reset time belong to one specific window. Listing every
|
|
178
|
+
// window and then the numbers invites reading them against the wrong one, so
|
|
179
|
+
// the binding window is named and its figures are attached to it.
|
|
180
|
+
const bound = [];
|
|
181
|
+
const described = describeWindow(parts.binding);
|
|
182
|
+
if (described) bound.push(described + (parts.binding.stale ? '' : ' used'));
|
|
152
183
|
if (Number.isFinite(parts.turnsLeft)) {
|
|
153
|
-
|
|
184
|
+
bound.push('about ' + parts.turnsLeft + ' turns of headroom');
|
|
154
185
|
}
|
|
155
|
-
if (parts.resetsIn)
|
|
186
|
+
if (parts.resetsIn) bound.push('resets in ' + parts.resetsIn);
|
|
187
|
+
|
|
188
|
+
const sentences = [];
|
|
189
|
+
sentences.push(
|
|
190
|
+
bound.length
|
|
191
|
+
? '[usage-limits] binding window is ' + bound.join(', ') + '.'
|
|
192
|
+
: '[usage-limits] no usable window reading.'
|
|
193
|
+
);
|
|
194
|
+
if (parts.othersSummary) sentences.push('Other windows: ' + parts.othersSummary + '.');
|
|
156
195
|
if (parts.session) {
|
|
157
|
-
|
|
158
|
-
'
|
|
159
|
-
usage.formatUSD(parts.session.cost)
|
|
196
|
+
sentences.push(
|
|
197
|
+
'This session: ' + parts.session.turns + ' turns, ' +
|
|
198
|
+
usage.formatUSD(parts.session.cost) + '.'
|
|
160
199
|
);
|
|
161
200
|
}
|
|
162
201
|
|
|
163
|
-
const head = '[usage-limits] ' + facts.join(', ') + '.';
|
|
164
|
-
|
|
165
202
|
const instruction =
|
|
166
203
|
parts.pressure === 'tight' || parts.pressure === 'gone'
|
|
167
204
|
? 'Open your reply with one line on where this leaves the budget, then say ' +
|
|
@@ -170,7 +207,14 @@ function briefText(parts) {
|
|
|
170
207
|
: 'Open your reply with one short line stating this and confirming the ' +
|
|
171
208
|
'request fits, then get on with the work. Keep it to a single line.';
|
|
172
209
|
|
|
173
|
-
|
|
210
|
+
// The mistake this guards against: quoting the roomiest window and pinning
|
|
211
|
+
// the binding window figures to it.
|
|
212
|
+
const care =
|
|
213
|
+
' Quote the binding window, not whichever one has the most left. The turns ' +
|
|
214
|
+
'and reset time above belong to the binding window alone; do not read them ' +
|
|
215
|
+
'against another window percentage.';
|
|
216
|
+
|
|
217
|
+
return sentences.join(' ') + '\n' + instruction + care;
|
|
174
218
|
}
|
|
175
219
|
|
|
176
220
|
async function run(now, hookInput) {
|
|
@@ -186,30 +230,27 @@ async function run(now, hookInput) {
|
|
|
186
230
|
const cheap = usage.buildWindows(base.utilization, [], now);
|
|
187
231
|
if (!cheap.length) return '';
|
|
188
232
|
|
|
189
|
-
const
|
|
190
|
-
const
|
|
191
|
-
cached &&
|
|
192
|
-
Number.isFinite(cached.at) &&
|
|
193
|
-
now - cached.at < config.cacheSeconds * SECOND &&
|
|
194
|
-
cached.sessionId === sessionId;
|
|
233
|
+
const all = readCache();
|
|
234
|
+
const cached = pickCached(all, sessionId, now, config.cacheSeconds * SECOND);
|
|
195
235
|
|
|
196
|
-
let turnsLeft =
|
|
197
|
-
let session =
|
|
236
|
+
let turnsLeft = cached ? cached.turnsLeft : null;
|
|
237
|
+
let session = cached ? cached.session : null;
|
|
198
238
|
let windows = cheap;
|
|
199
239
|
|
|
200
|
-
if (!
|
|
240
|
+
if (!cached) {
|
|
201
241
|
const events = await usage.readEvents(now - 8 * DAY);
|
|
202
242
|
windows = usage.buildWindows(base.utilization, events, now);
|
|
203
243
|
const binding = usage.bindingWindow(windows);
|
|
204
244
|
turnsLeft = binding && Number.isFinite(binding.turnsLeft) ? binding.turnsLeft : null;
|
|
205
245
|
session = sessionSpend(events, sessionId);
|
|
206
|
-
writeCache({ at: now, turnsLeft, session,
|
|
246
|
+
writeCache(mergeCache(all, sessionId, { at: now, turnsLeft, session }, KEEP_SESSIONS));
|
|
207
247
|
}
|
|
208
248
|
|
|
209
249
|
const binding = usage.bindingWindow(windows) || windows[0];
|
|
210
250
|
|
|
211
251
|
return briefText({
|
|
212
|
-
|
|
252
|
+
binding,
|
|
253
|
+
othersSummary: summariseOthers(windows, binding && binding.key),
|
|
213
254
|
turnsLeft,
|
|
214
255
|
resetsIn:
|
|
215
256
|
binding && !binding.stale && binding.msToReset !== null
|
|
@@ -240,9 +281,13 @@ module.exports = {
|
|
|
240
281
|
aheadOfPace,
|
|
241
282
|
pressure,
|
|
242
283
|
sessionSpend,
|
|
243
|
-
|
|
284
|
+
describeWindow,
|
|
285
|
+
summariseOthers,
|
|
244
286
|
briefText,
|
|
245
287
|
settings,
|
|
288
|
+
pickCached,
|
|
289
|
+
mergeCache,
|
|
290
|
+
KEEP_SESSIONS,
|
|
246
291
|
run,
|
|
247
292
|
cacheFile,
|
|
248
293
|
};
|
|
@@ -37,8 +37,43 @@ const RATES = {
|
|
|
37
37
|
'claude-sonnet-4-6': { input: 3, output: 15 },
|
|
38
38
|
'claude-haiku-4-5': { input: 1, output: 5 },
|
|
39
39
|
};
|
|
40
|
+
// A model can ship before this table knows about it. Rather than refusing to
|
|
41
|
+
// price it, fall back to the average of the family it names. Averaging assumes
|
|
42
|
+
// nothing about which direction prices moved, unlike pinning to one release.
|
|
43
|
+
// An unrecognised family falls back to Opus rates on purpose: over-estimating
|
|
44
|
+
// cost understates headroom, and that is the safe direction for a budget.
|
|
45
|
+
const FAMILIES = ['fable', 'mythos', 'opus', 'sonnet', 'haiku'];
|
|
40
46
|
const FALLBACK_RATE = { input: 5, output: 25 };
|
|
41
47
|
|
|
48
|
+
function familyOf(model) {
|
|
49
|
+
const id = String(model || '').toLowerCase();
|
|
50
|
+
for (const family of FAMILIES) {
|
|
51
|
+
// Mythos is priced with Fable, so it counts as the same family.
|
|
52
|
+
if (id.indexOf(family) !== -1) return family === 'mythos' ? 'fable' : family;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function familyAverage(family, table) {
|
|
58
|
+
if (!family) return null;
|
|
59
|
+
const rates = table || RATES;
|
|
60
|
+
const members = Object.keys(rates).filter((id) => familyOf(id) === family);
|
|
61
|
+
if (!members.length) return null;
|
|
62
|
+
|
|
63
|
+
let input = 0;
|
|
64
|
+
let output = 0;
|
|
65
|
+
for (const id of members) {
|
|
66
|
+
input += rates[id].input;
|
|
67
|
+
output += rates[id].output;
|
|
68
|
+
}
|
|
69
|
+
return { input: input / members.length, output: output / members.length };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Whether the price came from the table or from an assumption.
|
|
73
|
+
function isKnownModel(model) {
|
|
74
|
+
return Object.prototype.hasOwnProperty.call(RATES, String(model || '').toLowerCase());
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
// Cache traffic is priced as a multiple of the input rate.
|
|
43
78
|
const CACHE_WRITE_5M = 1.25;
|
|
44
79
|
const CACHE_WRITE_1H = 2;
|
|
@@ -129,11 +164,7 @@ function readJson(file) {
|
|
|
129
164
|
function rateFor(model) {
|
|
130
165
|
const id = String(model || '').toLowerCase();
|
|
131
166
|
if (RATES[id]) return RATES[id];
|
|
132
|
-
|
|
133
|
-
if (id.includes('opus')) return RATES['claude-opus-5'];
|
|
134
|
-
if (id.includes('sonnet')) return RATES['claude-sonnet-5'];
|
|
135
|
-
if (id.includes('haiku')) return RATES['claude-haiku-4-5'];
|
|
136
|
-
return FALLBACK_RATE;
|
|
167
|
+
return familyAverage(familyOf(id)) || FALLBACK_RATE;
|
|
137
168
|
}
|
|
138
169
|
|
|
139
170
|
// Cost of one assistant turn, in USD, from its usage record.
|
|
@@ -300,6 +331,7 @@ function byModel(events) {
|
|
|
300
331
|
if (!rows.has(id)) {
|
|
301
332
|
rows.set(id, {
|
|
302
333
|
model: id,
|
|
334
|
+
estimated: !isKnownModel(id),
|
|
303
335
|
turns: 0,
|
|
304
336
|
tokens: 0,
|
|
305
337
|
cost: 0,
|
|
@@ -829,12 +861,16 @@ function render(data) {
|
|
|
829
861
|
);
|
|
830
862
|
for (const row of data.models) {
|
|
831
863
|
lines.push(
|
|
832
|
-
' ' + pad(' ' + row.model
|
|
864
|
+
' ' + pad(' ' + row.model + (row.estimated ? ' *' : ''), 24) +
|
|
865
|
+
padLeft(row.turns, 7) +
|
|
833
866
|
padLeft(formatTokens(row.tokens), 10) +
|
|
834
867
|
padLeft(formatTokens(row.parts.output), 9) +
|
|
835
868
|
padLeft(Math.round(row.share * 100) + '%', 8)
|
|
836
869
|
);
|
|
837
870
|
}
|
|
871
|
+
if (data.models.some((row) => row.estimated)) {
|
|
872
|
+
lines.push(' * no published rate for this one yet, priced at the family average');
|
|
873
|
+
}
|
|
838
874
|
if (data.tokens) {
|
|
839
875
|
lines.push(
|
|
840
876
|
' Tokens input ' + formatTokens(data.tokens.input) +
|
|
@@ -1020,6 +1056,9 @@ module.exports = {
|
|
|
1020
1056
|
RATES,
|
|
1021
1057
|
WINDOWS,
|
|
1022
1058
|
rateFor,
|
|
1059
|
+
familyOf,
|
|
1060
|
+
familyAverage,
|
|
1061
|
+
isKnownModel,
|
|
1023
1062
|
costOf,
|
|
1024
1063
|
tokensOf,
|
|
1025
1064
|
eventFrom,
|