dsh-plugin-subscriptions 0.5.2 → 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 +79 -5
- package/README.zh.md +78 -4
- package/lib/auth/rpc.d.ts +64 -13
- package/lib/auth/rpc.js +75 -10
- package/lib/auth/store.d.ts +75 -17
- package/lib/auth/store.js +148 -27
- 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 +83 -3
- package/lib/client/SubscriptionsSection.js +411 -62
- 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 +46 -10
- package/lib/client/locales.js +46 -10
- package/lib/client.js +703 -132
- 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 +26 -1
- package/lib/index.js +2377 -309
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +237 -0
- package/lib/providers/accounts.d.ts +102 -0
- package/lib/providers/accounts.js +123 -0
- package/lib/providers/claude.d.ts +46 -7
- package/lib/providers/claude.js +125 -34
- package/lib/providers/codex.d.ts +45 -3
- package/lib/providers/codex.js +152 -26
- package/lib/providers/common.d.ts +87 -6
- package/lib/providers/common.js +185 -22
- package/lib/providers/copilot.d.ts +32 -3
- package/lib/providers/copilot.js +111 -19
- package/lib/providers/grok.d.ts +45 -4
- package/lib/providers/grok.js +136 -20
- package/lib/providers/pool-family.d.ts +56 -0
- package/lib/providers/pool-family.js +45 -0
- package/lib/providers/pool-health.d.ts +74 -0
- package/lib/providers/pool-health.js +148 -0
- package/lib/providers/pool-usage.d.ts +78 -0
- package/lib/providers/pool-usage.js +185 -0
- package/lib/providers/pool.d.ts +107 -0
- package/lib/providers/pool.js +371 -0
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/tools/image-generate.d.ts +3 -3
- package/lib/tools/image-generate.js +2 -1
- package/lib/tools/video-generate.d.ts +2 -2
- package/lib/tools/video-generate.js +2 -1
- package/lib/tools/x-search.d.ts +2 -2
- package/lib/tools/x-search.js +2 -1
- 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}.
|
|
@@ -141,6 +169,13 @@ export declare class TokenManager<S extends TimedSession> {
|
|
|
141
169
|
}
|
|
142
170
|
/** Fetch signature adapters accept for discovery calls (injectable for tests). */
|
|
143
171
|
export type FetchFn = typeof fetch;
|
|
172
|
+
/** Bound on one account catalog fetch or usage poll — a hang must not block the picker. */
|
|
173
|
+
export declare const DISCOVERY_TIMEOUT_MS = 10000;
|
|
174
|
+
/**
|
|
175
|
+
* Run `work` with an aborting signal. Resolves undefined when the timeout
|
|
176
|
+
* fires (the fetch is aborted); other failures propagate.
|
|
177
|
+
*/
|
|
178
|
+
export declare function withTimeout<T>(work: (signal: AbortSignal) => Promise<T>, timeoutMs: number): Promise<T | undefined>;
|
|
144
179
|
/** One rate-limit window reported by a provider's usage endpoint. */
|
|
145
180
|
export interface UsageWindow {
|
|
146
181
|
/** Window kind: `session` for the short rolling window, `weekly` for the 7-day one. */
|
|
@@ -196,6 +231,48 @@ export interface DiscoveredModel {
|
|
|
196
231
|
*/
|
|
197
232
|
copilotResponses?: boolean;
|
|
198
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;
|
|
270
|
+
/**
|
|
271
|
+
* First account catalog that lists `model` (callers pass default-first).
|
|
272
|
+
* One failing lookup sits that account out so a sibling's metadata still
|
|
273
|
+
* resolves — the same isolation as the picker catalog union.
|
|
274
|
+
*/
|
|
275
|
+
export declare function discoverAcrossAccounts(accounts: readonly string[], lookup: (account: string) => Promise<DiscoveredModel | undefined>): Promise<DiscoveredModel | undefined>;
|
|
199
276
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
200
277
|
export declare const DISCOVERY_TTL_MS: number;
|
|
201
278
|
/** A durable snapshot of one provider's discovered catalog. */
|
|
@@ -234,6 +311,8 @@ export declare class ModelCatalogCache {
|
|
|
234
311
|
private seeded;
|
|
235
312
|
/** Set by {@link invalidate} so an in-flight disk read cannot resurrect dropped state. */
|
|
236
313
|
private seedDisabled;
|
|
314
|
+
/** Bumped by {@link invalidate} so a loser in-flight fetch cannot write back. */
|
|
315
|
+
private generation;
|
|
237
316
|
constructor(persistence?: CatalogPersistence | undefined, ttlMs?: number);
|
|
238
317
|
/**
|
|
239
318
|
* The cached catalog when fresh, without fetching.
|
|
@@ -272,6 +351,8 @@ export declare class ModelCatalogCache {
|
|
|
272
351
|
}
|
|
273
352
|
/** Whether discovery failed because the stored login is gone. */
|
|
274
353
|
export declare function isMissingOrInvalidCredential(error: unknown): boolean;
|
|
354
|
+
/** Whether discovery stopped because the caller cancelled or the timeout fired. */
|
|
355
|
+
export declare function isDiscoveryAborted(error: unknown, signal?: AbortSignal): boolean;
|
|
275
356
|
/**
|
|
276
357
|
* Run a catalog fetch, retrying once after a forced token refresh when the
|
|
277
358
|
* first attempt is a 401/AUTH. Only {@link ModelCatalogCache.invalidate}s
|
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
|
|
@@ -263,6 +308,101 @@ export class TokenManager {
|
|
|
263
308
|
return next;
|
|
264
309
|
}
|
|
265
310
|
}
|
|
311
|
+
/** Bound on one account catalog fetch or usage poll — a hang must not block the picker. */
|
|
312
|
+
export const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
313
|
+
/**
|
|
314
|
+
* Run `work` with an aborting signal. Resolves undefined when the timeout
|
|
315
|
+
* fires (the fetch is aborted); other failures propagate.
|
|
316
|
+
*/
|
|
317
|
+
export function withTimeout(work, timeoutMs) {
|
|
318
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
319
|
+
const aborted = new Promise(resolve => {
|
|
320
|
+
if (signal.aborted)
|
|
321
|
+
resolve(undefined);
|
|
322
|
+
else
|
|
323
|
+
signal.addEventListener('abort', () => resolve(undefined), { once: true });
|
|
324
|
+
});
|
|
325
|
+
return Promise.race([
|
|
326
|
+
work(signal).then(value => (signal.aborted ? undefined : value), (error) => {
|
|
327
|
+
if (signal.aborted)
|
|
328
|
+
return undefined;
|
|
329
|
+
throw error;
|
|
330
|
+
}),
|
|
331
|
+
aborted,
|
|
332
|
+
]);
|
|
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
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* First account catalog that lists `model` (callers pass default-first).
|
|
390
|
+
* One failing lookup sits that account out so a sibling's metadata still
|
|
391
|
+
* resolves — the same isolation as the picker catalog union.
|
|
392
|
+
*/
|
|
393
|
+
export async function discoverAcrossAccounts(accounts, lookup) {
|
|
394
|
+
for (const account of accounts) {
|
|
395
|
+
try {
|
|
396
|
+
const found = await lookup(account);
|
|
397
|
+
if (found !== undefined)
|
|
398
|
+
return found;
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// sit out
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return undefined;
|
|
405
|
+
}
|
|
266
406
|
/** How long a discovered catalog is trusted before re-fetching. */
|
|
267
407
|
export const DISCOVERY_TTL_MS = 5 * 60_000;
|
|
268
408
|
/**
|
|
@@ -286,6 +426,8 @@ export class ModelCatalogCache {
|
|
|
286
426
|
seeded;
|
|
287
427
|
/** Set by {@link invalidate} so an in-flight disk read cannot resurrect dropped state. */
|
|
288
428
|
seedDisabled = false;
|
|
429
|
+
/** Bumped by {@link invalidate} so a loser in-flight fetch cannot write back. */
|
|
430
|
+
generation = 0;
|
|
289
431
|
constructor(persistence, ttlMs = DISCOVERY_TTL_MS) {
|
|
290
432
|
this.persistence = persistence;
|
|
291
433
|
this.ttlMs = ttlMs;
|
|
@@ -320,16 +462,25 @@ export class ModelCatalogCache {
|
|
|
320
462
|
}
|
|
321
463
|
/** Run (or join) the single in-flight fetch, updating memory and disk on success. */
|
|
322
464
|
refresh(fetcher) {
|
|
323
|
-
this.inflight
|
|
465
|
+
if (this.inflight !== undefined)
|
|
466
|
+
return this.inflight;
|
|
467
|
+
const gen = this.generation;
|
|
468
|
+
const pending = fetcher()
|
|
324
469
|
.then((models) => {
|
|
470
|
+
if (this.generation !== gen)
|
|
471
|
+
return models;
|
|
325
472
|
const snapshot = { at: Date.now(), models };
|
|
326
473
|
this.entry = snapshot;
|
|
327
474
|
// Write-through is fire-and-forget: a failed save only costs durability.
|
|
328
475
|
void this.persistence?.save(snapshot).catch(() => undefined);
|
|
329
476
|
return models;
|
|
330
477
|
})
|
|
331
|
-
.finally(() => {
|
|
332
|
-
|
|
478
|
+
.finally(() => {
|
|
479
|
+
if (this.generation === gen)
|
|
480
|
+
this.inflight = undefined;
|
|
481
|
+
});
|
|
482
|
+
this.inflight = pending;
|
|
483
|
+
return pending;
|
|
333
484
|
}
|
|
334
485
|
/**
|
|
335
486
|
* Return the cached catalog when fresh, otherwise fetch and cache it.
|
|
@@ -370,7 +521,9 @@ export class ModelCatalogCache {
|
|
|
370
521
|
}
|
|
371
522
|
/** Drop the cached catalog (e.g. after a 401 proved the credential changed). */
|
|
372
523
|
invalidate() {
|
|
524
|
+
this.generation += 1;
|
|
373
525
|
this.entry = undefined;
|
|
526
|
+
this.inflight = undefined;
|
|
374
527
|
this.seedDisabled = true;
|
|
375
528
|
void this.persistence?.clear().catch(() => undefined);
|
|
376
529
|
}
|
|
@@ -380,6 +533,16 @@ export function isMissingOrInvalidCredential(error) {
|
|
|
380
533
|
return error instanceof LlmError
|
|
381
534
|
&& (error.code === 'MISSING_CREDENTIAL' || error.code === 'INVALID_CREDENTIAL');
|
|
382
535
|
}
|
|
536
|
+
/** Whether discovery stopped because the caller cancelled or the timeout fired. */
|
|
537
|
+
export function isDiscoveryAborted(error, signal) {
|
|
538
|
+
if (signal?.aborted === true)
|
|
539
|
+
return true;
|
|
540
|
+
// Only treat abort-shaped errors as cancellation when this call had a signal;
|
|
541
|
+
// a refresh TimeoutError must not fail the whole picker union.
|
|
542
|
+
return signal !== undefined
|
|
543
|
+
&& error instanceof Error
|
|
544
|
+
&& (error.name === 'AbortError' || error.name === 'TimeoutError');
|
|
545
|
+
}
|
|
383
546
|
/** Whether discovery failed because the access token was rejected. */
|
|
384
547
|
function isDiscoveryAuthFailure(error) {
|
|
385
548
|
return (error instanceof OAuthEndpointError && error.status === 401)
|
|
@@ -17,10 +17,12 @@ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
|
17
17
|
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
18
18
|
import type { DeviceFlowSpec } from '../auth/device-flow.js';
|
|
19
19
|
import type { CopilotSession } from '../auth/store.js';
|
|
20
|
+
import type { PoolAdapter } from './pool.js';
|
|
20
21
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
21
22
|
import type { ReasoningReplayItem, ResponsesRequestInput, ResponsesStreamEvent } from '../translate/responses.js';
|
|
22
|
-
import {
|
|
23
|
+
import { AccountTokenManager } from './accounts.js';
|
|
23
24
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry } from './common.js';
|
|
25
|
+
import type { RateLimitWait } from './rate-limit.js';
|
|
24
26
|
/**
|
|
25
27
|
* Client id of the VS Code Copilot Chat GitHub App (pi-mono and
|
|
26
28
|
* copilot2api-go use the same value): the app is pre-authorized for the
|
|
@@ -118,9 +120,10 @@ export declare function isCopilotPermanentRefreshError(error: unknown): boolean;
|
|
|
118
120
|
* reasoning efforts (the endpoint discloses no default, so none is claimed).
|
|
119
121
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
120
122
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
123
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
121
124
|
* @returns discovered chat models in endpoint order.
|
|
122
125
|
*/
|
|
123
|
-
export declare function fetchCopilotModels(session: CopilotSession, fetchFn?: FetchFn): Promise<DiscoveredModel[]>;
|
|
126
|
+
export declare function fetchCopilotModels(session: CopilotSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<DiscoveredModel[]>;
|
|
124
127
|
/** Which upstream protocol one Copilot model speaks. */
|
|
125
128
|
export type CopilotWire = 'chat-completions' | 'responses';
|
|
126
129
|
/**
|
|
@@ -215,7 +218,9 @@ export declare class CopilotResponsesItemNormalizer {
|
|
|
215
218
|
export interface CopilotAdapterOptions {
|
|
216
219
|
models: readonly ModelEntry[];
|
|
217
220
|
streamIdleTimeoutMs: number;
|
|
218
|
-
tokens:
|
|
221
|
+
tokens: AccountTokenManager<CopilotSession>;
|
|
222
|
+
/** Late-bound pool facade (wired after adapter construction); pools list under their first member's provider. */
|
|
223
|
+
pool?: () => PoolAdapter | undefined;
|
|
219
224
|
/** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
|
|
220
225
|
discovery: boolean;
|
|
221
226
|
/** Warning sink for discovery failures that fall back to the static catalog. */
|
|
@@ -226,11 +231,23 @@ export interface CopilotAdapterOptions {
|
|
|
226
231
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
227
232
|
/** Durable catalog store seeding capability metadata across restarts. */
|
|
228
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;
|
|
229
242
|
}
|
|
230
243
|
/** Copilot wire adapter: one instance serves the `copilot` provider route. */
|
|
231
244
|
export declare class CopilotAdapter extends LlmAdapter {
|
|
232
245
|
private readonly options;
|
|
233
246
|
private readonly catalog;
|
|
247
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
248
|
+
private readonly accountCatalogs;
|
|
249
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
250
|
+
private catalogOwner;
|
|
234
251
|
/**
|
|
235
252
|
* [2026-08-23]-[a reasoning model continuing a tool chain must get its
|
|
236
253
|
* reasoning back or it restarts from scratch every tool round trip; the
|
|
@@ -250,9 +267,16 @@ export declare class CopilotAdapter extends LlmAdapter {
|
|
|
250
267
|
constructor(options: CopilotAdapterOptions);
|
|
251
268
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
252
269
|
private fetchCatalog;
|
|
270
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
271
|
+
clearAccountCatalog(account?: string): void;
|
|
272
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
273
|
+
private catalogFor;
|
|
253
274
|
providerInfo(provider: string): LlmProviderInfo;
|
|
275
|
+
providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy;
|
|
254
276
|
private staticModels;
|
|
255
277
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
278
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
279
|
+
listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
|
|
256
280
|
/**
|
|
257
281
|
* The discovered entry for one model. Resolved through the cache's
|
|
258
282
|
* stale-while-revalidate path: capability metadata must stay stable across
|
|
@@ -309,7 +333,12 @@ export declare class CopilotAdapter extends LlmAdapter {
|
|
|
309
333
|
*/
|
|
310
334
|
clearReplayState(): void;
|
|
311
335
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
336
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
337
|
+
resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
312
338
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
339
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
340
|
+
streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
|
|
341
|
+
private streamCore;
|
|
313
342
|
private request;
|
|
314
343
|
}
|
|
315
344
|
export {};
|