dsh-plugin-subscriptions 0.1.2 → 0.3.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/lib/index.js CHANGED
@@ -454,6 +454,7 @@ async function dispatch(controller, endpoint, payload, signal) {
454
454
  case "logout":
455
455
  await controller.logout(readProvider(payload));
456
456
  return ok({ ok: true });
457
+ case "usage": return ok(await controller.usage(readProvider(payload), signal));
457
458
  case "image": return ok(await controller.readImage(readImageRef(payload), signal));
458
459
  default: throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
459
460
  }
@@ -1323,6 +1324,55 @@ async function refreshCodex(session) {
1323
1324
  function isCodexPermanentRefreshError(error) {
1324
1325
  return error instanceof OAuthEndpointError && error.oauthCode !== void 0 && PERMANENT_REFRESH_CODES.has(error.oauthCode);
1325
1326
  }
1327
+ const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
1328
+ /** Map one wham/usage window into a {@link UsageWindow}; undefined when unusable. */
1329
+ function codexUsageWindow(value, kind) {
1330
+ if (typeof value !== "object" || value === null) return void 0;
1331
+ const window = value;
1332
+ if (typeof window.used_percent !== "number" || !Number.isFinite(window.used_percent)) return void 0;
1333
+ let resetsAt;
1334
+ if (typeof window.reset_at === "number" && window.reset_at > 0) resetsAt = window.reset_at * 1e3;
1335
+ else if (typeof window.reset_after_seconds === "number" && window.reset_after_seconds > 0) resetsAt = Date.now() + window.reset_after_seconds * 1e3;
1336
+ return {
1337
+ kind,
1338
+ usedPercent: window.used_percent,
1339
+ ...resetsAt === void 0 ? {} : { resetsAt }
1340
+ };
1341
+ }
1342
+ /**
1343
+ * Fetch the codex subscription usage from the ChatGPT backend wham/usage
1344
+ * endpoint (the source of the codex CLI `/status` rate-limit lines). The
1345
+ * primary window is the rolling session (5-hour) lane, the secondary window
1346
+ * the weekly lane; the lookup itself consumes no rate-limit budget.
1347
+ * @param session - the stored session (used as-is; never refreshed here).
1348
+ * @param fetchFn - fetch implementation (injectable for tests).
1349
+ * @param signal - caller cancellation from the RPC transport.
1350
+ * @returns the mapped usage snapshot.
1351
+ */
1352
+ async function fetchCodexUsage(session, fetchFn = fetch, signal) {
1353
+ const response = await fetchFn(CODEX_USAGE_URL, {
1354
+ headers: {
1355
+ "authorization": `Bearer ${session.accessToken}`,
1356
+ "chatgpt-account-id": session.accountId,
1357
+ "originator": "codex_cli_rs",
1358
+ "accept": "application/json",
1359
+ ...attributionHeaders()
1360
+ },
1361
+ ...signal === void 0 ? {} : { signal }
1362
+ });
1363
+ if (!response.ok) throw await oauthEndpointError(response, "codex usage");
1364
+ const payload = await response.json();
1365
+ const windows = [];
1366
+ const primary = codexUsageWindow(payload.rate_limit?.primary_window, "session");
1367
+ const secondary = codexUsageWindow(payload.rate_limit?.secondary_window, "weekly");
1368
+ if (primary !== void 0) windows.push(primary);
1369
+ if (secondary !== void 0) windows.push(secondary);
1370
+ return {
1371
+ supported: true,
1372
+ windows,
1373
+ ...typeof payload.plan_type === "string" && payload.plan_type.length > 0 ? { plan: payload.plan_type } : {}
1374
+ };
1375
+ }
1326
1376
  const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
1327
1377
  /**
1328
1378
  * Client version sent on the /models catalog request. The backend gates the
@@ -1979,6 +2029,86 @@ async function refreshClaude(session) {
1979
2029
  function isClaudePermanentRefreshError(error) {
1980
2030
  return error instanceof OAuthEndpointError && (error.oauthCode === "invalid_grant" || error.oauthCode === "invalid_token");
1981
2031
  }
2032
+ const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
2033
+ /** RFC3339 `resets_at` value → epoch ms, or undefined when absent/unparsable. */
2034
+ function claudeResetsAt(value) {
2035
+ if (typeof value !== "string" || value.length === 0) return void 0;
2036
+ const parsed = Date.parse(value);
2037
+ return Number.isFinite(parsed) ? parsed : void 0;
2038
+ }
2039
+ /** Map one legacy `{utilization, resets_at}` bucket; undefined when null or unusable. */
2040
+ function claudeLegacyWindow(value, kind, scope) {
2041
+ if (typeof value !== "object" || value === null) return void 0;
2042
+ const bucket = value;
2043
+ if (typeof bucket.utilization !== "number" || !Number.isFinite(bucket.utilization)) return void 0;
2044
+ const resetsAt = claudeResetsAt(bucket.resets_at);
2045
+ return {
2046
+ kind,
2047
+ ...scope === void 0 ? {} : { scope },
2048
+ usedPercent: bucket.utilization,
2049
+ ...resetsAt === void 0 ? {} : { resetsAt }
2050
+ };
2051
+ }
2052
+ /** Map the modern `limits` array; empty when absent or carrying nothing usable. */
2053
+ function claudeLimitsWindows(value) {
2054
+ if (!Array.isArray(value)) return [];
2055
+ const windows = [];
2056
+ for (const raw of value) {
2057
+ if (typeof raw !== "object" || raw === null) continue;
2058
+ const entry = raw;
2059
+ if (typeof entry.percent !== "number" || !Number.isFinite(entry.percent)) continue;
2060
+ const kind = entry.kind === "session" ? "session" : entry.kind === "weekly_all" || entry.kind === "weekly_scoped" ? "weekly" : "other";
2061
+ const scope = entry.scope?.model?.display_name;
2062
+ const resetsAt = claudeResetsAt(entry.resets_at);
2063
+ windows.push({
2064
+ kind,
2065
+ ...typeof scope === "string" && scope.length > 0 ? { scope } : {},
2066
+ usedPercent: entry.percent,
2067
+ ...resetsAt === void 0 ? {} : { resetsAt }
2068
+ });
2069
+ }
2070
+ return windows;
2071
+ }
2072
+ /**
2073
+ * Fetch the claude subscription usage from the OAuth usage endpoint (the
2074
+ * source of Claude Code's `/usage` screen). Newer responses carry a
2075
+ * structured `limits` array; older ones the flat `five_hour`/`seven_day*`
2076
+ * buckets — both shapes are read, the array winning when it has entries.
2077
+ * @param session - the stored session (used as-is; never refreshed here).
2078
+ * @param fetchFn - fetch implementation (injectable for tests).
2079
+ * @param signal - caller cancellation from the RPC transport.
2080
+ * @returns the mapped usage snapshot.
2081
+ */
2082
+ async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
2083
+ const response = await fetchFn(CLAUDE_USAGE_URL, {
2084
+ headers: {
2085
+ "authorization": `Bearer ${session.accessToken}`,
2086
+ "anthropic-beta": "oauth-2025-04-20",
2087
+ "user-agent": CLAUDE_CLI_USER_AGENT,
2088
+ "accept": "application/json"
2089
+ },
2090
+ ...signal === void 0 ? {} : { signal }
2091
+ });
2092
+ if (!response.ok) throw await oauthEndpointError(response, "claude usage");
2093
+ const payload = await response.json();
2094
+ const modern = claudeLimitsWindows(payload.limits);
2095
+ if (modern.length > 0) return {
2096
+ supported: true,
2097
+ windows: modern
2098
+ };
2099
+ const windows = [];
2100
+ const legacy = [
2101
+ claudeLegacyWindow(payload.five_hour, "session"),
2102
+ claudeLegacyWindow(payload.seven_day, "weekly"),
2103
+ claudeLegacyWindow(payload.seven_day_opus, "weekly", "Opus"),
2104
+ claudeLegacyWindow(payload.seven_day_sonnet, "weekly", "Sonnet")
2105
+ ];
2106
+ for (const window of legacy) if (window !== void 0) windows.push(window);
2107
+ return {
2108
+ supported: true,
2109
+ windows
2110
+ };
2111
+ }
1982
2112
  /** The Claude 4.5 family accepts image input. */
