dsh-plugin-subscriptions 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -6
- package/README.zh.md +43 -4
- package/lib/auth/rpc.d.ts +36 -2
- package/lib/auth/rpc.js +47 -5
- package/lib/client/ImageGenerateToolview.d.ts +1 -1
- package/lib/client/SpeedSelect.d.ts +25 -2
- package/lib/client/SpeedSelect.js +10 -6
- package/lib/client/SubscriptionsSection.d.ts +74 -0
- package/lib/client/SubscriptionsSection.js +325 -4
- package/lib/client/VideoGenerateToolview.d.ts +1 -1
- package/lib/client/index.d.ts +1 -9
- package/lib/client/index.js +7 -4
- package/lib/client/locales.d.ts +28 -0
- package/lib/client/locales.js +28 -0
- package/lib/client.js +458 -10
- package/lib/client.js.map +1 -1
- package/lib/compat.d.ts +36 -0
- package/lib/compat.js +20 -0
- package/lib/index.d.ts +5 -1
- package/lib/index.js +865 -111
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +237 -0
- package/lib/providers/claude.d.ts +24 -3
- package/lib/providers/claude.js +35 -24
- package/lib/providers/codex.d.ts +21 -0
- package/lib/providers/codex.js +37 -10
- package/lib/providers/common.d.ts +70 -6
- package/lib/providers/common.js +118 -19
- package/lib/providers/copilot.d.ts +10 -0
- package/lib/providers/copilot.js +21 -8
- package/lib/providers/grok.d.ts +21 -0
- package/lib/providers/grok.js +37 -7
- package/lib/providers/pool-usage.d.ts +23 -2
- package/lib/providers/pool-usage.js +70 -15
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/translate/anthropic.js +5 -4
- package/lib/translate/chat-completions.js +5 -4
- package/lib/translate/responses.js +5 -4
- package/package.json +21 -21
- package/lib/providers/antigravity.d.ts +0 -90
- package/lib/providers/antigravity.js +0 -392
- package/lib/translate/antigravity.d.ts +0 -110
- package/lib/translate/antigravity.js +0 -303
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* Concurrent refreshes for one provider coalesce behind a single in-flight
|
|
6
6
|
* promise (`inflight`), so a rotating refresh token is never spent twice.
|
|
7
7
|
*/
|
|
8
|
-
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
9
|
-
import type {
|
|
8
|
+
import { LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
9
|
+
import type { RateLimitResetReader } from './rate-limit.js';
|
|
10
10
|
/** One configured model catalog entry. */
|
|
11
11
|
export interface ModelEntry {
|
|
12
12
|
/** Wire model id; must be non-empty. */
|
|
@@ -33,14 +33,34 @@ export interface ModelEntry {
|
|
|
33
33
|
* @returns the validated entries.
|
|
34
34
|
*/
|
|
35
35
|
export declare function validateModels(models: readonly ModelEntry[], label: string): ModelEntry[];
|
|
36
|
+
/** Optional per-call hooks {@link httpLlmError} uses to read a rate-limit window. */
|
|
37
|
+
export interface HttpLlmErrorOptions {
|
|
38
|
+
/**
|
|
39
|
+
* The calling provider's reader for the instant its rate-limit window
|
|
40
|
+
* reopens. Consulted on a 429 only, and there ahead of the generic
|
|
41
|
+
* `retry-after` header, because a provider's own field names the window while
|
|
42
|
+
* `retry-after` often names a short backoff.
|
|
43
|
+
*/
|
|
44
|
+
rateLimitReset?: RateLimitResetReader;
|
|
45
|
+
/** Diagnostic sink for a 429 that disclosed no reset instant this code recognizes. */
|
|
46
|
+
onWarn?: (message: string) => void;
|
|
47
|
+
}
|
|
36
48
|
/**
|
|
37
|
-
* Build an LlmError from a non-2xx provider response,
|
|
38
|
-
*
|
|
49
|
+
* Build an LlmError from a non-2xx provider response, mapping the status to a
|
|
50
|
+
* stable code and, for a rate-limited request, the disclosed reset instant to
|
|
51
|
+
* the `providerRetryAfterMs` the retry plugin waits out.
|
|
52
|
+
*
|
|
53
|
+
* A 429 classifies as `RATE_LIMIT` on the strength of the status alone, ahead
|
|
54
|
+
* of the quota-wording check. On these routes there is no terminal quota to
|
|
55
|
+
* distinguish: a subscription has no balance to top up, only a window that
|
|
56
|
+
* reopens, and providers announce an exhausted window with wording
|
|
57
|
+
* (`usage_limit_reached`) the shared classifier reads as permanent.
|
|
39
58
|
* @param response - the failed response.
|
|
40
59
|
* @param label - diagnostic prefix naming the provider API.
|
|
60
|
+
* @param options - the calling provider's rate-limit reader and warning sink.
|
|
41
61
|
* @returns the classified error.
|
|
42
62
|
*/
|
|
43
|
-
export declare function httpLlmError(response: Response, label: string): Promise<LlmError>;
|
|
63
|
+
export declare function httpLlmError(response: Response, label: string, options?: HttpLlmErrorOptions): Promise<LlmError>;
|
|
44
64
|
/** An idle watchdog: aborts its signal when no SSE activity arrives within the timeout. */
|
|
45
65
|
export interface IdleWatchdog {
|
|
46
66
|
/** Signal to pass to fetch and body reads; aborts on caller cancel or idle expiry. */
|
|
@@ -76,7 +96,15 @@ export declare class OAuthEndpointError extends Error {
|
|
|
76
96
|
readonly status: number;
|
|
77
97
|
/** The provider's OAuth `error` code (e.g. `invalid_grant`), when present. */
|
|
78
98
|
readonly oauthCode: string | undefined;
|
|
79
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The endpoint's `retry-after`, in ms, when it sent one. Usage/models
|
|
101
|
+
* endpoints reuse this error type and can rate-limit progressively (each
|
|
102
|
+
* hit within the window extends the next one), so a caller retrying on a
|
|
103
|
+
* fixed schedule instead of honoring this can keep an account locked out
|
|
104
|
+
* indefinitely.
|
|
105
|
+
*/
|
|
106
|
+
readonly retryAfterMs: number | undefined;
|
|
107
|
+
constructor(message: string, status: number, oauthCode?: string, retryAfterMs?: number);
|
|
80
108
|
}
|
|
81
109
|
/**
|
|
82
110
|
* Read an OAuth JSON error body into an {@link OAuthEndpointError}.
|
|
@@ -203,6 +231,42 @@ export interface DiscoveredModel {
|
|
|
203
231
|
*/
|
|
204
232
|
copilotResponses?: boolean;
|
|
205
233
|
}
|
|
234
|
+
/** Display name for a wire reasoning-effort identifier. */
|
|
235
|
+
export declare function effortDisplayName(effort: string): string;
|
|
236
|
+
/** The reasoning-block shape every caller passes to {@link mergeReasoning}. */
|
|
237
|
+
export interface ReasoningBlock {
|
|
238
|
+
efforts: readonly {
|
|
239
|
+
id: ReasoningEffortId;
|
|
240
|
+
name: string;
|
|
241
|
+
description?: string;
|
|
242
|
+
}[];
|
|
243
|
+
defaultEffort?: ReasoningEffortId;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Fold a configured per-model default effort into a reasoning block, keeping
|
|
247
|
+
* the DSH runtime invariant `defaultEffort ∈ efforts` (the runtime rejects an
|
|
248
|
+
* unknown default with `INVALID_MODEL_REASONING`).
|
|
249
|
+
*
|
|
250
|
+
* A configured level the base set does not advertise is *dropped*, not
|
|
251
|
+
* appended: for claude/grok/copilot the base is the provider's live catalog,
|
|
252
|
+
* i.e. the truth about what the model accepts, so honouring a stale override
|
|
253
|
+
* would put an unsupported effort on every single request instead of letting
|
|
254
|
+
* the harness reject it before provider I/O. The override then simply falls
|
|
255
|
+
* back to the provider's own default until the user picks a level the catalog
|
|
256
|
+
* still lists.
|
|
257
|
+
*
|
|
258
|
+
* `extendable` opts into the opposite rule for a base that is a *built-in
|
|
259
|
+
* fallback* rather than discovered truth (codex, whose static effort list is
|
|
260
|
+
* known to trail the backend): there, appending the configured level is how a
|
|
261
|
+
* newly shipped tier becomes selectable at all.
|
|
262
|
+
* @param configuredDefault - the user-configured default effort id, or undefined.
|
|
263
|
+
* @param base - the discovered/built-in reasoning block, or undefined.
|
|
264
|
+
* @param options - `extendable` marks the base as a fallback that may be extended.
|
|
265
|
+
* @returns the merged block, or undefined when neither side contributes one.
|
|
266
|
+
*/
|
|
267
|
+
export declare function mergeReasoning(configuredDefault: string | undefined, base: ReasoningBlock | undefined, options?: {
|
|
268
|
+
extendable?: boolean;
|
|
269
|
+
}): DiscoveredModel['reasoning'] | undefined;
|
|
206
270
|
/**
|
|
207
271
|
* First account catalog that lists `model` (callers pass default-first).
|
|
208
272
|
* One failing lookup sits that account out so a sibling's metadata still
|
package/lib/providers/common.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Concurrent refreshes for one provider coalesce behind a single in-flight
|
|
6
6
|
* promise (`inflight`), so a rotating refresh token is never spent twice.
|
|
7
7
|
*/
|
|
8
|
-
import { CONTEXT_WINDOW_EXCEEDED_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, QUOTA_EXCEEDED_CODE, } from '@deepseek-ai/dsh-llm';
|
|
8
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
9
|
+
import { rateLimitDiagnostics, retryAfterInstant, waitFromReset } from './rate-limit.js';
|
|
9
10
|
/**
|
|
10
11
|
* Validate a configured model catalog (mirrors llm-deepseek's resolveModels).
|
|
11
12
|
* @param models - raw configured entries.
|
|
@@ -49,31 +50,41 @@ export function validateModels(models, label) {
|
|
|
49
50
|
});
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
52
|
-
* Build an LlmError from a non-2xx provider response,
|
|
53
|
-
*
|
|
53
|
+
* Build an LlmError from a non-2xx provider response, mapping the status to a
|
|
54
|
+
* stable code and, for a rate-limited request, the disclosed reset instant to
|
|
55
|
+
* the `providerRetryAfterMs` the retry plugin waits out.
|
|
56
|
+
*
|
|
57
|
+
* A 429 classifies as `RATE_LIMIT` on the strength of the status alone, ahead
|
|
58
|
+
* of the quota-wording check. On these routes there is no terminal quota to
|
|
59
|
+
* distinguish: a subscription has no balance to top up, only a window that
|
|
60
|
+
* reopens, and providers announce an exhausted window with wording
|
|
61
|
+
* (`usage_limit_reached`) the shared classifier reads as permanent.
|
|
54
62
|
* @param response - the failed response.
|
|
55
63
|
* @param label - diagnostic prefix naming the provider API.
|
|
64
|
+
* @param options - the calling provider's rate-limit reader and warning sink.
|
|
56
65
|
* @returns the classified error.
|
|
57
66
|
*/
|
|
58
|
-
export async function httpLlmError(response, label) {
|
|
67
|
+
export async function httpLlmError(response, label, options = {}) {
|
|
59
68
|
let body = '';
|
|
60
69
|
try {
|
|
61
|
-
body =
|
|
70
|
+
body = await response.text();
|
|
62
71
|
}
|
|
63
72
|
catch {
|
|
64
73
|
// Only swallow error-body reading: the HTTP status still identifies the failure.
|
|
65
74
|
}
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
// Truncated for display only; the readers below need the whole body to parse it.
|
|
76
|
+
const shown = body.slice(0, 500);
|
|
77
|
+
const message = shown.length > 0
|
|
78
|
+
? `${label} error (HTTP ${String(response.status)}): ${shown}`
|
|
68
79
|
: `${label} error (HTTP ${String(response.status)})`;
|
|
69
80
|
let code;
|
|
70
81
|
if (response.status === 401 || response.status === 403)
|
|
71
82
|
code = 'AUTH';
|
|
72
|
-
else if (isQuotaExceededError(body))
|
|
73
|
-
code = QUOTA_EXCEEDED_CODE;
|
|
74
83
|
else if (response.status === 429)
|
|
75
84
|
code = 'RATE_LIMIT';
|
|
76
|
-
else if (
|
|
85
|
+
else if (isQuotaExceededError(shown))
|
|
86
|
+
code = QUOTA_EXCEEDED_CODE;
|
|
87
|
+
else if (response.status === 400 && isContextWindowExceededError(shown))
|
|
77
88
|
code = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
78
89
|
else if (response.status === 408 || response.status === 504)
|
|
79
90
|
code = 'TIMEOUT';
|
|
@@ -81,18 +92,43 @@ export async function httpLlmError(response, label) {
|
|
|
81
92
|
code = 'SERVER';
|
|
82
93
|
else
|
|
83
94
|
code = `HTTP_${String(response.status)}`;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
// The provider's reader runs on a 429 and nowhere else. Providers attach
|
|
97
|
+
// their rate-limit headers to every response, so reading them on a transient
|
|
98
|
+
// 500 would report the current window's rollover — hours out — as the delay
|
|
99
|
+
// before retrying a failure that has nothing to do with the window, and the
|
|
100
|
+
// retry plugin honours `providerRetryAfterMs` for every retryable code.
|
|
101
|
+
// `retry-after` stays readable on any status: there it is a real backoff the
|
|
102
|
+
// provider asked for (a 503 shedding load), not a window snapshot.
|
|
103
|
+
//
|
|
104
|
+
// On a 429 the provider's own field wins outright rather than being raced
|
|
105
|
+
// against `retry-after`: a rejected window often carries both, and the
|
|
106
|
+
// generic header then names a short backoff that would burn the retry budget
|
|
107
|
+
// re-hitting the same closed window.
|
|
108
|
+
const rateLimited = response.status === 429;
|
|
109
|
+
const reset = rateLimited
|
|
110
|
+
? options.rateLimitReset?.(response, body, now) ?? retryAfterInstant(response, now)
|
|
111
|
+
: retryAfterInstant(response, now);
|
|
112
|
+
if (reset === undefined && rateLimited) {
|
|
113
|
+
options.onWarn?.(`${label}: ${rateLimitDiagnostics(response, body)}`);
|
|
90
114
|
}
|
|
91
115
|
return new LlmError(message, code, {
|
|
92
116
|
status: response.status,
|
|
93
|
-
...
|
|
117
|
+
...reset === undefined ? {} : { providerRetryAfterMs: waitFromReset(reset, now) },
|
|
94
118
|
});
|
|
95
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Parse a response's `retry-after` header (seconds) into milliseconds.
|
|
122
|
+
* @param response - the failed response.
|
|
123
|
+
* @returns the delay in ms, or undefined when absent/unusable.
|
|
124
|
+
*/
|
|
125
|
+
function parseRetryAfterMs(response) {
|
|
126
|
+
const retryAfter = response.headers.get('retry-after');
|
|
127
|
+
if (retryAfter === null)
|
|
128
|
+
return undefined;
|
|
129
|
+
const seconds = Number(retryAfter);
|
|
130
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : undefined;
|
|
131
|
+
}
|
|
96
132
|
/**
|
|
97
133
|
* Create an idle watchdog chained to the caller's signal.
|
|
98
134
|
* @param caller - the request's own abort signal, when present.
|
|
@@ -154,11 +190,20 @@ export class OAuthEndpointError extends Error {
|
|
|
154
190
|
status;
|
|
155
191
|
/** The provider's OAuth `error` code (e.g. `invalid_grant`), when present. */
|
|
156
192
|
oauthCode;
|
|
157
|
-
|
|
193
|
+
/**
|
|
194
|
+
* The endpoint's `retry-after`, in ms, when it sent one. Usage/models
|
|
195
|
+
* endpoints reuse this error type and can rate-limit progressively (each
|
|
196
|
+
* hit within the window extends the next one), so a caller retrying on a
|
|
197
|
+
* fixed schedule instead of honoring this can keep an account locked out
|
|
198
|
+
* indefinitely.
|
|
199
|
+
*/
|
|
200
|
+
retryAfterMs;
|
|
201
|
+
constructor(message, status, oauthCode, retryAfterMs) {
|
|
158
202
|
super(message);
|
|
159
203
|
this.name = 'OAuthEndpointError';
|
|
160
204
|
this.status = status;
|
|
161
205
|
this.oauthCode = oauthCode;
|
|
206
|
+
this.retryAfterMs = retryAfterMs;
|
|
162
207
|
}
|
|
163
208
|
}
|
|
164
209
|
/**
|
|
@@ -181,7 +226,7 @@ export async function oauthEndpointError(response, label) {
|
|
|
181
226
|
const message = detail.length > 0
|
|
182
227
|
? `${label} token endpoint error (HTTP ${String(response.status)}): ${detail}`
|
|
183
228
|
: `${label} token endpoint error (HTTP ${String(response.status)})`;
|
|
184
|
-
return new OAuthEndpointError(message, response.status, oauthCode);
|
|
229
|
+
return new OAuthEndpointError(message, response.status, oauthCode, parseRetryAfterMs(response));
|
|
185
230
|
}
|
|
186
231
|
/**
|
|
187
232
|
* Per-provider session freshness: loads the stored session, refreshes
|
|
@@ -286,6 +331,60 @@ export function withTimeout(work, timeoutMs) {
|
|
|
286
331
|
aborted,
|
|
287
332
|
]);
|
|
288
333
|
}
|
|
334
|
+
/** Display name for a wire reasoning-effort identifier. */
|
|
335
|
+
export function effortDisplayName(effort) {
|
|
336
|
+
return effort === 'xhigh' ? 'Extra High' : effort.charAt(0).toUpperCase() + effort.slice(1);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Fold a configured per-model default effort into a reasoning block, keeping
|
|
340
|
+
* the DSH runtime invariant `defaultEffort ∈ efforts` (the runtime rejects an
|
|
341
|
+
* unknown default with `INVALID_MODEL_REASONING`).
|
|
342
|
+
*
|
|
343
|
+
* A configured level the base set does not advertise is *dropped*, not
|
|
344
|
+
* appended: for claude/grok/copilot the base is the provider's live catalog,
|
|
345
|
+
* i.e. the truth about what the model accepts, so honouring a stale override
|
|
346
|
+
* would put an unsupported effort on every single request instead of letting
|
|
347
|
+
* the harness reject it before provider I/O. The override then simply falls
|
|
348
|
+
* back to the provider's own default until the user picks a level the catalog
|
|
349
|
+
* still lists.
|
|
350
|
+
*
|
|
351
|
+
* `extendable` opts into the opposite rule for a base that is a *built-in
|
|
352
|
+
* fallback* rather than discovered truth (codex, whose static effort list is
|
|
353
|
+
* known to trail the backend): there, appending the configured level is how a
|
|
354
|
+
* newly shipped tier becomes selectable at all.
|
|
355
|
+
* @param configuredDefault - the user-configured default effort id, or undefined.
|
|
356
|
+
* @param base - the discovered/built-in reasoning block, or undefined.
|
|
357
|
+
* @param options - `extendable` marks the base as a fallback that may be extended.
|
|
358
|
+
* @returns the merged block, or undefined when neither side contributes one.
|
|
359
|
+
*/
|
|
360
|
+
export function mergeReasoning(configuredDefault, base, options) {
|
|
361
|
+
const detached = base === undefined
|
|
362
|
+
? undefined
|
|
363
|
+
: {
|
|
364
|
+
efforts: [...base.efforts],
|
|
365
|
+
...(base.defaultEffort === undefined ? {} : { defaultEffort: base.defaultEffort }),
|
|
366
|
+
};
|
|
367
|
+
if (configuredDefault === undefined)
|
|
368
|
+
return detached;
|
|
369
|
+
const effort = ReasoningEffortId(configuredDefault);
|
|
370
|
+
if (base === undefined) {
|
|
371
|
+
// No capability information at all (catalog unavailable, or a model the
|
|
372
|
+
// catalog does not cover). Inventing a reasoning block here would claim a
|
|
373
|
+
// capability nobody advertised; only a fallback-based provider may.
|
|
374
|
+
return options?.extendable === true
|
|
375
|
+
? { efforts: [{ id: effort, name: effortDisplayName(effort) }], defaultEffort: effort }
|
|
376
|
+
: undefined;
|
|
377
|
+
}
|
|
378
|
+
if (base.efforts.some(entry => entry.id === effort)) {
|
|
379
|
+
return { efforts: [...base.efforts], defaultEffort: effort };
|
|
380
|
+
}
|
|
381
|
+
if (options?.extendable !== true)
|
|
382
|
+
return detached;
|
|
383
|
+
return {
|
|
384
|
+
efforts: [...base.efforts, { id: effort, name: effortDisplayName(effort) }],
|
|
385
|
+
defaultEffort: effort,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
289
388
|
/**
|
|
290
389
|
* First account catalog that lists `model` (callers pass default-first).
|
|
291
390
|
* One failing lookup sits that account out so a sibling's metadata still
|
|
@@ -22,6 +22,7 @@ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
|
22
22
|
import type { ReasoningReplayItem, ResponsesRequestInput, ResponsesStreamEvent } from '../translate/responses.js';
|
|
23
23
|
import { AccountTokenManager } from './accounts.js';
|
|
24
24
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry } from './common.js';
|
|
25
|
+
import type { RateLimitWait } from './rate-limit.js';
|
|
25
26
|
/**
|
|
26
27
|
* Client id of the VS Code Copilot Chat GitHub App (pi-mono and
|
|
27
28
|
* copilot2api-go use the same value): the app is pre-authorized for the
|
|
@@ -230,6 +231,14 @@ export interface CopilotAdapterOptions {
|
|
|
230
231
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
231
232
|
/** Durable catalog store seeding capability metadata across restarts. */
|
|
232
233
|
catalogStore?: CatalogPersistence;
|
|
234
|
+
/** How long this route may hold a turn open waiting for a rate-limit window; defaults to waiting on, six-hour ceiling. */
|
|
235
|
+
rateLimit?: RateLimitWait;
|
|
236
|
+
/**
|
|
237
|
+
* Per-model default reasoning effort override (the Settings page's picker).
|
|
238
|
+
* Returns the user-configured default for one model, or undefined to follow
|
|
239
|
+
* the provider's own default.
|
|
240
|
+
*/
|
|
241
|
+
defaultEffortOf?: (model: string) => string | undefined;
|
|
233
242
|
}
|
|
234
243
|
/** Copilot wire adapter: one instance serves the `copilot` provider route. */
|
|
235
244
|
export declare class CopilotAdapter extends LlmAdapter {
|
|
@@ -263,6 +272,7 @@ export declare class CopilotAdapter extends LlmAdapter {
|
|
|
263
272
|
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
264
273
|
private catalogFor;
|
|
265
274
|
providerInfo(provider: string): LlmProviderInfo;
|
|
275
|
+
providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy;
|
|
266
276
|
private staticModels;
|
|
267
277
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
268
278
|
/** The provider's own catalog: union of every account, or one account when named. */
|
package/lib/providers/copilot.js
CHANGED
|
@@ -17,9 +17,10 @@ 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, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
20
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
21
21
|
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
22
22
|
import { proxiedFetch } from '../http.js';
|
|
23
|
+
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
23
24
|
/**
|
|
24
25
|
* Client id of the VS Code Copilot Chat GitHub App (pi-mono and
|
|
25
26
|
* copilot2api-go use the same value): the app is pre-authorized for the
|
|
@@ -574,6 +575,9 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
574
575
|
providerInfo(provider) {
|
|
575
576
|
return { id: provider, name: 'GitHub Copilot' };
|
|
576
577
|
}
|
|
578
|
+
providerRetryPolicy(provider) {
|
|
579
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `copilot: provider "${provider}" retryPolicy`);
|
|
580
|
+
}
|
|
577
581
|
staticModels(provider) {
|
|
578
582
|
return this.options.models.map(model => ({
|
|
579
583
|
provider,
|
|
@@ -768,6 +772,13 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
768
772
|
async resolveOwnModel(provider, model) {
|
|
769
773
|
const discovered = await this.discovered(model);
|
|
770
774
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
775
|
+
// Efforts come from the discovered catalog's reasoning_effort array; a
|
|
776
|
+
// model that did not advertise one exposes none, so the harness rejects
|
|
777
|
+
// an explicit effort before provider I/O instead of the API 400ing
|
|
778
|
+
// (Copilot returns invalid_request_body for models that cannot reason).
|
|
779
|
+
// A configured default effort still merges in: the picker then
|
|
780
|
+
// preselects it even for models the catalog does not cover.
|
|
781
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning);
|
|
771
782
|
return {
|
|
772
783
|
provider,
|
|
773
784
|
id: model,
|
|
@@ -776,11 +787,7 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
776
787
|
inputModalities: discovered?.inputModalities ?? configured?.inputModalities ?? ['text'],
|
|
777
788
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? COPILOT_CONTEXT_WINDOW },
|
|
778
789
|
defaultMaxTokens: configured?.maxTokens ?? COPILOT_DEFAULT_MAX_TOKENS,
|
|
779
|
-
|
|
780
|
-
// model that did not advertise one exposes none, so the harness rejects
|
|
781
|
-
// an explicit effort before provider I/O instead of the API 400ing
|
|
782
|
-
// (Copilot returns invalid_request_body for models that cannot reason).
|
|
783
|
-
...discovered?.reasoning === undefined ? {} : { reasoning: discovered.reasoning },
|
|
790
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
784
791
|
};
|
|
785
792
|
}
|
|
786
793
|
async *stream(options) {
|
|
@@ -819,8 +826,14 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
819
826
|
session = await this.options.tokens.session(account, true);
|
|
820
827
|
response = await this.request(options, session, watchdog.signal, wire, scope);
|
|
821
828
|
}
|
|
822
|
-
if (!response.ok)
|
|
823
|
-
throw await httpLlmError(response, 'copilot API'
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
throw await httpLlmError(response, 'copilot API', {
|
|
831
|
+
// Copilot has no provider-specific reset reader yet. The shared
|
|
832
|
+
// mapper still honors its generic retry-after header and warns with
|
|
833
|
+
// rate-limit-shaped headers/body when GitHub sends another signal.
|
|
834
|
+
...this.options.onWarn === undefined ? {} : { onWarn: this.options.onWarn },
|
|
835
|
+
});
|
|
836
|
+
}
|
|
824
837
|
if (response.body === null) {
|
|
825
838
|
throw new LlmError('copilot API returned no response body', EMPTY_RESPONSE_CODE);
|
|
826
839
|
}
|
package/lib/providers/grok.d.ts
CHANGED
|
@@ -11,11 +11,23 @@ import type { PoolAdapter } from './pool.js';
|
|
|
11
11
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
12
12
|
import { AccountTokenManager } from './accounts.js';
|
|
13
13
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
14
|
+
import type { RateLimitResetReader, RateLimitWait } from './rate-limit.js';
|
|
14
15
|
export declare const GROK_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
15
16
|
export declare const GROK_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration";
|
|
16
17
|
export declare const GROK_API_URL = "https://api.x.ai/v1/responses";
|
|
17
18
|
/** Refresh when the access token has less than this much life left. */
|
|
18
19
|
export declare const GROK_PREEMPT_MS: number;
|
|
20
|
+
/**
|
|
21
|
+
* Reads the reset instant of the xAI window that rejected a request.
|
|
22
|
+
*
|
|
23
|
+
* Body only. xAI serves the OpenAI-compatible `x-ratelimit-reset-*` family,
|
|
24
|
+
* whose values are rollover durations (`6m0s`) present on every response, one
|
|
25
|
+
* per bucket — on a 429 the earliest of them is usually a bucket with room
|
|
26
|
+
* (`0s` for the request bucket while the token bucket is the one exhausted),
|
|
27
|
+
* which would burn the whole retry budget in seconds. They reach the operator
|
|
28
|
+
* through `rateLimitDiagnostics` instead.
|
|
29
|
+
*/
|
|
30
|
+
export declare const grokRateLimitReset: RateLimitResetReader;
|
|
19
31
|
/** Discovered OIDC endpoints for the xAI authorization server. */
|
|
20
32
|
export interface GrokDiscovery {
|
|
21
33
|
authorizationEndpoint: string;
|
|
@@ -132,6 +144,14 @@ export interface GrokAdapterOptions {
|
|
|
132
144
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
133
145
|
/** Durable catalog store seeding capability metadata across restarts. */
|
|
134
146
|
catalogStore?: CatalogPersistence;
|
|
147
|
+
/**
|
|
148
|
+
* Per-model default reasoning effort override (the Settings page's picker).
|
|
149
|
+
* Returns the user-configured default for one model, or undefined to follow
|
|
150
|
+
* the provider's own default.
|
|
151
|
+
*/
|
|
152
|
+
defaultEffortOf?: (model: string) => string | undefined;
|
|
153
|
+
/** How long this route may hold a turn open waiting for a rate-limit window; defaults to waiting on, six-hour ceiling. */
|
|
154
|
+
rateLimit?: RateLimitWait;
|
|
135
155
|
}
|
|
136
156
|
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
137
157
|
export declare class GrokAdapter extends LlmAdapter {
|
|
@@ -150,6 +170,7 @@ export declare class GrokAdapter extends LlmAdapter {
|
|
|
150
170
|
private catalogFor;
|
|
151
171
|
private listed;
|
|
152
172
|
providerInfo(provider: string): LlmProviderInfo;
|
|
173
|
+
providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy;
|
|
153
174
|
private staticModels;
|
|
154
175
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
155
176
|
/** The provider's own catalog: union of every account, or one account when named. */
|
package/lib/providers/grok.js
CHANGED
|
@@ -7,9 +7,10 @@ import { attributionHeaders, EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmErr
|
|
|
7
7
|
import { decodeJwtPayload } from '../auth/jwt.js';
|
|
8
8
|
import { resolveImages } from '../translate/resolved.js';
|
|
9
9
|
import { streamResponses, toResponsesInput, toResponsesTools } from '../translate/responses.js';
|
|
10
|
-
import { httpLlmError, idleWatchdog, mapFetchFailure, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
10
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
11
11
|
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
12
12
|
import { proxiedFetch } from '../http.js';
|
|
13
|
+
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, jsonBody, resetFromFields, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
13
14
|
export const GROK_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
|
|
14
15
|
export const GROK_DISCOVERY_URL = 'https://auth.x.ai/.well-known/openid-configuration';
|
|
15
16
|
export const GROK_API_URL = 'https://api.x.ai/v1/responses';
|
|
@@ -19,6 +20,19 @@ const GROK_CONTEXT_WINDOW = 256_000;
|
|
|
19
20
|
const GROK_DEFAULT_MAX_TOKENS = 32_000;
|
|
20
21
|
/** Refresh when the access token has less than this much life left. */
|
|
21
22
|
export const GROK_PREEMPT_MS = 2 * 60_000;
|
|
23
|
+
/** Body fields xAI uses to name a delay or reset. */
|
|
24
|
+
const GROK_RESET_FIELDS = ['retry_after', 'retry_after_seconds', 'resets_at', 'reset_at'];
|
|
25
|
+
/**
|
|
26
|
+
* Reads the reset instant of the xAI window that rejected a request.
|
|
27
|
+
*
|
|
28
|
+
* Body only. xAI serves the OpenAI-compatible `x-ratelimit-reset-*` family,
|
|
29
|
+
* whose values are rollover durations (`6m0s`) present on every response, one
|
|
30
|
+
* per bucket — on a 429 the earliest of them is usually a bucket with room
|
|
31
|
+
* (`0s` for the request bucket while the token bucket is the one exhausted),
|
|
32
|
+
* which would burn the whole retry budget in seconds. They reach the operator
|
|
33
|
+
* through `rateLimitDiagnostics` instead.
|
|
34
|
+
*/
|
|
35
|
+
export const grokRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), GROK_RESET_FIELDS, now);
|
|
22
36
|
/** A discovered URL must be https on x.ai or a subdomain; anything else is a hostile document. */
|
|
23
37
|
function assertXaiEndpoint(url, field) {
|
|
24
38
|
let parsed;
|
|
@@ -500,6 +514,9 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
500
514
|
providerInfo(provider) {
|
|
501
515
|
return { id: provider, name: 'Grok (Subscription)' };
|
|
502
516
|
}
|
|
517
|
+
providerRetryPolicy(provider) {
|
|
518
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `grok: provider "${provider}" retryPolicy`);
|
|
519
|
+
}
|
|
503
520
|
staticModels(provider) {
|
|
504
521
|
return this.options.models.map(model => ({
|
|
505
522
|
provider,
|
|
@@ -578,6 +595,12 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
578
595
|
async resolveOwnModel(provider, model) {
|
|
579
596
|
const discovered = await this.discovered(model);
|
|
580
597
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
598
|
+
// Efforts come from the discovered CLI catalog; models it does not
|
|
599
|
+
// cover expose none, so the harness rejects explicit efforts before
|
|
600
|
+
// provider I/O instead of the API 400ing. A configured default effort
|
|
601
|
+
// still merges in: the picker then preselects it even for models the
|
|
602
|
+
// CLI catalog does not cover.
|
|
603
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning);
|
|
581
604
|
return {
|
|
582
605
|
provider,
|
|
583
606
|
id: model,
|
|
@@ -586,10 +609,7 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
586
609
|
inputModalities: configured?.inputModalities ?? grokModalities(model),
|
|
587
610
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
588
611
|
defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
|
|
589
|
-
|
|
590
|
-
// cover expose none, so the harness rejects explicit efforts before
|
|
591
|
-
// provider I/O instead of the API 400ing.
|
|
592
|
-
...discovered?.reasoning === undefined ? {} : { reasoning: discovered.reasoning },
|
|
612
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
593
613
|
};
|
|
594
614
|
}
|
|
595
615
|
async *stream(options) {
|
|
@@ -614,8 +634,12 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
614
634
|
session = await this.options.tokens.session(account, true);
|
|
615
635
|
response = await this.request(options, session, watchdog.signal);
|
|
616
636
|
}
|
|
617
|
-
if (!response.ok)
|
|
618
|
-
throw await httpLlmError(response, 'grok API'
|
|
637
|
+
if (!response.ok) {
|
|
638
|
+
throw await httpLlmError(response, 'grok API', {
|
|
639
|
+
rateLimitReset: grokRateLimitReset,
|
|
640
|
+
...this.options.onWarn === undefined ? {} : { onWarn: this.options.onWarn },
|
|
641
|
+
});
|
|
642
|
+
}
|
|
619
643
|
if (response.body === null) {
|
|
620
644
|
throw new LlmError('grok API returned no response body', EMPTY_RESPONSE_CODE);
|
|
621
645
|
}
|
|
@@ -646,6 +670,12 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
646
670
|
...options.reasoningEffort !== undefined
|
|
647
671
|
? { reasoning: { effort: String(options.reasoningEffort) } }
|
|
648
672
|
: {},
|
|
673
|
+
// Cache-affinity hint (mirrors codex): xAI caches prompts per server,
|
|
674
|
+
// and `prompt_cache_key` is the Responses-API signal that routes repeat
|
|
675
|
+
// requests back to the cache-holding shard — without it every turn's
|
|
676
|
+
// cache hit is shard-routing luck. The session id is the stable key
|
|
677
|
+
// xAI's own docs recommend.
|
|
678
|
+
...options.sessionId !== undefined ? { prompt_cache_key: String(options.sessionId) } : {},
|
|
649
679
|
store: false,
|
|
650
680
|
stream: true,
|
|
651
681
|
};
|
|
@@ -43,14 +43,35 @@ export declare class PoolUsageTracker {
|
|
|
43
43
|
/**
|
|
44
44
|
* The quota view of one member. A cold cache awaits the first fetch; a
|
|
45
45
|
* stale one answers immediately while the refresh serves the NEXT call
|
|
46
|
-
* (member selection must never block on the network mid-conversation).
|
|
46
|
+
* (member selection must never block on the network mid-conversation). A
|
|
47
|
+
* failure still cooling down degrades immediately with no network call.
|
|
47
48
|
* @param member - the pool member to score (account resolved).
|
|
48
49
|
* @returns availability plus the urgency score.
|
|
49
50
|
*/
|
|
50
51
|
quotaFor(member: ConcretePoolMember): Promise<MemberQuota>;
|
|
52
|
+
/**
|
|
53
|
+
* Same cache as {@link quotaFor}, for direct display (the Settings page):
|
|
54
|
+
* the raw snapshot, or the original fetch error, instead of a routing
|
|
55
|
+
* score.
|
|
56
|
+
* @param provider - the account's provider.
|
|
57
|
+
* @param account - the account key.
|
|
58
|
+
* @param force - bypass a fresh cached SNAPSHOT for an honest re-check (the
|
|
59
|
+
* manual Refresh button). A live failure cooldown is never bypassed —
|
|
60
|
+
* retrying through it is exactly what turns a 429 into a permanent
|
|
61
|
+
* lockout, so even a forced call still answers from the negative cache.
|
|
62
|
+
* @returns `{ supported: false }` when the provider has no usage fetcher.
|
|
63
|
+
*/
|
|
64
|
+
snapshotFor(provider: ProviderId, account: string, force?: boolean): Promise<ProviderUsage>;
|
|
51
65
|
/** Drop cached snapshots: one account, or a whole provider when `account` is omitted. */
|
|
52
66
|
invalidate(provider: ProviderId, account?: string): void;
|
|
53
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Run (or join) the single in-flight fetch for one account key, caching
|
|
69
|
+
* either outcome. A missing/invalid credential is deliberately NOT
|
|
70
|
+
* negative-cached: it costs no network round trip (the session lookup
|
|
71
|
+
* fails before the request goes out) and re-checking live means the
|
|
72
|
+
* member rejoins routing the instant its login is fixed, rather than
|
|
73
|
+
* waiting out a stale cooldown.
|
|
74
|
+
*/
|
|
54
75
|
private refresh;
|
|
55
76
|
/** Score one member against a snapshot's windows. */
|
|
56
77
|
private score;
|