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/README.md +19 -1
- package/README.zh.md +19 -1
- package/lib/auth/rpc.d.ts +8 -0
- package/lib/auth/rpc.js +2 -0
- package/lib/client/SubscriptionsSection.d.ts +13 -0
- package/lib/client/SubscriptionsSection.js +111 -1
- package/lib/client/locales.d.ts +20 -0
- package/lib/client/locales.js +20 -0
- package/lib/client.js +228 -0
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.js +305 -25
- package/lib/providers/claude.d.ts +13 -1
- package/lib/providers/claude.js +89 -0
- package/lib/providers/codex.d.ts +13 -1
- package/lib/providers/codex.js +56 -0
- package/lib/providers/common.d.ts +20 -0
- package/lib/providers/grok.d.ts +51 -4
- package/lib/providers/grok.js +202 -15
- package/package.json +1 -1
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
|
-
* @
|
|
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,
|
|
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)
|
|
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[];
|
package/lib/providers/claude.js
CHANGED
|
@@ -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. */
|
package/lib/providers/codex.d.ts
CHANGED
|
@@ -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
|