claude-usage-limits 1.9.0 → 1.9.2

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "usage-limits",
3
3
  "displayName": "Usage Limits",
4
- "version": "1.9.0",
4
+ "version": "1.9.2",
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.9.0",
3
+ "version": "1.9.2",
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-usage-limits",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
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",
@@ -118,6 +118,14 @@ really somewhere in 1.5 to 2.5. At low readings the projection can be off by
118
118
  a quarter or more in either direction. The report flags this below 5 percent.
119
119
  Above about 20 percent it tightens up considerably.
120
120
 
121
+ **Two files can claim to be the account state.** The meter lives in
122
+ `~/.claude.json`, but a Claude Code migration also writes a small
123
+ `~/.claude/.claude.json` holding machine ids and no meter at all. Whichever
124
+ one actually carries `cachedUsageUtilization` is the one read. Choosing on
125
+ existence alone found the stub, concluded there was no Claude snapshot, and
126
+ sent host detection off to Codex, which reported that agent's meter inside a
127
+ Claude session.
128
+
121
129
  **One machine only.** Transcripts are local. Usage from another machine, from
122
130
  claude.ai, or from a cloud session counts against the same limit but leaves no
123
131
  local record. The percentages stay correct; the calibration reads low, which
@@ -130,7 +138,17 @@ calculation.
130
138
  **List prices are a proxy.** The rate table is first-party API pricing. How a
131
139
  subscription plan actually meters usage is not published, and the weighting
132
140
  almost certainly is not exactly this. It is close enough for ratios, which is
133
- all it is used for.
141
+ all it is used for. No published or community source shows the meter weighting
142
+ models differently from their dollar prices, so calibrating dollars against
143
+ your own meter remains the best method anyone outside Anthropic has.
144
+
145
+ **The snapshot is slow by design.** The percentages come from Claude Code's
146
+ own cache of the account meter, which refreshes on its own schedule - roughly
147
+ hourly in practice, because the endpoint behind it rate-limits aggressive
148
+ polling. Between refreshes every figure here is the last real reading plus
149
+ arithmetic. That is why an old snapshot is reported as a floor with its age
150
+ attached rather than dressed up as a current percentage, and why `/usage` is
151
+ the one way to force a fresh reading.
134
152
 
135
153
  **A reset time can be in the past.** The cache refreshes when Claude Code
136
154
  talks to the API, so an idle spell leaves it behind. A window whose `resets_at`
@@ -168,6 +186,13 @@ immediately. Re-run the report if the shape of the work changes.
168
186
  The rate table in `scripts/usage.js` is a plain object at the top of the file.
169
187
  When new models ship, add a row.
170
188
 
189
+ A bracketed suffix on a model id (`claude-sonnet-5[1m]`) is stripped before
190
+ the lookup: it marks a context-window variant of the same model, not a new
191
+ one. Cache reads price at a tenth of the input rate unless a row carries a
192
+ `cacheRead` figure of its own - Fable and Mythos 5.1 price reads outright at
193
+ $0.25 per million, far under the tenth rule, and reads are the dominant input
194
+ in exactly the long sessions where the difference matters.
195
+
171
196
  Until someone does, a model this table has not seen is priced at the average of
172
197
  the family its name contains: an unreleased `claude-opus-5-2` is charged at the
173
198
  mean of every Opus rate on record. Averaging assumes nothing about which
