dsh-plugin-subscription-usage 0.0.0-stage → 0.2.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.
Files changed (169) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +50 -2
  3. package/cordis.patch.yml +5 -0
  4. package/icon.svg +9 -0
  5. package/lib/auth/claude-code-creds.d.ts +23 -0
  6. package/lib/auth/claude-code-creds.js +172 -0
  7. package/lib/auth/device-flow.d.ts +55 -0
  8. package/lib/auth/device-flow.js +177 -0
  9. package/lib/auth/jwt.d.ts +10 -0
  10. package/lib/auth/jwt.js +25 -0
  11. package/lib/auth/oauth-flow.d.ts +91 -0
  12. package/lib/auth/oauth-flow.js +227 -0
  13. package/lib/auth/pkce.d.ts +31 -0
  14. package/lib/auth/pkce.js +35 -0
  15. package/lib/auth/rpc.d.ts +233 -0
  16. package/lib/auth/rpc.js +501 -0
  17. package/lib/auth/store.d.ts +198 -0
  18. package/lib/auth/store.js +473 -0
  19. package/lib/client/Footer.d.ts +3 -0
  20. package/lib/client/Footer.js +73 -0
  21. package/lib/client/Gauge.d.ts +28 -0
  22. package/lib/client/Gauge.js +139 -0
  23. package/lib/client/ImageGallery.d.ts +54 -0
  24. package/lib/client/ImageGallery.js +112 -0
  25. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  26. package/lib/client/ImageGenerateToolview.js +130 -0
  27. package/lib/client/Notice.d.ts +15 -0
  28. package/lib/client/Notice.js +93 -0
  29. package/lib/client/ProviderCard.d.ts +35 -0
  30. package/lib/client/ProviderCard.js +240 -0
  31. package/lib/client/SpeedSelect.d.ts +72 -0
  32. package/lib/client/SpeedSelect.js +214 -0
  33. package/lib/client/Usage.d.ts +7 -0
  34. package/lib/client/Usage.js +83 -0
  35. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  36. package/lib/client/VideoGenerateToolview.js +172 -0
  37. package/lib/client/context.d.ts +16 -0
  38. package/lib/client/context.js +11 -0
  39. package/lib/client/fast-command.d.ts +2 -0
  40. package/lib/client/fast-command.js +4 -0
  41. package/lib/client/hooks.d.ts +2 -0
  42. package/lib/client/hooks.js +10 -0
  43. package/lib/client/index.d.ts +3 -0
  44. package/lib/client/index.js +113 -0
  45. package/lib/client/locales.d.ts +139 -0
  46. package/lib/client/locales.js +267 -0
  47. package/lib/client/meter.d.ts +91 -0
  48. package/lib/client/meter.js +196 -0
  49. package/lib/client/prefs.d.ts +51 -0
  50. package/lib/client/prefs.js +71 -0
  51. package/lib/client/rpc.d.ts +161 -0
  52. package/lib/client/rpc.js +31 -0
  53. package/lib/client/store.d.ts +67 -0
  54. package/lib/client/store.js +129 -0
  55. package/lib/client/styles.d.ts +7 -0
  56. package/lib/client/styles.js +176 -0
  57. package/lib/client.js +3933 -0
  58. package/lib/client.js.map +1 -0
  59. package/lib/compat.d.ts +55 -0
  60. package/lib/compat.js +20 -0
  61. package/lib/http.d.ts +134 -0
  62. package/lib/http.js +431 -0
  63. package/lib/index.d.ts +121 -0
  64. package/lib/index.js +13399 -0
  65. package/lib/json-text.d.ts +2 -0
  66. package/lib/json-text.js +4 -0
  67. package/lib/model-defaults.d.ts +23 -0
  68. package/lib/model-defaults.js +238 -0
  69. package/lib/model-speeds.d.ts +10 -0
  70. package/lib/model-speeds.js +48 -0
  71. package/lib/models-directory.d.ts +17 -0
  72. package/lib/models-directory.js +25 -0
  73. package/lib/provider-settings.d.ts +55 -0
  74. package/lib/provider-settings.js +215 -0
  75. package/lib/providers/account-preferences.d.ts +46 -0
  76. package/lib/providers/account-preferences.js +188 -0
  77. package/lib/providers/accounts.d.ts +113 -0
  78. package/lib/providers/accounts.js +142 -0
  79. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  80. package/lib/providers/antigravity-oauth-client.js +7 -0
  81. package/lib/providers/antigravity.d.ts +119 -0
  82. package/lib/providers/antigravity.js +552 -0
  83. package/lib/providers/catalog-refresh.d.ts +4 -0
  84. package/lib/providers/catalog-refresh.js +30 -0
  85. package/lib/providers/catalog-store.d.ts +47 -0
  86. package/lib/providers/catalog-store.js +283 -0
  87. package/lib/providers/claude-cli-version.d.ts +15 -0
  88. package/lib/providers/claude-cli-version.js +24 -0
  89. package/lib/providers/claude.d.ts +212 -0
  90. package/lib/providers/claude.js +726 -0
  91. package/lib/providers/codex-client-version.d.ts +8 -0
  92. package/lib/providers/codex-client-version.js +17 -0
  93. package/lib/providers/codex-search.d.ts +29 -0
  94. package/lib/providers/codex-search.js +198 -0
  95. package/lib/providers/codex.d.ts +253 -0
  96. package/lib/providers/codex.js +930 -0
  97. package/lib/providers/common.d.ts +415 -0
  98. package/lib/providers/common.js +679 -0
  99. package/lib/providers/copilot.d.ts +365 -0
  100. package/lib/providers/copilot.js +956 -0
  101. package/lib/providers/grok.d.ts +214 -0
  102. package/lib/providers/grok.js +750 -0
  103. package/lib/providers/image-pool.d.ts +30 -0
  104. package/lib/providers/image-pool.js +89 -0
  105. package/lib/providers/npm-cli-version.d.ts +56 -0
  106. package/lib/providers/npm-cli-version.js +98 -0
  107. package/lib/providers/pool-family.d.ts +56 -0
  108. package/lib/providers/pool-family.js +45 -0
  109. package/lib/providers/pool-health.d.ts +91 -0
  110. package/lib/providers/pool-health.js +164 -0
  111. package/lib/providers/pool-usage.d.ts +106 -0
  112. package/lib/providers/pool-usage.js +302 -0
  113. package/lib/providers/pool.d.ts +115 -0
  114. package/lib/providers/pool.js +461 -0
  115. package/lib/providers/rate-limit.d.ts +192 -0
  116. package/lib/providers/rate-limit.js +338 -0
  117. package/lib/providers/replay.d.ts +4 -0
  118. package/lib/providers/replay.js +37 -0
  119. package/lib/providers/reset-redemption.d.ts +20 -0
  120. package/lib/providers/reset-redemption.js +70 -0
  121. package/lib/tools/image-generate.d.ts +128 -0
  122. package/lib/tools/image-generate.js +438 -0
  123. package/lib/tools/registration.d.ts +16 -0
  124. package/lib/tools/registration.js +25 -0
  125. package/lib/tools/usage.d.ts +62 -0
  126. package/lib/tools/usage.js +89 -0
  127. package/lib/tools/video-generate.d.ts +89 -0
  128. package/lib/tools/video-generate.js +257 -0
  129. package/lib/tools/x-search.d.ts +58 -0
  130. package/lib/tools/x-search.js +197 -0
  131. package/lib/translate/anthropic.d.ts +180 -0
  132. package/lib/translate/anthropic.js +534 -0
  133. package/lib/translate/antigravity-images.d.ts +11 -0
  134. package/lib/translate/antigravity-images.js +62 -0
  135. package/lib/translate/antigravity-schema.d.ts +4 -0
  136. package/lib/translate/antigravity-schema.js +145 -0
  137. package/lib/translate/antigravity-thinking.d.ts +5 -0
  138. package/lib/translate/antigravity-thinking.js +44 -0
  139. package/lib/translate/antigravity.d.ts +120 -0
  140. package/lib/translate/antigravity.js +412 -0
  141. package/lib/translate/chat-completions.d.ts +120 -0
  142. package/lib/translate/chat-completions.js +382 -0
  143. package/lib/translate/resolved.d.ts +98 -0
  144. package/lib/translate/resolved.js +186 -0
  145. package/lib/translate/responses.d.ts +192 -0
  146. package/lib/translate/responses.js +443 -0
  147. package/lib/translate/sse.d.ts +21 -0
  148. package/lib/translate/sse.js +62 -0
  149. package/lib/usage/headers.d.ts +21 -0
  150. package/lib/usage/headers.js +64 -0
  151. package/lib/usage/pace.d.ts +53 -0
  152. package/lib/usage/pace.js +48 -0
  153. package/lib/usage/service.d.ts +64 -0
  154. package/lib/usage/service.js +104 -0
  155. package/lib/usage-providers/common.d.ts +23 -0
  156. package/lib/usage-providers/common.js +28 -0
  157. package/lib/usage-providers/index.d.ts +42 -0
  158. package/lib/usage-providers/index.js +71 -0
  159. package/lib/usage-providers/keys.d.ts +11 -0
  160. package/lib/usage-providers/keys.js +52 -0
  161. package/lib/usage-providers/kimi.d.ts +4 -0
  162. package/lib/usage-providers/kimi.js +52 -0
  163. package/lib/usage-providers/ollama-cloud.d.ts +4 -0
  164. package/lib/usage-providers/ollama-cloud.js +42 -0
  165. package/lib/usage-providers/zai.d.ts +5 -0
  166. package/lib/usage-providers/zai.js +53 -0
  167. package/locale/en.json +6 -0
  168. package/locale/zh.json +6 -0
  169. package/package.json +115 -4
