claude-usage-limits 1.40.4 → 1.40.5
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 +17 -1
- package/commands/relay.md +3 -0
- package/package.json +1 -1
- package/skills/usage-limits/references/how-it-works.md +3 -2
- package/skills/usage-limits/scripts/brief.js +33 -4
- package/skills/usage-limits/scripts/mode.js +157 -8
- package/skills/usage-limits/scripts/usage.js +83 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usage-limits",
|
|
3
3
|
"displayName": "Usage Limits",
|
|
4
|
-
"version": "1.40.
|
|
4
|
+
"version": "1.40.5",
|
|
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.40.
|
|
3
|
+
"version": "1.40.5",
|
|
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
|
@@ -1120,7 +1120,23 @@ There are two separate things people mean by "change the model":
|
|
|
1120
1120
|
|
|
1121
1121
|
`claude-usage-limits mode --baseline` shows both side by side. The budget line
|
|
1122
1122
|
now says the tier as well, and where the reading came from, because the number
|
|
1123
|
-
that decides what a turn costs was the one number the line never printed.
|
|
1123
|
+
that decides what a turn costs was the one number the line never printed. It
|
|
1124
|
+
names the version, not only the family (`opus 5.5/xhigh`, not `opus/xhigh`),
|
|
1125
|
+
read from the newest assistant message in the session's transcript: a settings
|
|
1126
|
+
alias such as `opus` cannot say which Opus answered.
|
|
1127
|
+
|
|
1128
|
+
When the model running is an older release of a family that has a newer one at
|
|
1129
|
+
a lower price - Opus 5 or 4.8 against Opus 5.5 ($4/$20, cache reads $0.20
|
|
1130
|
+
against $0.50), Fable 5 against Fable 5.1 (reads $0.25 against $1) - the brief
|
|
1131
|
+
says so once per session, with the prices and how many turns the one-off cache
|
|
1132
|
+
rebuild takes to repay. In Claude Code it offers `/model <id>`, which switches
|
|
1133
|
+
the session and saves the model as the default for new sessions; elsewhere it
|
|
1134
|
+
offers the host's own model setting and names no slash command. A relay whose
|
|
1135
|
+
own `model` is pinned to the older release is named too, since a wake starts
|
|
1136
|
+
with that `--model` whatever the session switched to. Releases on
|
|
1137
|
+
either side of the 4.7 tokenizer change are not compared, because a price per
|
|
1138
|
+
token is not like for like across it. `mode --no-advice` mutes this with the
|
|
1139
|
+
rest of the advice.
|
|
1124
1140
|
|
|
1125
1141
|
What Claude can genuinely move, stated without embroidery: the model on an
|
|
1126
1142
|
`Agent` call, and the model and effort inside a `Workflow` script. Its own
|
package/commands/relay.md
CHANGED
|
@@ -29,6 +29,9 @@ The rest:
|
|
|
29
29
|
(or whichever mode you want) as well: a resume does **not** inherit the
|
|
30
30
|
session's permission mode, so without one it will sit waiting for an
|
|
31
31
|
approval nobody is there to give. `show off` makes it a headless run instead.
|
|
32
|
+
- `model <id>` - the `--model` a resumed run starts with; `model` alone goes
|
|
33
|
+
back to the default. It is separate from `/model` in the session, so a wake
|
|
34
|
+
pinned to an older model stays on it until this is changed.
|
|
32
35
|
- `voice on|off` - carry how you write in the hand-off, so the resumed session
|
|
33
36
|
answers in your voice without being reminded. On by default.
|
|
34
37
|
- `bugcheck on|always|off` - the hand-off asks for two bug passes before anything
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-usage-limits",
|
|
3
|
-
"version": "1.40.
|
|
3
|
+
"version": "1.40.5",
|
|
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",
|
|
@@ -256,8 +256,9 @@ A bracketed suffix on a model id (`claude-sonnet-5[1m]`) is stripped before
|
|
|
256
256
|
the lookup: it marks a context-window variant of the same model, not a new
|
|
257
257
|
one. Cache reads price at a tenth of the input rate unless a row carries a
|
|
258
258
|
`cacheRead` figure of its own - Fable and Mythos 5.1 price reads outright at
|
|
259
|
-
$0.25 per million
|
|
260
|
-
|
|
259
|
+
$0.25 per million (0.025x input) and Opus 5.5 at $0.20 (0.05x its $4 input),
|
|
260
|
+
all under the tenth rule, and reads are the dominant input in exactly the long
|
|
261
|
+
sessions where the difference matters.
|
|
261
262
|
|
|
262
263
|
Until someone does, a model this table has not seen is priced at the average of
|
|
263
264
|
the family its name contains: an unreleased `claude-opus-5-2` is charged at the
|
|
@@ -160,7 +160,7 @@ function readSaid() {
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
function keepSaidFor(key) {
|
|
163
|
-
return /#(standing|cachemiss|stale|relaylast)$/.test(key) ? 24 * 60 * 60 * 1000 : 60 * 60 * 1000;
|
|
163
|
+
return /#(standing|cachemiss|stale|relaylast|newer)$/.test(key) ? 24 * 60 * 60 * 1000 : 60 * 60 * 1000;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// A plugin update takes effect when Claude Code restarts, so a session that
|
|
@@ -202,6 +202,30 @@ function staleVersionFor(sessionId, now, dir) {
|
|
|
202
202
|
return 'usage-limits ' + installed + ' is installed but this session still runs ' + running +
|
|
203
203
|
', because a plugin update applies at the next start; a relay or cap set here follows the older rules until then.';
|
|
204
204
|
}
|
|
205
|
+
// A newer model of the same family at a lower price, said once a session.
|
|
206
|
+
// Once is the rule every recommendation here keeps: the fact does not change
|
|
207
|
+
// from prompt to prompt, and repeating it would be the plugin charging for its
|
|
208
|
+
// own presence. Keyed on the pair, so a session that moves to a different model
|
|
209
|
+
// with its own newer sibling hears about that one.
|
|
210
|
+
function newerModelFor(sessionId, advice, now) {
|
|
211
|
+
if (!advice || !advice.text) return null;
|
|
212
|
+
const at = Number.isFinite(now) ? now : Date.now();
|
|
213
|
+
const key = String(sessionId || '_') + '#newer';
|
|
214
|
+
const all = readSaid();
|
|
215
|
+
const entry = all[key];
|
|
216
|
+
if (entry && entry.seen === advice.id && Number.isFinite(entry.at) && at - entry.at < keepSaidFor(key)) return null;
|
|
217
|
+
all[key] = { at, seen: advice.id };
|
|
218
|
+
writeSaid(all, at);
|
|
219
|
+
return advice.text;
|
|
220
|
+
}
|
|
221
|
+
// The model the relay resumes with, when one is set: wake.js passes it as --model.
|
|
222
|
+
function relayModelNow() {
|
|
223
|
+
try {
|
|
224
|
+
return relay.settings(relay.read()).model || null;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
205
229
|
// How the last relay ended is news once. It used to ride along for six hours
|
|
206
230
|
// after any relay ended, on every prompt of every session: on 2026-09-22 a wake
|
|
207
231
|
// lost at 5:53 AM was repeated in two sessions' briefs all afternoon, about
|
|
@@ -784,6 +808,7 @@ function briefText(input) {
|
|
|
784
808
|
if (parts.tier) sentences.push(parts.tier);
|
|
785
809
|
// Once per session, and only when the installed version is not this one.
|
|
786
810
|
if (parts.staleVersion) sentences.push(parts.staleVersion);
|
|
811
|
+
if (parts.newerModel) sentences.push(parts.newerModel);
|
|
787
812
|
const bounded = mode.boundsNote(bounds);
|
|
788
813
|
if (bounded) sentences.push(bounded);
|
|
789
814
|
if (parts.planChanged) {
|
|
@@ -1403,7 +1428,7 @@ function briefText(input) {
|
|
|
1403
1428
|
// The one recommendation this session is allowed, in its short form. It
|
|
1404
1429
|
// still cites the measurement, still names the command: terse is fewer
|
|
1405
1430
|
// words, not less evidence.
|
|
1406
|
-
const adviceText = parts.adviceText ? ' ' + parts.adviceText : '';
|
|
1431
|
+
const adviceText = (parts.adviceText ? ' ' + parts.adviceText : '') + (parts.newerModel ? ' ' + parts.newerModel : '');
|
|
1407
1432
|
return (
|
|
1408
1433
|
sentences[0] + caveat + (parts.tier ? ' ' + parts.tier : '') + (bounded ? ' ' + bounded : '') + adviceText +
|
|
1409
1434
|
(escapeSentence ? ' ' + escapeSentence : '') +
|
|
@@ -1904,7 +1929,10 @@ async function run(now, hookInput, opts) {
|
|
|
1904
1929
|
// What tier is producing this turn, and what the user's own baseline is.
|
|
1905
1930
|
// Read, displayed, never written.
|
|
1906
1931
|
const terse = budget.policy.briefStyle === 'terse';
|
|
1907
|
-
const
|
|
1932
|
+
const tierReading = mode.tierNow({ sessionId, now, usage, env: process.env, transcriptPath: hookInput && hookInput.transcript_path });
|
|
1933
|
+
const tier = mode.tierLine(tierReading, { terse });
|
|
1934
|
+
// Muted advice is muted for this too: it is a recommendation like the others.
|
|
1935
|
+
const newerAdvice = budget.advice && budget.advice.off ? null : mode.newerModelAdvice(tierReading, { usage, host: usage.currentHost(), bounds: budget.bounds, relayModel: relayModelNow() });
|
|
1908
1936
|
|
|
1909
1937
|
// The recommendation channel. The measured fit sentence IS the
|
|
1910
1938
|
// recommendation - it cites this account's own numbers and names the exact
|
|
@@ -1959,6 +1987,7 @@ async function run(now, hookInput, opts) {
|
|
|
1959
1987
|
standingShort,
|
|
1960
1988
|
cacheMissWhy: cacheMissWhyFor(sessionId, now),
|
|
1961
1989
|
staleVersion: staleVersionFor(sessionId, now),
|
|
1990
|
+
newerModel: newerModelFor(sessionId, newerAdvice, now),
|
|
1962
1991
|
adviceText: terse && offering ? advice.text : null,
|
|
1963
1992
|
relay: carry,
|
|
1964
1993
|
voiceNote,
|
|
@@ -2065,7 +2094,7 @@ function withBugcheck(text) {
|
|
|
2065
2094
|
return text;
|
|
2066
2095
|
}
|
|
2067
2096
|
|
|
2068
|
-
module.exports = { wallFeatures, sweepDebris, withBugcheck, sayOnce, shapeOf, saidFile, REPEAT_MS, staleVersionFor, relayNewsFor, installedVersion, runningVersion,
|
|
2097
|
+
module.exports = { wallFeatures, sweepDebris, withBugcheck, sayOnce, shapeOf, saidFile, REPEAT_MS, staleVersionFor, newerModelFor, relayNewsFor, installedVersion, runningVersion,
|
|
2069
2098
|
readSaid, standingSaid, markStanding, standingShortFor, STANDING_SHORT, cacheMissWhyFor, missReason, MISS_RECENT_MS,
|
|
2070
2099
|
DEFAULTS,
|
|
2071
2100
|
HOOK_BUDGET_MS,
|
|
@@ -1057,9 +1057,15 @@ function tierNow(options) {
|
|
|
1057
1057
|
settings = null;
|
|
1058
1058
|
}
|
|
1059
1059
|
|
|
1060
|
+
// The newest assistant message is the model that actually answered, which a
|
|
1061
|
+
// settings alias ('opus') cannot say. The hook's own transcript_path is read
|
|
1062
|
+
// first when the caller has it: it is the file this very turn is written to,
|
|
1063
|
+
// where the session-id lookup has to find it under the config directory.
|
|
1060
1064
|
let running = null;
|
|
1061
1065
|
try {
|
|
1062
|
-
|
|
1066
|
+
let seen = null;
|
|
1067
|
+
if (opts.transcriptPath && typeof usage.transcriptModel === 'function') seen = usage.transcriptModel(opts.transcriptPath);
|
|
1068
|
+
if (!(seen && seen.model) && sessionId) seen = usage.liveModel(sessionId);
|
|
1063
1069
|
running = seen && seen.model ? seen.model : null;
|
|
1064
1070
|
} catch (err) {
|
|
1065
1071
|
running = null;
|
|
@@ -1112,6 +1118,150 @@ function sameFamily(a, b) {
|
|
|
1112
1118
|
return left === right;
|
|
1113
1119
|
}
|
|
1114
1120
|
|
|
1121
|
+
// The model as the brief names it: the family and the version. It used to be
|
|
1122
|
+
// the family alone, so claude-opus-5 and claude-opus-5-5 both printed "opus"
|
|
1123
|
+
// and a switch between them - a 20% price change, 60% on cache reads - was
|
|
1124
|
+
// invisible in the one line that exists to say what is producing the turn. A
|
|
1125
|
+
// bare alias ('opus') has no version to show and is shown as it is.
|
|
1126
|
+
function modelLabel(name) {
|
|
1127
|
+
if (!name) return null;
|
|
1128
|
+
let parsed = null;
|
|
1129
|
+
try {
|
|
1130
|
+
parsed = require('./usage.js').parseModelId(name);
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
parsed = null;
|
|
1133
|
+
}
|
|
1134
|
+
if (parsed) return parsed.version.length ? parsed.family + ' ' + parsed.version.join('.') : parsed.family;
|
|
1135
|
+
const rank = modelRank(name);
|
|
1136
|
+
return rank === null ? name : MODEL_ORDER[rank];
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Same model for the purpose of "is the running tier the baseline". Same
|
|
1140
|
+
// family, and where both sides carry a version, the same version: opus 5 and
|
|
1141
|
+
// opus 5.5 are different models at different prices, but a baseline of the
|
|
1142
|
+
// bare alias 'opus' says nothing about which Opus, so it disagrees with none.
|
|
1143
|
+
function sameModel(a, b) {
|
|
1144
|
+
if (!sameFamily(a, b)) return false;
|
|
1145
|
+
let left = null;
|
|
1146
|
+
let right = null;
|
|
1147
|
+
try {
|
|
1148
|
+
const usage = require('./usage.js');
|
|
1149
|
+
left = usage.parseModelId(a);
|
|
1150
|
+
right = usage.parseModelId(b);
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
return true;
|
|
1153
|
+
}
|
|
1154
|
+
if (!left || !right || !left.version.length || !right.version.length) return true;
|
|
1155
|
+
return left.version.join('.') === right.version.join('.');
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function money(value) {
|
|
1159
|
+
return Number.isInteger(value) ? '$' + value : '$' + value.toFixed(2);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// A newer model in the SAME family at a lower price is the cheapest saving
|
|
1163
|
+
// there is: no step down in tier, nothing given up, only the price. The advice
|
|
1164
|
+
// channel above only ever says "choose lower"; this says "choose newer", once
|
|
1165
|
+
// per session (brief.js keeps the count), and only from prices on record.
|
|
1166
|
+
//
|
|
1167
|
+
// What the user can do differs by host, and the sentence says only what is
|
|
1168
|
+
// real where it is read. In Claude Code, `/model <id>` switches the session and
|
|
1169
|
+
// saves the model as the default for new sessions (the picker's `s` key is the
|
|
1170
|
+
// this-session-only form), per code.claude.com/docs/en/model-config, read
|
|
1171
|
+
// 2026-09-25. A subagent with no model of its own falls through to the main
|
|
1172
|
+
// conversation's model (docs/en/sub-agents, same day), so a switch reaches
|
|
1173
|
+
// those from their next dispatch. Codex has no /model of that kind, so there
|
|
1174
|
+
// the only lever named is its config.
|
|
1175
|
+
//
|
|
1176
|
+
// The payback figure is exact arithmetic on the two rows, not an estimate of
|
|
1177
|
+
// the session: switching costs one write of the context at the new model's
|
|
1178
|
+
// write price instead of one read at the old read price, and every later turn
|
|
1179
|
+
// saves the difference in read price on that context. The context size cancels
|
|
1180
|
+
// out of the ratio, so the number of turns holds for any session. It counts the
|
|
1181
|
+
// reads alone; the cheaper input and output only shorten it.
|
|
1182
|
+
function newerModelAdvice(tier, options) {
|
|
1183
|
+
const opts = options || {};
|
|
1184
|
+
if (!tier) return null;
|
|
1185
|
+
const usage = opts.usage || require('./usage.js');
|
|
1186
|
+
if (typeof usage.newerSibling !== 'function') return null;
|
|
1187
|
+
const base = tier.baseline || {};
|
|
1188
|
+
const run = tier.running || {};
|
|
1189
|
+
const hostName = opts.host || host.CLAUDE;
|
|
1190
|
+
// A bound the user set outranks the price: nothing said points outside it.
|
|
1191
|
+
const sibling = (model) => {
|
|
1192
|
+
const found = model ? usage.newerSibling(model) : null;
|
|
1193
|
+
return found && allows(opts.bounds, { model: found.to }) ? found : null;
|
|
1194
|
+
};
|
|
1195
|
+
const current = run.model || base.model;
|
|
1196
|
+
const found = sibling(current);
|
|
1197
|
+
// The relay resumes with its own --model when one is set (wake.js), so a
|
|
1198
|
+
// session moved to the newer model still wakes on the old one. Claude Code
|
|
1199
|
+
// only: that is where `relay model` feeds a Claude CLI.
|
|
1200
|
+
const relayFound = hostName === host.CLAUDE && opts.relayModel ? sibling(opts.relayModel) : null;
|
|
1201
|
+
if (!found && !relayFound) return null;
|
|
1202
|
+
|
|
1203
|
+
const name = (family, version) => family.charAt(0).toUpperCase() + family.slice(1) + ' ' + version.join('.');
|
|
1204
|
+
const pct = (was, now) => Math.round((1 - now / was) * 100);
|
|
1205
|
+
const prices = (pair) => {
|
|
1206
|
+
const a = pair.fromRate;
|
|
1207
|
+
const b = pair.toRate;
|
|
1208
|
+
const bits = [];
|
|
1209
|
+
if (b.input < a.input || b.output < a.output) {
|
|
1210
|
+
bits.push(money(b.input) + '/' + money(b.output) + ' per million tokens in and out against ' + money(a.input) + '/' + money(a.output));
|
|
1211
|
+
}
|
|
1212
|
+
if (b.cacheRead < a.cacheRead) {
|
|
1213
|
+
bits.push('cache reads ' + money(b.cacheRead) + ' against ' + money(a.cacheRead) + ', ' + pct(a.cacheRead, b.cacheRead) +
|
|
1214
|
+
'% less on the reads that are most of what a long session spends');
|
|
1215
|
+
}
|
|
1216
|
+
return bits.join('; ');
|
|
1217
|
+
};
|
|
1218
|
+
const family = (pair) => pair.family.charAt(0).toUpperCase() + pair.family.slice(1);
|
|
1219
|
+
|
|
1220
|
+
const sentences = [];
|
|
1221
|
+
if (found) {
|
|
1222
|
+
const a = found.fromRate;
|
|
1223
|
+
const b = found.toRate;
|
|
1224
|
+
sentences.push(name(found.family, found.toVersion) + ' is a newer ' + family(found) + ' at a lower price than the ' +
|
|
1225
|
+
name(found.family, found.fromVersion) + ' running here. At first-party API prices: ' + prices(found) +
|
|
1226
|
+
'. Same tier and a newer release, so no step down.');
|
|
1227
|
+
const pinnedAlready = base.model && String(base.model).toLowerCase().replace(/\[[^\]]*\]\s*$/, '').trim() === found.to;
|
|
1228
|
+
if (hostName === host.CLAUDE) {
|
|
1229
|
+
sentences.push('It is the user\'s switch, not yours: offer `/model ' + found.to + '`, which moves this session and saves it as the ' +
|
|
1230
|
+
'default for new sessions' + (pinnedAlready ? ' (settings.json already names it, so new sessions start on it either way)' : '') +
|
|
1231
|
+
'. Subagents given no model of their own (and no CLAUDE_CODE_SUBAGENT_MODEL) run on the session\'s model, so they follow from their next dispatch.');
|
|
1232
|
+
// Only where the switch can be made mid-session is its one-off cost
|
|
1233
|
+
// worth a number; elsewhere the offer is a pin for new sessions, which
|
|
1234
|
+
// rebuild anyway.
|
|
1235
|
+
if (b.cacheRead < a.cacheRead) {
|
|
1236
|
+
const turns = (write) => Math.ceil(Math.round(((write * b.input - a.cacheRead) / (a.cacheRead - b.cacheRead)) * 100) / 100);
|
|
1237
|
+
sentences.push('Switching rebuilds the prompt cache once; the cheaper reads alone repay that in about ' + turns(1.25) +
|
|
1238
|
+
' turns (' + turns(2) + ' on the one-hour cache).');
|
|
1239
|
+
} else {
|
|
1240
|
+
sentences.push('Switching rebuilds the prompt cache once, so on a large context it is worth making at the next session start rather than now.');
|
|
1241
|
+
}
|
|
1242
|
+
} else if (hostName === host.CODEX) {
|
|
1243
|
+
sentences.push('It is the user\'s setting, not yours: offer pinning model = "' + found.to + '" in config.toml for new sessions.');
|
|
1244
|
+
} else {
|
|
1245
|
+
sentences.push('It is the user\'s setting, not yours: offer choosing ' + found.to + ' in this host\'s own model setting.');
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (relayFound) {
|
|
1249
|
+
const same = found && found.to === relayFound.to && found.from === relayFound.from;
|
|
1250
|
+
sentences.push('Relay wakes are pinned to ' + opts.relayModel + ' on their own' +
|
|
1251
|
+
(same ? '' : ', and ' + name(relayFound.family, relayFound.toVersion) + ' is a newer ' + family(relayFound) +
|
|
1252
|
+
' at a lower price (' + prices(relayFound) + ')') +
|
|
1253
|
+
', so a resumed run starts on the older model even after this session switches: offer `/usage-limits:relay model ' + relayFound.to +
|
|
1254
|
+
'`, which is the user\'s setting too.');
|
|
1255
|
+
}
|
|
1256
|
+
const id = (found ? found.from + '->' + found.to : '') + (relayFound ? '|relay:' + relayFound.from + '->' + relayFound.to : '');
|
|
1257
|
+
return {
|
|
1258
|
+
id,
|
|
1259
|
+
from: found ? found.from : null,
|
|
1260
|
+
to: found ? found.to : null,
|
|
1261
|
+
relay: relayFound ? { from: relayFound.from, to: relayFound.to } : null,
|
|
1262
|
+
text: sentences.join(' '),
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1115
1265
|
// One clause for the brief, or two lines for `--baseline`.
|
|
1116
1266
|
//
|
|
1117
1267
|
// Where the baseline and the running tier agree there is nothing interesting
|
|
@@ -1124,16 +1274,12 @@ function tierLine(tier, options) {
|
|
|
1124
1274
|
if (!tier) return null;
|
|
1125
1275
|
const base = tier.baseline || {};
|
|
1126
1276
|
const run = tier.running || {};
|
|
1127
|
-
const
|
|
1128
|
-
const rank = modelRank(name);
|
|
1129
|
-
return rank === null ? name : MODEL_ORDER[rank];
|
|
1130
|
-
};
|
|
1131
|
-
const runningText = [shortModel(run.model) || shortModel(base.model), run.effort || base.effort].filter(Boolean).join('/');
|
|
1277
|
+
const runningText = [modelLabel(run.model) || modelLabel(base.model), run.effort || base.effort].filter(Boolean).join('/');
|
|
1132
1278
|
if (!runningText) return null;
|
|
1133
|
-
const baseText = [
|
|
1279
|
+
const baseText = [modelLabel(base.model), base.effort].filter(Boolean).join('/');
|
|
1134
1280
|
const differs =
|
|
1135
1281
|
baseText && runningText !== baseText &&
|
|
1136
|
-
(!
|
|
1282
|
+
(!sameModel(run.model || base.model, base.model) || (run.effort || base.effort) !== base.effort);
|
|
1137
1283
|
const source = run.source ? ' (' + run.source + ')' : '';
|
|
1138
1284
|
if (opts.terse) {
|
|
1139
1285
|
return differs ? runningText + source + ', yours ' + baseText : runningText + source;
|
|
@@ -1876,6 +2022,9 @@ module.exports = {
|
|
|
1876
2022
|
ultracodeName,
|
|
1877
2023
|
topTier,
|
|
1878
2024
|
tierLine,
|
|
2025
|
+
modelLabel,
|
|
2026
|
+
sameModel,
|
|
2027
|
+
newerModelAdvice,
|
|
1879
2028
|
ledger,
|
|
1880
2029
|
explain,
|
|
1881
2030
|
list,
|
|
@@ -62,6 +62,10 @@ const RATES = {
|
|
|
62
62
|
'claude-mythos-5-1': { input: 10, output: 50, cacheRead: 0.25 },
|
|
63
63
|
'claude-fable-5': { input: 10, output: 50 },
|
|
64
64
|
'claude-mythos-5': { input: 10, output: 50 },
|
|
65
|
+
// Opus 5.5 prices reads outright: $0.20 is 0.05x its input, half the tenth
|
|
66
|
+
// rule every other Opus follows (platform.claude.com/docs/en/about-claude/pricing,
|
|
67
|
+
// read 2026-09-25). Left to the multiplier it would be priced at $0.40.
|
|
68
|
+
'claude-opus-5-5': { input: 4, output: 20, cacheRead: 0.2 },
|
|
65
69
|
'claude-opus-5': { input: 5, output: 25 },
|
|
66
70
|
'claude-opus-4-8': { input: 5, output: 25 },
|
|
67
71
|
'claude-opus-4-7': { input: 5, output: 25 },
|
|
@@ -129,6 +133,83 @@ const CACHE_WRITE_5M = 1.25;
|
|
|
129
133
|
const CACHE_WRITE_1H = 2;
|
|
130
134
|
const CACHE_READ = 0.1;
|
|
131
135
|
|
|
136
|
+
// The effective cache-read price of a row, in $/MTok.
|
|
137
|
+
function readRateOf(rate) {
|
|
138
|
+
return Number.isFinite(rate.cacheRead) ? rate.cacheRead : rate.input * CACHE_READ;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A model id taken apart into the family word and the version numbers:
|
|
142
|
+
// 'claude-opus-5-5' is opus [5, 5], 'us.anthropic.claude-opus-5-v1:0' is
|
|
143
|
+
// opus [5]. A dated snapshot suffix (20250929) is not part of the version, and a
|
|
144
|
+
// bare alias such as 'opus' has no version at all, so nothing is claimed about
|
|
145
|
+
// which release it resolved to. Mythos stays mythos here: it is priced with
|
|
146
|
+
// Fable, but it is not the same model line.
|
|
147
|
+
function parseModelId(model) {
|
|
148
|
+
const id = normalizeModel(model);
|
|
149
|
+
const found = id.match(/(haiku|sonnet|opus|mythos|fable)((?:-\d+)*)/);
|
|
150
|
+
if (!found) return null;
|
|
151
|
+
const version = [];
|
|
152
|
+
for (const part of found[2].split('-').filter(Boolean)) {
|
|
153
|
+
if (part.length > 2) break;
|
|
154
|
+
version.push(Number(part));
|
|
155
|
+
}
|
|
156
|
+
return { family: found[1], version };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function compareVersions(a, b) {
|
|
160
|
+
const length = Math.max(a.length, b.length);
|
|
161
|
+
for (let i = 0; i < length; i += 1) {
|
|
162
|
+
const diff = (a[i] || 0) - (b[i] || 0);
|
|
163
|
+
if (diff !== 0) return diff;
|
|
164
|
+
}
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Claude 4.7 and later count text with a newer tokenizer that produces about
|
|
169
|
+
// 30% more tokens for the same text (pricing page, read 2026-09-25). A price per
|
|
170
|
+
// token across that line is not a like-for-like price, so a sibling on the other
|
|
171
|
+
// side of it is never offered as the cheaper one.
|
|
172
|
+
function sameTokenizer(a, b) {
|
|
173
|
+
return (compareVersions(a, [4, 7]) >= 0) === (compareVersions(b, [4, 7]) >= 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The newest release of the SAME family that is newer than this one and no
|
|
177
|
+
// dearer on any rate - input, output or cache reads - and cheaper on at least
|
|
178
|
+
// one. That is a saving with no step down in tier. Only a model whose own price
|
|
179
|
+
// is on record qualifies: comparing against a family average would be
|
|
180
|
+
// comparing against a guess.
|
|
181
|
+
function newerSibling(model, table) {
|
|
182
|
+
const rates = table || RATES;
|
|
183
|
+
const me = parseModelId(model);
|
|
184
|
+
if (!me || !me.version.length) return null;
|
|
185
|
+
const from = 'claude-' + me.family + '-' + me.version.join('-');
|
|
186
|
+
const mine = rates[from];
|
|
187
|
+
if (!mine) return null;
|
|
188
|
+
let best = null;
|
|
189
|
+
for (const id of Object.keys(rates)) {
|
|
190
|
+
const other = parseModelId(id);
|
|
191
|
+
if (!other || other.family !== me.family || !other.version.length) continue;
|
|
192
|
+
if (compareVersions(other.version, me.version) <= 0) continue;
|
|
193
|
+
if (!sameTokenizer(other.version, me.version)) continue;
|
|
194
|
+
const theirs = rates[id];
|
|
195
|
+
const noDearer = theirs.input <= mine.input && theirs.output <= mine.output && readRateOf(theirs) <= readRateOf(mine);
|
|
196
|
+
const cheaper = theirs.input < mine.input || theirs.output < mine.output || readRateOf(theirs) < readRateOf(mine);
|
|
197
|
+
if (!noDearer || !cheaper) continue;
|
|
198
|
+
if (!best || compareVersions(other.version, best.version) > 0) best = { id, version: other.version, rate: theirs };
|
|
199
|
+
}
|
|
200
|
+
if (!best) return null;
|
|
201
|
+
const shape = (rate) => ({ input: rate.input, output: rate.output, cacheRead: readRateOf(rate) });
|
|
202
|
+
return {
|
|
203
|
+
family: me.family,
|
|
204
|
+
from,
|
|
205
|
+
fromVersion: me.version,
|
|
206
|
+
fromRate: shape(mine),
|
|
207
|
+
to: best.id,
|
|
208
|
+
toVersion: best.version,
|
|
209
|
+
toRate: shape(best.rate),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
132
213
|
// `family` marks a window that caps one model family rather than the account
|
|
133
214
|
// as a whole. It is what tells the rest of the file that a window cannot stop
|
|
134
215
|
// work which does not use that family.
|
|
@@ -4282,6 +4363,8 @@ module.exports = {
|
|
|
4282
4363
|
WINDOWS,
|
|
4283
4364
|
rateFor,
|
|
4284
4365
|
familyOf,
|
|
4366
|
+
parseModelId,
|
|
4367
|
+
newerSibling,
|
|
4285
4368
|
familyAverage,
|
|
4286
4369
|
familiesInUse,
|
|
4287
4370
|
appliesTo,
|