@smithers-orchestrator/usage 0.23.0
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/LICENSE +21 -0
- package/README.md +54 -0
- package/package.json +35 -0
- package/src/UsageReport.ts +33 -0
- package/src/UsageSource.ts +9 -0
- package/src/UsageWindow.ts +28 -0
- package/src/anthropicHeaderUsage.js +51 -0
- package/src/buildUsageReport.js +43 -0
- package/src/claudeOauthUsage.js +52 -0
- package/src/codexWhamUsage.js +45 -0
- package/src/decodeJwtClaims.js +26 -0
- package/src/formatRelativeReset.js +16 -0
- package/src/formatUsageReports.js +59 -0
- package/src/getAccountUsage.js +50 -0
- package/src/getUsageForAccounts.js +75 -0
- package/src/googleUsage.js +20 -0
- package/src/humanizeDurationShort.js +20 -0
- package/src/index.d.ts +442 -0
- package/src/index.js +21 -0
- package/src/openaiHeaderUsage.js +59 -0
- package/src/parseAnthropicRateLimitHeaders.js +57 -0
- package/src/parseClaudeOauthUsage.js +47 -0
- package/src/parseCodexUsage.js +77 -0
- package/src/parseDurationSeconds.js +38 -0
- package/src/parseOpenAiRateLimitHeaders.js +60 -0
- package/src/publishedCaps.js +29 -0
- package/src/readClaudeCredentials.js +69 -0
- package/src/readCodexCredentials.js +41 -0
- package/src/usageCache.js +55 -0
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import * as _smithers_orchestrator_accounts from '@smithers-orchestrator/accounts';
|
|
2
|
+
import { AccountProvider } from '@smithers-orchestrator/accounts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where a usage report's numbers came from.
|
|
6
|
+
*
|
|
7
|
+
* - `oauth` — an authenticated subscription usage endpoint (Claude, Codex).
|
|
8
|
+
* - `headers` — live rate-limit response headers from an API-key request.
|
|
9
|
+
* - `local` — estimated locally from token logs (Google providers).
|
|
10
|
+
* - `none` — the provider exposes no usage surface, or the probe failed.
|
|
11
|
+
*/
|
|
12
|
+
type UsageSource = "oauth" | "headers" | "local" | "none";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One quota window for an account: a 5-hour session, a weekly cap, a per-minute
|
|
16
|
+
* request bucket, and so on.
|
|
17
|
+
*
|
|
18
|
+
* The `unit` decides which fields are meaningful:
|
|
19
|
+
* - `percent` — subscription utilization; read `usedPercent` (0–100).
|
|
20
|
+
* - `count` — API-key buckets; read `limit`, `remaining`, `used`.
|
|
21
|
+
* - `estimated` — locally estimated; read `usedPercent`/`used`/`limit`, treat as
|
|
22
|
+
* a lower bound, never as authoritative.
|
|
23
|
+
*/
|
|
24
|
+
type UsageWindow$4 = {
|
|
25
|
+
/** Stable id, e.g. "5h" | "weekly" | "requests-per-min" | "tokens-per-min". */
|
|
26
|
+
id: string;
|
|
27
|
+
/** Human label, e.g. "5-hour session". */
|
|
28
|
+
label: string;
|
|
29
|
+
/** Which fields below are meaningful. */
|
|
30
|
+
unit: "percent" | "count" | "estimated";
|
|
31
|
+
/** 0–100. Set for `percent` and `estimated`. */
|
|
32
|
+
usedPercent?: number;
|
|
33
|
+
/** Absolute amount consumed. Set for `count` and `estimated`. */
|
|
34
|
+
used?: number;
|
|
35
|
+
/** Absolute cap. Set for `count` and `estimated`. */
|
|
36
|
+
limit?: number;
|
|
37
|
+
/** `limit - used`. Set for `count`. */
|
|
38
|
+
remaining?: number;
|
|
39
|
+
/** ISO-8601 timestamp when this window rolls over. */
|
|
40
|
+
resetsAt?: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalized usage for a single registered account. Every adapter — subscription
|
|
45
|
+
* utilization, API-key headers, local estimate — produces this same shape so the
|
|
46
|
+
* CLI, gateway, and UI render one model.
|
|
47
|
+
*/
|
|
48
|
+
type UsageReport$5 = {
|
|
49
|
+
/** The account's label in `~/.smithers/accounts.json`. */
|
|
50
|
+
accountLabel: string;
|
|
51
|
+
/** The account's provider. */
|
|
52
|
+
provider: AccountProvider;
|
|
53
|
+
/** How this account authenticates. */
|
|
54
|
+
authMode: "subscription" | "api-key";
|
|
55
|
+
/** Where the numbers came from. */
|
|
56
|
+
source: UsageSource;
|
|
57
|
+
/** Quota windows, possibly empty when `source` is `none`. */
|
|
58
|
+
windows: UsageWindow$4[];
|
|
59
|
+
/** Plan/tier label if the provider reports one, e.g. "max", "pro". */
|
|
60
|
+
planType?: string;
|
|
61
|
+
/** Pay-as-you-go credit balance, if the provider reports one (Codex). */
|
|
62
|
+
credits?: {
|
|
63
|
+
hasCredits: boolean;
|
|
64
|
+
unlimited: boolean;
|
|
65
|
+
balance?: string;
|
|
66
|
+
};
|
|
67
|
+
/** ISO-8601 timestamp of when this report was produced. */
|
|
68
|
+
fetchedAt: string;
|
|
69
|
+
/** True when served from cache past its soft TTL. */
|
|
70
|
+
stale: boolean;
|
|
71
|
+
/** True when the windows are locally estimated, not provider-authoritative. */
|
|
72
|
+
estimate: boolean;
|
|
73
|
+
/** Human-readable reason when `source` is `none` or a probe failed. */
|
|
74
|
+
error?: string;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** @typedef {import("@smithers-orchestrator/accounts").Account} Account */
|
|
78
|
+
/** @typedef {import("./UsageReport.ts").UsageReport} UsageReport */
|
|
79
|
+
/**
|
|
80
|
+
* Routes an account to its usage adapter and returns a normalized report. This
|
|
81
|
+
* switch mirrors `accountToProviderEnv` in the accounts package so the two stay
|
|
82
|
+
* structurally aligned. Adapters never throw; they degrade to a `none` report.
|
|
83
|
+
*
|
|
84
|
+
* Credentials are read on the host that owns them and only the normalized report
|
|
85
|
+
* leaves this function — no token is ever returned or logged.
|
|
86
|
+
*
|
|
87
|
+
* @param {Account} account
|
|
88
|
+
* @returns {Promise<UsageReport>}
|
|
89
|
+
*/
|
|
90
|
+
declare function getAccountUsage(account: Account$2): Promise<UsageReport$4>;
|
|
91
|
+
type Account$2 = _smithers_orchestrator_accounts.Account;
|
|
92
|
+
type UsageReport$4 = UsageReport$5;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gathers usage for many accounts in parallel, served through the on-disk cache.
|
|
96
|
+
* Cached reports come back with `stale: true`. The cache is read once and written
|
|
97
|
+
* once, so parallel probes never race on the file.
|
|
98
|
+
*
|
|
99
|
+
* @param {Account[]} accounts
|
|
100
|
+
* @param {{ fresh?: boolean; env?: NodeJS.ProcessEnv; nowMs?: number }} [options]
|
|
101
|
+
* @returns {Promise<UsageReport[]>}
|
|
102
|
+
*/
|
|
103
|
+
declare function getUsageForAccounts(accounts: Account$1[], options?: {
|
|
104
|
+
fresh?: boolean;
|
|
105
|
+
env?: NodeJS.ProcessEnv;
|
|
106
|
+
nowMs?: number;
|
|
107
|
+
}): Promise<UsageReport$3[]>;
|
|
108
|
+
type Account$1 = _smithers_orchestrator_accounts.Account;
|
|
109
|
+
type UsageReport$3 = UsageReport$5;
|
|
110
|
+
|
|
111
|
+
/** @typedef {import("@smithers-orchestrator/accounts").Account} Account */
|
|
112
|
+
/** @typedef {import("./UsageReport.ts").UsageReport} UsageReport */
|
|
113
|
+
/** @typedef {import("./UsageWindow.ts").UsageWindow} UsageWindow */
|
|
114
|
+
/**
|
|
115
|
+
* The partial result an adapter returns. The dispatcher wraps it with the
|
|
116
|
+
* account identity and timestamp to form a complete {@link UsageReport}.
|
|
117
|
+
*
|
|
118
|
+
* @typedef {object} UsageProbe
|
|
119
|
+
* @property {import("./UsageSource.ts").UsageSource} source
|
|
120
|
+
* @property {UsageWindow[]} [windows]
|
|
121
|
+
* @property {string} [planType]
|
|
122
|
+
* @property {{ hasCredits: boolean; unlimited: boolean; balance?: string }} [credits]
|
|
123
|
+
* @property {boolean} [estimate]
|
|
124
|
+
* @property {string} [error]
|
|
125
|
+
*/
|
|
126
|
+
/**
|
|
127
|
+
* Assembles a full usage report from an account and an adapter probe. Keeps the
|
|
128
|
+
* adapters free of repeated identity/timestamp boilerplate.
|
|
129
|
+
*
|
|
130
|
+
* @param {Account} account
|
|
131
|
+
* @param {UsageProbe} probe
|
|
132
|
+
* @param {{ nowIso?: string }} [options]
|
|
133
|
+
* @returns {UsageReport}
|
|
134
|
+
*/
|
|
135
|
+
declare function buildUsageReport(account: Account, probe: UsageProbe$5, options?: {
|
|
136
|
+
nowIso?: string;
|
|
137
|
+
}): UsageReport$2;
|
|
138
|
+
type Account = _smithers_orchestrator_accounts.Account;
|
|
139
|
+
type UsageReport$2 = UsageReport$5;
|
|
140
|
+
/**
|
|
141
|
+
* The partial result an adapter returns. The dispatcher wraps it with the
|
|
142
|
+
* account identity and timestamp to form a complete {@link UsageReport}.
|
|
143
|
+
*/
|
|
144
|
+
type UsageProbe$5 = {
|
|
145
|
+
source: UsageSource;
|
|
146
|
+
windows?: UsageWindow$4[] | undefined;
|
|
147
|
+
planType?: string | undefined;
|
|
148
|
+
credits?: {
|
|
149
|
+
hasCredits: boolean;
|
|
150
|
+
unlimited: boolean;
|
|
151
|
+
balance?: string;
|
|
152
|
+
} | undefined;
|
|
153
|
+
estimate?: boolean | undefined;
|
|
154
|
+
error?: string | undefined;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Renders an array of usage reports as an aligned text table. Pure: pass a fixed
|
|
159
|
+
* `nowMs` in tests to get deterministic "resets in" values.
|
|
160
|
+
*
|
|
161
|
+
* @param {UsageReport[]} reports
|
|
162
|
+
* @param {number} [nowMs]
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
declare function formatUsageReports(reports: UsageReport$1[], nowMs?: number): string;
|
|
166
|
+
type UsageReport$1 = UsageReport$5;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Formats an ISO reset timestamp as a relative "resets in" string. Returns an
|
|
170
|
+
* empty string when there is no timestamp, and `"now"` when it is in the past.
|
|
171
|
+
*
|
|
172
|
+
* @param {string | undefined} resetsAt
|
|
173
|
+
* @param {number} [nowMs]
|
|
174
|
+
* @returns {string}
|
|
175
|
+
*/
|
|
176
|
+
declare function formatRelativeReset(resetsAt: string | undefined, nowMs?: number): string;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Formats a number of seconds as a short human duration, e.g. `"2h 41m"`,
|
|
180
|
+
* `"5d 3h"`, `"42s"`. Used for "resets in" columns. Negative input renders as
|
|
181
|
+
* `"now"`.
|
|
182
|
+
*
|
|
183
|
+
* @param {number} seconds
|
|
184
|
+
* @returns {string}
|
|
185
|
+
*/
|
|
186
|
+
declare function humanizeDurationShort(seconds: number): string;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Normalizes the Claude Code subscription usage payload into usage windows. The
|
|
190
|
+
* payload powers the in-CLI `/usage` view: a 5-hour rolling window, a weekly
|
|
191
|
+
* window, and optional per-model weekly windows.
|
|
192
|
+
*
|
|
193
|
+
* @param {unknown} payload
|
|
194
|
+
* @returns {UsageWindow[]}
|
|
195
|
+
*/
|
|
196
|
+
declare function parseClaudeOauthUsage(payload: unknown): UsageWindow$3[];
|
|
197
|
+
type UsageWindow$3 = UsageWindow$4;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Normalizes the Codex usage payload (from `GET /backend-api/wham/usage`, or the
|
|
201
|
+
* `codex.rate_limits` event) into windows plus plan and credit metadata. The
|
|
202
|
+
* rate-limit object may sit at the top level or under a `rate_limits` key; both
|
|
203
|
+
* shapes are accepted.
|
|
204
|
+
*
|
|
205
|
+
* @param {unknown} payload
|
|
206
|
+
* @returns {{ windows: UsageWindow[]; planType?: string; credits?: { hasCredits: boolean; unlimited: boolean; balance?: string } }}
|
|
207
|
+
*/
|
|
208
|
+
declare function parseCodexUsage(payload: unknown): {
|
|
209
|
+
windows: UsageWindow$2[];
|
|
210
|
+
planType?: string;
|
|
211
|
+
credits?: {
|
|
212
|
+
hasCredits: boolean;
|
|
213
|
+
unlimited: boolean;
|
|
214
|
+
balance?: string;
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
type UsageWindow$2 = UsageWindow$4;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Parses Anthropic rate-limit response headers into usage windows. Anthropic
|
|
221
|
+
* returns RFC-3339 reset timestamps directly, so no clock math is needed.
|
|
222
|
+
*
|
|
223
|
+
* Pass a getter, e.g. `(name) => response.headers.get(name)`.
|
|
224
|
+
*
|
|
225
|
+
* @param {(name: string) => string | null | undefined} get
|
|
226
|
+
* @returns {UsageWindow[]}
|
|
227
|
+
*/
|
|
228
|
+
declare function parseAnthropicRateLimitHeaders(get: (name: string) => string | null | undefined): UsageWindow$1[];
|
|
229
|
+
type UsageWindow$1 = UsageWindow$4;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Parses OpenAI rate-limit response headers into usage windows. OpenAI's reset
|
|
233
|
+
* headers are Go-duration strings relative to "now", so `nowMs` is added to
|
|
234
|
+
* produce an absolute ISO reset time (pass a fixed value in tests).
|
|
235
|
+
*
|
|
236
|
+
* Pass a getter, e.g. `(name) => response.headers.get(name)`.
|
|
237
|
+
*
|
|
238
|
+
* @param {(name: string) => string | null | undefined} get
|
|
239
|
+
* @param {number} [nowMs]
|
|
240
|
+
* @returns {UsageWindow[]}
|
|
241
|
+
*/
|
|
242
|
+
declare function parseOpenAiRateLimitHeaders(get: (name: string) => string | null | undefined, nowMs?: number): UsageWindow[];
|
|
243
|
+
type UsageWindow = UsageWindow$4;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Parses a Go-style duration string into seconds. OpenAI's rate-limit reset
|
|
247
|
+
* headers use this format, e.g. `"1s"`, `"6m0s"`, `"1h2m3s"`, `"800ms"`.
|
|
248
|
+
*
|
|
249
|
+
* Returns `undefined` for input it cannot parse, so callers can omit a reset
|
|
250
|
+
* time rather than render a wrong one.
|
|
251
|
+
*
|
|
252
|
+
* @param {string | null | undefined} value
|
|
253
|
+
* @returns {number | undefined}
|
|
254
|
+
*/
|
|
255
|
+
declare function parseDurationSeconds(value: string | null | undefined): number | undefined;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Decodes the claims (the middle segment) of a JWT without verifying its
|
|
259
|
+
* signature. Used to read the `chatgpt_account_id` claim out of the Codex
|
|
260
|
+
* `id_token` when `auth.json` does not carry `tokens.account_id` directly.
|
|
261
|
+
*
|
|
262
|
+
* Verification is intentionally skipped: the token already authenticated the
|
|
263
|
+
* user with the provider, and we only read a non-secret routing claim from it.
|
|
264
|
+
*
|
|
265
|
+
* Returns an empty object for anything that is not a decodable JWT.
|
|
266
|
+
*
|
|
267
|
+
* @param {string | null | undefined} token
|
|
268
|
+
* @returns {Record<string, unknown>}
|
|
269
|
+
*/
|
|
270
|
+
declare function decodeJwtClaims(token: string | null | undefined): Record<string, unknown>;
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Reads the Claude Code subscription OAuth token for an account. Tries the
|
|
274
|
+
* account's `configDir/.credentials.json` first (the cross-platform location
|
|
275
|
+
* when `CLAUDE_CONFIG_DIR` is set), then falls back to the macOS Keychain item
|
|
276
|
+
* `Claude Code-credentials`.
|
|
277
|
+
*
|
|
278
|
+
* Returns `null` when no credential can be read, so the adapter degrades to a
|
|
279
|
+
* "none" report rather than throwing. The token is returned only to mint an
|
|
280
|
+
* outbound Authorization header; callers must never log or persist it.
|
|
281
|
+
*
|
|
282
|
+
* @param {{ configDir?: string }} account
|
|
283
|
+
* @param {NodeJS.Platform} [platform]
|
|
284
|
+
* @returns {{ accessToken: string; expiresAt?: number } | null}
|
|
285
|
+
*/
|
|
286
|
+
declare function readClaudeCredentials(account: {
|
|
287
|
+
configDir?: string;
|
|
288
|
+
}, platform?: NodeJS.Platform): {
|
|
289
|
+
accessToken: string;
|
|
290
|
+
expiresAt?: number;
|
|
291
|
+
} | null;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Reads the Codex ChatGPT-subscription OAuth token for an account from
|
|
295
|
+
* `configDir/auth.json` (the per-account `CODEX_HOME`). The ChatGPT account id
|
|
296
|
+
* comes from `tokens.account_id`, or failing that the `chatgpt_account_id`
|
|
297
|
+
* claim inside the `id_token` JWT.
|
|
298
|
+
*
|
|
299
|
+
* Returns `null` when no credential can be read or the account uses an API key
|
|
300
|
+
* instead of ChatGPT auth. The token is returned only to mint an outbound
|
|
301
|
+
* Authorization header.
|
|
302
|
+
*
|
|
303
|
+
* @param {{ configDir?: string }} account
|
|
304
|
+
* @returns {{ accessToken: string; accountId?: string } | null}
|
|
305
|
+
*/
|
|
306
|
+
declare function readCodexCredentials(account: {
|
|
307
|
+
configDir?: string;
|
|
308
|
+
}): {
|
|
309
|
+
accessToken: string;
|
|
310
|
+
accountId?: string;
|
|
311
|
+
} | null;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Probes the Claude Code subscription usage endpoint for an account's 5-hour and
|
|
315
|
+
* weekly utilization. Undocumented and best-effort: any failure degrades to a
|
|
316
|
+
* `none` report with a readable reason.
|
|
317
|
+
*
|
|
318
|
+
* @param {{ configDir?: string }} account
|
|
319
|
+
* @returns {Promise<UsageProbe>}
|
|
320
|
+
*/
|
|
321
|
+
declare function claudeOauthUsage(account: {
|
|
322
|
+
configDir?: string;
|
|
323
|
+
}): Promise<UsageProbe$4>;
|
|
324
|
+
type UsageProbe$4 = UsageProbe$5;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Probes the Codex ChatGPT-subscription usage endpoint for an account's 5-hour
|
|
328
|
+
* and weekly windows. This is the same data the Codex `/status` view shows and
|
|
329
|
+
* does not spend a turn. Undocumented and best-effort.
|
|
330
|
+
*
|
|
331
|
+
* @param {{ configDir?: string }} account
|
|
332
|
+
* @returns {Promise<UsageProbe>}
|
|
333
|
+
*/
|
|
334
|
+
declare function codexWhamUsage(account: {
|
|
335
|
+
configDir?: string;
|
|
336
|
+
}): Promise<UsageProbe$3>;
|
|
337
|
+
type UsageProbe$3 = UsageProbe$5;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Reads live Anthropic rate-limit headers for an API-key account. Uses the
|
|
341
|
+
* `count_tokens` endpoint, which returns the rate-limit header family without
|
|
342
|
+
* producing output tokens.
|
|
343
|
+
*
|
|
344
|
+
* @param {{ apiKey?: string }} account
|
|
345
|
+
* @returns {Promise<UsageProbe>}
|
|
346
|
+
*/
|
|
347
|
+
declare function anthropicHeaderUsage(account: {
|
|
348
|
+
apiKey?: string;
|
|
349
|
+
}): Promise<UsageProbe$2>;
|
|
350
|
+
type UsageProbe$2 = UsageProbe$5;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Reads live OpenAI rate-limit headers for an API-key account.
|
|
354
|
+
*
|
|
355
|
+
* @param {{ apiKey?: string }} account
|
|
356
|
+
* @returns {Promise<UsageProbe>}
|
|
357
|
+
*/
|
|
358
|
+
declare function openaiHeaderUsage(account: {
|
|
359
|
+
apiKey?: string;
|
|
360
|
+
}): Promise<UsageProbe$1>;
|
|
361
|
+
type UsageProbe$1 = UsageProbe$5;
|
|
362
|
+
|
|
363
|
+
/** @typedef {import("./buildUsageReport.js").UsageProbe} UsageProbe */
|
|
364
|
+
/**
|
|
365
|
+
* Google (Gemini, Antigravity, Gemini API) exposes no live "remaining quota"
|
|
366
|
+
* surface to a personal-login or API-key client: there are no rate-limit
|
|
367
|
+
* response headers, only a 429 `RESOURCE_EXHAUSTED` after the wall is hit. The
|
|
368
|
+
* documented path forward is local token-log accounting against published caps
|
|
369
|
+
* (see `publishedCaps.js`), which depends on run-history integration and lands
|
|
370
|
+
* in a later phase. Until then this reports `none` honestly rather than inventing
|
|
371
|
+
* a number.
|
|
372
|
+
*
|
|
373
|
+
* @param {{ provider: string }} account
|
|
374
|
+
* @returns {Promise<UsageProbe>}
|
|
375
|
+
*/
|
|
376
|
+
declare function googleUsage(account: {
|
|
377
|
+
provider: string;
|
|
378
|
+
}): Promise<UsageProbe>;
|
|
379
|
+
type UsageProbe = UsageProbe$5;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Looks up a published cap by tier id. Returns `undefined` for unknown tiers so
|
|
383
|
+
* the caller can degrade to "unknown" rather than invent a number.
|
|
384
|
+
*
|
|
385
|
+
* @param {string | undefined} tier
|
|
386
|
+
* @returns {{ label: string; requestsPerDay: number; rpm?: number } | undefined}
|
|
387
|
+
*/
|
|
388
|
+
declare function publishedCapForTier(tier: string | undefined): {
|
|
389
|
+
label: string;
|
|
390
|
+
requestsPerDay: number;
|
|
391
|
+
rpm?: number;
|
|
392
|
+
} | undefined;
|
|
393
|
+
/**
|
|
394
|
+
* Published daily request caps for Google providers, keyed by tier. Google does
|
|
395
|
+
* not expose a live "remaining quota" surface to a personal-login or API-key
|
|
396
|
+
* client, so any usage estimate must subtract local request counts from these
|
|
397
|
+
* documented caps. The numbers move (and the personal Code Assist tiers in the
|
|
398
|
+
* Gemini CLI stop serving 2026-06-18), so they live here as data, not logic.
|
|
399
|
+
*
|
|
400
|
+
* @type {Record<string, { label: string; requestsPerDay: number; rpm?: number }>}
|
|
401
|
+
*/
|
|
402
|
+
declare const PUBLISHED_CAPS: Record<string, {
|
|
403
|
+
label: string;
|
|
404
|
+
requestsPerDay: number;
|
|
405
|
+
rpm?: number;
|
|
406
|
+
}>;
|
|
407
|
+
|
|
408
|
+
/** @typedef {import("./UsageReport.ts").UsageReport} UsageReport */
|
|
409
|
+
/** @typedef {{ version: 1; entries: Record<string, { report: UsageReport }> }} UsageCacheFile */
|
|
410
|
+
/**
|
|
411
|
+
* Path to the on-disk usage cache. Lives next to `accounts.json` under the
|
|
412
|
+
* Smithers root so it honors `SMITHERS_HOME` in tests and CI.
|
|
413
|
+
*
|
|
414
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
415
|
+
* @returns {string}
|
|
416
|
+
*/
|
|
417
|
+
declare function usageCachePath(env?: NodeJS.ProcessEnv): string;
|
|
418
|
+
/**
|
|
419
|
+
* Reads the usage cache, returning an empty cache when the file is missing or
|
|
420
|
+
* malformed (a cold cache is the normal startup state, not an error).
|
|
421
|
+
*
|
|
422
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
423
|
+
* @returns {UsageCacheFile}
|
|
424
|
+
*/
|
|
425
|
+
declare function readUsageCache(env?: NodeJS.ProcessEnv): UsageCacheFile;
|
|
426
|
+
/**
|
|
427
|
+
* Writes the usage cache atomically with mode 0600.
|
|
428
|
+
*
|
|
429
|
+
* @param {UsageCacheFile} contents
|
|
430
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
431
|
+
* @returns {string} the path written
|
|
432
|
+
*/
|
|
433
|
+
declare function writeUsageCache(contents: UsageCacheFile, env?: NodeJS.ProcessEnv): string;
|
|
434
|
+
type UsageReport = UsageReport$5;
|
|
435
|
+
type UsageCacheFile = {
|
|
436
|
+
version: 1;
|
|
437
|
+
entries: Record<string, {
|
|
438
|
+
report: UsageReport;
|
|
439
|
+
}>;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
export { PUBLISHED_CAPS, anthropicHeaderUsage, buildUsageReport, claudeOauthUsage, codexWhamUsage, decodeJwtClaims, formatRelativeReset, formatUsageReports, getAccountUsage, getUsageForAccounts, googleUsage, humanizeDurationShort, openaiHeaderUsage, parseAnthropicRateLimitHeaders, parseClaudeOauthUsage, parseCodexUsage, parseDurationSeconds, parseOpenAiRateLimitHeaders, publishedCapForTier, readClaudeCredentials, readCodexCredentials, readUsageCache, usageCachePath, writeUsageCache };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { getAccountUsage } from "./getAccountUsage.js";
|
|
2
|
+
export { getUsageForAccounts } from "./getUsageForAccounts.js";
|
|
3
|
+
export { buildUsageReport } from "./buildUsageReport.js";
|
|
4
|
+
export { formatUsageReports } from "./formatUsageReports.js";
|
|
5
|
+
export { formatRelativeReset } from "./formatRelativeReset.js";
|
|
6
|
+
export { humanizeDurationShort } from "./humanizeDurationShort.js";
|
|
7
|
+
export { parseClaudeOauthUsage } from "./parseClaudeOauthUsage.js";
|
|
8
|
+
export { parseCodexUsage } from "./parseCodexUsage.js";
|
|
9
|
+
export { parseAnthropicRateLimitHeaders } from "./parseAnthropicRateLimitHeaders.js";
|
|
10
|
+
export { parseOpenAiRateLimitHeaders } from "./parseOpenAiRateLimitHeaders.js";
|
|
11
|
+
export { parseDurationSeconds } from "./parseDurationSeconds.js";
|
|
12
|
+
export { decodeJwtClaims } from "./decodeJwtClaims.js";
|
|
13
|
+
export { PUBLISHED_CAPS, publishedCapForTier } from "./publishedCaps.js";
|
|
14
|
+
export { readClaudeCredentials } from "./readClaudeCredentials.js";
|
|
15
|
+
export { readCodexCredentials } from "./readCodexCredentials.js";
|
|
16
|
+
export { claudeOauthUsage } from "./claudeOauthUsage.js";
|
|
17
|
+
export { codexWhamUsage } from "./codexWhamUsage.js";
|
|
18
|
+
export { anthropicHeaderUsage } from "./anthropicHeaderUsage.js";
|
|
19
|
+
export { openaiHeaderUsage } from "./openaiHeaderUsage.js";
|
|
20
|
+
export { googleUsage } from "./googleUsage.js";
|
|
21
|
+
export { usageCachePath, readUsageCache, writeUsageCache } from "./usageCache.js";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { parseOpenAiRateLimitHeaders } from "./parseOpenAiRateLimitHeaders.js";
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("./buildUsageReport.js").UsageProbe} UsageProbe */
|
|
4
|
+
|
|
5
|
+
const CHAT_URL = "https://api.openai.com/v1/chat/completions";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `GET /v1/models` does not return rate-limit headers; only a model request
|
|
9
|
+
* does. This minimal completion costs one request and a single output token,
|
|
10
|
+
* which is the cheapest documented way to read the headers.
|
|
11
|
+
*/
|
|
12
|
+
const PROBE_MODEL = process.env.SMITHERS_OPENAI_PROBE_MODEL ?? "gpt-4o-mini";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reads live OpenAI rate-limit headers for an API-key account.
|
|
16
|
+
*
|
|
17
|
+
* @param {{ apiKey?: string }} account
|
|
18
|
+
* @returns {Promise<UsageProbe>}
|
|
19
|
+
*/
|
|
20
|
+
export async function openaiHeaderUsage(account) {
|
|
21
|
+
const apiKey = account.apiKey;
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
return { source: "none", error: "Account has no API key set" };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(CHAT_URL, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${apiKey}`,
|
|
30
|
+
"content-type": "application/json",
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
model: PROBE_MODEL,
|
|
34
|
+
max_tokens: 1,
|
|
35
|
+
messages: [{ role: "user", content: "hi" }],
|
|
36
|
+
}),
|
|
37
|
+
signal: AbortSignal.timeout(6_000),
|
|
38
|
+
});
|
|
39
|
+
if (res.status === 401) {
|
|
40
|
+
return { source: "none", error: "OPENAI_API_KEY rejected (401)" };
|
|
41
|
+
}
|
|
42
|
+
const get = (name) => res.headers.get(name);
|
|
43
|
+
const windows = parseOpenAiRateLimitHeaders(get);
|
|
44
|
+
if (res.status === 429) {
|
|
45
|
+
const retryAfter = res.headers.get("retry-after");
|
|
46
|
+
return {
|
|
47
|
+
source: "headers",
|
|
48
|
+
windows,
|
|
49
|
+
error: `Rate limited (429)${retryAfter ? ` — retry after ${retryAfter}s` : ""}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (!res.ok && windows.length === 0) {
|
|
53
|
+
return { source: "none", error: `OpenAI returned ${res.status} with no rate-limit headers` };
|
|
54
|
+
}
|
|
55
|
+
return { source: "headers", windows };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { source: "none", error: `OpenAI header probe failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** @typedef {import("./UsageWindow.ts").UsageWindow} UsageWindow */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string | null | undefined} value
|
|
5
|
+
* @returns {number | undefined}
|
|
6
|
+
*/
|
|
7
|
+
function int(value) {
|
|
8
|
+
if (value == null) return undefined;
|
|
9
|
+
const n = parseInt(value, 10);
|
|
10
|
+
return Number.isNaN(n) ? undefined : n;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {(name: string) => string | null | undefined} get
|
|
15
|
+
* @param {string} prefix
|
|
16
|
+
* @param {string} id
|
|
17
|
+
* @param {string} label
|
|
18
|
+
* @returns {UsageWindow | undefined}
|
|
19
|
+
*/
|
|
20
|
+
function countWindow(get, prefix, id, label) {
|
|
21
|
+
const limit = int(get(`${prefix}-limit`));
|
|
22
|
+
const remaining = int(get(`${prefix}-remaining`));
|
|
23
|
+
if (limit === undefined && remaining === undefined) return undefined;
|
|
24
|
+
const reset = get(`${prefix}-reset`);
|
|
25
|
+
const used = limit !== undefined && remaining !== undefined ? limit - remaining : undefined;
|
|
26
|
+
return {
|
|
27
|
+
id,
|
|
28
|
+
label,
|
|
29
|
+
unit: "count",
|
|
30
|
+
limit,
|
|
31
|
+
remaining,
|
|
32
|
+
used,
|
|
33
|
+
resetsAt: typeof reset === "string" ? reset : undefined,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parses Anthropic rate-limit response headers into usage windows. Anthropic
|
|
39
|
+
* returns RFC-3339 reset timestamps directly, so no clock math is needed.
|
|
40
|
+
*
|
|
41
|
+
* Pass a getter, e.g. `(name) => response.headers.get(name)`.
|
|
42
|
+
*
|
|
43
|
+
* @param {(name: string) => string | null | undefined} get
|
|
44
|
+
* @returns {UsageWindow[]}
|
|
45
|
+
*/
|
|
46
|
+
export function parseAnthropicRateLimitHeaders(get) {
|
|
47
|
+
const windows = [];
|
|
48
|
+
const requests = countWindow(get, "anthropic-ratelimit-requests", "requests-per-min", "requests/min");
|
|
49
|
+
if (requests) windows.push(requests);
|
|
50
|
+
const tokens = countWindow(get, "anthropic-ratelimit-tokens", "tokens-per-min", "tokens/min");
|
|
51
|
+
if (tokens) windows.push(tokens);
|
|
52
|
+
const input = countWindow(get, "anthropic-ratelimit-input-tokens", "input-tokens-per-min", "input tokens/min");
|
|
53
|
+
if (input) windows.push(input);
|
|
54
|
+
const output = countWindow(get, "anthropic-ratelimit-output-tokens", "output-tokens-per-min", "output tokens/min");
|
|
55
|
+
if (output) windows.push(output);
|
|
56
|
+
return windows;
|
|
57
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** @typedef {import("./UsageWindow.ts").UsageWindow} UsageWindow */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds one percent window from a `{ utilization, resets_at }` block as
|
|
5
|
+
* returned by `GET https://api.anthropic.com/api/oauth/usage`.
|
|
6
|
+
*
|
|
7
|
+
* @param {unknown} block
|
|
8
|
+
* @param {string} id
|
|
9
|
+
* @param {string} label
|
|
10
|
+
* @returns {UsageWindow | undefined}
|
|
11
|
+
*/
|
|
12
|
+
function windowFrom(block, id, label) {
|
|
13
|
+
if (!block || typeof block !== "object") return undefined;
|
|
14
|
+
const util = /** @type {{ utilization?: unknown }} */ (block).utilization;
|
|
15
|
+
if (typeof util !== "number") return undefined;
|
|
16
|
+
const resetsAt = /** @type {{ resets_at?: unknown }} */ (block).resets_at;
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
label,
|
|
20
|
+
unit: "percent",
|
|
21
|
+
usedPercent: util,
|
|
22
|
+
resetsAt: typeof resetsAt === "string" ? resetsAt : undefined,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalizes the Claude Code subscription usage payload into usage windows. The
|
|
28
|
+
* payload powers the in-CLI `/usage` view: a 5-hour rolling window, a weekly
|
|
29
|
+
* window, and optional per-model weekly windows.
|
|
30
|
+
*
|
|
31
|
+
* @param {unknown} payload
|
|
32
|
+
* @returns {UsageWindow[]}
|
|
33
|
+
*/
|
|
34
|
+
export function parseClaudeOauthUsage(payload) {
|
|
35
|
+
if (!payload || typeof payload !== "object") return [];
|
|
36
|
+
const p = /** @type {Record<string, unknown>} */ (payload);
|
|
37
|
+
const windows = [];
|
|
38
|
+
const fiveHour = windowFrom(p.five_hour, "5h", "5-hour session");
|
|
39
|
+
if (fiveHour) windows.push(fiveHour);
|
|
40
|
+
const weekly = windowFrom(p.seven_day, "weekly", "weekly");
|
|
41
|
+
if (weekly) windows.push(weekly);
|
|
42
|
+
const opus = windowFrom(p.seven_day_opus, "weekly-opus", "weekly (Opus)");
|
|
43
|
+
if (opus) windows.push(opus);
|
|
44
|
+
const sonnet = windowFrom(p.seven_day_sonnet, "weekly-sonnet", "weekly (Sonnet)");
|
|
45
|
+
if (sonnet) windows.push(sonnet);
|
|
46
|
+
return windows;
|
|
47
|
+
}
|