dsh-subscriptions 0.2.0 → 0.2.2
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 +212 -180
- package/lib/auth/claude-code-creds.js +31 -9
- package/lib/auth/device-flow.js +16 -9
- package/lib/auth/oauth-flow.js +13 -10
- package/lib/auth/rpc.js +74 -23
- package/lib/auth/store.js +80 -31
- package/lib/client/CursorCard.d.ts +1 -1
- package/lib/client/CursorCard.js +50 -16
- package/lib/client/ExternalUsageCards.d.ts +1 -1
- package/lib/client/ExternalUsageCards.js +37 -16
- package/lib/client/ImageGallery.d.ts +2 -2
- package/lib/client/ImageGallery.js +92 -33
- package/lib/client/ImageGenerateToolview.js +50 -14
- package/lib/client/OllamaCard.d.ts +1 -1
- package/lib/client/OllamaCard.js +26 -8
- package/lib/client/OllamaSettingsManager.js +113 -30
- package/lib/client/ProviderAccountManager.d.ts +1 -1
- package/lib/client/ProviderAccountManager.js +164 -49
- package/lib/client/ProviderModelEditor.js +107 -26
- package/lib/client/SpeedSelect.d.ts +62 -14
- package/lib/client/SpeedSelect.js +257 -46
- package/lib/client/SubscriptionUsageBadge.d.ts +4 -10
- package/lib/client/SubscriptionUsageBadge.js +300 -112
- package/lib/client/SubscriptionsSection.js +258 -85
- package/lib/client/UsageBadgeDisplaySetting.js +35 -12
- package/lib/client/UsageMeter.d.ts +1 -1
- package/lib/client/UsageMeter.js +62 -24
- package/lib/client/VideoGenerateToolview.js +69 -17
- package/lib/client/account-preferences.d.ts +8 -0
- package/lib/client/account-preferences.js +20 -5
- package/lib/client/host-contract.d.ts +11 -2
- package/lib/client/host-contract.js +20 -3
- package/lib/client/index.d.ts +5 -5
- package/lib/client/index.js +34 -12
- package/lib/client/locales.d.ts +10 -0
- package/lib/client/locales.js +61 -51
- package/lib/client/ollama-draft.js +21 -12
- package/lib/client/reset-action.js +6 -4
- package/lib/client/reset-credits-view.d.ts +1 -1
- package/lib/client/reset-credits-view.js +66 -17
- package/lib/client/subscription-card-styles.js +94 -24
- package/lib/client/usage-color-preferences.js +4 -2
- package/lib/client/usage-pace.js +30 -12
- package/lib/client.js +287 -47
- package/lib/client.js.map +1 -1
- package/lib/http.js +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.js +118 -64
- package/lib/model-defaults.js +4 -2
- package/lib/provider-settings.d.ts +12 -0
- package/lib/provider-settings.js +58 -16
- package/lib/providers/account-preferences.js +51 -22
- package/lib/providers/accounts.js +15 -9
- package/lib/providers/antigravity-oauth-client.js +2 -2
- package/lib/providers/antigravity.js +141 -68
- package/lib/providers/catalog-store.js +120 -41
- package/lib/providers/claude-cli-version.js +3 -3
- package/lib/providers/claude.d.ts +9 -2
- package/lib/providers/claude.js +153 -90
- package/lib/providers/codex-client-version.js +3 -3
- package/lib/providers/codex-search.js +40 -13
- package/lib/providers/codex.js +196 -112
- package/lib/providers/common.js +71 -39
- package/lib/providers/copilot.js +143 -82
- package/lib/providers/cursor-adapter.js +42 -18
- package/lib/providers/cursor-auth.d.ts +1 -1
- package/lib/providers/cursor-auth.js +69 -24
- package/lib/providers/cursor-model-settings.js +14 -5
- package/lib/providers/cursor-usage.js +27 -13
- package/lib/providers/external-usage-controller.js +14 -5
- package/lib/providers/external-usage.js +59 -23
- package/lib/providers/grok.js +152 -89
- package/lib/providers/image-pool.js +29 -11
- package/lib/providers/minimax-usage.js +31 -11
- package/lib/providers/npm-cli-version.js +10 -3
- package/lib/providers/ollama-settings.js +24 -8
- package/lib/providers/ollama-web.js +29 -11
- package/lib/providers/ollama.js +129 -66
- package/lib/providers/pool-family.js +11 -3
- package/lib/providers/pool-health.js +28 -8
- package/lib/providers/pool-usage.js +16 -9
- package/lib/providers/pool.d.ts +8 -0
- package/lib/providers/pool.js +81 -42
- package/lib/providers/rate-limit.js +8 -2
- package/lib/providers/replay.js +40 -17
- package/lib/providers/reset-redemption.js +18 -6
- package/lib/tools/image-generate.js +123 -65
- package/lib/tools/registration.js +5 -2
- package/lib/tools/video-generate.js +51 -31
- package/lib/tools/x-search.js +50 -22
- package/lib/translate/anthropic.js +85 -37
- package/lib/translate/antigravity-schema.js +71 -28
- package/lib/translate/antigravity-thinking.js +22 -9
- package/lib/translate/antigravity.js +141 -50
- package/lib/translate/chat-completions.js +67 -23
- package/lib/translate/resolved.js +55 -21
- package/lib/translate/responses.js +69 -29
- package/lib/translate/sse.js +4 -1
- package/package.json +26 -4
package/lib/providers/codex.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
* Codex CLI client id, and streaming against the ChatGPT backend Responses
|
|
4
4
|
* endpoint.
|
|
5
5
|
*/
|
|
6
|
-
import { createHash
|
|
7
|
-
import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
8
8
|
import { decodeJwtPayload } from '../auth/jwt.js';
|
|
9
9
|
import { resolveImages } from '../translate/resolved.js';
|
|
10
|
-
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
10
|
+
import { streamResponses, toResponsesInput, toResponsesTools, } from '../translate/responses.js';
|
|
11
11
|
import { deterministicSessionId, effortDisplayName, httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isDiscoveryAuthFailure, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
12
|
-
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
12
|
+
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs, } from './accounts.js';
|
|
13
13
|
import { hostFetch } from '../http.js';
|
|
14
14
|
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, jsonBody, resetFromFields, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
15
15
|
export const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
@@ -27,7 +27,12 @@ export const CODEX_PREEMPT_MS = 5 * 60_000;
|
|
|
27
27
|
* carries `usage_limit_reached` with the seconds left on the window — the case
|
|
28
28
|
* that used to classify as a terminal quota and never be retried at all.
|
|
29
29
|
*/
|
|
30
|
-
const CODEX_RESET_FIELDS = [
|
|
30
|
+
const CODEX_RESET_FIELDS = [
|
|
31
|
+
'resets_in_seconds',
|
|
32
|
+
'reset_after_seconds',
|
|
33
|
+
'resets_at',
|
|
34
|
+
'reset_at',
|
|
35
|
+
];
|
|
31
36
|
/**
|
|
32
37
|
* Reads the reset instant of the Codex window that rejected a request.
|
|
33
38
|
*
|
|
@@ -40,8 +45,8 @@ const CODEX_RESET_FIELDS = ['resets_in_seconds', 'reset_after_seconds', 'resets_
|
|
|
40
45
|
*/
|
|
41
46
|
export const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
|
|
42
47
|
/** Default instruction when the request carries no system prompt. */
|
|
43
|
-
const DEFAULT_CODEX_INSTRUCTIONS = 'You are Codex, a coding agent based on GPT-5. '
|
|
44
|
-
|
|
48
|
+
const DEFAULT_CODEX_INSTRUCTIONS = 'You are Codex, a coding agent based on GPT-5. ' +
|
|
49
|
+
'Help the user with their software engineering tasks.';
|
|
45
50
|
/** Refresh-grant rejections that mean the login is gone for good. */
|
|
46
51
|
const PERMANENT_REFRESH_CODES = new Set([
|
|
47
52
|
'refresh_token_expired',
|
|
@@ -122,13 +127,16 @@ export function codexProfileClaims(idToken) {
|
|
|
122
127
|
? auth.chatgpt_plan_type
|
|
123
128
|
: undefined;
|
|
124
129
|
return {
|
|
125
|
-
...typeof email === 'string' && email.length > 0
|
|
126
|
-
|
|
130
|
+
...(typeof email === 'string' && email.length > 0
|
|
131
|
+
? { emailAddress: email }
|
|
132
|
+
: {}),
|
|
133
|
+
...(typeof plan === 'string' && plan.length > 0 ? { planType: plan } : {}),
|
|
127
134
|
};
|
|
128
135
|
}
|
|
129
136
|
/** Build a session from a token response; expires_in wins, JWT exp is the fallback. */
|
|
130
137
|
function codexSession(tokens, fallback) {
|
|
131
|
-
if (typeof tokens.access_token !== 'string' ||
|
|
138
|
+
if (typeof tokens.access_token !== 'string' ||
|
|
139
|
+
tokens.access_token.length === 0) {
|
|
132
140
|
throw new Error('codex token endpoint returned no access token');
|
|
133
141
|
}
|
|
134
142
|
const refreshToken = tokens.refresh_token ?? fallback?.refreshToken;
|
|
@@ -149,8 +157,12 @@ function codexSession(tokens, fallback) {
|
|
|
149
157
|
// one keeps the claims the stored session already had.
|
|
150
158
|
const idToken = tokens.id_token ?? fallback?.idToken;
|
|
151
159
|
const claims = {
|
|
152
|
-
...fallback?.emailAddress === undefined
|
|
153
|
-
|
|
160
|
+
...(fallback?.emailAddress === undefined
|
|
161
|
+
? {}
|
|
162
|
+
: { emailAddress: fallback.emailAddress }),
|
|
163
|
+
...(fallback?.planType === undefined
|
|
164
|
+
? {}
|
|
165
|
+
: { planType: fallback.planType }),
|
|
154
166
|
...codexProfileClaims(tokens.id_token),
|
|
155
167
|
};
|
|
156
168
|
return {
|
|
@@ -160,7 +172,7 @@ function codexSession(tokens, fallback) {
|
|
|
160
172
|
accountId: tokens.id_token === undefined && fallback !== undefined
|
|
161
173
|
? fallback.accountId
|
|
162
174
|
: accountIdOf(tokens.id_token),
|
|
163
|
-
...idToken === undefined ? {} : { idToken },
|
|
175
|
+
...(idToken === undefined ? {} : { idToken }),
|
|
164
176
|
...claims,
|
|
165
177
|
};
|
|
166
178
|
}
|
|
@@ -185,7 +197,7 @@ export async function exchangeCodexCode(code, verifier, redirectUri) {
|
|
|
185
197
|
});
|
|
186
198
|
if (!response.ok)
|
|
187
199
|
throw await oauthEndpointError(response, 'codex');
|
|
188
|
-
return codexSession(await parseProviderJson(response, 'codex token exchange'));
|
|
200
|
+
return codexSession((await parseProviderJson(response, 'codex token exchange')));
|
|
189
201
|
}
|
|
190
202
|
/**
|
|
191
203
|
* Refresh a codex session (JSON grant — unlike the code exchange).
|
|
@@ -204,7 +216,7 @@ export async function refreshCodex(session) {
|
|
|
204
216
|
});
|
|
205
217
|
if (!response.ok)
|
|
206
218
|
throw await oauthEndpointError(response, 'codex');
|
|
207
|
-
return codexSession(await parseProviderJson(response, 'codex token refresh'), session);
|
|
219
|
+
return codexSession((await parseProviderJson(response, 'codex token refresh')), session);
|
|
208
220
|
}
|
|
209
221
|
/**
|
|
210
222
|
* Whether a codex refresh failure means the login is permanently gone.
|
|
@@ -212,9 +224,9 @@ export async function refreshCodex(session) {
|
|
|
212
224
|
* @returns true when re-login is the only fix.
|
|
213
225
|
*/
|
|
214
226
|
export function isCodexPermanentRefreshError(error) {
|
|
215
|
-
return error instanceof OAuthEndpointError
|
|
216
|
-
|
|
217
|
-
|
|
227
|
+
return (error instanceof OAuthEndpointError &&
|
|
228
|
+
error.oauthCode !== undefined &&
|
|
229
|
+
PERMANENT_REFRESH_CODES.has(error.oauthCode));
|
|
218
230
|
}
|
|
219
231
|
export const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
|
220
232
|
export const CODEX_RESET_CREDITS_URL = 'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits';
|
|
@@ -235,22 +247,29 @@ export async function fetchCodexResetCredits(session, fetchFn = hostFetch, signa
|
|
|
235
247
|
accept: 'application/json',
|
|
236
248
|
...attributionHeaders(),
|
|
237
249
|
},
|
|
238
|
-
...signal === undefined ? {} : { signal },
|
|
250
|
+
...(signal === undefined ? {} : { signal }),
|
|
239
251
|
});
|
|
240
252
|
if (!response.ok)
|
|
241
253
|
throw await oauthEndpointError(response, 'codex reset credits');
|
|
242
|
-
const payload = await parseProviderJson(response, 'codex reset credits');
|
|
254
|
+
const payload = (await parseProviderJson(response, 'codex reset credits'));
|
|
243
255
|
if (!Array.isArray(payload?.credits))
|
|
244
256
|
throw new Error('codex reset credits: unexpected response (missing credits array)');
|
|
245
257
|
return payload.credits.flatMap((value) => {
|
|
246
258
|
if (typeof value !== 'object' || value === null)
|
|
247
259
|
return [];
|
|
248
260
|
const credit = value;
|
|
249
|
-
if (credit.status !== 'available' ||
|
|
261
|
+
if (credit.status !== 'available' ||
|
|
262
|
+
credit.reset_type !== 'codex_rate_limits')
|
|
250
263
|
return [];
|
|
251
264
|
const grantedAt = parseResetDate(credit.granted_at);
|
|
252
265
|
const expiresAt = parseResetDate(credit.expires_at);
|
|
253
|
-
return [
|
|
266
|
+
return [
|
|
267
|
+
{
|
|
268
|
+
...(typeof credit.id === 'string' ? { id: credit.id } : {}),
|
|
269
|
+
...(grantedAt === undefined ? {} : { grantedAt }),
|
|
270
|
+
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
271
|
+
},
|
|
272
|
+
];
|
|
254
273
|
});
|
|
255
274
|
}
|
|
256
275
|
/** Submit one explicit reset; ambiguous or unconfirmed responses throw without retry. */
|
|
@@ -260,16 +279,18 @@ export async function consumeCodexResetCredit(session, creditId, requestId, fetc
|
|
|
260
279
|
headers: {
|
|
261
280
|
authorization: 'Bearer ' + session.accessToken,
|
|
262
281
|
'chatgpt-account-id': session.accountId,
|
|
263
|
-
originator: 'Codex Desktop',
|
|
264
|
-
'
|
|
282
|
+
originator: 'Codex Desktop',
|
|
283
|
+
'openai-beta': 'codex-1',
|
|
284
|
+
'content-type': 'application/json',
|
|
285
|
+
accept: 'application/json',
|
|
265
286
|
...attributionHeaders(),
|
|
266
287
|
},
|
|
267
288
|
body: JSON.stringify({ redeem_request_id: requestId, credit_id: creditId }),
|
|
268
|
-
...signal === undefined ? {} : { signal },
|
|
289
|
+
...(signal === undefined ? {} : { signal }),
|
|
269
290
|
});
|
|
270
291
|
if (!response.ok)
|
|
271
292
|
throw await oauthEndpointError(response, 'codex consume reset');
|
|
272
|
-
const payload = await parseProviderJson(response, 'codex consume reset');
|
|
293
|
+
const payload = (await parseProviderJson(response, 'codex consume reset'));
|
|
273
294
|
if (payload?.code !== 'reset')
|
|
274
295
|
throw new Error('Reset was not confirmed. Verify the account in Codex before retrying.');
|
|
275
296
|
}
|
|
@@ -301,24 +322,30 @@ function codexUsageWindow(value, fallbackKind) {
|
|
|
301
322
|
if (typeof value !== 'object' || value === null)
|
|
302
323
|
return undefined;
|
|
303
324
|
const window = value;
|
|
304
|
-
if (typeof window.used_percent !== 'number' ||
|
|
325
|
+
if (typeof window.used_percent !== 'number' ||
|
|
326
|
+
!Number.isFinite(window.used_percent))
|
|
305
327
|
return undefined;
|
|
306
328
|
let resetsAt;
|
|
307
329
|
if (typeof window.reset_at === 'number' && window.reset_at > 0) {
|
|
308
330
|
resetsAt = window.reset_at * 1000;
|
|
309
331
|
}
|
|
310
|
-
else if (typeof window.reset_after_seconds === 'number' &&
|
|
332
|
+
else if (typeof window.reset_after_seconds === 'number' &&
|
|
333
|
+
window.reset_after_seconds > 0) {
|
|
311
334
|
resetsAt = Date.now() + window.reset_after_seconds * 1000;
|
|
312
335
|
}
|
|
313
336
|
return {
|
|
314
337
|
kind: codexWindowKind(window, fallbackKind),
|
|
315
|
-
...typeof window.limit_window_seconds === 'number' &&
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
338
|
+
...(typeof window.limit_window_seconds === 'number' &&
|
|
339
|
+
Number.isFinite(window.limit_window_seconds) &&
|
|
340
|
+
window.limit_window_seconds > 0
|
|
341
|
+
? {
|
|
342
|
+
// wham reports the duration and reset of this same quota window.
|
|
343
|
+
windowDurationMs: window.limit_window_seconds * 1000,
|
|
344
|
+
fixedWindow: true,
|
|
345
|
+
}
|
|
346
|
+
: {}),
|
|
320
347
|
usedPercent: window.used_percent,
|
|
321
|
-
...resetsAt === undefined ? {} : { resetsAt },
|
|
348
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
322
349
|
};
|
|
323
350
|
}
|
|
324
351
|
/**
|
|
@@ -337,17 +364,17 @@ function codexUsageWindow(value, fallbackKind) {
|
|
|
337
364
|
export async function fetchCodexUsage(session, fetchFn = hostFetch, signal) {
|
|
338
365
|
const response = await fetchFn(CODEX_USAGE_URL, {
|
|
339
366
|
headers: {
|
|
340
|
-
|
|
367
|
+
authorization: `Bearer ${session.accessToken}`,
|
|
341
368
|
'chatgpt-account-id': session.accountId,
|
|
342
|
-
|
|
343
|
-
|
|
369
|
+
originator: 'codex_cli_rs',
|
|
370
|
+
accept: 'application/json',
|
|
344
371
|
...attributionHeaders(),
|
|
345
372
|
},
|
|
346
|
-
...signal === undefined ? {} : { signal },
|
|
373
|
+
...(signal === undefined ? {} : { signal }),
|
|
347
374
|
});
|
|
348
375
|
if (!response.ok)
|
|
349
376
|
throw await oauthEndpointError(response, 'codex usage');
|
|
350
|
-
const payload = await parseProviderJson(response, 'codex usage');
|
|
377
|
+
const payload = (await parseProviderJson(response, 'codex usage'));
|
|
351
378
|
const windows = [];
|
|
352
379
|
const primary = codexUsageWindow(payload.rate_limit?.primary_window, 'session');
|
|
353
380
|
const secondary = codexUsageWindow(payload.rate_limit?.secondary_window, 'weekly');
|
|
@@ -358,9 +385,9 @@ export async function fetchCodexUsage(session, fetchFn = hostFetch, signal) {
|
|
|
358
385
|
return {
|
|
359
386
|
supported: true,
|
|
360
387
|
windows,
|
|
361
|
-
...typeof payload.plan_type === 'string' && payload.plan_type.length > 0
|
|
388
|
+
...(typeof payload.plan_type === 'string' && payload.plan_type.length > 0
|
|
362
389
|
? { plan: payload.plan_type }
|
|
363
|
-
: {},
|
|
390
|
+
: {}),
|
|
364
391
|
};
|
|
365
392
|
}
|
|
366
393
|
export const CODEX_MODELS_URL = 'https://chatgpt.com/backend-api/codex/models';
|
|
@@ -384,8 +411,7 @@ export function codexClientVersion(value = CODEX_CLIENT_VERSION) {
|
|
|
384
411
|
* wire value, or the legacy `additional_speed_tiers` "fast" entry.
|
|
385
412
|
*/
|
|
386
413
|
function supportsFastTier(entry) {
|
|
387
|
-
return (entry.service_tiers ?? []).some(tier => tier.id === CODEX_FAST_SERVICE_TIER)
|
|
388
|
-
|| (entry.additional_speed_tiers ?? []).includes(CODEX_FAST_SPEED_TIER);
|
|
414
|
+
return ((entry.service_tiers ?? []).some((tier) => tier.id === CODEX_FAST_SERVICE_TIER) || (entry.additional_speed_tiers ?? []).includes(CODEX_FAST_SPEED_TIER));
|
|
389
415
|
}
|
|
390
416
|
/**
|
|
391
417
|
* Fetch the live codex model catalog with the session's auth headers.
|
|
@@ -400,17 +426,17 @@ export async function fetchCodexModels(session, fetchFn = hostFetch, signal, cli
|
|
|
400
426
|
const url = `${CODEX_MODELS_URL}?client_version=${encodeURIComponent(version)}`;
|
|
401
427
|
const response = await fetchFn(url, {
|
|
402
428
|
headers: {
|
|
403
|
-
|
|
429
|
+
authorization: `Bearer ${session.accessToken}`,
|
|
404
430
|
'chatgpt-account-id': session.accountId,
|
|
405
|
-
|
|
406
|
-
|
|
431
|
+
originator: 'codex_cli_rs',
|
|
432
|
+
accept: 'application/json',
|
|
407
433
|
...attributionHeaders(),
|
|
408
434
|
},
|
|
409
|
-
...signal === undefined ? {} : { signal },
|
|
435
|
+
...(signal === undefined ? {} : { signal }),
|
|
410
436
|
});
|
|
411
437
|
if (!response.ok)
|
|
412
438
|
throw await oauthEndpointError(response, 'codex models');
|
|
413
|
-
const payload = await parseProviderJson(response, 'codex models');
|
|
439
|
+
const payload = (await parseProviderJson(response, 'codex models'));
|
|
414
440
|
if (!Array.isArray(payload.models))
|
|
415
441
|
throw new Error('codex models endpoint returned no models array');
|
|
416
442
|
const discovered = [];
|
|
@@ -424,15 +450,20 @@ export async function fetchCodexModels(session, fetchFn = hostFetch, signal, cli
|
|
|
424
450
|
// "ultra" is a Codex-client mode (max reasoning + automatic sub-agent
|
|
425
451
|
// delegation), not a wire value: the Responses API rejects it with HTTP 400.
|
|
426
452
|
const efforts = (entry.supported_reasoning_levels ?? [])
|
|
427
|
-
.filter(level => typeof level.effort === 'string' &&
|
|
428
|
-
.
|
|
453
|
+
.filter((level) => typeof level.effort === 'string' &&
|
|
454
|
+
level.effort.length > 0 &&
|
|
455
|
+
level.effort !== 'ultra')
|
|
456
|
+
.map((level) => ({
|
|
429
457
|
id: ReasoningEffortId(level.effort),
|
|
430
458
|
name: effortDisplayName(level.effort),
|
|
431
|
-
...level.description === undefined
|
|
459
|
+
...(level.description === undefined
|
|
460
|
+
? {}
|
|
461
|
+
: { description: level.description }),
|
|
432
462
|
}));
|
|
433
|
-
const defaultEffort = typeof entry.default_reasoning_level === 'string'
|
|
434
|
-
|
|
435
|
-
|
|
463
|
+
const defaultEffort = typeof entry.default_reasoning_level === 'string' &&
|
|
464
|
+
entry.default_reasoning_level.length > 0 &&
|
|
465
|
+
efforts.some((effort) => effort.id ===
|
|
466
|
+
ReasoningEffortId(entry.default_reasoning_level))
|
|
436
467
|
? ReasoningEffortId(entry.default_reasoning_level)
|
|
437
468
|
: undefined;
|
|
438
469
|
const model = {
|
|
@@ -440,24 +471,33 @@ export async function fetchCodexModels(session, fetchFn = hostFetch, signal, cli
|
|
|
440
471
|
name: typeof entry.display_name === 'string' && entry.display_name.length > 0
|
|
441
472
|
? entry.display_name
|
|
442
473
|
: entry.slug,
|
|
443
|
-
...typeof entry.description === 'string' && entry.description.length > 0
|
|
474
|
+
...(typeof entry.description === 'string' && entry.description.length > 0
|
|
444
475
|
? { description: entry.description }
|
|
445
|
-
: {},
|
|
446
|
-
...typeof entry.context_window === 'number' && entry.context_window > 0
|
|
476
|
+
: {}),
|
|
477
|
+
...(typeof entry.context_window === 'number' && entry.context_window > 0
|
|
447
478
|
? { contextWindow: entry.context_window }
|
|
448
|
-
: {},
|
|
449
|
-
...Number.isSafeInteger(entry.max_context_window) &&
|
|
479
|
+
: {}),
|
|
480
|
+
...(Number.isSafeInteger(entry.max_context_window) &&
|
|
481
|
+
entry.max_context_window > 0
|
|
450
482
|
? { maxContextWindow: entry.max_context_window }
|
|
451
|
-
: {},
|
|
452
|
-
...typeof entry.priority === 'number'
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
483
|
+
: {}),
|
|
484
|
+
...(typeof entry.priority === 'number'
|
|
485
|
+
? { priority: entry.priority }
|
|
486
|
+
: {}),
|
|
487
|
+
...(efforts.length > 0
|
|
488
|
+
? {
|
|
489
|
+
reasoning: {
|
|
490
|
+
efforts,
|
|
491
|
+
...(defaultEffort === undefined ? {} : { defaultEffort }),
|
|
492
|
+
},
|
|
493
|
+
}
|
|
494
|
+
: {}),
|
|
495
|
+
...(supportsFastTier(entry) ? { fastTier: true } : {}),
|
|
457
496
|
};
|
|
458
497
|
discovered.push(model);
|
|
459
498
|
}
|
|
460
|
-
discovered.sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) -
|
|
499
|
+
discovered.sort((a, b) => (a.priority ?? Number.MAX_SAFE_INTEGER) -
|
|
500
|
+
(b.priority ?? Number.MAX_SAFE_INTEGER));
|
|
461
501
|
// An empty catalog from a 200 response means the backend gated us out (e.g.
|
|
462
502
|
// client_version too old): surface it as a discovery failure so the adapter
|
|
463
503
|
// falls back to the static catalog instead of vanishing from the picker.
|
|
@@ -478,7 +518,8 @@ const CODEX_CALL_ID_PREFIX = 'call_';
|
|
|
478
518
|
function normalizeCodexCallIds(input) {
|
|
479
519
|
const mapping = new Map();
|
|
480
520
|
const used = new Set();
|
|
481
|
-
const callId = (item) => (item.type === 'function_call' || item.type === 'function_call_output') &&
|
|
521
|
+
const callId = (item) => (item.type === 'function_call' || item.type === 'function_call_output') &&
|
|
522
|
+
typeof item.call_id === 'string'
|
|
482
523
|
? item.call_id
|
|
483
524
|
: undefined;
|
|
484
525
|
for (const item of input) {
|
|
@@ -531,20 +572,28 @@ export function reconcileResponsesToolCalls(input) {
|
|
|
531
572
|
for (const item of input) {
|
|
532
573
|
if (item.type === 'function_call' && typeof item.call_id === 'string')
|
|
533
574
|
calls.add(item.call_id);
|
|
534
|
-
else if (item.type === 'function_call_output' &&
|
|
575
|
+
else if (item.type === 'function_call_output' &&
|
|
576
|
+
typeof item.call_id === 'string')
|
|
535
577
|
outputs.add(item.call_id);
|
|
536
578
|
}
|
|
537
|
-
const missing = new Set([...calls].filter(callId => !outputs.has(callId)));
|
|
538
|
-
if (missing.size === 0 && [...outputs].every(callId => calls.has(callId)))
|
|
579
|
+
const missing = new Set([...calls].filter((callId) => !outputs.has(callId)));
|
|
580
|
+
if (missing.size === 0 && [...outputs].every((callId) => calls.has(callId)))
|
|
539
581
|
return input;
|
|
540
582
|
const balanced = [];
|
|
541
583
|
for (const item of input) {
|
|
542
|
-
if (item.type === 'function_call_output' &&
|
|
584
|
+
if (item.type === 'function_call_output' &&
|
|
585
|
+
(typeof item.call_id !== 'string' || !calls.has(item.call_id))) {
|
|
543
586
|
continue;
|
|
544
587
|
}
|
|
545
588
|
balanced.push(item);
|
|
546
|
-
if (item.type === 'function_call' &&
|
|
547
|
-
|
|
589
|
+
if (item.type === 'function_call' &&
|
|
590
|
+
typeof item.call_id === 'string' &&
|
|
591
|
+
missing.delete(item.call_id)) {
|
|
592
|
+
balanced.push({
|
|
593
|
+
type: 'function_call_output',
|
|
594
|
+
call_id: item.call_id,
|
|
595
|
+
output: CODEX_UNKNOWN_TOOL_OUTCOME,
|
|
596
|
+
});
|
|
548
597
|
}
|
|
549
598
|
}
|
|
550
599
|
return balanced;
|
|
@@ -573,17 +622,28 @@ export function codexRequestBody(options, resolved, fast) {
|
|
|
573
622
|
// Omit tool controls for tool-less requests, matching the other adapters.
|
|
574
623
|
// This is request-shape consistency, not a claim that Codex rejects the
|
|
575
624
|
// controls when no tools are supplied.
|
|
576
|
-
...options.tools !== undefined && options.tools.length > 0
|
|
577
|
-
? {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
625
|
+
...(options.tools !== undefined && options.tools.length > 0
|
|
626
|
+
? {
|
|
627
|
+
tools: toResponsesTools(options.tools, { strict: false }),
|
|
628
|
+
tool_choice: 'auto',
|
|
629
|
+
parallel_tool_calls: true,
|
|
630
|
+
}
|
|
631
|
+
: {}),
|
|
632
|
+
...(options.reasoningEffort !== undefined
|
|
633
|
+
? {
|
|
634
|
+
reasoning: {
|
|
635
|
+
effort: codexWireEffort(String(options.reasoningEffort)),
|
|
636
|
+
summary: 'auto',
|
|
637
|
+
},
|
|
638
|
+
}
|
|
639
|
+
: {}),
|
|
582
640
|
store: false,
|
|
583
641
|
stream: true,
|
|
584
642
|
include: ['reasoning.encrypted_content'],
|
|
585
|
-
...options.sessionId !== undefined
|
|
586
|
-
|
|
643
|
+
...(options.sessionId !== undefined
|
|
644
|
+
? { prompt_cache_key: String(options.sessionId) }
|
|
645
|
+
: {}),
|
|
646
|
+
...(fast ? { service_tier: CODEX_FAST_SERVICE_TIER } : {}),
|
|
587
647
|
};
|
|
588
648
|
}
|
|
589
649
|
/** Codex wire adapter: one instance serves the `codex` provider route. */
|
|
@@ -602,7 +662,8 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
602
662
|
}
|
|
603
663
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
604
664
|
async fetchCatalog(account, signal) {
|
|
605
|
-
const version = this.options.clientVersion ??
|
|
665
|
+
const version = this.options.clientVersion ??
|
|
666
|
+
(await this.options.resolveClientVersion?.());
|
|
606
667
|
signal?.throwIfAborted();
|
|
607
668
|
return fetchCodexModels(await this.options.tokens.session(account), this.options.fetchFn, signal, version);
|
|
608
669
|
}
|
|
@@ -619,7 +680,9 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
619
680
|
this.accountCatalogs.get(account)?.invalidate();
|
|
620
681
|
this.accountCatalogs.delete(account);
|
|
621
682
|
}
|
|
622
|
-
if (account === undefined ||
|
|
683
|
+
if (account === undefined ||
|
|
684
|
+
this.catalogOwner === account ||
|
|
685
|
+
this.catalogOwner === undefined) {
|
|
623
686
|
this.catalogOwner = undefined;
|
|
624
687
|
this.catalog.invalidate();
|
|
625
688
|
}
|
|
@@ -644,13 +707,15 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
644
707
|
}
|
|
645
708
|
/** Picker rows for discovered entries (the wire id doubles as the route id). */
|
|
646
709
|
listed(provider, models) {
|
|
647
|
-
return models.map(model => ({
|
|
710
|
+
return models.map((model) => ({
|
|
648
711
|
provider,
|
|
649
712
|
id: model.id,
|
|
650
713
|
name: model.name,
|
|
651
|
-
...model.description === undefined
|
|
714
|
+
...(model.description === undefined
|
|
715
|
+
? {}
|
|
716
|
+
: { description: model.description }),
|
|
652
717
|
inputModalities: CODEX_MODALITIES,
|
|
653
|
-
...model.priority === undefined ? {} : { priority: model.priority },
|
|
718
|
+
...(model.priority === undefined ? {} : { priority: model.priority }),
|
|
654
719
|
}));
|
|
655
720
|
}
|
|
656
721
|
/**
|
|
@@ -659,7 +724,8 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
659
724
|
* listed a model minutes ago is still a better answer than "no models".
|
|
660
725
|
*/
|
|
661
726
|
async lastKnownOwnModels(provider, account) {
|
|
662
|
-
if (!this.options.discovery ||
|
|
727
|
+
if (!this.options.discovery ||
|
|
728
|
+
!(await this.options.tokens.hasSession(account)))
|
|
663
729
|
return undefined;
|
|
664
730
|
const known = (await this.catalogFor(account)).lastKnown();
|
|
665
731
|
return known === undefined ? undefined : this.listed(provider, known);
|
|
@@ -671,7 +737,7 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
671
737
|
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `codex: provider "${provider}" retryPolicy`);
|
|
672
738
|
}
|
|
673
739
|
staticModels(provider) {
|
|
674
|
-
return this.options.models.map(model => ({
|
|
740
|
+
return this.options.models.map((model) => ({
|
|
675
741
|
provider,
|
|
676
742
|
id: model.id,
|
|
677
743
|
name: model.name ?? model.id,
|
|
@@ -684,19 +750,22 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
684
750
|
if (pool === undefined)
|
|
685
751
|
return own;
|
|
686
752
|
const extra = await pool.modelsForProvider(provider);
|
|
687
|
-
const seen = new Set(own.map(model => model.id));
|
|
753
|
+
const seen = new Set(own.map((model) => model.id));
|
|
688
754
|
// Account pools reuse the catalog row; only configured tiers are extra.
|
|
689
|
-
return [...own, ...extra.filter(model => !seen.has(model.id))];
|
|
755
|
+
return [...own, ...extra.filter((model) => !seen.has(model.id))];
|
|
690
756
|
}
|
|
691
757
|
/** The provider's own catalog: union of every account, or one account when named. */
|
|
692
758
|
async listOwnModels(provider, account, signal) {
|
|
693
759
|
if (account === undefined) {
|
|
694
|
-
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
760
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
695
761
|
if (accounts.length === 0)
|
|
696
762
|
return [];
|
|
697
|
-
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), {
|
|
763
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), {
|
|
764
|
+
timeoutMs: this.options.discoveryTimeoutMs ?? DISCOVERY_TIMEOUT_MS,
|
|
765
|
+
...(signal === undefined ? {} : { signal }),
|
|
766
|
+
});
|
|
698
767
|
}
|
|
699
|
-
if (!await this.options.tokens.hasSession(account)) {
|
|
768
|
+
if (!(await this.options.tokens.hasSession(account))) {
|
|
700
769
|
return [];
|
|
701
770
|
}
|
|
702
771
|
if (!this.options.discovery)
|
|
@@ -706,7 +775,7 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
706
775
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
707
776
|
// through the refresh-aware path so an expired access token renews here
|
|
708
777
|
// instead of failing discovery into the static fallback.
|
|
709
|
-
const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)));
|
|
778
|
+
const discovered = await discoverOrRetryAuth((force) => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)));
|
|
710
779
|
return this.listed(provider, discovered);
|
|
711
780
|
}
|
|
712
781
|
catch (error) {
|
|
@@ -722,7 +791,9 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
722
791
|
// the login is revoked server-side. Listing the built-in catalog here
|
|
723
792
|
// would route requests to a dead account and show phantom models.
|
|
724
793
|
if (isDiscoveryAuthFailure(error)) {
|
|
725
|
-
const who = (await this.options.tokens.peek(account))?.emailAddress ??
|
|
794
|
+
const who = (await this.options.tokens.peek(account))?.emailAddress ??
|
|
795
|
+
account ??
|
|
796
|
+
'default account';
|
|
726
797
|
this.options.onWarn?.(`codex rejected the login of ${who}; hiding its models until it logs in again (${errorChain(error)})`);
|
|
727
798
|
return [];
|
|
728
799
|
}
|
|
@@ -749,12 +820,12 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
749
820
|
if (!this.options.discovery)
|
|
750
821
|
return undefined;
|
|
751
822
|
const accounts = account === undefined
|
|
752
|
-
? (await this.options.tokens.list()).map(entry => entry.key)
|
|
823
|
+
? (await this.options.tokens.list()).map((entry) => entry.key)
|
|
753
824
|
: [account];
|
|
754
825
|
return discoverAcrossAccounts(accounts, async (account) => {
|
|
755
826
|
const catalog = await this.catalogFor(account);
|
|
756
827
|
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
757
|
-
return models?.find(entry => entry.id === model);
|
|
828
|
+
return models?.find((entry) => entry.id === model);
|
|
758
829
|
});
|
|
759
830
|
}
|
|
760
831
|
/** Whether the discovered catalog advertises a fast tier for this model. */
|
|
@@ -768,7 +839,7 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
768
839
|
// Not logged in → no fast models, so the Speed toggle hides after logout
|
|
769
840
|
// (mirrors the listModels guard above). Union every account: a fast-capable
|
|
770
841
|
// model only the non-default lists (e.g. gpt-5.6-sol) must still show Speed.
|
|
771
|
-
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
842
|
+
const accounts = (await this.options.tokens.list()).map((entry) => entry.key);
|
|
772
843
|
if (accounts.length === 0)
|
|
773
844
|
return [];
|
|
774
845
|
const seen = new Set();
|
|
@@ -792,7 +863,8 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
792
863
|
}
|
|
793
864
|
async resolveModel(provider, model) {
|
|
794
865
|
const pool = this.options.pool?.();
|
|
795
|
-
if (pool !== undefined &&
|
|
866
|
+
if (pool !== undefined &&
|
|
867
|
+
(await pool.owns(provider, model))) {
|
|
796
868
|
return pool.resolveModel(provider, model);
|
|
797
869
|
}
|
|
798
870
|
return this.resolveOwnModel(provider, model);
|
|
@@ -803,17 +875,22 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
803
875
|
// the static entry wins over the built-in defaults. A configured default
|
|
804
876
|
// effort merges over both.
|
|
805
877
|
const discovered = await this.discovered(model, account);
|
|
806
|
-
const configured = this.options.models.find(entry => entry.id === model);
|
|
878
|
+
const configured = this.options.models.find((entry) => entry.id === model);
|
|
807
879
|
// `extendable` only while falling back to the built-in list: that one is
|
|
808
880
|
// known to trail the backend, so a configured level it omits still has to
|
|
809
881
|
// be selectable. A discovered catalog is the truth about what the model
|
|
810
882
|
// accepts, and a stale override must not be forced onto every request.
|
|
811
|
-
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning ?? {
|
|
883
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning ?? {
|
|
884
|
+
efforts: CODEX_EFFORTS,
|
|
885
|
+
defaultEffort: CODEX_DEFAULT_EFFORT,
|
|
886
|
+
}, { extendable: discovered?.reasoning === undefined });
|
|
812
887
|
return {
|
|
813
888
|
provider,
|
|
814
889
|
id: model,
|
|
815
890
|
name: discovered?.name ?? configured?.name ?? model,
|
|
816
|
-
...discovered?.description === undefined
|
|
891
|
+
...(discovered?.description === undefined
|
|
892
|
+
? {}
|
|
893
|
+
: { description: discovered.description }),
|
|
817
894
|
inputModalities: configured?.inputModalities ?? CODEX_MODALITIES,
|
|
818
895
|
context: { contextWindow: await this.contextWindowFor(model, account) },
|
|
819
896
|
defaultMaxTokens: configured?.maxTokens ?? CODEX_DEFAULT_MAX_TOKENS,
|
|
@@ -823,8 +900,10 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
823
900
|
/** Account-specific bounds; absent maximum conservatively keeps the advertised default. */
|
|
824
901
|
async contextLimits(model, account) {
|
|
825
902
|
const discovered = await this.discovered(model, account);
|
|
826
|
-
const configured = this.options.models.find(entry => entry.id === model);
|
|
827
|
-
const fallback = discovered?.contextWindow ??
|
|
903
|
+
const configured = this.options.models.find((entry) => entry.id === model);
|
|
904
|
+
const fallback = discovered?.contextWindow ??
|
|
905
|
+
configured?.contextWindow ??
|
|
906
|
+
CODEX_CONTEXT_WINDOW;
|
|
828
907
|
return { default: fallback, max: discovered?.maxContextWindow ?? fallback };
|
|
829
908
|
}
|
|
830
909
|
async contextWindowFor(model, account) {
|
|
@@ -833,7 +912,8 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
833
912
|
}
|
|
834
913
|
async *stream(options) {
|
|
835
914
|
const pool = this.options.pool?.();
|
|
836
|
-
if (pool !== undefined &&
|
|
915
|
+
if (pool !== undefined &&
|
|
916
|
+
(await pool.owns(options.provider, options.model))) {
|
|
837
917
|
yield* pool.stream(options);
|
|
838
918
|
return;
|
|
839
919
|
}
|
|
@@ -856,13 +936,17 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
856
936
|
if (!response.ok) {
|
|
857
937
|
throw await httpLlmError(response, 'codex API', {
|
|
858
938
|
rateLimitReset: codexRateLimitReset,
|
|
859
|
-
...this.options.onWarn === undefined
|
|
939
|
+
...(this.options.onWarn === undefined
|
|
940
|
+
? {}
|
|
941
|
+
: { onWarn: this.options.onWarn }),
|
|
860
942
|
});
|
|
861
943
|
}
|
|
862
944
|
if (response.body === null) {
|
|
863
945
|
throw new LlmError('codex API returned no response body', EMPTY_RESPONSE_CODE);
|
|
864
946
|
}
|
|
865
|
-
yield* streamResponses(response.body, () => {
|
|
947
|
+
yield* streamResponses(response.body, () => {
|
|
948
|
+
watchdog.pulse();
|
|
949
|
+
});
|
|
866
950
|
}
|
|
867
951
|
catch (error) {
|
|
868
952
|
throw mapFetchFailure('codex API', error, watchdog, options.signal);
|
|
@@ -873,17 +957,17 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
873
957
|
}
|
|
874
958
|
async request(options, session, signal) {
|
|
875
959
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal);
|
|
876
|
-
const fast = this.options.speedFor !== undefined
|
|
877
|
-
|
|
960
|
+
const fast = this.options.speedFor !== undefined &&
|
|
961
|
+
(await this.options.speedFor(options.sessionId, options.model));
|
|
878
962
|
const body = codexRequestBody(options, toResponsesInput(messages, options.system), fast);
|
|
879
963
|
return hostFetch(CODEX_API_URL, {
|
|
880
964
|
method: 'POST',
|
|
881
965
|
headers: {
|
|
882
|
-
|
|
966
|
+
authorization: `Bearer ${session.accessToken}`,
|
|
883
967
|
'chatgpt-account-id': session.accountId,
|
|
884
|
-
|
|
968
|
+
originator: 'codex_cli_rs',
|
|
885
969
|
'session-id': deterministicSessionId(options.sessionId),
|
|
886
|
-
|
|
970
|
+
accept: 'text/event-stream',
|
|
887
971
|
'content-type': 'application/json',
|
|
888
972
|
...attributionHeaders(),
|
|
889
973
|
},
|