1983
2113
  const CLAUDE_MODALITIES = ["text", "image"];
1984
2114
  /** Claude wire adapter: one instance serves the `claude` provider route. */
@@ -2131,6 +2261,33 @@ async function grokFlow() {
2131
2261
  }
2132
2262
  };
2133
2263
  }
2264
+ /**
2265
+ * Display names for the numeric `tier` claim xAI stamps on OAuth access
2266
+ * tokens (the `prod_auth.SubscriptionTier` proto enum; the mapping mirrors
2267
+ * grok-build's `jwt_tier_claim`). Unknown values fall through to the raw
2268
+ * number so a future tier still shows something.
2269
+ */
2270
+ const GROK_TIER_NAMES = {
2271
+ 0: "Free",
2272
+ 1: "SuperGrok",
2273
+ 2: "X Basic",
2274
+ 3: "X Premium",
2275
+ 4: "X Premium+",
2276
+ 5: "SuperGrok Heavy",
2277
+ 6: "SuperGrok Lite",
2278
+ 7: "SuperGrok Plus"
2279
+ };
2280
+ /**
2281
+ * The subscription tier encoded in a grok access token's `tier` claim (no
2282
+ * verification — same trust posture as the other claim reads).
2283
+ * @param accessToken - the stored access token.
2284
+ * @returns the display tier name, or undefined when the claim is absent.
2285
+ */
2286
+ function grokTierName(accessToken) {
2287
+ const tier = decodeJwtPayload(accessToken)?.tier;
2288
+ if (typeof tier !== "number" || !Number.isInteger(tier)) return void 0;
2289
+ return GROK_TIER_NAMES[tier] ?? String(tier);
2290
+ }
2134
2291
  /** Pick a display account from an id token's claims. */
