claude-usage-limits 1.8.0 → 1.9.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +42 -0
- package/bin/cli.js +1 -0
- package/package.json +1 -1
- package/skills/usage-limits/SKILL.md +40 -0
- package/skills/usage-limits/references/how-it-works.md +18 -1
- package/skills/usage-limits/references/tactics.md +7 -0
- package/skills/usage-limits/scripts/brief.js +3 -1
- package/skills/usage-limits/scripts/lowpower.js +9 -0
- package/skills/usage-limits/scripts/recommend.js +295 -0
- package/skills/usage-limits/scripts/usage.js +101 -20
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.9.1",
|
|
5
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"description": "Reports how much of your Codex usage limit is left as turns of work rather than a percentage, prices a job before you start it, and counts the other agents sharing the same budget.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ridelink",
|
package/README.md
CHANGED
|
@@ -430,6 +430,44 @@ times one that answers from context. The upper end is the honest one for a long
|
|
|
430
430
|
run, since turns get dearer as the context grows.
|
|
431
431
|
|
|
432
432
|
|
|
433
|
+
## Which effort and model should this run at
|
|
434
|
+
|
|
435
|
+
```
|
|
436
|
+
node skills/usage-limits/scripts/usage.js --recommend # against the headroom
|
|
437
|
+
node skills/usage-limits/scripts/usage.js --recommend 15 # against a 15 turn job
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
Recommendation for 15 turns
|
|
442
|
+
|
|
443
|
+
Posture tight - a 15 turn job fits, but only just (5-hour window, 12% left, resets in 1h 40m)
|
|
444
|
+
Effort xhigh -> medium; one notch covers the mechanical stretches; keep judgement calls at full effort
|
|
445
|
+
this session: /effort medium (only the user can run it)
|
|
446
|
+
new sessions: node scripts/lowpower.js on --effort medium
|
|
447
|
+
Model keep opus for the judgement; the saving is in where the mechanical bulk runs
|
|
448
|
+
dispatch self-contained mechanical work to a subagent on sonnet at low effort, and keep the judgement here
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
It weighs the binding window, the measured cost of a turn, and how much of the
|
|
452
|
+
output is actually reasoning, then names a posture - roomy, tight, critical, or
|
|
453
|
+
reset-first - and the exact commands. When there is room it says to keep
|
|
454
|
+
everything as it is, out loud, because turning effort down when the budget is
|
|
455
|
+
not tight buys nothing and costs quality. When reasoning is only a sliver of
|
|
456
|
+
the output it says so too, and leaves effort alone: the reasoning share is the
|
|
457
|
+
ceiling on what lowering effort can save. `--json` returns the decision as an
|
|
458
|
+
object.
|
|
459
|
+
|
|
460
|
+
The three levers it recommends across belong to different hands. The running
|
|
461
|
+
session's effort and model are the user's alone (`/effort`, `/model`, applied
|
|
462
|
+
immediately); new sessions belong to `lowpower.js`, which writes `settings.json`
|
|
463
|
+
for the next launch; and delegated work belongs to the agent itself, which can
|
|
464
|
+
dispatch a subagent on any model at any effort, mid-session, with no one asked.
|
|
465
|
+
No script or hook can change the model or effort of a session already running -
|
|
466
|
+
`settings.json` is read at launch and hook output has no model field - which is
|
|
467
|
+
why the recommendation separates "this session" from "new sessions" instead of
|
|
468
|
+
pretending one command covers both.
|
|
469
|
+
|
|
470
|
+
|
|
433
471
|
## Plans
|
|
434
472
|
|
|
435
473
|
It reads which plan you are on and adjusts what it tells you, because the
|
|
@@ -545,6 +583,10 @@ overwrite the saved originals.
|
|
|
545
583
|
The file change applies to new sessions. For a session already running,
|
|
546
584
|
`/effort low` does the same thing immediately.
|
|
547
585
|
|
|
586
|
+
`--effort max` is refused: `settings.json` does not accept `max`, so saving it
|
|
587
|
+
would store a value the next session silently ignores. `max` lives in
|
|
588
|
+
`/effort` and `CLAUDE_CODE_EFFORT_LEVEL` only.
|
|
589
|
+
|
|
548
590
|
That covers the setting. The larger saving is behavioural, and the skill file
|
|
549
591
|
spells it out: batch tool calls, read line ranges instead of whole files, skip
|
|
550
592
|
subagents when the context already exists, stop retrying a fix that is not
|
package/bin/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ const HELP = `claude-usage-limits - how much agent usage is left, and whether th
|
|
|
11
11
|
claude-usage-limits --json the same numbers, machine readable
|
|
12
12
|
claude-usage-limits --status one short line, for a status line
|
|
13
13
|
claude-usage-limits --forecast 15 what a 15 turn job would cost
|
|
14
|
+
claude-usage-limits --recommend [15] which effort and model this budget calls for
|
|
14
15
|
claude-usage-limits --sessions what recent sessions cost
|
|
15
16
|
claude-usage-limits --session last one session in full, or give its id
|
|
16
17
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
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",
|
|
@@ -272,6 +272,45 @@ to `low` is the largest per-turn saving available without changing model or
|
|
|
272
272
|
scope. The file change applies to new sessions; for the session already
|
|
273
273
|
running, `/effort low` takes effect immediately.
|
|
274
274
|
|
|
275
|
+
### Choosing the level
|
|
276
|
+
|
|
277
|
+
Do not guess which effort or model the budget calls for; the report can say:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
node scripts/usage.js --recommend # against the headroom in general
|
|
281
|
+
node scripts/usage.js --recommend 15 # against a 15 turn job
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
It weighs the binding window, the measured cost of a turn, and how much of
|
|
285
|
+
the output is actually reasoning, then names the posture (roomy, tight,
|
|
286
|
+
critical, or reset-first) and the exact commands. When there is room it says
|
|
287
|
+
to keep everything as it is, out loud, so cheapness never becomes a habit.
|
|
288
|
+
Add `--json` for the decision as fields.
|
|
289
|
+
|
|
290
|
+
The three levers it recommends across belong to different hands, and keeping
|
|
291
|
+
that straight is the whole trick:
|
|
292
|
+
|
|
293
|
+
| Lever | Whose hand | When it acts |
|
|
294
|
+
| --- | --- | --- |
|
|
295
|
+
| `/effort`, `/model` | the user's only | this session, immediately |
|
|
296
|
+
| `lowpower.js on` (writes settings.json) | yours, right now | new sessions, at launch |
|
|
297
|
+
| subagent model and effort | yours, freely | that dispatch, immediately |
|
|
298
|
+
|
|
299
|
+
The running session's own model and effort cannot be changed by any script or
|
|
300
|
+
hook: settings.json is read at launch and hook output has no model field. So
|
|
301
|
+
when the recommendation says `/effort low`, put that in front of the user as
|
|
302
|
+
one short line and keep working; do not wait on it. What can be done without
|
|
303
|
+
asking anyone is the other two rows: write the next session's settings with
|
|
304
|
+
`lowpower.js`, and send self-contained mechanical bulk to a subagent on a
|
|
305
|
+
cheaper model at low effort, which is a change of model that needs nobody's
|
|
306
|
+
permission. The cold start still costs (see `references/tactics.md` on
|
|
307
|
+
subagents), so delegate work that is big and mechanical, not quick questions.
|
|
308
|
+
|
|
309
|
+
One catch to know about: `settings.json` does not accept `max`, so a saved
|
|
310
|
+
effort level tops out at `xhigh`. `max` only survives through `/effort` or the
|
|
311
|
+
`CLAUDE_CODE_EFFORT_LEVEL` environment variable, and `lowpower.js` refuses to
|
|
312
|
+
write it rather than save a value the next session would silently ignore.
|
|
313
|
+
|
|
275
314
|
Do not take that on trust: the report measures it. Under the model table it
|
|
276
315
|
says how much of the output was reasoning and what that cost, for example
|
|
277
316
|
|
|
@@ -394,5 +433,6 @@ a turn, which is the thing it is trying to save.
|
|
|
394
433
|
| `scripts/codex.js` | The Codex reader: the meter and the pace out of `~/.codex/sessions`, plus the live `--refresh` call. |
|
|
395
434
|
| `scripts/install-codex-hook.js` | `status`, `on`, `off`. Installs the Codex-side instruction, which Claude Code does not need. |
|
|
396
435
|
| `scripts/lowpower.js` | `status`, `on`, `off`. Restores what it replaced. Claude Code only. |
|
|
436
|
+
| `scripts/recommend.js` | The chooser behind `usage.js --recommend`: posture, then the effort and model commands for each lever. Not meant to be called by hand. |
|
|
397
437
|
| `references/tactics.md` | Every lever that lowers cost, and why it works. |
|
|
398
438
|
| `references/how-it-works.md` | Where the numbers come from and where they are soft. |
|
|
@@ -130,7 +130,17 @@ calculation.
|
|
|
130
130
|
**List prices are a proxy.** The rate table is first-party API pricing. How a
|
|
131
131
|
subscription plan actually meters usage is not published, and the weighting
|
|
132
132
|
almost certainly is not exactly this. It is close enough for ratios, which is
|
|
133
|
-
all it is used for.
|
|
133
|
+
all it is used for. No published or community source shows the meter weighting
|
|
134
|
+
models differently from their dollar prices, so calibrating dollars against
|
|
135
|
+
your own meter remains the best method anyone outside Anthropic has.
|
|
136
|
+
|
|
137
|
+
**The snapshot is slow by design.** The percentages come from Claude Code's
|
|
138
|
+
own cache of the account meter, which refreshes on its own schedule - roughly
|
|
139
|
+
hourly in practice, because the endpoint behind it rate-limits aggressive
|
|
140
|
+
polling. Between refreshes every figure here is the last real reading plus
|
|
141
|
+
arithmetic. That is why an old snapshot is reported as a floor with its age
|
|
142
|
+
attached rather than dressed up as a current percentage, and why `/usage` is
|
|
143
|
+
the one way to force a fresh reading.
|
|
134
144
|
|
|
135
145
|
**A reset time can be in the past.** The cache refreshes when Claude Code
|
|
136
146
|
talks to the API, so an idle spell leaves it behind. A window whose `resets_at`
|
|
@@ -168,6 +178,13 @@ immediately. Re-run the report if the shape of the work changes.
|
|
|
168
178
|
The rate table in `scripts/usage.js` is a plain object at the top of the file.
|
|
169
179
|
When new models ship, add a row.
|
|
170
180
|
|
|
181
|
+
A bracketed suffix on a model id (`claude-sonnet-5[1m]`) is stripped before
|
|
182
|
+
the lookup: it marks a context-window variant of the same model, not a new
|
|
183
|
+
one. Cache reads price at a tenth of the input rate unless a row carries a
|
|
184
|
+
`cacheRead` figure of its own - Fable and Mythos 5.1 price reads outright at
|
|
185
|
+
$0.25 per million, far under the tenth rule, and reads are the dominant input
|
|
186
|
+
in exactly the long sessions where the difference matters.
|
|
187
|
+
|
|
171
188
|
Until someone does, a model this table has not seen is priced at the average of
|
|
172
189
|
the family its name contains: an unreleased `claude-opus-5-2` is charged at the
|
|
173
190
|
mean of every Opus rate on record. Averaging assumes nothing about which
|
|
@@ -56,6 +56,13 @@ Switch at a task boundary rather than mid-task, because a model switch
|
|
|
56
56
|
invalidates the prompt cache and the rebuild can cost more than the saving on
|
|
57
57
|
a short remaining task.
|
|
58
58
|
|
|
59
|
+
The switch needs no boundary at all when the work is delegated: a subagent
|
|
60
|
+
can be dispatched on a cheaper model at low effort mid-session, and that is
|
|
61
|
+
the one model change the agent can make without anyone's hand on `/model`.
|
|
62
|
+
It pays the cold start from lever 7, so it is for self-contained bulky work,
|
|
63
|
+
not quick questions. `node scripts/usage.js --recommend` prices whether the
|
|
64
|
+
budget calls for it.
|
|
65
|
+
|
|
59
66
|
### 3. Read less, and read it once
|
|
60
67
|
|
|
61
68
|
- Line ranges instead of whole files. Grep with a result limit instead of
|
|
@@ -502,7 +502,9 @@ function briefText(parts) {
|
|
|
502
502
|
'save at clean boundaries as you go, and keep a short running note of ' +
|
|
503
503
|
'what is done, what is next, and which files are mid-change, so that ' +
|
|
504
504
|
'stopping at any moment loses nothing. Say in one line what may not ' +
|
|
505
|
-
'land before the reset, then keep working.'
|
|
505
|
+
'land before the reset, then keep working. If part of what remains is ' +
|
|
506
|
+
'mechanical, node scripts/usage.js --recommend (from the skill directory) ' +
|
|
507
|
+
'names the effort and model it should run at.'
|
|
506
508
|
: 'Open your reply with one short line stating this and confirming the ' +
|
|
507
509
|
'request fits, then get on with the work. Keep it to a single line. ' +
|
|
508
510
|
'There is room, so use it: work at full quality, take on the whole ' +
|
|
@@ -81,6 +81,15 @@ function planApply(settings, options, existingState) {
|
|
|
81
81
|
if (EFFORT_LEVELS.indexOf(effort) === -1) {
|
|
82
82
|
throw new Error('unknown effort "' + effort + '", expected one of ' + EFFORT_LEVELS.join(', '));
|
|
83
83
|
}
|
|
84
|
+
// Claude Code refuses 'max' in settings.json; it only survives through
|
|
85
|
+
// /effort or CLAUDE_CODE_EFFORT_LEVEL. Writing it here would save a value
|
|
86
|
+
// the next session silently ignores, which is worse than an error.
|
|
87
|
+
if (effort === 'max') {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"settings.json does not accept 'max'. Use --effort xhigh here, and /effort max " +
|
|
90
|
+
'or CLAUDE_CODE_EFFORT_LEVEL=max for the sessions that need it.'
|
|
91
|
+
);
|
|
92
|
+
}
|
|
84
93
|
wanted.effortLevel = effort;
|
|
85
94
|
if (options.model) wanted.model = options.model;
|
|
86
95
|
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Turns the budget figures into a choice of effort and model, and says where
|
|
4
|
+
// each half of that choice can actually be applied. There are three levers and
|
|
5
|
+
// they belong to different hands:
|
|
6
|
+
//
|
|
7
|
+
// - The running session's effort and model are the user's: only /effort and
|
|
8
|
+
// /model change them, and they change them immediately.
|
|
9
|
+
// - New sessions are the script's: lowpower.js writes effortLevel and model
|
|
10
|
+
// into settings.json, which Claude Code reads at launch.
|
|
11
|
+
// - Delegated work is Claude's alone: a subagent can be dispatched on any
|
|
12
|
+
// model at any effort, mid-session, with no one asked.
|
|
13
|
+
//
|
|
14
|
+
// Nothing here touches disk. decide() is a pure function over the same report
|
|
15
|
+
// data usage.js already gathers, so the reasoning can be tested without a
|
|
16
|
+
// transcript in sight.
|
|
17
|
+
|
|
18
|
+
// One notch down, not a cliff. Dropping xhigh to low on work that still has
|
|
19
|
+
// judgement in it costs more in rework than it saves; the ladder loses height
|
|
20
|
+
// a step at a time and 'critical' is the only posture that goes straight to
|
|
21
|
+
// the floor.
|
|
22
|
+
const NEXT_LOWER = { max: 'high', xhigh: 'medium', high: 'medium', medium: 'low', low: 'low' };
|
|
23
|
+
|
|
24
|
+
// Below this share of output, reasoning is not where the money is going, and
|
|
25
|
+
// turning effort down would trade quality for a saving that is not there.
|
|
26
|
+
const REASONING_FLOOR = 0.1;
|
|
27
|
+
|
|
28
|
+
// Turns-left walls used when no job size is given. Ten turns is barely a
|
|
29
|
+
// feature; twenty-five is room for one, carefully.
|
|
30
|
+
const CRITICAL_TURNS = 10;
|
|
31
|
+
const TIGHT_TURNS = 25;
|
|
32
|
+
|
|
33
|
+
const HOUR = 60 * 60 * 1000;
|
|
34
|
+
|
|
35
|
+
// Where the mechanical bulk should go when it is delegated. One tier down
|
|
36
|
+
// from whatever is doing the judgement; haiku is already the floor.
|
|
37
|
+
function delegateModel(model) {
|
|
38
|
+
const name = String(model || '').toLowerCase();
|
|
39
|
+
if (name.indexOf('haiku') !== -1) return 'haiku';
|
|
40
|
+
if (name.indexOf('sonnet') !== -1) return 'haiku';
|
|
41
|
+
return 'sonnet';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The effort actually in force. settings.json says 'default' when nothing is
|
|
45
|
+
// set, and the measured dominant effort of recent turns is better evidence
|
|
46
|
+
// than a guess; xhigh is what Claude Code defaults to when neither knows.
|
|
47
|
+
function currentEffort(settings, recentEffort) {
|
|
48
|
+
const set = settings && settings.effortLevel;
|
|
49
|
+
if (set && set !== 'default') return set;
|
|
50
|
+
if (recentEffort) return recentEffort;
|
|
51
|
+
return 'xhigh';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function decide(inputs) {
|
|
55
|
+
const binding = inputs.binding;
|
|
56
|
+
const rates = inputs.rates;
|
|
57
|
+
const settings = inputs.settings || {};
|
|
58
|
+
const effortNow = currentEffort(settings, inputs.recentEffort);
|
|
59
|
+
const modelNow = (settings.model && settings.model !== 'default' && settings.model) || 'default';
|
|
60
|
+
|
|
61
|
+
const base = {
|
|
62
|
+
posture: 'unknown',
|
|
63
|
+
reason: null,
|
|
64
|
+
turnsLeft: null,
|
|
65
|
+
effort: { current: effortNow, target: effortNow, changes: false, why: null },
|
|
66
|
+
model: { current: modelNow, delegate: null, nextSession: null, why: null },
|
|
67
|
+
apply: { now: null, next: null, delegate: null },
|
|
68
|
+
notes: [],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (!binding || binding.stale) {
|
|
72
|
+
base.reason = 'no fresh reading of the binding window';
|
|
73
|
+
return base;
|
|
74
|
+
}
|
|
75
|
+
if (!rates || !Number.isFinite(rates.median) || rates.median <= 0) {
|
|
76
|
+
base.reason = 'no measured turn cost to price the budget in turns';
|
|
77
|
+
return base;
|
|
78
|
+
}
|
|
79
|
+
if (!Number.isFinite(binding.usdPerPercent) || binding.usdPerPercent <= 0) {
|
|
80
|
+
base.reason = 'the binding window has no calibrated price per point yet';
|
|
81
|
+
return base;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const percentLeft = Number.isFinite(binding.percentLeft) ? binding.percentLeft : 0;
|
|
85
|
+
const turnsLeft = Math.floor((percentLeft * binding.usdPerPercent) / rates.median);
|
|
86
|
+
base.turnsLeft = turnsLeft;
|
|
87
|
+
|
|
88
|
+
// When the clock wins the race, the limit is not the constraint and there
|
|
89
|
+
// is nothing to buy by economising: whatever is left at the reset is lost.
|
|
90
|
+
const pace = inputs.recentTurnsPerHour;
|
|
91
|
+
if (
|
|
92
|
+
Number.isFinite(binding.msToReset) &&
|
|
93
|
+
binding.msToReset > 0 &&
|
|
94
|
+
Number.isFinite(pace) &&
|
|
95
|
+
pace > 0 &&
|
|
96
|
+
(binding.msToReset / HOUR) * pace < turnsLeft * 0.8
|
|
97
|
+
) {
|
|
98
|
+
base.posture = 'reset-first';
|
|
99
|
+
base.reason = 'the window resets before this pace can spend it';
|
|
100
|
+
base.effort.why = 'the budget is not the constraint';
|
|
101
|
+
base.model.why = 'the budget is not the constraint';
|
|
102
|
+
return base;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// With a job size, the forecast arithmetic decides. Without one, the raw
|
|
106
|
+
// turns of headroom do. Both use the expensive end of the measured spread,
|
|
107
|
+
// because that is the honest number for a long run.
|
|
108
|
+
if (Number.isFinite(inputs.turns) && inputs.turns > 0) {
|
|
109
|
+
const percentHigh = (inputs.turns * rates.high) / binding.usdPerPercent;
|
|
110
|
+
if (percentHigh > percentLeft) {
|
|
111
|
+
base.posture = 'critical';
|
|
112
|
+
base.reason = 'a ' + inputs.turns + ' turn job does not fit in what is left';
|
|
113
|
+
} else if (percentHigh > percentLeft * 0.75) {
|
|
114
|
+
base.posture = 'tight';
|
|
115
|
+
base.reason = 'a ' + inputs.turns + ' turn job fits, but only just';
|
|
116
|
+
} else {
|
|
117
|
+
base.posture = 'roomy';
|
|
118
|
+
base.reason = 'a ' + inputs.turns + ' turn job fits with room to spare';
|
|
119
|
+
}
|
|
120
|
+
} else if (percentLeft <= 0 || turnsLeft <= 0) {
|
|
121
|
+
base.posture = 'critical';
|
|
122
|
+
base.reason = 'the binding window is spent';
|
|
123
|
+
} else if (turnsLeft <= CRITICAL_TURNS) {
|
|
124
|
+
base.posture = 'critical';
|
|
125
|
+
base.reason = 'about ' + turnsLeft + ' turns of headroom';
|
|
126
|
+
} else if (turnsLeft <= TIGHT_TURNS) {
|
|
127
|
+
base.posture = 'tight';
|
|
128
|
+
base.reason = 'about ' + turnsLeft + ' turns of headroom';
|
|
129
|
+
} else {
|
|
130
|
+
base.posture = 'roomy';
|
|
131
|
+
base.reason = 'about ' + turnsLeft + ' turns of headroom';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (base.posture === 'roomy') {
|
|
135
|
+
base.effort.why = 'cheapness is not a virtue when the budget is not tight';
|
|
136
|
+
base.model.why = 'cheapness is not a virtue when the budget is not tight';
|
|
137
|
+
return base;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Effort is the biggest per-turn lever, but only when reasoning is actually
|
|
141
|
+
// where the money goes. The reasoning share is the ceiling on the saving,
|
|
142
|
+
// so a small share means the honest advice is to leave effort alone.
|
|
143
|
+
const share = inputs.reasoningShare;
|
|
144
|
+
if (Number.isFinite(share) && share < REASONING_FLOOR) {
|
|
145
|
+
base.effort.why =
|
|
146
|
+
'reasoning is only ' + Math.round(share * 100) +
|
|
147
|
+
'% of output, so effort is not where the money is going';
|
|
148
|
+
} else {
|
|
149
|
+
const target = base.posture === 'critical' ? 'low' : NEXT_LOWER[effortNow] || 'medium';
|
|
150
|
+
if (target !== effortNow) {
|
|
151
|
+
base.effort.target = target;
|
|
152
|
+
base.effort.changes = true;
|
|
153
|
+
base.effort.why =
|
|
154
|
+
base.posture === 'critical'
|
|
155
|
+
? 'reasoning is billed as output, and low is the largest saving that changes nothing else'
|
|
156
|
+
: 'one notch covers the mechanical stretches; keep judgement calls at full effort';
|
|
157
|
+
} else {
|
|
158
|
+
base.effort.why = 'already at the floor for this posture';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The main model is only worth flipping when things are critical, and even
|
|
163
|
+
// then it lands in settings.json for the next session: switching the running
|
|
164
|
+
// session's model mid-task invalidates the prompt cache, so the change
|
|
165
|
+
// belongs at a session boundary.
|
|
166
|
+
base.model.delegate = delegateModel(modelNow);
|
|
167
|
+
base.model.why =
|
|
168
|
+
'keep ' + (modelNow === 'default' ? 'the current model' : modelNow) +
|
|
169
|
+
' for the judgement; the saving is in where the mechanical bulk runs';
|
|
170
|
+
if (base.posture === 'critical' && base.model.delegate !== 'haiku') {
|
|
171
|
+
base.model.nextSession = 'sonnet';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The commands, spelled out, because the point of a recommendation is that
|
|
175
|
+
// it can be acted on without working anything out.
|
|
176
|
+
if (base.effort.changes) {
|
|
177
|
+
base.apply.now = '/effort ' + base.effort.target;
|
|
178
|
+
}
|
|
179
|
+
if (inputs.codex) {
|
|
180
|
+
base.notes.push(
|
|
181
|
+
'Under Codex, settings.json is not in play: change model or effort through ' +
|
|
182
|
+
"Codex's own controls."
|
|
183
|
+
);
|
|
184
|
+
} else if (base.effort.changes || base.model.nextSession) {
|
|
185
|
+
const settingsEffort = base.effort.changes ? base.effort.target : effortNow;
|
|
186
|
+
base.apply.next =
|
|
187
|
+
'node scripts/lowpower.js on --effort ' +
|
|
188
|
+
(settingsEffort === 'max' ? 'xhigh' : settingsEffort) +
|
|
189
|
+
(base.model.nextSession ? ' --model ' + base.model.nextSession : '');
|
|
190
|
+
}
|
|
191
|
+
base.apply.delegate =
|
|
192
|
+
'dispatch self-contained mechanical work to a subagent on ' +
|
|
193
|
+
base.model.delegate +
|
|
194
|
+
' at low effort, and keep the judgement here';
|
|
195
|
+
|
|
196
|
+
if (Number.isFinite(inputs.sessions) && inputs.sessions > 1) {
|
|
197
|
+
base.notes.push(
|
|
198
|
+
inputs.sessions + ' sessions are spending this budget at once, so the headroom ' +
|
|
199
|
+
'drains faster than these figures alone suggest.'
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (effortNow === 'max') {
|
|
203
|
+
base.notes.push(
|
|
204
|
+
"settings.json does not accept 'max', so a saved level can only go up to " +
|
|
205
|
+
'xhigh; max survives only through /effort or CLAUDE_CODE_EFFORT_LEVEL.'
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return base;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// The report data usage.js gathers, reduced to what decide() reads.
|
|
213
|
+
function fromReport(data, turns) {
|
|
214
|
+
return {
|
|
215
|
+
binding: (data && data.binding) || null,
|
|
216
|
+
rates: (data && data.rates) || null,
|
|
217
|
+
settings: (data && data.settings) || {},
|
|
218
|
+
recentEffort: data && data.recent ? data.recent.effort : null,
|
|
219
|
+
recentTurnsPerHour: data && data.recent ? data.recent.turns : null,
|
|
220
|
+
reasoningShare: data && data.reasoning ? data.reasoning.shareOfOutput : null,
|
|
221
|
+
sessions: data && data.sessions ? data.sessions.length : 1,
|
|
222
|
+
codex: Boolean(data && data.money === false),
|
|
223
|
+
turns: Number.isFinite(turns) ? turns : null,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Local and small on purpose: requiring usage.js back for its formatters
|
|
228
|
+
// would make the two modules a cycle.
|
|
229
|
+
function fmtDuration(ms) {
|
|
230
|
+
if (!Number.isFinite(ms) || ms <= 0) return 'now';
|
|
231
|
+
const minutes = Math.round(ms / 60000);
|
|
232
|
+
if (minutes < 60) return minutes + 'm';
|
|
233
|
+
const hours = Math.floor(minutes / 60);
|
|
234
|
+
if (hours < 48) return hours + 'h ' + (minutes % 60) + 'm';
|
|
235
|
+
return Math.floor(hours / 24) + 'd ' + (hours % 24) + 'h';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function renderRecommend(data, turns) {
|
|
239
|
+
const decision = decide(fromReport(data, turns));
|
|
240
|
+
const lines = [];
|
|
241
|
+
lines.push('Recommendation' + (Number.isFinite(turns) && turns > 0 ? ' for ' + turns + ' turns' : ''));
|
|
242
|
+
lines.push('');
|
|
243
|
+
|
|
244
|
+
if (decision.posture === 'unknown') {
|
|
245
|
+
lines.push(' Nothing to recommend yet: ' + decision.reason + '.');
|
|
246
|
+
lines.push(' Run /usage once, do a little work, then ask again.');
|
|
247
|
+
return lines.join('\n');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const binding = data.binding;
|
|
251
|
+
const where =
|
|
252
|
+
binding.label +
|
|
253
|
+
' window, ' +
|
|
254
|
+
(Number.isFinite(binding.percentLeft) ? Math.max(0, Math.round(binding.percentLeft)) : '?') +
|
|
255
|
+
'% left' +
|
|
256
|
+
(Number.isFinite(binding.msToReset) ? ', resets in ' + fmtDuration(binding.msToReset) : '');
|
|
257
|
+
lines.push(' Posture ' + decision.posture + ' - ' + decision.reason + ' (' + where + ')');
|
|
258
|
+
|
|
259
|
+
if (decision.posture === 'roomy' || decision.posture === 'reset-first') {
|
|
260
|
+
lines.push(' Effort keep ' + decision.effort.current + '; ' + decision.effort.why);
|
|
261
|
+
lines.push(' Model keep ' + decision.model.current + '; do not economise');
|
|
262
|
+
return lines.join('\n');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (decision.effort.changes) {
|
|
266
|
+
lines.push(' Effort ' + decision.effort.current + ' -> ' + decision.effort.target + '; ' + decision.effort.why);
|
|
267
|
+
lines.push(' this session: ' + decision.apply.now + ' (only the user can run it)');
|
|
268
|
+
} else {
|
|
269
|
+
lines.push(' Effort keep ' + decision.effort.current + '; ' + decision.effort.why);
|
|
270
|
+
}
|
|
271
|
+
if (decision.apply.next) {
|
|
272
|
+
lines.push(' new sessions: ' + decision.apply.next);
|
|
273
|
+
}
|
|
274
|
+
lines.push(' Model ' + decision.model.why);
|
|
275
|
+
lines.push(' ' + decision.apply.delegate);
|
|
276
|
+
if (decision.model.nextSession) {
|
|
277
|
+
lines.push(' new sessions: main model to ' + decision.model.nextSession + ' until the window resets');
|
|
278
|
+
}
|
|
279
|
+
for (const note of decision.notes) {
|
|
280
|
+
lines.push(' Note ' + note);
|
|
281
|
+
}
|
|
282
|
+
return lines.join('\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
decide,
|
|
287
|
+
fromReport,
|
|
288
|
+
renderRecommend,
|
|
289
|
+
delegateModel,
|
|
290
|
+
currentEffort,
|
|
291
|
+
NEXT_LOWER,
|
|
292
|
+
REASONING_FLOOR,
|
|
293
|
+
CRITICAL_TURNS,
|
|
294
|
+
TIGHT_TURNS,
|
|
295
|
+
};
|
|
@@ -47,8 +47,12 @@ const MINUTE = 60 * 1000;
|
|
|
47
47
|
const HOUR = 60 * MINUTE;
|
|
48
48
|
const DAY = 24 * HOUR;
|
|
49
49
|
|
|
50
|
-
// USD per million tokens, first-party API rates.
|
|
50
|
+
// USD per million tokens, first-party API rates. `cacheRead` is an absolute
|
|
51
|
+
// $/MTok override for the few models that price reads outright instead of at
|
|
52
|
+
// a tenth of input; everything else uses the CACHE_READ multiplier below.
|
|
51
53
|
const RATES = {
|
|
54
|
+
'claude-fable-5-1': { input: 10, output: 50, cacheRead: 0.25 },
|
|
55
|
+
'claude-mythos-5-1': { input: 10, output: 50, cacheRead: 0.25 },
|
|
52
56
|
'claude-fable-5': { input: 10, output: 50 },
|
|
53
57
|
'claude-mythos-5': { input: 10, output: 50 },
|
|
54
58
|
'claude-opus-5': { input: 5, output: 25 },
|
|
@@ -91,9 +95,21 @@ function familyAverage(family, table) {
|
|
|
91
95
|
return { input: input / members.length, output: output / members.length };
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
// Claude Code aliases and some transcript records carry a bracketed variant
|
|
99
|
+
// suffix - "fable[1m]" is the 1M-context toggle on the same model, not a
|
|
100
|
+
// different one. Left in place it misses the exact rate lookup and lands on
|
|
101
|
+
// the family average, which is wrong whenever a family's members price
|
|
102
|
+
// differently (sonnet 5 at $2 against sonnet 4.6 at $3).
|
|
103
|
+
function normalizeModel(model) {
|
|
104
|
+
return String(model || '')
|
|
105
|
+
.toLowerCase()
|
|
106
|
+
.replace(/\[[^\]]*\]\s*$/, '')
|
|
107
|
+
.trim();
|
|
108
|
+
}
|
|
109
|
+
|
|
94
110
|
// Whether the price came from the table or from an assumption.
|
|
95
111
|
function isKnownModel(model) {
|
|
96
|
-
return Object.prototype.hasOwnProperty.call(RATES,
|
|
112
|
+
return Object.prototype.hasOwnProperty.call(RATES, normalizeModel(model));
|
|
97
113
|
}
|
|
98
114
|
|
|
99
115
|
// Cache traffic is priced as a multiple of the input rate.
|
|
@@ -184,7 +200,7 @@ function readJson(file) {
|
|
|
184
200
|
}
|
|
185
201
|
|
|
186
202
|
function rateFor(model) {
|
|
187
|
-
const id =
|
|
203
|
+
const id = normalizeModel(model);
|
|
188
204
|
if (RATES[id]) return RATES[id];
|
|
189
205
|
return familyAverage(familyOf(id)) || FALLBACK_RATE;
|
|
190
206
|
}
|
|
@@ -199,16 +215,26 @@ function costOf(usage, model) {
|
|
|
199
215
|
|
|
200
216
|
let writeUnits = write5m * CACHE_WRITE_5M + write1h * CACHE_WRITE_1H;
|
|
201
217
|
if (writeUnits === 0) {
|
|
202
|
-
// Older records only carry the undifferentiated total.
|
|
218
|
+
// Older records only carry the undifferentiated total. Five minutes is
|
|
219
|
+
// the default TTL, so that is the assumption; an old-format one-hour
|
|
220
|
+
// session is under-priced by it, but assuming 2x would overcharge the
|
|
221
|
+
// common case to be right about the rare one.
|
|
203
222
|
writeUnits = (usage.cache_creation_input_tokens || 0) * CACHE_WRITE_5M;
|
|
204
223
|
}
|
|
205
224
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
225
|
+
// Reads price at a tenth of the input rate unless the model prices them
|
|
226
|
+
// outright. The distinction matters most exactly where reads dominate: a
|
|
227
|
+
// long session re-reads its whole context every turn, and pricing Fable
|
|
228
|
+
// 5.1's $0.25 reads by the tenth rule would overstate that spend fourfold.
|
|
229
|
+
const readTokens = usage.cache_read_input_tokens || 0;
|
|
230
|
+
const readCost = Number.isFinite(rate.cacheRead)
|
|
231
|
+
? readTokens * rate.cacheRead
|
|
232
|
+
: readTokens * CACHE_READ * rate.input;
|
|
210
233
|
|
|
211
|
-
|
|
234
|
+
const inputUnits = (usage.input_tokens || 0) + writeUnits;
|
|
235
|
+
return (
|
|
236
|
+
(inputUnits * rate.input + readCost + (usage.output_tokens || 0) * rate.output) / 1e6
|
|
237
|
+
);
|
|
212
238
|
}
|
|
213
239
|
|
|
214
240
|
function tokensOf(usage) {
|
|
@@ -812,13 +838,24 @@ function stampPlan(all, planId) {
|
|
|
812
838
|
return stamped;
|
|
813
839
|
}
|
|
814
840
|
|
|
815
|
-
// A sample is better when it rests on more
|
|
816
|
-
// numbers, so a
|
|
841
|
+
// A sample is better when it rests on more of the meter. Percentages read in
|
|
842
|
+
// whole numbers, so a reading at 1% prices a point against a bracket that is
|
|
843
|
+
// mostly rounding, while one at 60% divides by a number that means something.
|
|
844
|
+
// Turn count only breaks the tie: it says how much local spend sat behind the
|
|
845
|
+
// reading, not how precise the denominator was, and preferring it outright is
|
|
846
|
+
// how a 44-turn baseline read at 1% once beat every honest sample after it.
|
|
817
847
|
function betterCalibration(current, candidate) {
|
|
818
848
|
if (!candidate || !Number.isFinite(candidate.usdPerPercent) || candidate.usdPerPercent <= 0) {
|
|
819
849
|
return current || null;
|
|
820
850
|
}
|
|
821
851
|
if (!current || !Number.isFinite(current.turns)) return candidate;
|
|
852
|
+
if (
|
|
853
|
+
Number.isFinite(candidate.percent) &&
|
|
854
|
+
Number.isFinite(current.percent) &&
|
|
855
|
+
candidate.percent !== current.percent
|
|
856
|
+
) {
|
|
857
|
+
return candidate.percent > current.percent ? candidate : current;
|
|
858
|
+
}
|
|
822
859
|
return candidate.turns > current.turns ? candidate : current;
|
|
823
860
|
}
|
|
824
861
|
|
|
@@ -933,16 +970,23 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
933
970
|
// holding. The learned price covers it: what a point costs is a property
|
|
934
971
|
// of the plan, not of this reading.
|
|
935
972
|
const selfPriced =
|
|
936
|
-
rawPercent
|
|
973
|
+
rawPercent >= MIN_BASELINE_PERCENT && upTo.cost > 0 && upTo.turns >= MIN_BASELINE_TURNS
|
|
937
974
|
? { usdPerPercent: upTo.cost / rawPercent, turns: upTo.turns, percent: rawPercent }
|
|
938
975
|
: null;
|
|
939
976
|
// A metered window has nothing to learn: its price per point is stated.
|
|
940
977
|
if (selfPriced && !extra.metered) window.calibration = selfPriced;
|
|
941
978
|
|
|
942
979
|
const known = extra.knownCalibration;
|
|
980
|
+
// A remembered price read off a near-empty meter is the same rounding
|
|
981
|
+
// bracket in disguise, so it is no more usable than measuring one now.
|
|
943
982
|
const usable =
|
|
944
|
-
known &&
|
|
945
|
-
|
|
983
|
+
known &&
|
|
984
|
+
Number.isFinite(known.usdPerPercent) &&
|
|
985
|
+
known.usdPerPercent > 0 &&
|
|
986
|
+
!(Number.isFinite(known.percent) && known.percent < MIN_BASELINE_PERCENT)
|
|
987
|
+
? known
|
|
988
|
+
: null;
|
|
989
|
+
// Trust the better-measured of the two, whichever that is; a stated price
|
|
946
990
|
// beats both.
|
|
947
991
|
const stated =
|
|
948
992
|
extra.metered && Number.isFinite(extra.usdPerPercent) && extra.usdPerPercent > 0
|
|
@@ -950,9 +994,7 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
950
994
|
: null;
|
|
951
995
|
const chosen =
|
|
952
996
|
stated ||
|
|
953
|
-
(selfPriced && usable
|
|
954
|
-
? (usable.turns > selfPriced.turns ? usable : selfPriced)
|
|
955
|
-
: selfPriced || usable);
|
|
997
|
+
(selfPriced && usable ? betterCalibration(usable, selfPriced) : selfPriced || usable);
|
|
956
998
|
|
|
957
999
|
if (after.cost > 0 && chosen) {
|
|
958
1000
|
const pricePerPoint = chosen.usdPerPercent;
|
|
@@ -988,7 +1030,12 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
988
1030
|
// so when the two disagree that badly it is the local history that is
|
|
989
1031
|
// incomplete, not the meter.
|
|
990
1032
|
const thin = spent.turns < MIN_BASELINE_TURNS && percent >= UNEXPLAINED_PERCENT;
|
|
991
|
-
|
|
1033
|
+
// The same rounding bracket again, in the optimistic direction this time: a
|
|
1034
|
+
// window reading 1% divided a full hour of spend by one and priced the
|
|
1035
|
+
// remaining 99 points at thousands of dollars. Below the floor the learned
|
|
1036
|
+
// price takes over through the fallback chain.
|
|
1037
|
+
const measured =
|
|
1038
|
+
percent !== null && percent >= MIN_BASELINE_PERCENT && spent.cost > 0 && !thin;
|
|
992
1039
|
const derived = measured ? spent.cost / percent : null;
|
|
993
1040
|
const known = extra.knownCalibration;
|
|
994
1041
|
const metered =
|
|
@@ -1004,11 +1051,14 @@ function buildWindow(spec, snapshot, events, now, options) {
|
|
|
1004
1051
|
: null;
|
|
1005
1052
|
window.metered = metered;
|
|
1006
1053
|
|
|
1054
|
+
// The API reports whole numbers, so a low reading is a wide bracket. A fact
|
|
1055
|
+
// about the reading, not the pricing, so it is set whether or not a price
|
|
1056
|
+
// per point could be found.
|
|
1057
|
+
window.coarse = percent !== null && percent < MIN_BASELINE_PERCENT;
|
|
1058
|
+
|
|
1007
1059
|
if (percent !== null && priced !== null) {
|
|
1008
1060
|
window.usdPerPercent = priced;
|
|
1009
1061
|
window.remainingUSD = window.usdPerPercent * window.percentLeft;
|
|
1010
|
-
// The API reports whole numbers, so a low reading is a wide bracket.
|
|
1011
|
-
window.coarse = percent < 5;
|
|
1012
1062
|
|
|
1013
1063
|
const perTurn = typicalTurnCost(recentEvents, inWindow, events, MIN_PACE_SAMPLE);
|
|
1014
1064
|
window.percentPerTurn = perTurn === null ? null : perTurn / window.usdPerPercent;
|
|
@@ -1313,6 +1363,15 @@ const SATURATION_LIMIT = 105;
|
|
|
1313
1363
|
// Fewer turns than this before the snapshot and a point cannot be priced.
|
|
1314
1364
|
const MIN_BASELINE_TURNS = 5;
|
|
1315
1365
|
|
|
1366
|
+
// A reading below this cannot price a point either. The API reports whole
|
|
1367
|
+
// numbers, so at 1% the denominator is mostly rounding: the true figure is
|
|
1368
|
+
// anywhere in a bracket as wide as the reading itself, and a point priced
|
|
1369
|
+
// against it converts later spend into several times the points it really
|
|
1370
|
+
// moved. A snapshot taken just after a reset is the common case - one sat at
|
|
1371
|
+
// 1% while the local spend divided by it asserted 97% of a window that was
|
|
1372
|
+
// truly at 35.
|
|
1373
|
+
const MIN_BASELINE_PERCENT = 5;
|
|
1374
|
+
|
|
1316
1375
|
// Past this much of a window, a handful of local turns is not what spent it,
|
|
1317
1376
|
// so their total is not a fair price for a point.
|
|
1318
1377
|
const UNEXPLAINED_PERCENT = 20;
|
|
@@ -1328,6 +1387,10 @@ function reconstructWindow(spec, snapshot, events, now) {
|
|
|
1328
1387
|
const past = totals(events.filter((e) => e.at >= pastStart && e.at <= resetsAt));
|
|
1329
1388
|
if (past.cost <= 0) return null;
|
|
1330
1389
|
|
|
1390
|
+
// The same rounding bracket that poisons the live correction poisons a
|
|
1391
|
+
// rebuild: a closed window that read 1% prices a point off almost nothing.
|
|
1392
|
+
if (snapshot.utilization < MIN_BASELINE_PERCENT) return null;
|
|
1393
|
+
|
|
1331
1394
|
const usdPerPercent = past.cost / snapshot.utilization;
|
|
1332
1395
|
|
|
1333
1396
|
// The window running now began when the old one reset, not five hours ago.
|
|
@@ -2261,6 +2324,23 @@ async function main(argv) {
|
|
|
2261
2324
|
}
|
|
2262
2325
|
return 0;
|
|
2263
2326
|
}
|
|
2327
|
+
const recommendAt = argv.indexOf('--recommend');
|
|
2328
|
+
if (recommendAt !== -1) {
|
|
2329
|
+
// The turn count is optional: with one the verdict is about that job,
|
|
2330
|
+
// without one it is about the headroom in general.
|
|
2331
|
+
const next = argv[recommendAt + 1];
|
|
2332
|
+
const turns = next && next.indexOf('--') !== 0 ? Number(next) : null;
|
|
2333
|
+
const recommend = require('./recommend.js');
|
|
2334
|
+
if (wantsJson) {
|
|
2335
|
+
process.stdout.write(
|
|
2336
|
+
JSON.stringify(recommend.decide(recommend.fromReport(data, turns)), null, 2) + '\n'
|
|
2337
|
+
);
|
|
2338
|
+
} else {
|
|
2339
|
+
process.stdout.write(recommend.renderRecommend(data, turns) + '\n');
|
|
2340
|
+
}
|
|
2341
|
+
return 0;
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2264
2344
|
if (wantsJson) {
|
|
2265
2345
|
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
2266
2346
|
} else {
|
|
@@ -2295,6 +2375,7 @@ module.exports = {
|
|
|
2295
2375
|
reconstructWindow,
|
|
2296
2376
|
SATURATION_LIMIT,
|
|
2297
2377
|
MIN_BASELINE_TURNS,
|
|
2378
|
+
MIN_BASELINE_PERCENT,
|
|
2298
2379
|
buildWindows,
|
|
2299
2380
|
limitWindows,
|
|
2300
2381
|
lastRejections,
|