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