@@ -0,0 +1,750 @@
1
+ /**
2
+ * Grok (X Premium / xAI) subscription provider: OIDC-discovered OAuth against
3
+ * auth.x.ai with the Grok CLI client id, and streaming against the xAI
4
+ * Responses-style endpoint.
5
+ */
6
+ import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
7
+ import { decodeJwtPayload } from '../auth/jwt.js';
8
+ import { resolveImages } from '../translate/resolved.js';
9
+ import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
10
+ import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
11
+ import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
12
+ import { proxiedFetch } from '../http.js';
13
+ import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, jsonBody, resetFromFields, subscriptionRetryPolicy, } from './rate-limit.js';
14
+ export const GROK_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
15
+ export const GROK_DISCOVERY_URL = 'https://auth.x.ai/.well-known/openid-configuration';
16
+ export const GROK_API_URL = 'https://api.x.ai/v1/responses';
17
+ const GROK_SCOPE = 'openid profile email offline_access grok-cli:access api:access';
18
+ const GROK_CALLBACK_PATH = '/callback';
19
+ const GROK_CONTEXT_WINDOW = 256_000;
20
+ const GROK_DEFAULT_MAX_TOKENS = 32_000;
21
+ /** Refresh when the access token has less than this much life left. */
22
+ export const GROK_PREEMPT_MS = 2 * 60_000;
23
+ /** Body fields xAI uses to name a delay or reset. */
24
+ const GROK_RESET_FIELDS = ['retry_after', 'retry_after_seconds', 'resets_at', 'reset_at'];
25
+ /**
26
+ * Reads the reset instant of the xAI window that rejected a request.
27
+ *
28
+ * Body only. xAI serves the OpenAI-compatible `x-ratelimit-reset-*` family,
29
+ * whose values are rollover durations (`6m0s`) present on every response, one
30
+ * per bucket — on a 429 the earliest of them is usually a bucket with room
31
+ * (`0s` for the request bucket while the token bucket is the one exhausted),
32
+ * which would burn the whole retry budget in seconds. They reach the operator
33
+ * through `rateLimitDiagnostics` instead.
34
+ */
35
+ export const grokRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), GROK_RESET_FIELDS, now);
36
+ /** A discovered URL must be https on x.ai or a subdomain; anything else is a hostile document. */
37
+ function assertXaiEndpoint(url, field) {
38
+ let parsed;
39
+ try {
40
+ parsed = new URL(url);
41
+ }
42
+ catch {
43
+ throw new Error(`grok OIDC discovery returned an invalid ${field}`);
44
+ }
45
+ if (parsed.protocol !== 'https:'
46
+ || (parsed.hostname !== 'x.ai' && !parsed.hostname.endsWith('.x.ai'))) {
47
+ throw new Error(`grok OIDC discovery returned a non-x.ai ${field}: ${url}`);
48
+ }
49
+ return url;
50
+ }
51
+ let discoveryCache;
52
+ /**
53
+ * Resolve the xAI OIDC endpoints (cached after the first fetch).
54
+ * @returns validated authorization and token endpoints.
55
+ */
56
+ export async function grokDiscovery() {
57
+ if (discoveryCache !== undefined)
58
+ return discoveryCache;
59
+ const response = await proxiedFetch(GROK_DISCOVERY_URL);
60
+ if (!response.ok)
61
+ throw await oauthEndpointError(response, 'grok OIDC discovery');
62
+ const document = await response.json();
63
+ if (typeof document.authorization_endpoint !== 'string' || typeof document.token_endpoint !== 'string') {
64
+ throw new Error('grok OIDC discovery document is missing endpoints');
65
+ }
66
+ discoveryCache = {
67
+ authorizationEndpoint: assertXaiEndpoint(document.authorization_endpoint, 'authorization_endpoint'),
68
+ tokenEndpoint: assertXaiEndpoint(document.token_endpoint, 'token_endpoint'),
69
+ };
70
+ return discoveryCache;
71
+ }
72
+ /**
73
+ * Build the grok flow facts for the OAuth flow engine (async because the
74
+ * authorize URL comes from OIDC discovery).
75
+ * @returns the flow spec for one attempt.
76
+ */
77
+ export async function grokFlow() {
78
+ const discovery = await grokDiscovery();
79
+ return {
80
+ callbackPath: GROK_CALLBACK_PATH,
81
+ listen: { host: '127.0.0.1', ports: [56121] },
82
+ buildAuthorizeUrl({ redirectUri, state, pkce, nonce }) {
83
+ const params = new URLSearchParams({
84
+ response_type: 'code',
85
+ client_id: GROK_CLIENT_ID,
86
+ redirect_uri: redirectUri,
87
+ scope: GROK_SCOPE,
88
+ code_challenge: pkce.challenge,
89
+ code_challenge_method: 'S256',
90
+ state,
91
+ nonce,
92
+ plan: 'generic',
93
+ referrer: 'dsh-plugin-subscription-usage',
94
+ });
95
+ return `${discovery.authorizationEndpoint}?${params.toString()}`;
96
+ },
97
+ };
98
+ }
99
+ /**
100
+ * Display names for the numeric `tier` claim xAI stamps on OAuth access
101
+ * tokens (the `prod_auth.SubscriptionTier` proto enum; the mapping mirrors
102
+ * grok-build's `jwt_tier_claim`). Unknown values fall through to the raw
103
+ * number so a future tier still shows something.
104
+ */
105
+ const GROK_TIER_NAMES = {
106
+ 0: 'Free',
107
+ 1: 'SuperGrok',
108
+ 2: 'X Basic',
109
+ 3: 'X Premium',
110
+ 4: 'X Premium+',
111
+ 5: 'SuperGrok Heavy',
112
+ 6: 'SuperGrok Lite',
113
+ 7: 'SuperGrok Plus',
114
+ };
115
+ /**
116
+ * The subscription tier encoded in a grok access token's `tier` claim (no
117
+ * verification — same trust posture as the other claim reads).
118
+ * @param accessToken - the stored access token.
119
+ * @returns the display tier name, or undefined when the claim is absent.
120
+ */
121
+ export function grokTierName(accessToken) {
122
+ const tier = decodeJwtPayload(accessToken)?.tier;
123
+ if (typeof tier !== 'number' || !Number.isInteger(tier))
124
+ return undefined;
125
+ return GROK_TIER_NAMES[tier] ?? String(tier);
126
+ }
127
+ /** Pick a display account from an id token's claims. */
128
+ function grokAccount(idToken) {
129
+ const payload = idToken === undefined ? undefined : decodeJwtPayload(idToken);
130
+ const claim = payload?.email ?? payload?.preferred_username ?? payload?.name ?? payload?.sub;
131
+ return typeof claim === 'string' && claim.length > 0 ? claim : undefined;
132
+ }
133
+ /** Build a session from a token response. */
134
+ function grokSession(tokens, tokenEndpoint, fallbackRefreshToken) {
135
+ if (typeof tokens.access_token !== 'string' || tokens.access_token.length === 0) {
136
+ throw new Error('grok token endpoint returned no access token');
137
+ }
138
+ const refreshToken = tokens.refresh_token ?? fallbackRefreshToken;
139
+ if (refreshToken === undefined)
140
+ throw new Error('grok token endpoint returned no refresh token');
141
+ if (typeof tokens.expires_in !== 'number' || tokens.expires_in <= 0) {
142
+ throw new Error('grok token endpoint returned no usable expiry');
143
+ }
144
+ const account = grokAccount(tokens.id_token);
145
+ return {
146
+ accessToken: tokens.access_token,
147
+ refreshToken,
148
+ expiresAt: Date.now() + tokens.expires_in * 1000,
149
+ tokenEndpoint,
150
+ ...typeof tokens.scope === 'string' ? { scopes: tokens.scope } : {},
151
+ ...account === undefined ? {} : { account },
152
+ };
153
+ }
154
+ /**
155
+ * Exchange an authorization code for a grok session (form-encoded grant that
156
+ * echoes the PKCE challenge as well as the verifier, per the xAI flow).
157
+ * A 403 here means the X plan lacks the API OAuth entitlement.
158
+ * @param code - the authorization code from the callback.
159
+ * @param verifier - the PKCE verifier minted for the attempt.
160
+ * @param redirectUri - the attempt's redirect URI.
161
+ * @param challenge - the PKCE challenge sent at authorize time.
162
+ * @returns the session to store.
163
+ */
164
+ export async function exchangeGrokCode(code, verifier, redirectUri, challenge) {
165
+ const discovery = await grokDiscovery();
166
+ const response = await proxiedFetch(discovery.tokenEndpoint, {
167
+ method: 'POST',
168
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
169
+ body: new URLSearchParams({
170
+ grant_type: 'authorization_code',
171
+ client_id: GROK_CLIENT_ID,
172
+ code,
173
+ redirect_uri: redirectUri,
174
+ code_verifier: verifier,
175
+ code_challenge: challenge,
176
+ code_challenge_method: 'S256',
177
+ }).toString(),
178
+ });
179
+ if (response.status === 403) {
180
+ throw new OAuthEndpointError('grok token endpoint refused the exchange (HTTP 403): your X plan does not include '
181
+ + 'the API OAuth entitlement; an X Premium or xAI subscription with API access is required', 403);
182
+ }
183
+ if (!response.ok)
184
+ throw await oauthEndpointError(response, 'grok');
185
+ return grokSession(await response.json(), discovery.tokenEndpoint);
186
+ }
187
+ /**
188
+ * Refresh a grok session (form-encoded grant).
189
+ * @param session - the stored session.
190
+ * @returns the fresh session to store.
191
+ */
192
+ export async function refreshGrok(session) {
193
+ const response = await proxiedFetch(session.tokenEndpoint, {
194
+ method: 'POST',
195
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
196
+ body: new URLSearchParams({
197
+ grant_type: 'refresh_token',
198
+ client_id: GROK_CLIENT_ID,
199
+ refresh_token: session.refreshToken,
200
+ }).toString(),
201
+ });
202
+ if (!response.ok)
203
+ throw await oauthEndpointError(response, 'grok');
204
+ const next = grokSession(await response.json(), session.tokenEndpoint, session.refreshToken);
205
+ return {
206
+ ...next,
207
+ ...session.account === undefined ? {} : { account: session.account },
208
+ ...next.scopes === undefined && session.scopes !== undefined ? { scopes: session.scopes } : {},
209
+ };
210
+ }
211
+ /**
212
+ * Whether a grok refresh failure means the login is permanently gone.
213
+ * @param error - the thrown refresh error.
214
+ * @returns true when re-login is the only fix.
215
+ */
216
+ export function isGrokPermanentRefreshError(error) {
217
+ return error instanceof OAuthEndpointError && error.oauthCode === 'invalid_grant';
218
+ }
219
+ /**
220
+ * The Grok Build CLI chat proxy's billing endpoint (the source of the CLI's
221
+ * `/usage` "Usage limit" panel; see xai-org/grok-build
222
+ * `extensions/billing.rs`). Forwards to the backend `GetGrokCreditsConfig`.
223
+ */
224
+ export const GROK_BILLING_URL = 'https://cli-chat-proxy.grok.com/v1/billing?format=credits';
225
+ /** RFC3339 timestamp → epoch ms, or undefined when absent/unparsable. */
226
+ function grokResetsAt(value) {
227
+ if (typeof value !== 'string' || value.length === 0)
228
+ return undefined;
229
+ const parsed = Date.parse(value);
230
+ return Number.isFinite(parsed) ? parsed : undefined;
231
+ }
232
+ /**
233
+ * Fetch the grok subscription usage from the Grok Build CLI chat proxy. The
234
+ * newer credits config carries a ready-made percentage plus the current
235
+ * (typically weekly) period; the legacy shape carries cent-valued
236
+ * `monthlyLimit`/`used`, from which the percentage is derived.
237
+ * @param session - the stored session (used as-is; never refreshed here).
238
+ * @param fetchFn - fetch implementation (injectable for tests).
239
+ * @param signal - caller cancellation from the RPC transport.
240
+ * @returns the mapped usage snapshot.
241
+ */
242
+ export async function fetchGrokUsage(session, fetchFn = proxiedFetch, signal) {
243
+ const response = await fetchFn(GROK_BILLING_URL, {
244
+ headers: {
245
+ 'authorization': `Bearer ${session.accessToken}`,
246
+ // The proxy only honors bearer tokens presented as the Grok CLI.
247
+ 'x-xai-token-auth': 'xai-grok-cli',
248
+ 'accept': 'application/json',
249
+ ...attributionHeaders(),
250
+ },
251
+ ...signal === undefined ? {} : { signal },
252
+ });
253
+ if (!response.ok)
254
+ throw await oauthEndpointError(response, 'grok billing');
255
+ const payload = await response.json();
256
+ const config = typeof payload.config === 'object' && payload.config !== null ? payload.config : {};
257
+ const windows = [];
258
+ if (config.currentPeriod || (typeof config.creditUsagePercent === 'number' && Number.isFinite(config.creditUsagePercent))) {
259
+ const kind = config.currentPeriod?.type === 'USAGE_PERIOD_TYPE_WEEKLY'
260
+ ? 'weekly'
261
+ : 'other';
262
+ const resetsAt = grokResetsAt(config.currentPeriod?.end);
263
+ const usedPercent = typeof config.creditUsagePercent === 'number' && Number.isFinite(config.creditUsagePercent)
264
+ ? config.creditUsagePercent
265
+ : 0;
266
+ const start = grokResetsAt(config.currentPeriod?.start);
267
+ windows.push({ kind, usedPercent,
268
+ ...(start !== undefined && resetsAt !== undefined && resetsAt > start ? { durationMs: resetsAt - start } : {}),
269
+ ...resetsAt === undefined ? {} : { resetsAt } });
270
+ }
271
+ else if (typeof config.monthlyLimit?.val === 'number' && config.monthlyLimit.val > 0) {
272
+ const used = typeof config.used?.val === 'number' ? config.used.val : 0;
273
+ const resetsAt = grokResetsAt(config.billingPeriodEnd);
274
+ windows.push({
275
+ kind: 'other',
276
+ usedPercent: (used / config.monthlyLimit.val) * 100,
277
+ ...resetsAt === undefined ? {} : { resetsAt },
278
+ });
279
+ }
280
+ // The upstream billing response rarely carries `subscriptionTier` (the CLI
281
+ // enriches it locally from its settings cache), so the access token's
282
+ // `tier` claim is the working fallback.
283
+ const plan = typeof payload.subscriptionTier === 'string' && payload.subscriptionTier.length > 0
284
+ ? payload.subscriptionTier
285
+ : grokTierName(session.accessToken);
286
+ return {
287
+ supported: true,
288
+ windows,
289
+ ...plan === undefined ? {} : { plan },
290
+ };
291
+ }
292
+ export const GROK_MODELS_URL = 'https://api.x.ai/v1/models';
293
+ /**
294
+ * Input modalities for one grok model: chat models (grok-4 family) accept
295
+ * images; code and embedding models are text-only.
296
+ */
297
+ function grokModalities(id) {
298
+ return /code|embed/i.test(id) ? ['text'] : ['text', 'image'];
299
+ }
300
+ /**
301
+ * The Grok Build CLI chat proxy's model catalog — the only grok endpoint that
302
+ * advertises reasoning capability. The `api.x.ai/v1/models` and
303
+ * `/v1/language-models` payloads carry pricing, context, and aliases only, so
304
+ * effort metadata must come from here (the same source the official CLI's
305
+ * picker uses).
306
+ */
307
+ export const GROK_CLI_MODELS_URL = 'https://cli-chat-proxy.grok.com/v1/models';
308
+ /**
309
+ * Strength rank of the well-known effort ids, weakest first. The harness
310
+ * treats `LlmModelReasoningInfo.efforts` as weakest-to-strongest (the order
311
+ * the claude/codex catalogs already use), so effort pickers that place
312
+ * levels by array position need it; the CLI catalog lists levels
313
+ * strongest-first. A Map (not an object literal) so an unknown id can never
314
+ * hit an `Object.prototype` key such as `constructor`.
315
+ */
316
+ const EFFORT_STRENGTH = new Map([
317
+ ['off', 0],
318
+ ['minimal', 1],
319
+ ['low', 2],
320
+ ['medium', 3],
321
+ ['high', 4],
322
+ ['xhigh', 5],
323
+ ['max', 6],
324
+ ]);
325
+ /** Rank of one effort id; unknown ids sort after every known tier. */
326
+ function effortStrength(id) {
327
+ return EFFORT_STRENGTH.get(id) ?? Number.MAX_SAFE_INTEGER;
328
+ }
329
+ /** Map one CLI catalog entry's reasoning fields, or undefined when unsupported. */
330
+ function grokCliReasoning(entry) {
331
+ if (entry.supports_reasoning_effort !== true)
332
+ return undefined;
333
+ const efforts = (entry.reasoning_efforts ?? [])
334
+ .filter(level => typeof level.value === 'string' && level.value.length > 0)
335
+ .map(level => ({
336
+ id: ReasoningEffortId(level.value),
337
+ name: typeof level.label === 'string' && level.label.length > 0 ? level.label : level.value,
338
+ ...typeof level.description === 'string' && level.description.length > 0
339
+ ? { description: level.description }
340
+ : {},
341
+ }))
342
+ // Weakest first; Array#sort is stable, so unknown ids keep catalog order.
343
+ .sort((left, right) => effortStrength(left.id) - effortStrength(right.id));
344
+ if (efforts.length === 0)
345
+ return undefined;
346
+ // The per-entry `default` flags are unreliable (the live catalog marks
347
+ // several levels default at once), so the top-level `reasoning_effort`
348
+ // field is the trusted default.
349
+ const defaultEffort = typeof entry.reasoning_effort === 'string'
350
+ && efforts.some(effort => effort.id === ReasoningEffortId(entry.reasoning_effort))
351
+ ? ReasoningEffortId(entry.reasoning_effort)
352
+ : undefined;
353
+ return { efforts, ...defaultEffort === undefined ? {} : { defaultEffort } };
354
+ }
355
+ /**
356
+ * Fetch the CLI catalog and index its per-model metadata by model id.
357
+ * @param session - the stored session (used as-is; never refreshed here).
358
+ * @param fetchFn - fetch implementation (injectable for tests).
359
+ * @param signal - caller cancellation (pool-assembly timeout).
360
+ * @returns model id → contributed metadata.
361
+ */
362
+ export async function fetchGrokCliCatalog(session, fetchFn = proxiedFetch, signal) {
363
+ const response = await fetchFn(GROK_CLI_MODELS_URL, {
364
+ headers: {
365
+ 'authorization': `Bearer ${session.accessToken}`,
366
+ // The proxy only honors bearer tokens presented as the Grok CLI.
367
+ 'x-xai-token-auth': 'xai-grok-cli',
368
+ 'accept': 'application/json',
369
+ ...attributionHeaders(),
370
+ },
371
+ ...signal === undefined ? {} : { signal },
372
+ });
373
+ if (!response.ok)
374
+ throw await oauthEndpointError(response, 'grok CLI catalog');
375
+ const payload = await response.json();
376
+ if (!Array.isArray(payload.data))
377
+ throw new Error('grok CLI catalog returned no data array');
378
+ const catalog = new Map();
379
+ for (const entry of payload.data) {
380
+ if (typeof entry.id !== 'string' || entry.id.length === 0)
381
+ continue;
382
+ const reasoning = grokCliReasoning(entry);
383
+ catalog.set(entry.id, {
384
+ ...typeof entry.name === 'string' && entry.name.length > 0 ? { name: entry.name } : {},
385
+ ...typeof entry.description === 'string' && entry.description.length > 0
386
+ ? { description: entry.description }
387
+ : {},
388
+ ...typeof entry.context_window === 'number' && entry.context_window > 0
389
+ ? { contextWindow: entry.context_window }
390
+ : {},
391
+ ...reasoning === undefined ? {} : { reasoning },
392
+ });
393
+ }
394
+ return catalog;
395
+ }
396
+ /**
397
+ * The /v1/models list also serves generation models that cannot chat
398
+ * (grok-imagine-image*, grok-imagine-video*) and embedding models; the picker
399
+ * must not offer them. Heuristic over the id substring, verified against the
400
+ * live catalog (grok-build-0.1 and the grok-4 family pass).
401
+ */
402
+ function isChatModel(id) {
403
+ return !/imagine|image-|video|embed/i.test(id);
404
+ }
405
+ /**
406
+ * CLI-contributed fields carried forward from a previously discovered model.
407
+ * @param prior - the last-known entry for this id, if any.
408
+ * @returns enrichment to apply when the live CLI catalog cannot contribute.
409
+ */
410
+ function grokPriorMeta(prior) {
411
+ if (prior === undefined)
412
+ return {};
413
+ return {
414
+ ...(prior.name.length > 0 ? { name: prior.name } : {}),
415
+ ...(prior.description === undefined ? {} : { description: prior.description }),
416
+ ...(prior.contextWindow === undefined ? {} : { contextWindow: prior.contextWindow }),
417
+ ...(prior.reasoning === undefined ? {} : { reasoning: prior.reasoning }),
418
+ };
419
+ }
420
+ /**
421
+ * Fetch the live grok model list, enriched with the CLI catalog's per-model
422
+ * metadata (display name, context window, reasoning efforts). The api.x.ai
423
+ * list stays authoritative for which models exist; the CLI catalog is
424
+ * enrichment only, so its failure degrades to a plain list instead of taking
425
+ * discovery down. When enrichment is missing, last-known capability metadata
426
+ * is carried forward so a transient CLI outage cannot strip efforts a
427
+ * session already selected.
428
+ * @param session - the stored session (used as-is; never refreshed here).
429
+ * @param fetchFn - fetch implementation (injectable for tests).
430
+ * @param onWarn - warning sink for a failed CLI catalog fetch.
431
+ * @param previous - last-known catalog used to keep enrichment when the CLI
432
+ * catalog is down or omits a model.
433
+ * @param signal - caller cancellation (pool-assembly timeout).
434
+ * @returns discovered chat models in endpoint order.
435
+ */
436
+ export async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, previous, signal) {
437
+ const previousById = previous === undefined || previous.length === 0
438
+ ? undefined
439
+ : new Map(previous.map(model => [model.id, model]));
440
+ const [response, cliCatalog] = await Promise.all([
441
+ fetchFn(GROK_MODELS_URL, {
442
+ headers: {
443
+ 'authorization': `Bearer ${session.accessToken}`,
444
+ 'accept': 'application/json',
445
+ ...attributionHeaders(),
446
+ },
447
+ ...signal === undefined ? {} : { signal },
448
+ }),
449
+ fetchGrokCliCatalog(session, fetchFn, signal).catch((error) => {
450
+ if (isDiscoveryAborted(error, signal))
451
+ throw error;
452
+ onWarn?.(previousById === undefined
453
+ ? `grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`
454
+ : `grok CLI catalog fetch failed; keeping last-known reasoning efforts (${errorChain(error)})`);
455
+ return undefined;
456
+ }),
457
+ ]);
458
+ if (!response.ok)
459
+ throw await oauthEndpointError(response, 'grok models');
460
+ const payload = await response.json();
461
+ if (!Array.isArray(payload.data))
462
+ throw new Error('grok models endpoint returned no data array');
463
+ const seen = new Set();
464
+ const discovered = [];
465
+ for (const entry of payload.data) {
466
+ if (typeof entry.id !== 'string' || entry.id.length === 0 || seen.has(entry.id))
467
+ continue;
468
+ if (!isChatModel(entry.id))
469
+ continue;
470
+ seen.add(entry.id);
471
+ const cli = cliCatalog?.get(entry.id);
472
+ discovered.push({
473
+ id: entry.id,
474
+ name: entry.id,
475
+ ...(cli ?? grokPriorMeta(previousById?.get(entry.id))),
476
+ });
477
+ }
478
+ // An empty catalog from a 200 response is treated as a discovery failure so
479
+ // the adapter falls back to the static catalog instead of vanishing from
480
+ // the picker.
481
+ if (discovered.length === 0)
482
+ throw new Error('grok models endpoint returned an empty catalog');
483
+ return discovered;
484
+ }
485
+ /**
486
+ * Build one xAI Responses request body (exported for tests, like
487
+ * {@link codexRequestBody}). The tool trio renders only when tools exist:
488
+ * A reported xAI 400 invalid-argument on tool-less calls motivated omitting
489
+ * tool controls when no tools are supplied. Keep this endpoint-specific
490
+ * compatibility measure separate from assumptions about other backends.
491
+ */
492
+ export function grokRequestBody(options, resolved) {
493
+ return {
494
+ model: options.model,
495
+ ...resolved.instructions === undefined ? {} : { instructions: resolved.instructions },
496
+ input: resolved.input,
497
+ ...options.tools !== undefined && options.tools.length > 0
498
+ ? { tools: toResponsesTools(options.tools), tool_choice: 'auto', parallel_tool_calls: true }
499
+ : {},
500
+ ...options.maxTokens !== undefined ? { max_output_tokens: options.maxTokens } : {},
501
+ // The harness only passes an effort the resolved model advertised (the
502
+ // CLI catalog's), so this never reaches a model that rejects it.
503
+ ...options.reasoningEffort !== undefined
504
+ ? { reasoning: { effort: String(options.reasoningEffort) } }
505
+ : {},
506
+ // Cache-affinity hint (mirrors codex): xAI caches prompts per server,
507
+ // and `prompt_cache_key` is the Responses-API signal that routes repeat
508
+ // requests back to the cache-holding shard — without it every turn's
509
+ // cache hit is shard-routing luck. The session id is the stable key
510
+ // xAI's own docs recommend.
511
+ ...options.sessionId !== undefined ? { prompt_cache_key: String(options.sessionId) } : {},
512
+ store: false,
513
+ stream: true,
514
+ };
515
+ }
516
+ /** Grok wire adapter: one instance serves the `grok` provider route. */
517
+ export class GrokAdapter extends LlmAdapter {
518
+ options;
519
+ catalog;
520
+ /** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
521
+ accountCatalogs = new Map();
522
+ accountCatalogsInvalidated = false;
523
+ invalidatedAccounts = new Set();
524
+ /** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
525
+ catalogOwner;
526
+ constructor(options) {
527
+ super();
528
+ this.options = options;
529
+ this.catalog = new ModelCatalogCache(options.catalogStore, undefined, options.onCatalogChanged);
530
+ }
531
+ /** Discovery fetcher: resolves the session through the refresh-aware path. */
532
+ async fetchCatalog(account, signal) {
533
+ const lastKnown = account === undefined || account === await this.options.tokens.defaultAccount()
534
+ ? this.catalog.lastKnown()
535
+ : this.accountCatalogs.get(account)?.lastKnown();
536
+ return fetchGrokModels(await this.options.tokens.session(account), this.options.fetchFn, this.options.onWarn, lastKnown, signal);
537
+ }
538
+ /** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
539
+ clearAccountCatalog(account) {
540
+ if (account === undefined) {
541
+ this.accountCatalogsInvalidated = true;
542
+ this.invalidatedAccounts.clear();
543
+ }
544
+ else
545
+ this.invalidatedAccounts.add(account);
546
+ if (account === undefined) {
547
+ for (const cache of this.accountCatalogs.values())
548
+ cache.invalidate();
549
+ this.accountCatalogs.clear();
550
+ }
551
+ else {
552
+ this.accountCatalogs.get(account)?.invalidate();
553
+ this.accountCatalogs.delete(account);
554
+ }
555
+ if (account === undefined || this.catalogOwner === account || this.catalogOwner === undefined) {
556
+ this.catalogOwner = undefined;
557
+ this.catalog.invalidate();
558
+ }
559
+ }
560
+ /** Default and secondary accounts each seed their own persisted catalog. */
561
+ async catalogFor(account) {
562
+ const defaultKey = await this.options.tokens.defaultAccount();
563
+ const key = account ?? defaultKey;
564
+ if (key === undefined || key === defaultKey) {
565
+ if (this.catalogOwner !== undefined && this.catalogOwner !== defaultKey) {
566
+ this.catalog.invalidate();
567
+ }
568
+ this.catalogOwner = defaultKey;
569
+ return this.catalog;
570
+ }
571
+ let cache = this.accountCatalogs.get(key);
572
+ if (cache === undefined) {
573
+ cache = new ModelCatalogCache(this.options.accountCatalogStore?.(key), undefined, this.options.onCatalogChanged);
574
+ const invalidated = this.invalidatedAccounts.delete(key);
575
+ if (this.accountCatalogsInvalidated || invalidated)
576
+ cache.invalidate();
577
+ this.accountCatalogs.set(key, cache);
578
+ }
579
+ return cache;
580
+ }
581
+ listed(provider, discovered) {
582
+ return discovered.map(model => ({
583
+ provider,
584
+ id: model.id,
585
+ name: model.name,
586
+ ...model.description === undefined ? {} : { description: model.description },
587
+ inputModalities: grokModalities(model.id),
588
+ }));
589
+ }
590
+ providerInfo(provider) {
591
+ return { id: provider, name: 'Grok (Subscription)' };
592
+ }
593
+ providerRetryPolicy(provider) {
594
+ return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `grok: provider "${provider}" retryPolicy`);
595
+ }
596
+ staticModels(provider) {
597
+ return this.options.models.map(model => ({
598
+ provider,
599
+ id: model.id,
600
+ name: model.name ?? model.id,
601
+ inputModalities: model.inputModalities ?? grokModalities(model.id),
602
+ }));
603
+ }
604
+ async listModels(provider) {
605
+ const own = await this.listOwnModels(provider, undefined, undefined, true);
606
+ const pool = this.options.pool?.();
607
+ if (pool === undefined)
608
+ return own;
609
+ const extra = await pool.modelsForProvider(provider);
610
+ const seen = new Set(own.map(model => model.id));
611
+ // Account pools reuse the catalog row; only configured tiers are extra.
612
+ return [...own, ...extra.filter(model => !seen.has(model.id))];
613
+ }
614
+ /** The provider's own catalog: union of every account, or one account when named. */
615
+ async listOwnModels(provider, account, signal, preferCached = false) {
616
+ if (account === undefined) {
617
+ const accounts = (await this.options.tokens.list()).map(entry => entry.key);
618
+ if (accounts.length === 0)
619
+ return [];
620
+ return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal, preferCached), { timeoutMs: DISCOVERY_TIMEOUT_MS, ...signal === undefined ? {} : { signal } });
621
+ }
622
+ if (!await this.options.tokens.hasSession(account)) {
623
+ return [];
624
+ }
625
+ if (!this.options.discovery)
626
+ return this.staticModels(provider);
627
+ const catalog = await this.catalogFor(account);
628
+ try {
629
+ // The fetcher runs only on a cache miss, and resolves the session
630
+ // through the refresh-aware path so an expired access token renews here
631
+ // instead of failing discovery into the static fallback.
632
+ return this.listed(provider, await catalog.get(() => discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => this.fetchCatalog(account, signal)), preferCached));
633
+ }
634
+ catch (error) {
635
+ if (isDiscoveryAborted(error, signal))
636
+ throw error;
637
+ // A permanent refresh failure deletes the stored session: the provider
638
+ // is logged out, so hide it instead of showing a stale static catalog.
639
+ if (isMissingOrInvalidCredential(error))
640
+ return [];
641
+ this.options.onWarn?.(`grok model discovery failed; using the built-in catalog (${errorChain(error)})`);
642
+ return this.staticModels(provider);
643
+ }
644
+ }
645
+ /**
646
+ * The discovered entry for one model. Resolved through the cache's
647
+ * stale-while-revalidate path: capability metadata must stay stable across
648
+ * a long conversation — a session that selected a reasoning effort calls
649
+ * this on EVERY step, and forgetting the efforts just because the TTL
650
+ * lapsed mid-turn would fail the call with UNSUPPORTED_REASONING_EFFORT
651
+ * before provider I/O.
652
+ */
653
+ async discovered(model) {
654
+ if (!this.options.discovery)
655
+ return undefined;
656
+ const accounts = (await this.options.tokens.list()).map(entry => entry.key);
657
+ return discoverAcrossAccounts(accounts, async (account) => {
658
+ const catalog = await this.catalogFor(account);
659
+ const models = await catalog.resolve(() => this.fetchCatalog(account));
660
+ return models?.find(entry => entry.id === model);
661
+ });
662
+ }
663
+ async resolveModel(provider, model) {
664
+ const pool = this.options.pool?.();
665
+ if (pool !== undefined && await pool.owns(provider, model)) {
666
+ return pool.resolveModel(provider, model);
667
+ }
668
+ return this.resolveOwnModel(provider, model);
669
+ }
670
+ /** Capability resolution of the provider's own models (the pool resolves members here). */
671
+ async resolveOwnModel(provider, model) {
672
+ const discovered = await this.discovered(model);
673
+ const configured = this.options.models.find(entry => entry.id === model);
674
+ // Efforts come from the discovered CLI catalog; models it does not
675
+ // cover expose none, so the harness rejects explicit efforts before
676
+ // provider I/O instead of the API 400ing. A configured default effort
677
+ // still merges in: the picker then preselects it even for models the
678
+ // CLI catalog does not cover.
679
+ const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning);
680
+ return {
681
+ provider,
682
+ id: model,
683
+ name: discovered?.name ?? configured?.name ?? model,
684
+ ...discovered?.description === undefined ? {} : { description: discovered.description },
685
+ inputModalities: configured?.inputModalities ?? grokModalities(model),
686
+ context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
687
+ defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
688
+ ...reasoning === undefined ? {} : { reasoning },
689
+ };
690
+ }
691
+ async *stream(options) {
692
+ const pool = this.options.pool?.();
693
+ if (pool !== undefined && await pool.owns(options.provider, options.model)) {
694
+ yield* pool.stream(options);
695
+ return;
696
+ }
697
+ yield* this.streamCore(options);
698
+ }
699
+ /** Pool seam: stream through one specific account instead of the default. */
700
+ streamAccount(options, account) {
701
+ return this.streamCore(options, account);
702
+ }
703
+ async *streamCore(options, account) {
704
+ const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
705
+ try {
706
+ const served = account ?? await this.options.tokens.defaultAccount();
707
+ let session = await this.options.tokens.session(served);
708
+ let response = await this.request(options, session, watchdog.signal);
709
+ if (response.status === 401) {
710
+ // One forced refresh + retry on an unexpired-but-rejected token.
711
+ session = await this.options.tokens.session(served, true);
712
+ response = await this.request(options, session, watchdog.signal);
713
+ }
714
+ if (served !== undefined) {
715
+ this.options.onResponse?.(response, await this.options.tokens.resolveAccount(served), options.sessionId);
716
+ }
717
+ if (!response.ok) {
718
+ throw await httpLlmError(response, 'grok API', {
719
+ rateLimitReset: grokRateLimitReset,
720
+ ...this.options.onWarn === undefined ? {} : { onWarn: this.options.onWarn },
721
+ });
722
+ }
723
+ if (response.body === null) {
724
+ throw new LlmError('grok API returned no response body', EMPTY_RESPONSE_CODE);
725
+ }
726
+ yield* streamResponses(response.body, () => { watchdog.pulse(); });
727
+ }
728
+ catch (error) {
729
+ throw mapFetchFailure('grok API', error, watchdog, options.signal);
730
+ }
731
+ finally {
732
+ watchdog.stop();
733
+ }
734
+ }
735
+ async request(options, session, signal) {
736
+ const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
737
+ const body = grokRequestBody(options, toResponsesInput(messages, options.system));
738
+ return proxiedFetch(GROK_API_URL, {
739
+ method: 'POST',
740
+ headers: {
741
+ 'authorization': `Bearer ${session.accessToken}`,
742
+ 'accept': 'text/event-stream',
743
+ 'content-type': 'application/json',
744
+ ...attributionHeaders(),
745
+ },
746
+ body: JSON.stringify(body),
747
+ signal,
748
+ });
749
+ }
750
+ }