dsh-plugin-subscriptions 0.5.0 → 0.5.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 +21 -6
- package/README.zh.md +20 -6
- package/lib/auth/device-flow.d.ts +55 -0
- package/lib/auth/device-flow.js +177 -0
- package/lib/auth/oauth-flow.js +1 -1
- package/lib/auth/rpc.d.ts +18 -2
- package/lib/auth/rpc.js +98 -3
- package/lib/auth/store.d.ts +20 -2
- package/lib/auth/store.js +45 -9
- package/lib/client/SubscriptionsSection.d.ts +18 -1
- package/lib/client/SubscriptionsSection.js +216 -6
- package/lib/client/index.js +11 -0
- package/lib/client/locales.d.ts +72 -0
- package/lib/client/locales.js +72 -0
- package/lib/client.js +725 -144
- package/lib/client.js.map +1 -1
- package/lib/http.d.ts +114 -0
- package/lib/http.js +402 -0
- package/lib/index.d.ts +3 -2
- package/lib/index.js +2256 -226
- package/lib/providers/antigravity.d.ts +90 -0
- package/lib/providers/antigravity.js +392 -0
- package/lib/providers/catalog-store.js +15 -0
- package/lib/providers/claude.d.ts +20 -1
- package/lib/providers/claude.js +51 -33
- package/lib/providers/codex.js +58 -13
- package/lib/providers/common.d.ts +32 -1
- package/lib/providers/common.js +48 -1
- package/lib/providers/copilot.d.ts +315 -0
- package/lib/providers/copilot.js +787 -0
- package/lib/providers/grok.d.ts +7 -2
- package/lib/providers/grok.js +53 -24
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.js +2 -1
- package/lib/tools/x-search.js +2 -1
- package/lib/translate/anthropic.d.ts +47 -6
- package/lib/translate/anthropic.js +135 -20
- package/lib/translate/antigravity.d.ts +110 -0
- package/lib/translate/antigravity.js +303 -0
- package/lib/translate/chat-completions.d.ts +120 -0
- package/lib/translate/chat-completions.js +363 -0
- package/lib/translate/responses.d.ts +49 -5
- package/lib/translate/responses.js +40 -7
- package/package.json +11 -7
package/lib/providers/claude.js
CHANGED
|
@@ -6,16 +6,17 @@
|
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
7
|
import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId, resolveRetryPolicy } from '@deepseek-ai/dsh-llm';
|
|
8
8
|
import { resolveImages } from '../translate/resolved.js';
|
|
9
|
-
import { streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.js';
|
|
10
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
|
|
9
|
+
import { markMessageCache, streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.js';
|
|
10
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
|
|
11
|
+
import { proxiedFetch } from '../http.js';
|
|
11
12
|
export const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
12
|
-
export const CLAUDE_AUTHORIZE_URL = 'https://claude.
|
|
13
|
+
export const CLAUDE_AUTHORIZE_URL = 'https://claude.ai/oauth/authorize';
|
|
13
14
|
export const CLAUDE_TOKEN_URL = 'https://claude.ai/v1/oauth/token';
|
|
14
15
|
export const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages?beta=true';
|
|
15
16
|
export const CLAUDE_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
|
|
16
17
|
export const CLAUDE_MODELS_URL = 'https://api.anthropic.com/v1/models?beta=true';
|
|
17
|
-
const CLAUDE_SCOPE = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
|
|
18
|
-
const CLAUDE_CALLBACK_PATH = '/callback';
|
|
18
|
+
export const CLAUDE_SCOPE = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
|
|
19
|
+
export const CLAUDE_CALLBACK_PATH = '/callback';
|
|
19
20
|
const CLAUDE_CONTEXT_WINDOW = 200_000;
|
|
20
21
|
const CLAUDE_DEFAULT_MAX_TOKENS = 32_000;
|
|
21
22
|
/** Refresh when the access token has less than this much life left. */
|
|
@@ -61,13 +62,13 @@ const CLAUDE_BETA_FLAGS = CLAUDE_BETA_FALLBACK;
|
|
|
61
62
|
export const claudeFlow = {
|
|
62
63
|
callbackPath: CLAUDE_CALLBACK_PATH,
|
|
63
64
|
// The redirect URI embeds the port, so it must be an ephemeral one.
|
|
64
|
-
listen: { host: '
|
|
65
|
+
listen: { host: 'localhost', ports: [0] },
|
|
65
66
|
buildAuthorizeUrl({ redirectUri, state, pkce }) {
|
|
66
67
|
const params = new URLSearchParams({
|
|
67
68
|
code: 'true',
|
|
68
69
|
client_id: CLAUDE_CLIENT_ID,
|
|
69
70
|
response_type: 'code',
|
|
70
|
-
redirect_uri:
|
|
71
|
+
redirect_uri: redirectUri,
|
|
71
72
|
scope: CLAUDE_SCOPE,
|
|
72
73
|
code_challenge: pkce.challenge,
|
|
73
74
|
code_challenge_method: 'S256',
|
|
@@ -79,7 +80,7 @@ export const claudeFlow = {
|
|
|
79
80
|
/** Best-effort account profile; login must not fail when this does. */
|
|
80
81
|
async function fetchClaudeProfile(accessToken) {
|
|
81
82
|
try {
|
|
82
|
-
const response = await
|
|
83
|
+
const response = await proxiedFetch(CLAUDE_PROFILE_URL, {
|
|
83
84
|
headers: { authorization: `Bearer ${accessToken}` },
|
|
84
85
|
});
|
|
85
86
|
if (!response.ok)
|
|
@@ -129,7 +130,7 @@ async function claudeSession(tokens, fallbackRefreshToken, withProfile) {
|
|
|
129
130
|
* @returns the session to store.
|
|
130
131
|
*/
|
|
131
132
|
export async function exchangeClaudeCode(code, verifier, redirectUri, state) {
|
|
132
|
-
const response = await
|
|
133
|
+
const response = await proxiedFetch(CLAUDE_TOKEN_URL, {
|
|
133
134
|
method: 'POST',
|
|
134
135
|
headers: { 'content-type': 'application/json' },
|
|
135
136
|
body: JSON.stringify({
|
|
@@ -151,7 +152,7 @@ export async function exchangeClaudeCode(code, verifier, redirectUri, state) {
|
|
|
151
152
|
* @returns the fresh session to store.
|
|
152
153
|
*/
|
|
153
154
|
export async function refreshClaude(session) {
|
|
154
|
-
const response = await
|
|
155
|
+
const response = await proxiedFetch(CLAUDE_TOKEN_URL, {
|
|
155
156
|
method: 'POST',
|
|
156
157
|
headers: { 'content-type': 'application/json' },
|
|
157
158
|
body: JSON.stringify({
|
|
@@ -237,7 +238,7 @@ function claudeLimitsWindows(value) {
|
|
|
237
238
|
* @param signal - caller cancellation from the RPC transport.
|
|
238
239
|
* @returns the mapped usage snapshot.
|
|
239
240
|
*/
|
|
240
|
-
export async function fetchClaudeUsage(session, fetchFn =
|
|
241
|
+
export async function fetchClaudeUsage(session, fetchFn = proxiedFetch, signal) {
|
|
241
242
|
const response = await fetchFn(CLAUDE_USAGE_URL, {
|
|
242
243
|
headers: {
|
|
243
244
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -288,7 +289,7 @@ function claudeReasoning(capabilities) {
|
|
|
288
289
|
return efforts.length > 0 ? { efforts } : undefined;
|
|
289
290
|
}
|
|
290
291
|
/** Fetch the live model catalog from the subscription endpoint. */
|
|
291
|
-
export async function fetchClaudeModels(session, fetchFn =
|
|
292
|
+
export async function fetchClaudeModels(session, fetchFn = proxiedFetch) {
|
|
292
293
|
const response = await fetchFn(CLAUDE_MODELS_URL, {
|
|
293
294
|
headers: {
|
|
294
295
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -331,6 +332,38 @@ const CLAUDE_RETRY_MAX_DELAY_MS = 60_000;
|
|
|
331
332
|
const CLAUDE_RETRY_JITTER_RATIO = 0.2;
|
|
332
333
|
/** The Claude 4.5 family accepts image input. */
|
|
333
334
|
const CLAUDE_MODALITIES = ['text', 'image'];
|
|
335
|
+
/**
|
|
336
|
+
* Assemble the Anthropic request body.
|
|
337
|
+
*
|
|
338
|
+
* Extracted from the adapter so the wire shape — cache breakpoints above all —
|
|
339
|
+
* is testable without a network round trip. The message array is marked before
|
|
340
|
+
* it is placed so the breakpoints land on the blocks the body ships: one on the
|
|
341
|
+
* last `system` block (covering `tools` + `system`, which render ahead of it)
|
|
342
|
+
* and up to three across the history, Anthropic's four-slot maximum.
|
|
343
|
+
* @param options - the generate request.
|
|
344
|
+
* @param messages - conversation messages with images already resolved.
|
|
345
|
+
* @param maxTokens - the resolved output cap.
|
|
346
|
+
* @param thinking - the thinking parameter, when the model takes one.
|
|
347
|
+
* @param effort - the reasoning effort, when the model advertises efforts.
|
|
348
|
+
* @returns the JSON body to POST.
|
|
349
|
+
*/
|
|
350
|
+
export function claudeRequestBody(options, messages, maxTokens, thinking, effort) {
|
|
351
|
+
const anthropicMessages = toAnthropicMessages(messages);
|
|
352
|
+
markMessageCache(anthropicMessages);
|
|
353
|
+
return {
|
|
354
|
+
model: options.model,
|
|
355
|
+
max_tokens: maxTokens,
|
|
356
|
+
system: toAnthropicSystem(options.system, messages),
|
|
357
|
+
messages: anthropicMessages,
|
|
358
|
+
...options.tools !== undefined && options.tools.length > 0
|
|
359
|
+
? { tools: toAnthropicTools(options.tools) }
|
|
360
|
+
: {},
|
|
361
|
+
...thinking === undefined ? {} : { thinking },
|
|
362
|
+
...effort === undefined ? {} : { output_config: { effort } },
|
|
363
|
+
stream: true,
|
|
364
|
+
...options.sessionId !== undefined ? { metadata: { user_id: String(options.sessionId) } } : {},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
334
367
|
/** Claude wire adapter: one instance serves the `claude` provider route. */
|
|
335
368
|
export class ClaudeAdapter extends LlmAdapter {
|
|
336
369
|
options;
|
|
@@ -379,7 +412,7 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
379
412
|
if (!this.options.discovery)
|
|
380
413
|
return this.staticModels(provider);
|
|
381
414
|
try {
|
|
382
|
-
const models = await this.catalog.get(() => this.fetchCatalog());
|
|
415
|
+
const models = await discoverOrRetryAuth(force => this.options.tokens.session(force), this.catalog, () => this.catalog.get(() => this.fetchCatalog()));
|
|
383
416
|
return models.map(model => ({
|
|
384
417
|
provider,
|
|
385
418
|
id: model.id,
|
|
@@ -388,11 +421,8 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
388
421
|
}));
|
|
389
422
|
}
|
|
390
423
|
catch (error) {
|
|
391
|
-
if (error
|
|
392
|
-
&& (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL'))
|
|
424
|
+
if (isMissingOrInvalidCredential(error))
|
|
393
425
|
return [];
|
|
394
|
-
if (error instanceof LlmError && error.code === 'AUTH')
|
|
395
|
-
this.catalog.invalidate();
|
|
396
426
|
this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
397
427
|
return this.staticModels(provider);
|
|
398
428
|
}
|
|
@@ -462,22 +492,10 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
462
492
|
const disc = await this.discovered(options.model);
|
|
463
493
|
const thinking = this.thinkingParam(disc?.thinkingType, maxTokens);
|
|
464
494
|
const effort = options.reasoningEffort !== undefined && disc?.reasoning !== undefined
|
|
465
|
-
?
|
|
466
|
-
:
|
|
467
|
-
const body =
|
|
468
|
-
|
|
469
|
-
max_tokens: maxTokens,
|
|
470
|
-
system: toAnthropicSystem(options.system, messages),
|
|
471
|
-
messages: toAnthropicMessages(messages),
|
|
472
|
-
...options.tools !== undefined && options.tools.length > 0
|
|
473
|
-
? { tools: toAnthropicTools(options.tools) }
|
|
474
|
-
: {},
|
|
475
|
-
...thinking === undefined ? {} : { thinking },
|
|
476
|
-
...effort,
|
|
477
|
-
stream: true,
|
|
478
|
-
...options.sessionId !== undefined ? { metadata: { user_id: String(options.sessionId) } } : {},
|
|
479
|
-
};
|
|
480
|
-
return fetch(CLAUDE_API_URL, {
|
|
495
|
+
? String(options.reasoningEffort)
|
|
496
|
+
: undefined;
|
|
497
|
+
const body = claudeRequestBody(options, messages, maxTokens, thinking, effort);
|
|
498
|
+
return proxiedFetch(CLAUDE_API_URL, {
|
|
481
499
|
method: 'POST',
|
|
482
500
|
headers: {
|
|
483
501
|
'authorization': `Bearer ${session.accessToken}`,
|
package/lib/providers/codex.js
CHANGED
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* Codex CLI client id, and streaming against the ChatGPT backend Responses
|
|
4
4
|
* endpoint.
|
|
5
5
|
*/
|
|
6
|
-
import { randomUUID } from 'node:crypto';
|
|
6
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
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
10
|
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
11
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
|
|
11
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, TokenManager, } from './common.js';
|
|
12
|
+
import { proxiedFetch } from '../http.js';
|
|
12
13
|
export const CODEX_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
13
14
|
export const CODEX_AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
|
|
14
15
|
export const CODEX_TOKEN_URL = 'https://auth.openai.com/oauth/token';
|
|
@@ -152,7 +153,7 @@ function codexSession(tokens, fallback) {
|
|
|
152
153
|
* @returns the session to store.
|
|
153
154
|
*/
|
|
154
155
|
export async function exchangeCodexCode(code, verifier, redirectUri) {
|
|
155
|
-
const response = await
|
|
156
|
+
const response = await proxiedFetch(CODEX_TOKEN_URL, {
|
|
156
157
|
method: 'POST',
|
|
157
158
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
158
159
|
body: new URLSearchParams({
|
|
@@ -173,7 +174,7 @@ export async function exchangeCodexCode(code, verifier, redirectUri) {
|
|
|
173
174
|
* @returns the fresh session to store.
|
|
174
175
|
*/
|
|
175
176
|
export async function refreshCodex(session) {
|
|
176
|
-
const response = await
|
|
177
|
+
const response = await proxiedFetch(CODEX_TOKEN_URL, {
|
|
177
178
|
method: 'POST',
|
|
178
179
|
headers: { 'content-type': 'application/json' },
|
|
179
180
|
body: JSON.stringify({
|
|
@@ -253,7 +254,7 @@ function codexUsageWindow(value, fallbackKind) {
|
|
|
253
254
|
* @param signal - caller cancellation from the RPC transport.
|
|
254
255
|
* @returns the mapped usage snapshot.
|
|
255
256
|
*/
|
|
256
|
-
export async function fetchCodexUsage(session, fetchFn =
|
|
257
|
+
export async function fetchCodexUsage(session, fetchFn = proxiedFetch, signal) {
|
|
257
258
|
const response = await fetchFn(CODEX_USAGE_URL, {
|
|
258
259
|
headers: {
|
|
259
260
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -309,7 +310,7 @@ function supportsFastTier(entry) {
|
|
|
309
310
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
310
311
|
* @returns discovered models: hidden entries dropped, sorted by priority.
|
|
311
312
|
*/
|
|
312
|
-
export async function fetchCodexModels(session, fetchFn =
|
|
313
|
+
export async function fetchCodexModels(session, fetchFn = proxiedFetch) {
|
|
313
314
|
const url = `${CODEX_MODELS_URL}?client_version=${CODEX_CLIENT_VERSION}`;
|
|
314
315
|
const response = await fetchFn(url, {
|
|
315
316
|
headers: {
|
|
@@ -373,6 +374,53 @@ export async function fetchCodexModels(session, fetchFn = fetch) {
|
|
|
373
374
|
}
|
|
374
375
|
return discovered;
|
|
375
376
|
}
|
|
377
|
+
const CODEX_CALL_ID_MAX_LENGTH = 64;
|
|
378
|
+
const CODEX_CALL_ID_PREFIX = 'call_';
|
|
379
|
+
/**
|
|
380
|
+
* Bound tool-call ids at the Codex wire boundary without changing the shared
|
|
381
|
+
* Responses translation used by Grok. Short ids stay verbatim. Oversized ids
|
|
382
|
+
* become deterministic hashes, and every id already present in this request
|
|
383
|
+
* is reserved first so a generated id cannot collide with a legitimate short
|
|
384
|
+
* one (or another oversized id).
|
|
385
|
+
*/
|
|
386
|
+
function normalizeCodexCallIds(input) {
|
|
387
|
+
const mapping = new Map();
|
|
388
|
+
const used = new Set();
|
|
389
|
+
const callId = (item) => (item.type === 'function_call' || item.type === 'function_call_output') && typeof item.call_id === 'string'
|
|
390
|
+
? item.call_id
|
|
391
|
+
: undefined;
|
|
392
|
+
for (const item of input) {
|
|
393
|
+
const id = callId(item);
|
|
394
|
+
if (id !== undefined && id.length <= CODEX_CALL_ID_MAX_LENGTH) {
|
|
395
|
+
mapping.set(id, id);
|
|
396
|
+
used.add(id);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
for (const item of input) {
|
|
400
|
+
const id = callId(item);
|
|
401
|
+
if (id === undefined || mapping.has(id))
|
|
402
|
+
continue;
|
|
403
|
+
let attempt = 0;
|
|
404
|
+
let normalized;
|
|
405
|
+
do {
|
|
406
|
+
const hash = createHash('sha256');
|
|
407
|
+
if (attempt > 0)
|
|
408
|
+
hash.update(String(attempt)).update('\0');
|
|
409
|
+
const digest = hash.update(id).digest('hex');
|
|
410
|
+
normalized = `${CODEX_CALL_ID_PREFIX}${digest.slice(0, CODEX_CALL_ID_MAX_LENGTH - CODEX_CALL_ID_PREFIX.length)}`;
|
|
411
|
+
attempt += 1;
|
|
412
|
+
} while (used.has(normalized));
|
|
413
|
+
mapping.set(id, normalized);
|
|
414
|
+
used.add(normalized);
|
|
415
|
+
}
|
|
416
|
+
return input.map((item) => {
|
|
417
|
+
const id = callId(item);
|
|
418
|
+
if (id === undefined)
|
|
419
|
+
return item;
|
|
420
|
+
const normalized = mapping.get(id) ?? id;
|
|
421
|
+
return normalized === id ? item : { ...item, call_id: normalized };
|
|
422
|
+
});
|
|
423
|
+
}
|
|
376
424
|
/**
|
|
377
425
|
* The Responses request body for one generation. A fast-tier request (the
|
|
378
426
|
* composer Speed toggle, the codex CLI's fast mode) carries
|
|
@@ -383,7 +431,7 @@ export function codexRequestBody(options, resolved, fast) {
|
|
|
383
431
|
return {
|
|
384
432
|
model: options.model,
|
|
385
433
|
instructions: resolved.instructions ?? DEFAULT_CODEX_INSTRUCTIONS,
|
|
386
|
-
input: resolved.input,
|
|
434
|
+
input: normalizeCodexCallIds(resolved.input),
|
|
387
435
|
...options.tools !== undefined && options.tools.length > 0
|
|
388
436
|
? { tools: toResponsesTools(options.tools) }
|
|
389
437
|
: {},
|
|
@@ -434,7 +482,7 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
434
482
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
435
483
|
// through the refresh-aware path so an expired access token renews here
|
|
436
484
|
// instead of failing discovery into the static fallback.
|
|
437
|
-
const discovered = await this.catalog.get(() => this.fetchCatalog());
|
|
485
|
+
const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(force), this.catalog, () => this.catalog.get(() => this.fetchCatalog()));
|
|
438
486
|
return discovered.map(model => ({
|
|
439
487
|
provider,
|
|
440
488
|
id: model.id,
|
|
@@ -446,11 +494,8 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
446
494
|
catch (error) {
|
|
447
495
|
// A permanent refresh failure deletes the stored session: the provider
|
|
448
496
|
// is logged out, so hide it instead of showing a stale static catalog.
|
|
449
|
-
if (error
|
|
450
|
-
&& (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL'))
|
|
497
|
+
if (isMissingOrInvalidCredential(error))
|
|
451
498
|
return [];
|
|
452
|
-
if (error instanceof OAuthEndpointError && error.status === 401)
|
|
453
|
-
this.catalog.invalidate();
|
|
454
499
|
this.options.onWarn?.(`codex model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
455
500
|
return this.staticModels(provider);
|
|
456
501
|
}
|
|
@@ -529,7 +574,7 @@ export class CodexAdapter extends LlmAdapter {
|
|
|
529
574
|
const fast = this.options.speedFor !== undefined
|
|
530
575
|
&& await this.options.speedFor(options.sessionId, options.model);
|
|
531
576
|
const body = codexRequestBody(options, toResponsesInput(messages, options.system), fast);
|
|
532
|
-
return
|
|
577
|
+
return proxiedFetch(CODEX_API_URL, {
|
|
533
578
|
method: 'POST',
|
|
534
579
|
headers: {
|
|
535
580
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -19,6 +19,12 @@ export interface ModelEntry {
|
|
|
19
19
|
maxTokens?: number;
|
|
20
20
|
/** Accepted request modalities; when set, wins over the provider default. */
|
|
21
21
|
inputModalities?: ('text' | 'image')[];
|
|
22
|
+
/**
|
|
23
|
+
* Force this model's upstream protocol. Only the copilot adapter consumes
|
|
24
|
+
* the semantics; the union is inlined here to avoid a circular import of
|
|
25
|
+
* the copilot module's `CopilotWire`.
|
|
26
|
+
*/
|
|
27
|
+
wire?: 'chat-completions' | 'responses';
|
|
22
28
|
}
|
|
23
29
|
/**
|
|
24
30
|
* Validate a configured model catalog (mirrors llm-deepseek's resolveModels).
|
|
@@ -175,10 +181,20 @@ export interface DiscoveredModel {
|
|
|
175
181
|
}[];
|
|
176
182
|
defaultEffort?: ReasoningEffortId;
|
|
177
183
|
};
|
|
184
|
+
/** Accepted request modalities the endpoint advertised (e.g. Copilot's vision support flag). */
|
|
185
|
+
inputModalities?: ('text' | 'image')[];
|
|
178
186
|
/** Claude-specific: which extended-thinking wire shape this model accepts. */
|
|
179
187
|
thinkingType?: 'enabled' | 'adaptive';
|
|
180
188
|
/** Codex-specific: the catalog advertises a fast (priority) service tier. */
|
|
181
189
|
fastTier?: boolean;
|
|
190
|
+
/** Copilot-specific: which upstream protocol the model's endpoints speak. */
|
|
191
|
+
copilotWire?: 'chat-completions' | 'responses';
|
|
192
|
+
/**
|
|
193
|
+
* Copilot-specific: the catalog also lists `/responses` for this model
|
|
194
|
+
* (dual-protocol entries, e.g. gpt-5.4), so a chat-wire request may reroute
|
|
195
|
+
* there when it combines function tools with a reasoning effort.
|
|
196
|
+
*/
|
|
197
|
+
copilotResponses?: boolean;
|
|
182
198
|
}
|
|
183
199
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
184
200
|
export declare const DISCOVERY_TTL_MS: number;
|
|
@@ -207,7 +223,7 @@ export interface CatalogPersistence {
|
|
|
207
223
|
* while a stale entry refreshes in the background, and only awaits the fetch
|
|
208
224
|
* when nothing is known yet. An optional {@link CatalogPersistence} seeds the
|
|
209
225
|
* last-known state across restarts and receives every successful fetch. A 401
|
|
210
|
-
*
|
|
226
|
+
* that still fails after a forced token refresh must call {@link invalidate}.
|
|
211
227
|
*/
|
|
212
228
|
export declare class ModelCatalogCache {
|
|
213
229
|
private readonly persistence?;
|
|
@@ -224,6 +240,12 @@ export declare class ModelCatalogCache {
|
|
|
224
240
|
* @returns the cached models, or `undefined` when absent or stale.
|
|
225
241
|
*/
|
|
226
242
|
cached(): readonly DiscoveredModel[] | undefined;
|
|
243
|
+
/**
|
|
244
|
+
* The last successfully fetched catalog, ignoring TTL. Used to carry
|
|
245
|
+
* capability metadata forward when a later fetch cannot re-enrich.
|
|
246
|
+
* @returns the last-known models, or `undefined` when nothing has been stored.
|
|
247
|
+
*/
|
|
248
|
+
lastKnown(): readonly DiscoveredModel[] | undefined;
|
|
227
249
|
/** Load the persisted snapshot once; a fetch or invalidate that landed first wins. */
|
|
228
250
|
private ensureSeeded;
|
|
229
251
|
/** Run (or join) the single in-flight fetch, updating memory and disk on success. */
|
|
@@ -248,4 +270,13 @@ export declare class ModelCatalogCache {
|
|
|
248
270
|
/** Drop the cached catalog (e.g. after a 401 proved the credential changed). */
|
|
249
271
|
invalidate(): void;
|
|
250
272
|
}
|
|
273
|
+
/** Whether discovery failed because the stored login is gone. */
|
|
274
|
+
export declare function isMissingOrInvalidCredential(error: unknown): boolean;
|
|
275
|
+
/**
|
|
276
|
+
* Run a catalog fetch, retrying once after a forced token refresh when the
|
|
277
|
+
* first attempt is a 401/AUTH. Only {@link ModelCatalogCache.invalidate}s
|
|
278
|
+
* when the retry is also an auth failure, so a refresh race cannot erase
|
|
279
|
+
* last-known capability metadata.
|
|
280
|
+
*/
|
|
281
|
+
export declare function discoverOrRetryAuth<T>(session: (forceRefresh?: boolean) => Promise<unknown>, catalog: ModelCatalogCache, run: () => Promise<T>): Promise<T>;
|
|
251
282
|
export {};
|
package/lib/providers/common.js
CHANGED
|
@@ -32,6 +32,9 @@ export function validateModels(models, label) {
|
|
|
32
32
|
|| model.inputModalities.some(modality => modality !== 'text' && modality !== 'image'))) {
|
|
33
33
|
throw new Error(`${label}: catalog model "${model.id}" inputModalities must be a non-empty list of "text"/"image"`);
|
|
34
34
|
}
|
|
35
|
+
if (model.wire !== undefined && model.wire !== 'chat-completions' && model.wire !== 'responses') {
|
|
36
|
+
throw new Error(`${label}: catalog model "${model.id}" wire must be "chat-completions" or "responses"`);
|
|
37
|
+
}
|
|
35
38
|
if (seen.has(model.id))
|
|
36
39
|
throw new Error(`${label}: duplicate catalog model "${model.id}"`);
|
|
37
40
|
seen.add(model.id);
|
|
@@ -41,6 +44,7 @@ export function validateModels(models, label) {
|
|
|
41
44
|
...model.contextWindow === undefined ? {} : { contextWindow: model.contextWindow },
|
|
42
45
|
...model.maxTokens === undefined ? {} : { maxTokens: model.maxTokens },
|
|
43
46
|
...model.inputModalities === undefined ? {} : { inputModalities: [...model.inputModalities] },
|
|
47
|
+
...model.wire === undefined ? {} : { wire: model.wire },
|
|
44
48
|
};
|
|
45
49
|
});
|
|
46
50
|
}
|
|
@@ -271,7 +275,7 @@ export const DISCOVERY_TTL_MS = 5 * 60_000;
|
|
|
271
275
|
* while a stale entry refreshes in the background, and only awaits the fetch
|
|
272
276
|
* when nothing is known yet. An optional {@link CatalogPersistence} seeds the
|
|
273
277
|
* last-known state across restarts and receives every successful fetch. A 401
|
|
274
|
-
*
|
|
278
|
+
* that still fails after a forced token refresh must call {@link invalidate}.
|
|
275
279
|
*/
|
|
276
280
|
export class ModelCatalogCache {
|
|
277
281
|
persistence;
|
|
@@ -295,6 +299,14 @@ export class ModelCatalogCache {
|
|
|
295
299
|
return undefined;
|
|
296
300
|
return this.entry.models;
|
|
297
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* The last successfully fetched catalog, ignoring TTL. Used to carry
|
|
304
|
+
* capability metadata forward when a later fetch cannot re-enrich.
|
|
305
|
+
* @returns the last-known models, or `undefined` when nothing has been stored.
|
|
306
|
+
*/
|
|
307
|
+
lastKnown() {
|
|
308
|
+
return this.entry?.models;
|
|
309
|
+
}
|
|
298
310
|
/** Load the persisted snapshot once; a fetch or invalidate that landed first wins. */
|
|
299
311
|
ensureSeeded() {
|
|
300
312
|
if (this.persistence === undefined)
|
|
@@ -363,3 +375,38 @@ export class ModelCatalogCache {
|
|
|
363
375
|
void this.persistence?.clear().catch(() => undefined);
|
|
364
376
|
}
|
|
365
377
|
}
|
|
378
|
+
/** Whether discovery failed because the stored login is gone. */
|
|
379
|
+
export function isMissingOrInvalidCredential(error) {
|
|
380
|
+
return error instanceof LlmError
|
|
381
|
+
&& (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL');
|
|
382
|
+
}
|
|
383
|
+
/** Whether discovery failed because the access token was rejected. */
|
|
384
|
+
function isDiscoveryAuthFailure(error) {
|
|
385
|
+
return (error instanceof OAuthEndpointError && error.status === 401)
|
|
386
|
+
|| (error instanceof LlmError && error.code === 'AUTH');
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Run a catalog fetch, retrying once after a forced token refresh when the
|
|
390
|
+
* first attempt is a 401/AUTH. Only {@link ModelCatalogCache.invalidate}s
|
|
391
|
+
* when the retry is also an auth failure, so a refresh race cannot erase
|
|
392
|
+
* last-known capability metadata.
|
|
393
|
+
*/
|
|
394
|
+
export async function discoverOrRetryAuth(session, catalog, run) {
|
|
395
|
+
try {
|
|
396
|
+
return await run();
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
if (isMissingOrInvalidCredential(error) || !isDiscoveryAuthFailure(error))
|
|
400
|
+
throw error;
|
|
401
|
+
try {
|
|
402
|
+
await session(true);
|
|
403
|
+
return await run();
|
|
404
|
+
}
|
|
405
|
+
catch (retryError) {
|
|
406
|
+
if (!isMissingOrInvalidCredential(retryError) && isDiscoveryAuthFailure(retryError)) {
|
|
407
|
+
catalog.invalidate();
|
|
408
|
+
}
|
|
409
|
+
throw retryError;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|