dsh-subscriptions 0.1.3 → 0.1.5
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 +64 -0
- package/lib/auth/device-flow.js +6 -3
- package/lib/auth/oauth-flow.js +41 -8
- package/lib/auth/rpc.d.ts +3 -1
- package/lib/auth/rpc.js +10 -1
- package/lib/client/ProviderAccountManager.js +34 -41
- package/lib/client/ProviderModelEditor.js +5 -9
- package/lib/client/SpeedSelect.js +8 -7
- package/lib/client/SubscriptionUsageBadge.js +6 -5
- package/lib/client/SubscriptionsSection.d.ts +21 -73
- package/lib/client/SubscriptionsSection.js +130 -104
- package/lib/client/UsageBadgeDisplaySetting.js +4 -8
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +22 -0
- package/lib/client/locales.d.ts +58 -0
- package/lib/client/locales.js +58 -0
- package/lib/client/provider-settings-styles.d.ts +3 -0
- package/lib/client/provider-settings-styles.js +206 -0
- package/lib/client/reset-action.d.ts +14 -0
- package/lib/client/reset-action.js +15 -0
- package/lib/client/reset-credits-view.d.ts +53 -0
- package/lib/client/reset-credits-view.js +103 -0
- package/lib/client.js +937 -310
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +9 -0
- package/lib/index.js +6058 -5298
- package/lib/model-defaults.d.ts +4 -5
- package/lib/model-defaults.js +18 -14
- package/lib/providers/account-preferences.js +3 -2
- package/lib/providers/antigravity.d.ts +0 -2
- package/lib/providers/antigravity.js +5 -9
- package/lib/providers/catalog-store.d.ts +3 -2
- package/lib/providers/catalog-store.js +57 -26
- package/lib/providers/claude.d.ts +22 -3
- package/lib/providers/claude.js +118 -20
- package/lib/providers/codex-search.js +5 -2
- package/lib/providers/codex.d.ts +15 -2
- package/lib/providers/codex.js +77 -9
- package/lib/providers/common.d.ts +38 -0
- package/lib/providers/common.js +44 -18
- package/lib/providers/copilot.d.ts +2 -1
- package/lib/providers/copilot.js +13 -6
- package/lib/providers/cursor-adapter.d.ts +35 -0
- package/lib/providers/cursor-adapter.js +245 -4
- package/lib/providers/cursor-auth.js +11 -4
- package/lib/providers/cursor-usage.js +2 -1
- package/lib/providers/external-usage.js +2 -1
- package/lib/providers/grok.d.ts +2 -2
- package/lib/providers/grok.js +16 -11
- package/lib/providers/minimax-usage.js +2 -1
- package/lib/providers/pool-health.d.ts +2 -0
- package/lib/providers/pool-health.js +9 -0
- package/lib/providers/pool-usage.d.ts +13 -1
- package/lib/providers/pool-usage.js +39 -7
- package/lib/providers/pool.d.ts +7 -5
- package/lib/providers/pool.js +89 -12
- package/lib/providers/rate-limit.d.ts +0 -14
- package/lib/providers/rate-limit.js +0 -26
- package/lib/providers/replay.d.ts +4 -0
- package/lib/providers/replay.js +50 -0
- package/lib/providers/reset-redemption.d.ts +51 -0
- package/lib/providers/reset-redemption.js +102 -0
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.d.ts +0 -5
- package/lib/tools/video-generate.js +25 -14
- package/lib/tools/x-search.js +2 -2
- package/lib/translate/anthropic.d.ts +21 -2
- package/lib/translate/anthropic.js +35 -11
- package/lib/translate/antigravity.js +13 -6
- package/lib/translate/chat-completions.js +1 -1
- package/lib/translate/resolved.d.ts +24 -0
- package/lib/translate/resolved.js +66 -3
- package/lib/translate/responses.js +8 -6
- package/lib/translate/sse.js +8 -0
- package/package.json +1 -1
|
@@ -86,8 +86,11 @@ export class CodexWebSearchProvider {
|
|
|
86
86
|
try {
|
|
87
87
|
return JSON.parse(text);
|
|
88
88
|
}
|
|
89
|
-
catch
|
|
90
|
-
|
|
89
|
+
catch {
|
|
90
|
+
// No cause: the host expands causes recursively, and Node's parse error
|
|
91
|
+
// quotes a bounded excerpt of the body, so attaching it would relocate
|
|
92
|
+
// the leak rather than remove it.
|
|
93
|
+
throw new WebError('Codex Web Search returned invalid JSON', 'CODEX_SEARCH_RESPONSE');
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
throw new WebError('Codex Web Search exhausted its retry policy', 'CODEX_SEARCH_UPSTREAM');
|
package/lib/providers/codex.d.ts
CHANGED
|
@@ -26,8 +26,8 @@ export declare const CODEX_PREEMPT_MS: number;
|
|
|
26
26
|
* rollover snapshots the backend attaches to every response, one per window,
|
|
27
27
|
* so they say nothing about which window refused: a burst 429 that would clear
|
|
28
28
|
* in seconds still carries a primary rollover hours out, and reading it would
|
|
29
|
-
* park the turn for those hours.
|
|
30
|
-
*
|
|
29
|
+
* park the turn for those hours. The parsed body reset supplies the retry hint;
|
|
30
|
+
* diagnostics omit raw provider headers and body text.
|
|
31
31
|
*/
|
|
32
32
|
export declare const codexRateLimitReset: RateLimitResetReader;
|
|
33
33
|
/**
|
|
@@ -77,6 +77,11 @@ export declare function refreshCodex(session: CodexSession): Promise<CodexSessio
|
|
|
77
77
|
*/
|
|
78
78
|
export declare function isCodexPermanentRefreshError(error: unknown): boolean;
|
|
79
79
|
export declare const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
80
|
+
export declare const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
81
|
+
/** Read banked resets without spending them; consumption uses a separate explicit POST. */
|
|
82
|
+
export declare function fetchCodexResetCredits(session: CodexSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<NonNullable<ProviderUsage['resetCredits']>>;
|
|
83
|
+
/** Submit one explicit reset; ambiguous or unconfirmed responses throw without retry. */
|
|
84
|
+
export declare function consumeCodexResetCredit(session: CodexSession, creditId: string, requestId: string, fetchFn?: FetchFn, signal?: AbortSignal): Promise<void>;
|
|
80
85
|
/**
|
|
81
86
|
* Fetch the codex subscription usage from the ChatGPT backend wham/usage
|
|
82
87
|
* endpoint (the source of the codex CLI `/status` rate-limit lines). The
|
|
@@ -164,6 +169,14 @@ export declare const CODEX_UNKNOWN_TOOL_OUTCOME = "The tool call has no recorded
|
|
|
164
169
|
* @returns the same array when already balanced, otherwise a repaired array.
|
|
165
170
|
*/
|
|
166
171
|
export declare function reconcileResponsesToolCalls(input: ResponsesRequestInput['input']): ResponsesRequestInput['input'];
|
|
172
|
+
/**
|
|
173
|
+
* The `reasoning.effort` wire value for a selected effort. Codex's "ultra" is
|
|
174
|
+
* a client-side selection (max reasoning plus proactive sub-agent delegation)
|
|
175
|
+
* that the Codex CLI itself lowers to `max` at the Responses API boundary;
|
|
176
|
+
* the backend answers HTTP 400 to a literal `ultra`. Discovery no longer
|
|
177
|
+
* lists it, but an effort saved before that change may still arrive here.
|
|
178
|
+
*/
|
|
179
|
+
export declare function codexWireEffort(effort: string): string;
|
|
167
180
|
/**
|
|
168
181
|
* The Responses request body for one generation. A fast-tier request (the
|
|
169
182
|
* composer Speed toggle, the codex CLI's fast mode) carries
|
package/lib/providers/codex.js
CHANGED
|
@@ -8,7 +8,7 @@ import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmErr
|
|
|
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 { deterministicSessionId, effortDisplayName, httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isDiscoveryAuthFailure, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
11
|
+
import { deterministicSessionId, effortDisplayName, httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isDiscoveryAuthFailure, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
12
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';
|
|
@@ -35,8 +35,8 @@ const CODEX_RESET_FIELDS = ['resets_in_seconds', 'reset_after_seconds', 'resets_
|
|
|
35
35
|
* rollover snapshots the backend attaches to every response, one per window,
|
|
36
36
|
* so they say nothing about which window refused: a burst 429 that would clear
|
|
37
37
|
* in seconds still carries a primary rollover hours out, and reading it would
|
|
38
|
-
* park the turn for those hours.
|
|
39
|
-
*
|
|
38
|
+
* park the turn for those hours. The parsed body reset supplies the retry hint;
|
|
39
|
+
* diagnostics omit raw provider headers and body text.
|
|
40
40
|
*/
|
|
41
41
|
export const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
|
|
42
42
|
/** Default instruction when the request carries no system prompt. */
|
|
@@ -185,7 +185,7 @@ export async function exchangeCodexCode(code, verifier, redirectUri) {
|
|
|
185
185
|
});
|
|
186
186
|
if (!response.ok)
|
|
187
187
|
throw await oauthEndpointError(response, 'codex');
|
|
188
|
-
return codexSession(await response
|
|
188
|
+
return codexSession(await parseProviderJson(response, 'codex token exchange'));
|
|
189
189
|
}
|
|
190
190
|
/**
|
|
191
191
|
* Refresh a codex session (JSON grant — unlike the code exchange).
|
|
@@ -204,7 +204,7 @@ export async function refreshCodex(session) {
|
|
|
204
204
|
});
|
|
205
205
|
if (!response.ok)
|
|
206
206
|
throw await oauthEndpointError(response, 'codex');
|
|
207
|
-
return codexSession(await response
|
|
207
|
+
return codexSession(await parseProviderJson(response, 'codex token refresh'), session);
|
|
208
208
|
}
|
|
209
209
|
/**
|
|
210
210
|
* Whether a codex refresh failure means the login is permanently gone.
|
|
@@ -217,6 +217,62 @@ export function isCodexPermanentRefreshError(error) {
|
|
|
217
217
|
&& PERMANENT_REFRESH_CODES.has(error.oauthCode);
|
|
218
218
|
}
|
|
219
219
|
export const CODEX_USAGE_URL = 'https://chatgpt.com/backend-api/wham/usage';
|
|
220
|
+
export const CODEX_RESET_CREDITS_URL = 'https://chatgpt.com/backend-api/wham/rate-limit-reset-credits';
|
|
221
|
+
function parseResetDate(value) {
|
|
222
|
+
if (typeof value !== 'string')
|
|
223
|
+
return undefined;
|
|
224
|
+
const timestamp = Date.parse(value);
|
|
225
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
226
|
+
}
|
|
227
|
+
/** Read banked resets without spending them; consumption uses a separate explicit POST. */
|
|
228
|
+
export async function fetchCodexResetCredits(session, fetchFn = hostFetch, signal) {
|
|
229
|
+
const response = await fetchFn(CODEX_RESET_CREDITS_URL, {
|
|
230
|
+
headers: {
|
|
231
|
+
authorization: `Bearer ${session.accessToken}`,
|
|
232
|
+
'chatgpt-account-id': session.accountId,
|
|
233
|
+
originator: 'Codex Desktop',
|
|
234
|
+
'openai-beta': 'codex-1',
|
|
235
|
+
accept: 'application/json',
|
|
236
|
+
...attributionHeaders(),
|
|
237
|
+
},
|
|
238
|
+
...signal === undefined ? {} : { signal },
|
|
239
|
+
});
|
|
240
|
+
if (!response.ok)
|
|
241
|
+
throw await oauthEndpointError(response, 'codex reset credits');
|
|
242
|
+
const payload = await parseProviderJson(response, 'codex reset credits');
|
|
243
|
+
if (!Array.isArray(payload?.credits))
|
|
244
|
+
throw new Error('codex reset credits: unexpected response (missing credits array)');
|
|
245
|
+
return payload.credits.flatMap((value) => {
|
|
246
|
+
if (typeof value !== 'object' || value === null)
|
|
247
|
+
return [];
|
|
248
|
+
const credit = value;
|
|
249
|
+
if (credit.status !== 'available' || credit.reset_type !== 'codex_rate_limits')
|
|
250
|
+
return [];
|
|
251
|
+
const grantedAt = parseResetDate(credit.granted_at);
|
|
252
|
+
const expiresAt = parseResetDate(credit.expires_at);
|
|
253
|
+
return [{ ...(typeof credit.id === 'string' ? { id: credit.id } : {}), ...(grantedAt === undefined ? {} : { grantedAt }), ...(expiresAt === undefined ? {} : { expiresAt }) }];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/** Submit one explicit reset; ambiguous or unconfirmed responses throw without retry. */
|
|
257
|
+
export async function consumeCodexResetCredit(session, creditId, requestId, fetchFn = hostFetch, signal) {
|
|
258
|
+
const response = await fetchFn(CODEX_RESET_CREDITS_URL + '/consume', {
|
|
259
|
+
method: 'POST',
|
|
260
|
+
headers: {
|
|
261
|
+
authorization: 'Bearer ' + session.accessToken,
|
|
262
|
+
'chatgpt-account-id': session.accountId,
|
|
263
|
+
originator: 'Codex Desktop', 'openai-beta': 'codex-1',
|
|
264
|
+
'content-type': 'application/json', accept: 'application/json',
|
|
265
|
+
...attributionHeaders(),
|
|
266
|
+
},
|
|
267
|
+
body: JSON.stringify({ redeem_request_id: requestId, credit_id: creditId }),
|
|
268
|
+
...signal === undefined ? {} : { signal },
|
|
269
|
+
});
|
|
270
|
+
if (!response.ok)
|
|
271
|
+
throw await oauthEndpointError(response, 'codex consume reset');
|
|
272
|
+
const payload = await parseProviderJson(response, 'codex consume reset');
|
|
273
|
+
if (payload?.code !== 'reset')
|
|
274
|
+
throw new Error('Reset was not confirmed. Verify the account in Codex before retrying.');
|
|
275
|
+
}
|
|
220
276
|
/** Seconds of the canonical 5-hour session and 7-day weekly windows. */
|
|
221
277
|
const SESSION_WINDOW_SECONDS = 5 * 60 * 60;
|
|
222
278
|
const WEEKLY_WINDOW_SECONDS = 7 * 24 * 60 * 60;
|
|
@@ -291,7 +347,7 @@ export async function fetchCodexUsage(session, fetchFn = hostFetch, signal) {
|
|
|
291
347
|
});
|
|
292
348
|
if (!response.ok)
|
|
293
349
|
throw await oauthEndpointError(response, 'codex usage');
|
|
294
|
-
const payload = await response
|
|
350
|
+
const payload = await parseProviderJson(response, 'codex usage');
|
|
295
351
|
const windows = [];
|
|
296
352
|
const primary = codexUsageWindow(payload.rate_limit?.primary_window, 'session');
|
|
297
353
|
const secondary = codexUsageWindow(payload.rate_limit?.secondary_window, 'weekly');
|
|
@@ -354,7 +410,7 @@ export async function fetchCodexModels(session, fetchFn = hostFetch, signal, cli
|
|
|
354
410
|
});
|
|
355
411
|
if (!response.ok)
|
|
356
412
|
throw await oauthEndpointError(response, 'codex models');
|
|
357
|
-
const payload = await response
|
|
413
|
+
const payload = await parseProviderJson(response, 'codex models');
|
|
358
414
|
if (!Array.isArray(payload.models))
|
|
359
415
|
throw new Error('codex models endpoint returned no models array');
|
|
360
416
|
const discovered = [];
|
|
@@ -365,8 +421,10 @@ export async function fetchCodexModels(session, fetchFn = hostFetch, signal, cli
|
|
|
365
421
|
// dropped, and an absent or unknown value is included (in doubt, include).
|
|
366
422
|
if (entry.visibility === 'hide' || entry.visibility === 'none')
|
|
367
423
|
continue;
|
|
424
|
+
// "ultra" is a Codex-client mode (max reasoning + automatic sub-agent
|
|
425
|
+
// delegation), not a wire value: the Responses API rejects it with HTTP 400.
|
|
368
426
|
const efforts = (entry.supported_reasoning_levels ?? [])
|
|
369
|
-
.filter(level => typeof level.effort === 'string' && level.effort.length > 0)
|
|
427
|
+
.filter(level => typeof level.effort === 'string' && level.effort.length > 0 && level.effort !== 'ultra')
|
|
370
428
|
.map(level => ({
|
|
371
429
|
id: ReasoningEffortId(level.effort),
|
|
372
430
|
name: effortDisplayName(level.effort),
|
|
@@ -491,6 +549,16 @@ export function reconcileResponsesToolCalls(input) {
|
|
|
491
549
|
}
|
|
492
550
|
return balanced;
|
|
493
551
|
}
|
|
552
|
+
/**
|
|
553
|
+
* The `reasoning.effort` wire value for a selected effort. Codex's "ultra" is
|
|
554
|
+
* a client-side selection (max reasoning plus proactive sub-agent delegation)
|
|
555
|
+
* that the Codex CLI itself lowers to `max` at the Responses API boundary;
|
|
556
|
+
* the backend answers HTTP 400 to a literal `ultra`. Discovery no longer
|
|
557
|
+
* lists it, but an effort saved before that change may still arrive here.
|
|
558
|
+
*/
|
|
559
|
+
export function codexWireEffort(effort) {
|
|
560
|
+
return effort === 'ultra' ? 'max' : effort;
|
|
561
|
+
}
|
|
494
562
|
/**
|
|
495
563
|
* The Responses request body for one generation. A fast-tier request (the
|
|
496
564
|
* composer Speed toggle, the codex CLI's fast mode) carries
|
|
@@ -509,7 +577,7 @@ export function codexRequestBody(options, resolved, fast) {
|
|
|
509
577
|
? { tools: toResponsesTools(options.tools, { strict: false }), tool_choice: 'auto', parallel_tool_calls: true }
|
|
510
578
|
: {},
|
|
511
579
|
...options.reasoningEffort !== undefined
|
|
512
|
-
? { reasoning: { effort: String(options.reasoningEffort), summary: 'auto' } }
|
|
580
|
+
? { reasoning: { effort: codexWireEffort(String(options.reasoningEffort)), summary: 'auto' } }
|
|
513
581
|
: {},
|
|
514
582
|
store: false,
|
|
515
583
|
stream: true,
|
|
@@ -33,6 +33,19 @@ export interface ModelEntry {
|
|
|
33
33
|
* @returns the validated entries.
|
|
34
34
|
*/
|
|
35
35
|
export declare function validateModels(models: readonly ModelEntry[], label: string): ModelEntry[];
|
|
36
|
+
/**
|
|
37
|
+
* Parse a provider response or stored JSON string without exposing input excerpts.
|
|
38
|
+
* Success returns the same JSON value; only SyntaxError is replaced, preserving
|
|
39
|
+
* parse-failure type checks and leaving body-read/abort failures unchanged.
|
|
40
|
+
* Node's Unexpected token errors include a bounded input excerpt. Do not retain
|
|
41
|
+
* the original error as cause: the host displays causes recursively.
|
|
42
|
+
* @param input - response to consume once, or JSON text to parse.
|
|
43
|
+
* @param origin - local endpoint/operation label; never response.url, a discovered
|
|
44
|
+
* URL, a credential, or any other provider-controlled text.
|
|
45
|
+
* @returns the parsed value, without validation or credential filtering.
|
|
46
|
+
* @throws SyntaxError with the local origin and a fixed omission marker.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseProviderJson<T = unknown>(input: Response | string, origin: string): Promise<T>;
|
|
36
49
|
/** Optional per-call hooks {@link httpLlmError} uses to read a rate-limit window. */
|
|
37
50
|
export interface HttpLlmErrorOptions {
|
|
38
51
|
/**
|
|
@@ -200,6 +213,27 @@ export interface UsageWindow {
|
|
|
200
213
|
/** Provider-verified fixed interval; permits deriving start from end and duration. */
|
|
201
214
|
fixedWindow?: boolean;
|
|
202
215
|
}
|
|
216
|
+
/** One banked reset credit (Codex), or one grant containing resets (Claude). */
|
|
217
|
+
export interface ResetCredit {
|
|
218
|
+
id?: string;
|
|
219
|
+
grantedAt?: number;
|
|
220
|
+
/** Expiration in epoch ms; never a recurring usage-window reset. */
|
|
221
|
+
expiresAt?: number;
|
|
222
|
+
/** Claude grant counts; one array entry per grant, not per remaining reset. */
|
|
223
|
+
resetsTotal?: number;
|
|
224
|
+
resetsLeft?: number;
|
|
225
|
+
/** Claude's disclosed grant state, absent when the server omits it. */
|
|
226
|
+
usableNow?: boolean;
|
|
227
|
+
paused?: boolean;
|
|
228
|
+
/**
|
|
229
|
+
* Claude server-selected grant is currently usable. Read-only metadata, not
|
|
230
|
+
* permission to redeem; no Claude redemption endpoint is implemented.
|
|
231
|
+
* Codex entries omit this field and retain their existing semantics.
|
|
232
|
+
*/
|
|
233
|
+
claimable?: boolean;
|
|
234
|
+
/** Claude program cooldown in epoch ms, repeated on each grant when disclosed. */
|
|
235
|
+
cooldownUntil?: number;
|
|
236
|
+
}
|
|
203
237
|
/** Subscription usage of one provider, as served by the `usage` RPC endpoint. */
|
|
204
238
|
export interface ProviderUsage {
|
|
205
239
|
/** Original successful observation time, preserved across cache reads. */
|
|
@@ -212,6 +246,10 @@ export interface ProviderUsage {
|
|
|
212
246
|
windows?: UsageWindow[];
|
|
213
247
|
/** Plan name the usage endpoint reported, when present. */
|
|
214
248
|
plan?: string;
|
|
249
|
+
/** Banked Codex resets or Claude grants, when the provider discloses them. */
|
|
250
|
+
resetCredits?: ResetCredit[];
|
|
251
|
+
/** Optional reset lookup failure; ordinary usage remains usable. */
|
|
252
|
+
resetCreditsError?: string;
|
|
215
253
|
}
|
|
216
254
|
/** One model discovered from a provider's live model-list endpoint. */
|
|
217
255
|
export interface DiscoveredModel {
|
package/lib/providers/common.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createHash, randomUUID } from 'node:crypto';
|
|
9
9
|
import { CONTEXT_WINDOW_EXCEEDED_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
10
|
-
import { durationMs,
|
|
10
|
+
import { durationMs, retryAfterInstant, waitFromReset } from './rate-limit.js';
|
|
11
11
|
/**
|
|
12
12
|
* Validate a configured model catalog (mirrors llm-deepseek's resolveModels).
|
|
13
13
|
* @param models - raw configured entries.
|
|
@@ -50,6 +50,39 @@ export function validateModels(models, label) {
|
|
|
50
50
|
};
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Provider failures can echo credentials or customer input in any format.
|
|
55
|
+
* Shape-based filtering cannot make arbitrary response text safe to display.
|
|
56
|
+
* Here, safe means this diagnostic carries no provider-controlled free text,
|
|
57
|
+
* not that secrets cannot exist elsewhere in the system. Keep raw responses
|
|
58
|
+
* internal for classification and reset parsing; display only local summaries.
|
|
59
|
+
* This deliberately loses model, parameter and plan details useful to support.
|
|
60
|
+
*/
|
|
61
|
+
function providerDiagnostic(summary, hasBody) {
|
|
62
|
+
return hasBody ? `${summary}: [provider response body omitted]` : summary;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse a provider response or stored JSON string without exposing input excerpts.
|
|
66
|
+
* Success returns the same JSON value; only SyntaxError is replaced, preserving
|
|
67
|
+
* parse-failure type checks and leaving body-read/abort failures unchanged.
|
|
68
|
+
* Node's Unexpected token errors include a bounded input excerpt. Do not retain
|
|
69
|
+
* the original error as cause: the host displays causes recursively.
|
|
70
|
+
* @param input - response to consume once, or JSON text to parse.
|
|
71
|
+
* @param origin - local endpoint/operation label; never response.url, a discovered
|
|
72
|
+
* URL, a credential, or any other provider-controlled text.
|
|
73
|
+
* @returns the parsed value, without validation or credential filtering.
|
|
74
|
+
* @throws SyntaxError with the local origin and a fixed omission marker.
|
|
75
|
+
*/
|
|
76
|
+
export async function parseProviderJson(input, origin) {
|
|
77
|
+
try {
|
|
78
|
+
return (typeof input === 'string' ? JSON.parse(input) : await input.json());
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (!(error instanceof SyntaxError))
|
|
82
|
+
throw error;
|
|
83
|
+
throw new SyntaxError(providerDiagnostic(`${origin}: invalid JSON`, true));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
53
86
|
/**
|
|
54
87
|
* Build an LlmError from a non-2xx provider response, mapping the status to a
|
|
55
88
|
* stable code and, for a rate-limited request, the disclosed reset instant to
|
|
@@ -73,11 +106,8 @@ export async function httpLlmError(response, label, options = {}) {
|
|
|
73
106
|
catch {
|
|
74
107
|
// Only swallow error-body reading: the HTTP status still identifies the failure.
|
|
75
108
|
}
|
|
76
|
-
//
|
|
109
|
+
// Preserve the existing classification window; reset readers need the full body.
|
|
77
110
|
const shown = body.slice(0, 500);
|
|
78
|
-
const message = shown.length > 0
|
|
79
|
-
? `${label} error (HTTP ${String(response.status)}): ${shown}`
|
|
80
|
-
: `${label} error (HTTP ${String(response.status)})`;
|
|
81
111
|
let code;
|
|
82
112
|
if (response.status === 401 || response.status === 403)
|
|
83
113
|
code = 'AUTH';
|
|
@@ -93,6 +123,7 @@ export async function httpLlmError(response, label, options = {}) {
|
|
|
93
123
|
code = 'SERVER';
|
|
94
124
|
else
|
|
95
125
|
code = `HTTP_${String(response.status)}`;
|
|
126
|
+
const message = providerDiagnostic(`${label} error (HTTP ${String(response.status)}, ${code})`, body.length > 0);
|
|
96
127
|
const now = Date.now();
|
|
97
128
|
// The provider's reader runs on a 429 and nowhere else. Providers attach
|
|
98
129
|
// their rate-limit headers to every response, so reading them on a transient
|
|
@@ -111,7 +142,8 @@ export async function httpLlmError(response, label, options = {}) {
|
|
|
111
142
|
? options.rateLimitReset?.(response, body, now) ?? googleQuotaReset(body, now) ?? retryAfterInstant(response, now)
|
|
112
143
|
: retryAfterInstant(response, now);
|
|
113
144
|
if (reset === undefined && rateLimited) {
|
|
114
|
-
|
|
145
|
+
// Header names and values are provider-controlled too; none are displayed.
|
|
146
|
+
options.onWarn?.(providerDiagnostic(`${label}: 429 disclosed no reset time; [provider response headers omitted]`, body.length > 0));
|
|
115
147
|
}
|
|
116
148
|
return new LlmError(message, code, {
|
|
117
149
|
status: response.status,
|
|
@@ -246,9 +278,10 @@ export class OAuthEndpointError extends Error {
|
|
|
246
278
|
*/
|
|
247
279
|
export async function oauthEndpointError(response, label) {
|
|
248
280
|
let oauthCode;
|
|
249
|
-
let
|
|
281
|
+
let body = '';
|
|
250
282
|
try {
|
|
251
|
-
|
|
283
|
+
body = await response.text();
|
|
284
|
+
const parsed = JSON.parse(body);
|
|
252
285
|
if (typeof parsed.error === 'string') {
|
|
253
286
|
oauthCode = parsed.error;
|
|
254
287
|
}
|
|
@@ -256,20 +289,13 @@ export async function oauthEndpointError(response, label) {
|
|
|
256
289
|
const nested = parsed.error;
|
|
257
290
|
if (typeof nested.code === 'string' && nested.code.length > 0)
|
|
258
291
|
oauthCode = nested.code;
|
|
259
|
-
if (typeof nested.message === 'string')
|
|
260
|
-
detail = nested.message;
|
|
261
292
|
}
|
|
262
|
-
if (typeof parsed.error_description === 'string')
|
|
263
|
-
detail = parsed.error_description;
|
|
264
|
-
if (detail.length === 0)
|
|
265
|
-
detail = oauthCode ?? '';
|
|
266
293
|
}
|
|
267
294
|
catch {
|
|
268
|
-
// Only swallow error-body parsing:
|
|
295
|
+
// Only swallow error-body reading/parsing: HTTP status still identifies the failure.
|
|
269
296
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
: `${label} token endpoint error (HTTP ${String(response.status)})`;
|
|
297
|
+
// oauthCode remains structural for auth classification, never display text.
|
|
298
|
+
const message = providerDiagnostic(`${label} token endpoint error (HTTP ${String(response.status)})`, body.length > 0);
|
|
273
299
|
return new OAuthEndpointError(message, response.status, oauthCode, parseRetryAfterMs(response));
|
|
274
300
|
}
|
|
275
301
|
/**
|
|
@@ -297,7 +297,8 @@ export declare class CopilotAdapter extends LlmAdapter {
|
|
|
297
297
|
* account identity is the session's long-lived GitHub token (stable across
|
|
298
298
|
* Copilot-token refreshes, different per GitHub login); the conversation is
|
|
299
299
|
* the loop-stamped `sessionId`, falling back to the first message's id
|
|
300
|
-
* when a hand-built request carries no session stamp
|
|
300
|
+
* when a hand-built request carries no session stamp. Identity-free
|
|
301
|
+
* requests have no replay scope; the model separates
|
|
301
302
|
* wire families. A call id captured in one scope is invisible to every
|
|
302
303
|
* other scope, so reused ids cannot leak reasoning across accounts,
|
|
303
304
|
* conversations, or models.
|
package/lib/providers/copilot.js
CHANGED
|
@@ -17,7 +17,7 @@ import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortI
|
|
|
17
17
|
import { resolveImages } from '../translate/resolved.js';
|
|
18
18
|
import { streamChatCompletions, toChatMessages, toChatTools, } from '../translate/chat-completions.js';
|
|
19
19
|
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
20
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
20
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, parseProviderJson, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
21
21
|
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
22
22
|
import { hostFetch } from '../http.js';
|
|
23
23
|
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
@@ -144,7 +144,7 @@ export async function exchangeCopilotToken(githubToken, fetchFn = hostFetch) {
|
|
|
144
144
|
});
|
|
145
145
|
if (!response.ok)
|
|
146
146
|
throw await oauthEndpointError(response, 'copilot');
|
|
147
|
-
const wire = await response
|
|
147
|
+
const wire = await parseProviderJson(response, 'copilot token exchange');
|
|
148
148
|
if (typeof wire.token !== 'string' || wire.token.length === 0) {
|
|
149
149
|
throw new Error('copilot token endpoint returned no token');
|
|
150
150
|
}
|
|
@@ -268,7 +268,7 @@ export async function fetchCopilotModels(session, fetchFn = hostFetch, signal) {
|
|
|
268
268
|
});
|
|
269
269
|
if (!response.ok)
|
|
270
270
|
throw await oauthEndpointError(response, 'copilot models');
|
|
271
|
-
const payload = await response
|
|
271
|
+
const payload = await parseProviderJson(response, 'copilot models');
|
|
272
272
|
if (!Array.isArray(payload.data))
|
|
273
273
|
throw new Error('copilot models endpoint returned no data array');
|
|
274
274
|
const seen = new Set();
|
|
@@ -677,7 +677,8 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
677
677
|
* account identity is the session's long-lived GitHub token (stable across
|
|
678
678
|
* Copilot-token refreshes, different per GitHub login); the conversation is
|
|
679
679
|
* the loop-stamped `sessionId`, falling back to the first message's id
|
|
680
|
-
* when a hand-built request carries no session stamp
|
|
680
|
+
* when a hand-built request carries no session stamp. Identity-free
|
|
681
|
+
* requests have no replay scope; the model separates
|
|
681
682
|
* wire families. A call id captured in one scope is invisible to every
|
|
682
683
|
* other scope, so reused ids cannot leak reasoning across accounts,
|
|
683
684
|
* conversations, or models.
|
|
@@ -685,9 +686,11 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
685
686
|
replayScope(tokenKey, options) {
|
|
686
687
|
const conversation = options.sessionId !== undefined
|
|
687
688
|
? `session:${String(options.sessionId)}`
|
|
688
|
-
: options.messages[0] !== undefined
|
|
689
|
+
: options.messages[0]?.id !== undefined
|
|
689
690
|
? `anchor:${String(options.messages[0].id)}`
|
|
690
|
-
:
|
|
691
|
+
: undefined;
|
|
692
|
+
if (conversation === undefined)
|
|
693
|
+
return undefined;
|
|
691
694
|
return `${tokenKey}\u0000${conversation}\u0000${options.model}`;
|
|
692
695
|
}
|
|
693
696
|
/**
|
|
@@ -701,6 +704,8 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
701
704
|
* per call.
|
|
702
705
|
*/
|
|
703
706
|
captureReasoning(scope, callIds, items) {
|
|
707
|
+
if (scope === undefined)
|
|
708
|
+
return;
|
|
704
709
|
let entries = this.replayByScope.get(scope);
|
|
705
710
|
if (entries === undefined) {
|
|
706
711
|
entries = new Map();
|
|
@@ -745,6 +750,8 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
745
750
|
* degradation, never an error.
|
|
746
751
|
*/
|
|
747
752
|
replayFor(scope, callId) {
|
|
753
|
+
if (scope === undefined)
|
|
754
|
+
return undefined;
|
|
748
755
|
const entries = this.replayByScope.get(scope);
|
|
749
756
|
const entry = entries?.get(callId);
|
|
750
757
|
if (entries === undefined || entry === undefined)
|
|
@@ -5,8 +5,43 @@ import { CursorAdapter } from 'dsh-subscriptions/cursor-transport';
|
|
|
5
5
|
* blocks. DSH 0.1.7 carries them as first-class role=tool messages instead.
|
|
6
6
|
*/
|
|
7
7
|
export declare function projectCursorMessages(messages: GenerateOptions['messages']): unknown[];
|
|
8
|
+
/**
|
|
9
|
+
* Cursor adapter for the DSH host.
|
|
10
|
+
*
|
|
11
|
+
* Failure text contract: `finish/error` messages that originate from the
|
|
12
|
+
* vendor stream and can quote provider-controlled text (the response content
|
|
13
|
+
* type, trailers, end-stream errors, a frame-reader or other error whose message
|
|
14
|
+
* is not one of the literals below, and a transport-coded failure that is not a
|
|
15
|
+
* socket-layer error) are replaced by `<code> [provider response text omitted]`. Failures that are
|
|
16
|
+
* wholly local keep their own message and code:
|
|
17
|
+
*
|
|
18
|
+
* - anything the auth service threw (not signed in, sign-in expired, token
|
|
19
|
+
* refresh status), and a host attachment read failure;
|
|
20
|
+
* - the vendor's own request validation, by code (`UNSUPPORTED_OPTION`,
|
|
21
|
+
* `UNSUPPORTED_CONTENT`, `TOOL_LIMIT`);
|
|
22
|
+
* - the vendor's fixed-literal plain errors, by exact message: the HTTP status
|
|
23
|
+
* (`Cursor agent returned HTTP 429`), the response, idle and progress
|
|
24
|
+
* timeouts, a closed bridge or stream, an unreadable compressed frame, and
|
|
25
|
+
* an invalid Cursor setting;
|
|
26
|
+
* - a network error raised by the operating system (`ECONNREFUSED`,
|
|
27
|
+
* `ENOTFOUND`, ...), recognised from the thrown error's own code, not from
|
|
28
|
+
* the vendor's classification.
|
|
29
|
+
*
|
|
30
|
+
* Not a provider-origin class: a network error such as `ECONNREFUSED` is a
|
|
31
|
+
* local failure (no network, daemon down, DNS), and is kept. What is still
|
|
32
|
+
* replaced although it is probably local is a transport failure with no
|
|
33
|
+
* socket-layer errno: a TLS or certificate failure and an HTTP/2 protocol
|
|
34
|
+
* error. The vendor's `TRANSPORT` code cannot decide those, because
|
|
35
|
+
* `classifyCursorError` derives it from message text (`network`, `connection`,
|
|
36
|
+
* `socket`, `fetch`, `ECONN`, `http2`) and a provider's end-stream message or
|
|
37
|
+
* `grpc-message` can contain any of them; so the code is never trusted alone.
|
|
38
|
+
* Telling these apart would need a check on the thrown error object, as the
|
|
39
|
+
* errno check above does, extended to the TLS and HTTP/2 error codes. That is
|
|
40
|
+
* not done here and is not verified.
|
|
41
|
+
*/
|
|
8
42
|
export declare class CursorCompatAdapter extends CursorAdapter {
|
|
9
43
|
private readonly visibleModels;
|
|
44
|
+
private readonly localFailures;
|
|
10
45
|
constructor(options: ConstructorParameters<typeof CursorAdapter>[0] & {
|
|
11
46
|
visibleModels?: () => readonly string[] | undefined;
|
|
12
47
|
});
|