2135
2292
  function grokAccount(idToken) {
2136
2293
  const payload = idToken === void 0 ? void 0 : decodeJwtPayload(idToken);
@@ -2213,6 +2370,66 @@ async function refreshGrok(session) {
2213
2370
  function isGrokPermanentRefreshError(error) {
2214
2371
  return error instanceof OAuthEndpointError && error.oauthCode === "invalid_grant";
2215
2372
  }
2373
+ /**
2374
+ * The Grok Build CLI chat proxy's billing endpoint (the source of the CLI's
2375
+ * `/usage` "Usage limit" panel; see xai-org/grok-build
2376
+ * `extensions/billing.rs`). Forwards to the backend `GetGrokCreditsConfig`.
2377
+ */
2378
+ const GROK_BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
2379
+ /** RFC3339 timestamp → epoch ms, or undefined when absent/unparsable. */
2380
+ function grokResetsAt(value) {
2381
+ if (typeof value !== "string" || value.length === 0) return void 0;
2382
+ const parsed = Date.parse(value);
2383
+ return Number.isFinite(parsed) ? parsed : void 0;
2384
+ }
2385
+ /**
2386
+ * Fetch the grok subscription usage from the Grok Build CLI chat proxy. The
2387
+ * newer credits config carries a ready-made percentage plus the current
2388
+ * (typically weekly) period; the legacy shape carries cent-valued
2389
+ * `monthlyLimit`/`used`, from which the percentage is derived.
2390
+ * @param session - the stored session (used as-is; never refreshed here).
2391
+ * @param fetchFn - fetch implementation (injectable for tests).
2392
+ * @param signal - caller cancellation from the RPC transport.
2393
+ * @returns the mapped usage snapshot.
2394
+ */
2395
+ async function fetchGrokUsage(session, fetchFn = fetch, signal) {
2396
+ const response = await fetchFn(GROK_BILLING_URL, {
2397
+ headers: {
2398
+ "authorization": `Bearer ${session.accessToken}`,
2399
+ "x-xai-token-auth": "xai-grok-cli",
2400
+ "accept": "application/json",
2401
+ ...attributionHeaders()
2402
+ },
2403
+ ...signal === void 0 ? {} : { signal }
2404
+ });
2405
+ if (!response.ok) throw await oauthEndpointError(response, "grok billing");
2406
+ const payload = await response.json();
2407
+ const config = typeof payload.config === "object" && payload.config !== null ? payload.config : {};
2408
+ const windows = [];
2409
+ if (typeof config.creditUsagePercent === "number" && Number.isFinite(config.creditUsagePercent)) {
2410
+ const kind = config.currentPeriod?.type === "USAGE_PERIOD_TYPE_WEEKLY" ? "weekly" : "other";
2411
+ const resetsAt = grokResetsAt(config.currentPeriod?.end);
2412
+ windows.push({
2413
+ kind,
2414
+ usedPercent: config.creditUsagePercent,
2415
+ ...resetsAt === void 0 ? {} : { resetsAt }
2416
+ });
2417
+ } else if (typeof config.monthlyLimit?.val === "number" && config.monthlyLimit.val > 0) {
2418
+ const used = typeof config.used?.val === "number" ? config.used.val : 0;
2419
+ const resetsAt = grokResetsAt(config.billingPeriodEnd);
2420
+ windows.push({
2421
+ kind: "other",
2422
+ usedPercent: used / config.monthlyLimit.val * 100,
2423
+ ...resetsAt === void 0 ? {} : { resetsAt }
2424
+ });
2425
+ }
2426
+ const plan = typeof payload.subscriptionTier === "string" && payload.subscriptionTier.length > 0 ? payload.subscriptionTier : grokTierName(session.accessToken);
2427
+ return {
2428
+ supported: true,
2429
+ windows,
2430
+ ...plan === void 0 ? {} : { plan }
2431
+ };
2432
+ }
2216
2433
  const GROK_MODELS_URL = "https://api.x.ai/v1/models";
2217
2434
  /**
2218
2435
  * Input modalities for one grok model: chat models (grok-4 family) accept
@@ -2222,6 +2439,58 @@ function grokModalities(id) {
2222
2439
  return /code|embed/i.test(id) ? ["text"] : ["text", "image"];
2223
2440
  }
2224
2441
  /**
2442
+ * The Grok Build CLI chat proxy's model catalog — the only grok endpoint that
2443
+ * advertises reasoning capability. The `api.x.ai/v1/models` and
2444
+ * `/v1/language-models` payloads carry pricing, context, and aliases only, so
2445
+ * effort metadata must come from here (the same source the official CLI's
2446
+ * picker uses).
2447
+ */
2448
+ const GROK_CLI_MODELS_URL = "https://cli-chat-proxy.grok.com/v1/models";
2449
+ /** Map one CLI catalog entry's reasoning fields, or undefined when unsupported. */
2450
+ function grokCliReasoning(entry) {
2451
+ if (entry.supports_reasoning_effort !== true) return void 0;
2452
+ const efforts = (entry.reasoning_efforts ?? []).filter((level) => typeof level.value === "string" && level.value.length > 0).map((level) => ({
2453
+ id: ReasoningEffortId(level.value),
2454
+ name: typeof level.label === "string" && level.label.length > 0 ? level.label : level.value,
2455
+ ...typeof level.description === "string" && level.description.length > 0 ? { description: level.description } : {}
2456
+ }));
2457
+ if (efforts.length === 0) return void 0;
2458
+ const defaultEffort = typeof entry.reasoning_effort === "string" && efforts.some((effort) => effort.id === ReasoningEffortId(entry.reasoning_effort)) ? ReasoningEffortId(entry.reasoning_effort) : void 0;
2459
+ return {
2460
+ efforts,
2461
+ ...defaultEffort === void 0 ? {} : { defaultEffort }
2462
+ };
2463
+ }
2464
+ /**
2465
+ * Fetch the CLI catalog and index its per-model metadata by model id.
2466
+ * @param session - the stored session (used as-is; never refreshed here).
2467
+ * @param fetchFn - fetch implementation (injectable for tests).
2468
+ * @returns model id → contributed metadata.
2469
+ */
2470
+ async function fetchGrokCliCatalog(session, fetchFn = fetch) {
2471
+ const response = await fetchFn(GROK_CLI_MODELS_URL, { headers: {
2472
+ "authorization": `Bearer ${session.accessToken}`,
2473
+ "x-xai-token-auth": "xai-grok-cli",
2474
+ "accept": "application/json",
2475
+ ...attributionHeaders()
2476
+ } });
2477
+ if (!response.ok) throw await oauthEndpointError(response, "grok CLI catalog");
2478
+ const payload = await response.json();
2479
+ if (!Array.isArray(payload.data)) throw new Error("grok CLI catalog returned no data array");
2480
+ const catalog = /* @__PURE__ */ new Map();
2481
+ for (const entry of payload.data) {
2482
+ if (typeof entry.id !== "string" || entry.id.length === 0) continue;
2483
+ const reasoning = grokCliReasoning(entry);
2484
+ catalog.set(entry.id, {
2485
+ ...typeof entry.name === "string" && entry.name.length > 0 ? { name: entry.name } : {},
2486
+ ...typeof entry.description === "string" && entry.description.length > 0 ? { description: entry.description } : {},
2487
+ ...typeof entry.context_window === "number" && entry.context_window > 0 ? { contextWindow: entry.context_window } : {},
2488
+ ...reasoning === void 0 ? {} : { reasoning }
2489
+ });
2490
+ }
2491
+ return catalog;
2492
+ }
2493
+ /**
2225
2494
  * The /v1/models list also serves generation models that cannot chat
2226
2495
  * (grok-imagine-image*, grok-imagine-video*) and embedding models; the picker
2227
2496
  * must not offer them. Heuristic over the id substring, verified against the
@@ -2231,17 +2500,24 @@ function isChatModel(id) {
2231
2500
  return !/imagine|image-|video|embed/i.test(id);
2232
2501
  }
2233
2502
  /**
2234
- * Fetch the live grok model list.
2503
+ * Fetch the live grok model list, enriched with the CLI catalog's per-model
2504
+ * metadata (display name, context window, reasoning efforts). The api.x.ai
2505
+ * list stays authoritative for which models exist; the CLI catalog is
2506
+ * enrichment only, so its failure degrades to a plain list instead of taking
2507
+ * discovery down — models it does not cover simply expose no efforts.
2235
2508
  * @param session - the stored session (used as-is; never refreshed here).
2236
2509
  * @param fetchFn - fetch implementation (injectable for tests).
2237
- * @returns discovered chat models in endpoint order (id doubles as the name).
2510
+ * @param onWarn - warning sink for a failed CLI catalog fetch.
2511
+ * @returns discovered chat models in endpoint order.
2238
2512
  */
2239
- async function fetchGrokModels(session, fetchFn = fetch) {
2240
- const response = await fetchFn(GROK_MODELS_URL, { headers: {
2513
+ async function fetchGrokModels(session, fetchFn = fetch, onWarn) {
2514
+ const [response, cliCatalog] = await Promise.all([fetchFn(GROK_MODELS_URL, { headers: {
2241
2515
  "authorization": `Bearer ${session.accessToken}`,
2242
2516
  "accept": "application/json",
2243
2517
  ...attributionHeaders()
2244
- } });
2518
+ } }), fetchGrokCliCatalog(session, fetchFn).catch((error) => {
2519
+ onWarn?.(`grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`);
2520
+ })]);
2245
2521
  if (!response.ok) throw await oauthEndpointError(response, "grok models");
2246
2522
  const payload = await response.json();
2247
2523
  if (!Array.isArray(payload.data)) throw new Error("grok models endpoint returned no data array");
@@ -2253,7 +2529,8 @@ async function fetchGrokModels(session, fetchFn = fetch) {
2253
2529
  seen.add(entry.id);
2254
2530
  discovered.push({
2255
2531
  id: entry.id,
2256
- name: entry.id
2532
+ name: entry.id,
2533
+ ...cliCatalog?.get(entry.id)
2257
2534
  });
2258
2535
  }
2259
2536
  if (discovered.length === 0) throw new Error("grok models endpoint returned an empty catalog");
@@ -2284,10 +2561,11 @@ var GrokAdapter = class extends LlmAdapter {
2284
2561
  if (await this.options.tokens.peek() === void 0) return [];
2285
2562
  if (!this.options.discovery) return this.staticModels(provider);
2286
2563
  try {
2287
- return (await this.catalog.get(async () => fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn))).map((model) => ({
2564
+ return (await this.catalog.get(async () => fetchGrokModels(await this.options.tokens.session(), this.options.fetchFn, this.options.onWarn))).map((model) => ({
2288
2565
  provider,
2289
2566
  id: model.id,
2290
2567
  name: model.name,
2568
+ ...model.description === void 0 ? {} : { description: model.description },
2291
2569
  inputModalities: grokModalities(model.id)
2292
2570
  }));
2293
2571
  } catch (error) {
@@ -2304,9 +2582,11 @@ var GrokAdapter = class extends LlmAdapter {
2304
2582
  provider,
2305
2583
  id: model,
2306
2584
  name: discovered?.name ?? configured?.name ?? model,
2585
+ ...discovered?.description === void 0 ? {} : { description: discovered.description },
2307
2586
  inputModalities: configured?.inputModalities ?? grokModalities(model),
2308
- context: { contextWindow: configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
2309
- defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS
2587
+ context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
2588
+ defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
2589
+ ...discovered?.reasoning === void 0 ? {} : { reasoning: discovered.reasoning }
2310
2590
  });
2311
2591
  }
2312
2592
  async *stream(options) {
@@ -2339,6 +2619,7 @@ var GrokAdapter = class extends LlmAdapter {
2339
2619
  tool_choice: "auto",
2340
2620
  parallel_tool_calls: true,
2341
2621
  ...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
2622
+ ...options.reasoningEffort !== void 0 ? { reasoning: { effort: String(options.reasoningEffort) } } : {},
2342
2623
  store: false,
2343
2624
  stream: true
2344
2625
  };
@@ -2893,29 +3174,24 @@ function accountOf(provider, session) {
2893
3174
  case "grok": return session.account;
2894
3175
  }
2895
3176
  }
2896
- /** The subscription detail of a stored session (plan type), for the status endpoint. */
2897
- function planOf(provider, session) {
2898
- if (session === void 0) return void 0;
2899
- switch (provider) {
2900
- case "codex": {
2901
- const codex = session;
2902
- return codex.planType ?? codexProfileClaims(codex.idToken).planType;
2903
- }
2904
- case "claude": return session.subscriptionType;
2905
- case "grok": return;
2906
- }
2907
- }
2908
3177
  /**
2909
3178
  * Auth operations behind the `/subscriptions-auth` RPC channel: start/complete
2910
- * OAuth attempts in the background, feed pasted codes, cancel, and log out.
3179
+ * OAuth attempts in the background, feed pasted codes, cancel, log out, and
3180
+ * answer usage lookups.
2911
3181
  */
2912
3182
  var SubscriptionsAuthController = class {
2913
3183
  /** Last login failure per provider, surfaced as `detail` until the next success. */
2914
3184
  lastError = /* @__PURE__ */ new Map();
2915
- constructor(flows, onAuthChanged, resolveAttachments) {
3185
+ constructor(flows, onAuthChanged, resolveAttachments, usageFetchers = {}) {
2916
3186
  this.flows = flows;
2917
3187
  this.onAuthChanged = onAuthChanged;
2918
3188
  this.resolveAttachments = resolveAttachments;
3189
+ this.usageFetchers = usageFetchers;
3190
+ }
3191
+ usage(provider, signal) {
3192
+ const fetcher = this.usageFetchers[provider];
3193
+ if (fetcher === void 0) return Promise.resolve({ supported: false });
3194
+ return fetcher(signal);
2919
3195
  }
2920
3196
  async readImage(ref, signal) {
2921
3197
  const attachments = this.resolveAttachments();
@@ -2929,7 +3205,7 @@ var SubscriptionsAuthController = class {
2929
3205
  async status(provider) {
2930
3206
  const session = await getSession(provider);
2931
3207
  const account = accountOf(provider, session);
2932
- const detail = this.lastError.get(provider) ?? planOf(provider, session);
3208
+ const detail = this.lastError.get(provider);
2933
3209
  return {
2934
3210
  loggedIn: session !== void 0,
2935
3211
  busy: this.flows.isBusy(provider),
@@ -3004,6 +3280,7 @@ function apply(ctx, config) {
3004
3280
  };
3005
3281
  let codexTokens;
3006
3282
  let grokTokens;
3283
+ const usageFetchers = {};
3007
3284
  for (const provider of providers) switch (provider) {
3008
3285
  case "codex": {
3009
3286
  const tokens = new TokenManager({
@@ -3019,6 +3296,7 @@ function apply(ctx, config) {
3019
3296
  }
3020
3297
  });
3021
3298
  codexTokens = tokens;
3299
+ usageFetchers.codex = async (signal) => fetchCodexUsage(await tokens.session(), fetch, signal);
3022
3300
  handles.set("codex", ctx.llm.registerAdapter(["codex"], new CodexAdapter({
3023
3301
  models: catalog.codex,
3024
3302
  streamIdleTimeoutMs,
@@ -3042,6 +3320,7 @@ function apply(ctx, config) {
3042
3320
  authChanged("claude");
3043
3321
  }
3044
3322
  });
3323
+ usageFetchers.claude = async (signal) => fetchClaudeUsage(await tokens.session(), fetch, signal);
3045
3324
  handles.set("claude", ctx.llm.registerAdapter(["claude"], new ClaudeAdapter({
3046
3325
  models: catalog.claude,
3047
3326
  streamIdleTimeoutMs,
@@ -3064,6 +3343,7 @@ function apply(ctx, config) {
3064
3343
  }
3065
3344
  });
3066
3345
  grokTokens = tokens;
3346
+ usageFetchers.grok = async (signal) => fetchGrokUsage(await tokens.session(), fetch, signal);
3067
3347
  handles.set("grok", ctx.llm.registerAdapter(["grok"], new GrokAdapter({
3068
3348
  models: catalog.grok,
3069
3349
  streamIdleTimeoutMs,
@@ -3075,7 +3355,7 @@ function apply(ctx, config) {
3075
3355
  break;
3076
3356
  }
3077
3357
  }
3078
- registerAuthRpc(ctx, new SubscriptionsAuthController(flows, authChanged, resolveAttachments));
3358
+ registerAuthRpc(ctx, new SubscriptionsAuthController(flows, authChanged, resolveAttachments, usageFetchers));
3079
3359
  ctx.inject(["tools"], (toolsCtx) => {
3080
3360
  if (grokTokens !== void 0) toolsCtx.tools.register(createXSearchTool({ tokens: grokTokens }));
3081
3361
  if (codexTokens !== void 0) toolsCtx.tools.register(createImageGenerateTool({
@@ -9,7 +9,7 @@ import type { FlowSpec } from '../auth/oauth-flow.js';
9
9
  import type { ClaudeSession } from '../auth/store.js';
10
10
  import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
11
11
  import { TokenManager } from './common.js';
12
- import type { ModelEntry } from './common.js';
12
+ import type { FetchFn, ModelEntry, ProviderUsage } from './common.js';
13
13
  export declare const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
14
14
  export declare const CLAUDE_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
15
15
  export declare const CLAUDE_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
@@ -40,6 +40,18 @@ export declare function refreshClaude(session: ClaudeSession): Promise<ClaudeSes
40
40
  * @returns true when re-login is the only fix.
41
41
  */
42
42
  export declare function isClaudePermanentRefreshError(error: unknown): boolean;
43
+ export declare const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
44
+ /**
45
+ * Fetch the claude subscription usage from the OAuth usage endpoint (the
46
+ * source of Claude Code's `/usage` screen). Newer responses carry a
47
+ * structured `limits` array; older ones the flat `five_hour`/`seven_day*`
48
+ * buckets — both shapes are read, the array winning when it has entries.
49
+ * @param session - the stored session (used as-is; never refreshed here).
50
+ * @param fetchFn - fetch implementation (injectable for tests).
51
+ * @param signal - caller cancellation from the RPC transport.
52
+ * @returns the mapped usage snapshot.
53
+ */
54
+ export declare function fetchClaudeUsage(session: ClaudeSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
43
55
  /** Constructor dependencies for {@link ClaudeAdapter}. */
44
56
  export interface ClaudeAdapterOptions {
45
57
  models: readonly ModelEntry[];
@@ -147,6 +147,95 @@ export function isClaudePermanentRefreshError(error) {
147
147
  return error instanceof OAuthEndpointError
148
148
  && (error.oauthCode === 'invalid_grant' || error.oauthCode === 'invalid_token');
149
149
  }
150
+ export const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
151
+ /** RFC3339 `resets_at` value → epoch ms, or undefined when absent/unparsable. */
152
+ function claudeResetsAt(value) {
153
+ if (typeof value !== 'string' || value.length === 0)
154
+ return undefined;
155
+ const parsed = Date.parse(value);
156
+ return Number.isFinite(parsed) ? parsed : undefined;
157
+ }
158
+ /** Map one legacy `{utilization, resets_at}` bucket; undefined when null or unusable. */
159
+ function claudeLegacyWindow(value, kind, scope) {
160
+ if (typeof value !== 'object' || value === null)
161
+ return undefined;
162
+ const bucket = value;
163
+ if (typeof bucket.utilization !== 'number' || !Number.isFinite(bucket.utilization))
164
+ return undefined;
165
+ const resetsAt = claudeResetsAt(bucket.resets_at);
166
+ return {
167
+ kind,
168
+ ...scope === undefined ? {} : { scope },
169
+ usedPercent: bucket.utilization,
170
+ ...resetsAt === undefined ? {} : { resetsAt },
171
+ };
172
+ }
173
+ /** Map the modern `limits` array; empty when absent or carrying nothing usable. */
174
+ function claudeLimitsWindows(value) {
175
+ if (!Array.isArray(value))
176
+ return [];
177
+ const windows = [];
178
+ for (const raw of value) {
179
+ if (typeof raw !== 'object' || raw === null)
180
+ continue;
181
+ const entry = raw;
182
+ if (typeof entry.percent !== 'number' || !Number.isFinite(entry.percent))
183
+ continue;
184
+ const kind = entry.kind === 'session'
185
+ ? 'session'
186
+ : entry.kind === 'weekly_all' || entry.kind === 'weekly_scoped' ? 'weekly' : 'other';
187
+ const scope = entry.scope?.model?.display_name;
188
+ const resetsAt = claudeResetsAt(entry.resets_at);
189
+ windows.push({
190
+ kind,
191
+ ...typeof scope === 'string' && scope.length > 0 ? { scope } : {},
192
+ usedPercent: entry.percent,
193
+ ...resetsAt === undefined ? {} : { resetsAt },
194
+ });
195
+ }
196
+ return windows;
197
+ }
198
+ /**
199
+ * Fetch the claude subscription usage from the OAuth usage endpoint (the
200
+ * source of Claude Code's `/usage` screen). Newer responses carry a
201
+ * structured `limits` array; older ones the flat `five_hour`/`seven_day*`
202
+ * buckets — both shapes are read, the array winning when it has entries.
203
+ * @param session - the stored session (used as-is; never refreshed here).
204
+ * @param fetchFn - fetch implementation (injectable for tests).
205
+ * @param signal - caller cancellation from the RPC transport.
206
+ * @returns the mapped usage snapshot.
207
+ */
208
+ export async function fetchClaudeUsage(session, fetchFn = fetch, signal) {
209
+ const response = await fetchFn(CLAUDE_USAGE_URL, {
210
+ headers: {
211
+ 'authorization': `Bearer ${session.accessToken}`,
212
+ 'anthropic-beta': 'oauth-2025-04-20',
213
+ // Unrecognized clients are aggressively rate-limited on this endpoint,
214
+ // so it presents as the CLI like every other subscription request.
215
+ 'user-agent': CLAUDE_CLI_USER_AGENT,
216
+ 'accept': 'application/json',
217
+ },
218
+ ...signal === undefined ? {} : { signal },
219
+ });
220
+ if (!response.ok)
221
+ throw await oauthEndpointError(response, 'claude usage');
222
+ const payload = await response.json();
223
+ const modern = claudeLimitsWindows(payload.limits);
224
+ if (modern.length > 0)
225
+ return { supported: true, windows: modern };
226
+ const windows = [];
227
+ const legacy = [
228
+ claudeLegacyWindow(payload.five_hour, 'session'),
229
+ claudeLegacyWindow(payload.seven_day, 'weekly'),
230
+ claudeLegacyWindow(payload.seven_day_opus, 'weekly', 'Opus'),
231
+ claudeLegacyWindow(payload.seven_day_sonnet, 'weekly', 'Sonnet'),
232
+ ];
233
+ for (const window of legacy) {
234
+ if (window !== undefined)
235
+ windows.push(window);
236
+ }
237
+ return { supported: true, windows };
238
+ }
150
239
  /** The Claude 4.5 family accepts image input. */
151
240
  const CLAUDE_MODALITIES = ['text', 'image'];
152
241
  /** Claude wire adapter: one instance serves the `claude` provider route. */
@@ -9,7 +9,7 @@ import type { FlowSpec } from '../auth/oauth-flow.js';
9
9
  import type { CodexSession } from '../auth/store.js';
10
10
  import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
11
11
  import { TokenManager } from './common.js';
12
- import type { DiscoveredModel, FetchFn, ModelEntry } from './common.js';
12
+ import type { DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
13
13
  export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
14
14
  export declare const CODEX_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
15
15
  export declare const CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
@@ -53,6 +53,18 @@ export declare function refreshCodex(session: CodexSession): Promise<CodexSessio
53
53
  * @returns true when re-login is the only fix.
54
54
  */
55
55
  export declare function isCodexPermanentRefreshError(error: unknown): boolean;
56
+ export declare const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
57
+ /**
58
+ * Fetch the codex subscription usage from the ChatGPT backend wham/usage
59
+ * endpoint (the source of the codex CLI `/status` rate-limit lines). The
60
+ * primary window is the rolling session (5-hour) lane, the secondary window
61
+ * the weekly lane; the lookup itself consumes no rate-limit budget.
62
+ * @param session - the stored session (used as-is; never refreshed here).
63
+ * @param fetchFn - fetch implementation (injectable for tests).
64
+ * @param signal - caller cancellation from the RPC transport.
65
+ * @returns the mapped usage snapshot.
66
+ */
67
+ export declare function fetchCodexUsage(session: CodexSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
56
68
  export declare const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
57
69
  /**
58
70
  * Client version sent on the /models catalog request. The backend gates the