@@ -38,15 +38,28 @@ function exists(file) {
38
38
 
39
39
  // Claude Code only writes this once it has talked to the API, so its presence
40
40
  // is a stronger signal than the directory existing.
41
- function claudeHasSnapshot() {
41
+ //
42
+ // Look in both places rather than stopping at whichever exists. A migration
43
+ // leaves a small ~/.claude/.claude.json carrying machine ids and no meter,
44
+ // while the account state stays in the home directory file; stopping at the
45
+ // stub answered "no Claude snapshot" on a machine plainly running Claude Code,
46
+ // and detection then fell through to Codex and reported its meter instead.
47
+ function claudeSnapshotFile() {
42
48
  const scoped = path.join(claudeConfigDir(), '.claude.json');
43
- const file = exists(scoped) ? scoped : path.join(os.homedir(), '.claude.json');
44
- try {
45
- const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
46
- return Boolean(parsed && parsed.cachedUsageUtilization);
47
- } catch (err) {
48
- return false;
49
+ const home = path.join(os.homedir(), '.claude.json');
50
+ for (const file of scoped === home ? [home] : [scoped, home]) {
51
+ try {
52
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
53
+ if (parsed && parsed.cachedUsageUtilization) return file;
54
+ } catch (err) {
55
+ // Missing or unreadable is just "not this one".
56
+ }
49
57
  }
58
+ return null;
59
+ }
60
+
61
+ function claudeHasSnapshot() {
62
+ return claudeSnapshotFile() !== null;
50
63
  }
51
64
 
52
65
  function codexHasSessions() {
@@ -91,6 +104,7 @@ module.exports = {
91
104
  codexHome,
92
105
  claudeConfigDir,
93
106
  claudeHasSnapshot,
107
+ claudeSnapshotFile,
94
108
  codexHasSessions,
95
109
  exists,
96
110
  };
@@ -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, String(model || '').toLowerCase());
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.
@@ -169,10 +185,35 @@ function configDir() {
169
185
 
170
186
  // The CLI keeps its account state in ~/.claude.json, or next to the config
171
187
  // directory when CLAUDE_CONFIG_DIR moves it.
172
- function accountFile() {
188
+ //
189
+ // Both can exist at once, and the one in the config directory is not
190
+ // necessarily the one with the meter in it: a Claude Code migration writes a
191
+ // small ~/.claude/.claude.json holding machine ids and migration flags while
192
+ // the account state, including cachedUsageUtilization, stays in the home
193
+ // directory file. Picking on existence alone found that stub, reported no
194
+ // snapshot, and sent host detection off to Codex - which is how a Claude
195
+ // session ends up quoting another agent's meter entirely. So choose the file
196
+ // that actually carries a snapshot, and only fall back to existence.
197
+ function accountFiles() {
173
198
  const scoped = path.join(configDir(), '.claude.json');
174
- if (fs.existsSync(scoped)) return scoped;
175
- return path.join(os.homedir(), '.claude.json');
199
+ const home = path.join(os.homedir(), '.claude.json');
200
+ return scoped === home ? [home] : [scoped, home];
201
+ }
202
+
203
+ function hasSnapshot(file) {
204
+ const parsed = readJson(file);
205
+ return Boolean(parsed && parsed.cachedUsageUtilization);
206
+ }
207
+
208
+ function accountFile() {
209
+ const candidates = accountFiles();
210
+ for (const file of candidates) {
211
+ if (hasSnapshot(file)) return file;
212
+ }
213
+ for (const file of candidates) {
214
+ if (fs.existsSync(file)) return file;
215
+ }
216
+ return candidates[candidates.length - 1];
176
217
  }
177
218
 
178
219
  function readJson(file) {
@@ -184,7 +225,7 @@ function readJson(file) {
184
225
  }
185
226
 
186
227
  function rateFor(model) {
187
- const id = String(model || '').toLowerCase();
228
+ const id = normalizeModel(model);
188
229
  if (RATES[id]) return RATES[id];
189
230
  return familyAverage(familyOf(id)) || FALLBACK_RATE;
190
231
  }
@@ -199,16 +240,26 @@ function costOf(usage, model) {
199
240
 
200
241
  let writeUnits = write5m * CACHE_WRITE_5M + write1h * CACHE_WRITE_1H;
201
242
  if (writeUnits === 0) {
202
- // Older records only carry the undifferentiated total.
243
+ // Older records only carry the undifferentiated total. Five minutes is
244
+ // the default TTL, so that is the assumption; an old-format one-hour
245
+ // session is under-priced by it, but assuming 2x would overcharge the
246
+ // common case to be right about the rare one.
203
247
  writeUnits = (usage.cache_creation_input_tokens || 0) * CACHE_WRITE_5M;
204
248
  }
205
249
 
206
- const inputUnits =
207
- (usage.input_tokens || 0) +
208
- (usage.cache_read_input_tokens || 0) * CACHE_READ +
209
- writeUnits;
250
+ // Reads price at a tenth of the input rate unless the model prices them
251
+ // outright. The distinction matters most exactly where reads dominate: a
252
+ // long session re-reads its whole context every turn, and pricing Fable
253
+ // 5.1's $0.25 reads by the tenth rule would overstate that spend fourfold.
254
+ const readTokens = usage.cache_read_input_tokens || 0;
255
+ const readCost = Number.isFinite(rate.cacheRead)
256
+ ? readTokens * rate.cacheRead
257
+ : readTokens * CACHE_READ * rate.input;
210
258
 
211
- return (inputUnits * rate.input + (usage.output_tokens || 0) * rate.output) / 1e6;
259
+ const inputUnits = (usage.input_tokens || 0) + writeUnits;
260
+ return (
261
+ (inputUnits * rate.input + readCost + (usage.output_tokens || 0) * rate.output) / 1e6
262
+ );
212
263
  }
213
264
 
214
265
  function tokensOf(usage) {
@@ -1749,6 +1800,25 @@ function statusLine(collected) {
1749
1800
  unreported: snapshot.utilization === 0 && !Number.isFinite(resetsAt),
1750
1801
  });
1751
1802
  }
1803
+
1804
+ // The per-model weeklies are not bucket keys, they are entries in the
1805
+ // account's own `limits` list, so a loop over the bucket table never saw
1806
+ // them. On a plan where the Fable weekly is the limit that actually binds,
1807
+ // that meant the status line quoting the shared weekly at 24% while the
1808
+ // window about to stop the work sat at 76, which is the wrong number in the
1809
+ // most convincing possible place.
1810
+ for (const limit of limitWindows(utilization)) {
1811
+ if (!limit.family) continue;
1812
+ const msToReset = Number.isFinite(limit.resetsAt) ? limit.resetsAt - now : null;
1813
+ parts.push({
1814
+ label: limit.family,
1815
+ percent: limit.percent,
1816
+ msToReset,
1817
+ stale: msToReset !== null && msToReset <= 0,
1818
+ unreported: false,
1819
+ });
1820
+ }
1821
+
1752
1822
  if (!parts.length) return '';
1753
1823
 
1754
1824
  const trusted = parts.filter((part) => !part.stale && !part.unreported);
@@ -2350,6 +2420,7 @@ module.exports = {
2350
2420
  SATURATION_LIMIT,
2351
2421
  MIN_BASELINE_TURNS,
2352
2422
  MIN_BASELINE_PERCENT,
2423
+ accountFile,
2353
2424
  buildWindows,
2354
2425
  limitWindows,
2355
2426
  lastRejections,