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
package/lib/providers/copilot.js
CHANGED
|
@@ -17,8 +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, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError,
|
|
20
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
21
|
+
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
21
22
|
import { proxiedFetch } from '../http.js';
|
|
23
|
+
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
22
24
|
/**
|
|
23
25
|
* Client id of the VS Code Copilot Chat GitHub App (pi-mono and
|
|
24
26
|
* copilot2api-go use the same value): the app is pre-authorized for the
|
|
@@ -243,15 +245,17 @@ function copilotReasoning(entry) {
|
|
|
243
245
|
* reasoning efforts (the endpoint discloses no default, so none is claimed).
|
|
244
246
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
245
247
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
248
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
246
249
|
* @returns discovered chat models in endpoint order.
|
|
247
250
|
*/
|
|
248
|
-
export async function fetchCopilotModels(session, fetchFn = proxiedFetch) {
|
|
251
|
+
export async function fetchCopilotModels(session, fetchFn = proxiedFetch, signal) {
|
|
249
252
|
const response = await fetchFn(COPILOT_MODELS_URL, {
|
|
250
253
|
headers: {
|
|
251
254
|
'authorization': `Bearer ${session.accessToken}`,
|
|
252
255
|
'accept': 'application/json',
|
|
253
256
|
...copilotHeaders(false, await latestVsCodeVersion(fetchFn)),
|
|
254
257
|
},
|
|
258
|
+
...signal === undefined ? {} : { signal },
|
|
255
259
|
});
|
|
256
260
|
if (!response.ok)
|
|
257
261
|
throw await oauthEndpointError(response, 'copilot models');
|
|
@@ -510,6 +514,10 @@ export class CopilotResponsesItemNormalizer {
|
|
|
510
514
|
export class CopilotAdapter extends LlmAdapter {
|
|
511
515
|
options;
|
|
512
516
|
catalog;
|
|
517
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
518
|
+
accountCatalogs = new Map();
|
|
519
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
520
|
+
catalogOwner;
|
|
513
521
|
/**
|
|
514
522
|
* [2026-08-23]-[a reasoning model continuing a tool chain must get its
|
|
515
523
|
* reasoning back or it restarts from scratch every tool round trip; the
|
|
@@ -532,12 +540,44 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
532
540
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
533
541
|
}
|
|
534
542
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
535
|
-
async fetchCatalog() {
|
|
536
|
-
return fetchCopilotModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
543
|
+
async fetchCatalog(account, signal) {
|
|
544
|
+
return fetchCopilotModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
545
|
+
}
|
|
546
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
547
|
+
clearAccountCatalog(account) {
|
|
548
|
+
if (account === undefined)
|
|
549
|
+
this.accountCatalogs.clear();
|
|
550
|
+
else
|
|
551
|
+
this.accountCatalogs.delete(account);
|
|
552
|
+
if (account === undefined || this.catalogOwner === account || this.catalogOwner === undefined) {
|
|
553
|
+
this.catalogOwner = undefined;
|
|
554
|
+
this.catalog.invalidate();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
558
|
+
async catalogFor(account) {
|
|
559
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
560
|
+
const key = account ?? defaultKey;
|
|
561
|
+
if (key === undefined || key === defaultKey) {
|
|
562
|
+
if (this.catalogOwner !== undefined && this.catalogOwner !== defaultKey) {
|
|
563
|
+
this.catalog.invalidate();
|
|
564
|
+
}
|
|
565
|
+
this.catalogOwner = defaultKey;
|
|
566
|
+
return this.catalog;
|
|
567
|
+
}
|
|
568
|
+
let cache = this.accountCatalogs.get(key);
|
|
569
|
+
if (cache === undefined) {
|
|
570
|
+
cache = new ModelCatalogCache();
|
|
571
|
+
this.accountCatalogs.set(key, cache);
|
|
572
|
+
}
|
|
573
|
+
return cache;
|
|
537
574
|
}
|
|
538
575
|
providerInfo(provider) {
|
|
539
576
|
return { id: provider, name: 'GitHub Copilot' };
|
|
540
577
|
}
|
|
578
|
+
providerRetryPolicy(provider) {
|
|
579
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `copilot: provider "${provider}" retryPolicy`);
|
|
580
|
+
}
|
|
541
581
|
staticModels(provider) {
|
|
542
582
|
return this.options.models.map(model => ({
|
|
543
583
|
provider,
|
|
@@ -547,17 +587,34 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
547
587
|
}));
|
|
548
588
|
}
|
|
549
589
|
async listModels(provider) {
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
if (
|
|
590
|
+
const own = await this.listOwnModels(provider);
|
|
591
|
+
const pool = this.options.pool?.();
|
|
592
|
+
if (pool === undefined)
|
|
593
|
+
return own;
|
|
594
|
+
const extra = await pool.modelsForProvider(provider);
|
|
595
|
+
const seen = new Set(own.map(model => model.id));
|
|
596
|
+
// Account pools reuse the catalog row; only configured tiers are extra.
|
|
597
|
+
return [...own, ...extra.filter(model => !seen.has(model.id))];
|
|
598
|
+
}
|
|
599
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
600
|
+
async listOwnModels(provider, account, signal) {
|
|
601
|
+
if (account === undefined) {
|
|
602
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
603
|
+
if (accounts.length === 0)
|
|
604
|
+
return [];
|
|
605
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), { timeoutMs: DISCOVERY_TIMEOUT_MS, ...signal === undefined ? {} : { signal } });
|
|
606
|
+
}
|
|
607
|
+
if (!await this.options.tokens.hasSession(account)) {
|
|
553
608
|
return [];
|
|
609
|
+
}
|
|
554
610
|
if (!this.options.discovery)
|
|
555
611
|
return this.staticModels(provider);
|
|
612
|
+
const catalog = await this.catalogFor(account);
|
|
556
613
|
try {
|
|
557
614
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
558
615
|
// through the refresh-aware path so an expired access token renews here
|
|
559
616
|
// instead of failing discovery into the static fallback.
|
|
560
|
-
const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(force),
|
|
617
|
+
const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)));
|
|
561
618
|
return discovered.map(model => ({
|
|
562
619
|
provider,
|
|
563
620
|
id: model.id,
|
|
@@ -567,6 +624,8 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
567
624
|
}));
|
|
568
625
|
}
|
|
569
626
|
catch (error) {
|
|
627
|
+
if (isDiscoveryAborted(error, signal))
|
|
628
|
+
throw error;
|
|
570
629
|
// A permanent refresh failure deletes the stored session: the provider
|
|
571
630
|
// is logged out, so hide it instead of showing a stale static catalog.
|
|
572
631
|
if (isMissingOrInvalidCredential(error))
|
|
@@ -584,8 +643,12 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
584
643
|
async discovered(model) {
|
|
585
644
|
if (!this.options.discovery)
|
|
586
645
|
return undefined;
|
|
587
|
-
const
|
|
588
|
-
return
|
|
646
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
647
|
+
return discoverAcrossAccounts(accounts, async (account) => {
|
|
648
|
+
const catalog = await this.catalogFor(account);
|
|
649
|
+
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
650
|
+
return models?.find(entry => entry.id === model);
|
|
651
|
+
});
|
|
589
652
|
}
|
|
590
653
|
/**
|
|
591
654
|
* [2026-08-23]-[a manually configured responses-only model combined with
|
|
@@ -699,8 +762,23 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
699
762
|
this.replayByScope.clear();
|
|
700
763
|
}
|
|
701
764
|
async resolveModel(provider, model) {
|
|
765
|
+
const pool = this.options.pool?.();
|
|
766
|
+
if (pool !== undefined && await pool.owns(provider, model)) {
|
|
767
|
+
return pool.resolveModel(provider, model);
|
|
768
|
+
}
|
|
769
|
+
return this.resolveOwnModel(provider, model);
|
|
770
|
+
}
|
|
771
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
772
|
+
async resolveOwnModel(provider, model) {
|
|
702
773
|
const discovered = await this.discovered(model);
|
|
703
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);
|
|
704
782
|
return {
|
|
705
783
|
provider,
|
|
706
784
|
id: model,
|
|
@@ -709,14 +787,22 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
709
787
|
inputModalities: discovered?.inputModalities ?? configured?.inputModalities ?? ['text'],
|
|
710
788
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? COPILOT_CONTEXT_WINDOW },
|
|
711
789
|
defaultMaxTokens: configured?.maxTokens ?? COPILOT_DEFAULT_MAX_TOKENS,
|
|
712
|
-
|
|
713
|
-
// model that did not advertise one exposes none, so the harness rejects
|
|
714
|
-
// an explicit effort before provider I/O instead of the API 400ing
|
|
715
|
-
// (Copilot returns invalid_request_body for models that cannot reason).
|
|
716
|
-
...discovered?.reasoning === undefined ? {} : { reasoning: discovered.reasoning },
|
|
790
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
717
791
|
};
|
|
718
792
|
}
|
|
719
793
|
async *stream(options) {
|
|
794
|
+
const pool = this.options.pool?.();
|
|
795
|
+
if (pool !== undefined && await pool.owns(options.provider, options.model)) {
|
|
796
|
+
yield* pool.stream(options);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
yield* this.streamCore(options);
|
|
800
|
+
}
|
|
801
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
802
|
+
streamAccount(options, account) {
|
|
803
|
+
return this.streamCore(options, account);
|
|
804
|
+
}
|
|
805
|
+
async *streamCore(options, account) {
|
|
720
806
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
721
807
|
try {
|
|
722
808
|
// The discovered catalog decides the protocol: `/responses`-only model
|
|
@@ -725,7 +811,7 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
725
811
|
// tools with a reasoning effort (gpt-5.4 400s on the chat wire then).
|
|
726
812
|
// A configured `wire` outranks the catalog (see configuredWireEntry).
|
|
727
813
|
const wire = copilotRequestWire(this.configuredWireEntry(options.model) ?? await this.discovered(options.model), options);
|
|
728
|
-
let session = await this.options.tokens.session();
|
|
814
|
+
let session = await this.options.tokens.session(account);
|
|
729
815
|
// Replay scope: account identity × conversation × model (see
|
|
730
816
|
// replayScope); a Copilot-token refresh preserves the GitHub token, so
|
|
731
817
|
// the 401 retry below reuses it too.
|
|
@@ -737,11 +823,17 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
737
823
|
// means GitHub raised its minimum VS Code version, and only a fresh
|
|
738
824
|
// Editor-Version header fixes that (a new token does not).
|
|
739
825
|
await latestVsCodeVersion(this.options.fetchFn ?? proxiedFetch, true);
|
|
740
|
-
session = await this.options.tokens.session(true);
|
|
826
|
+
session = await this.options.tokens.session(account, true);
|
|
741
827
|
response = await this.request(options, session, watchdog.signal, wire, scope);
|
|
742
828
|
}
|
|
743
|
-
if (!response.ok)
|
|
744
|
-
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
|
+
}
|
|
745
837
|
if (response.body === null) {
|
|
746
838
|
throw new LlmError('copilot API returned no response body', EMPTY_RESPONSE_CODE);
|
|
747
839
|
}
|
package/lib/providers/grok.d.ts
CHANGED
|
@@ -7,14 +7,27 @@ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
|
7
7
|
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
8
8
|
import type { FlowSpec } from '../auth/oauth-flow.js';
|
|
9
9
|
import type { GrokSession } from '../auth/store.js';
|
|
10
|
+
import type { PoolAdapter } from './pool.js';
|
|
10
11
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
11
|
-
import {
|
|
12
|
+
import { AccountTokenManager } from './accounts.js';
|
|
12
13
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
14
|
+
import type { RateLimitResetReader, RateLimitWait } from './rate-limit.js';
|
|
13
15
|
export declare const GROK_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
14
16
|
export declare const GROK_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration";
|
|
15
17
|
export declare const GROK_API_URL = "https://api.x.ai/v1/responses";
|
|
16
18
|
/** Refresh when the access token has less than this much life left. */
|
|
17
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;
|
|
18
31
|
/** Discovered OIDC endpoints for the xAI authorization server. */
|
|
19
32
|
export interface GrokDiscovery {
|
|
20
33
|
authorizationEndpoint: string;
|
|
@@ -93,9 +106,10 @@ type GrokCliModelMeta = Partial<Pick<DiscoveredModel, 'name' | 'description' | '
|
|
|
93
106
|
* Fetch the CLI catalog and index its per-model metadata by model id.
|
|
94
107
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
95
108
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
109
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
96
110
|
* @returns model id → contributed metadata.
|
|
97
111
|
*/
|
|
98
|
-
export declare function fetchGrokCliCatalog(session: GrokSession, fetchFn?: FetchFn): Promise<Map<string, GrokCliModelMeta>>;
|
|
112
|
+
export declare function fetchGrokCliCatalog(session: GrokSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<Map<string, GrokCliModelMeta>>;
|
|
99
113
|
/**
|
|
100
114
|
* Fetch the live grok model list, enriched with the CLI catalog's per-model
|
|
101
115
|
* metadata (display name, context window, reasoning efforts). The api.x.ai
|
|
@@ -109,14 +123,17 @@ export declare function fetchGrokCliCatalog(session: GrokSession, fetchFn?: Fetc
|
|
|
109
123
|
* @param onWarn - warning sink for a failed CLI catalog fetch.
|
|
110
124
|
* @param previous - last-known catalog used to keep enrichment when the CLI
|
|
111
125
|
* catalog is down or omits a model.
|
|
126
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
112
127
|
* @returns discovered chat models in endpoint order.
|
|
113
128
|
*/
|
|
114
|
-
export declare function fetchGrokModels(session: GrokSession, fetchFn?: FetchFn, onWarn?: (message: string) => void, previous?: readonly DiscoveredModel[]): Promise<DiscoveredModel[]>;
|
|
129
|
+
export declare function fetchGrokModels(session: GrokSession, fetchFn?: FetchFn, onWarn?: (message: string) => void, previous?: readonly DiscoveredModel[], signal?: AbortSignal): Promise<DiscoveredModel[]>;
|
|
115
130
|
/** Constructor dependencies for {@link GrokAdapter}. */
|
|
116
131
|
export interface GrokAdapterOptions {
|
|
117
132
|
models: readonly ModelEntry[];
|
|
118
133
|
streamIdleTimeoutMs: number;
|
|
119
|
-
tokens:
|
|
134
|
+
tokens: AccountTokenManager<GrokSession>;
|
|
135
|
+
/** Late-bound pool facade (wired after adapter construction); pools list under their first member's provider. */
|
|
136
|
+
pool?: () => PoolAdapter | undefined;
|
|
120
137
|
/** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
|
|
121
138
|
discovery: boolean;
|
|
122
139
|
/** Warning sink for discovery failures that fall back to the static catalog. */
|
|
@@ -127,18 +144,37 @@ export interface GrokAdapterOptions {
|
|
|
127
144
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
128
145
|
/** Durable catalog store seeding capability metadata across restarts. */
|
|
129
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;
|
|
130
155
|
}
|
|
131
156
|
/** Grok wire adapter: one instance serves the `grok` provider route. */
|
|
132
157
|
export declare class GrokAdapter extends LlmAdapter {
|
|
133
158
|
private readonly options;
|
|
134
159
|
private readonly catalog;
|
|
160
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
161
|
+
private readonly accountCatalogs;
|
|
162
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
163
|
+
private catalogOwner;
|
|
135
164
|
constructor(options: GrokAdapterOptions);
|
|
136
165
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
137
166
|
private fetchCatalog;
|
|
167
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
168
|
+
clearAccountCatalog(account?: string): void;
|
|
169
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
170
|
+
private catalogFor;
|
|
138
171
|
private listed;
|
|
139
172
|
providerInfo(provider: string): LlmProviderInfo;
|
|
173
|
+
providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy;
|
|
140
174
|
private staticModels;
|
|
141
175
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
176
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
177
|
+
listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
|
|
142
178
|
/**
|
|
143
179
|
* The discovered entry for one model. Resolved through the cache's
|
|
144
180
|
* stale-while-revalidate path: capability metadata must stay stable across
|
|
@@ -149,7 +185,12 @@ export declare class GrokAdapter extends LlmAdapter {
|
|
|
149
185
|
*/
|
|
150
186
|
private discovered;
|
|
151
187
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
188
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
189
|
+
resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
152
190
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
191
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
192
|
+
streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
|
|
193
|
+
private streamCore;
|
|
153
194
|
private request;
|
|
154
195
|
}
|
|
155
196
|
export {};
|
package/lib/providers/grok.js
CHANGED
|
@@ -7,8 +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, discoverOrRetryAuth, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError,
|
|
10
|
+
import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
11
|
+
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
11
12
|
import { proxiedFetch } from '../http.js';
|
|
13
|
+
import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, jsonBody, resetFromFields, subscriptionRetryPolicy, } from './rate-limit.js';
|
|
12
14
|
export const GROK_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
|
|
13
15
|
export const GROK_DISCOVERY_URL = 'https://auth.x.ai/.well-known/openid-configuration';
|
|
14
16
|
export const GROK_API_URL = 'https://api.x.ai/v1/responses';
|
|
@@ -18,6 +20,19 @@ const GROK_CONTEXT_WINDOW = 256_000;
|
|
|
18
20
|
const GROK_DEFAULT_MAX_TOKENS = 32_000;
|
|
19
21
|
/** Refresh when the access token has less than this much life left. */
|
|
20
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);
|
|
21
36
|
/** A discovered URL must be https on x.ai or a subdomain; anything else is a hostile document. */
|
|
22
37
|
function assertXaiEndpoint(url, field) {
|
|
23
38
|
let parsed;
|
|
@@ -312,9 +327,10 @@ function grokCliReasoning(entry) {
|
|
|
312
327
|
* Fetch the CLI catalog and index its per-model metadata by model id.
|
|
313
328
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
314
329
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
330
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
315
331
|
* @returns model id → contributed metadata.
|
|
316
332
|
*/
|
|
317
|
-
export async function fetchGrokCliCatalog(session, fetchFn = proxiedFetch) {
|
|
333
|
+
export async function fetchGrokCliCatalog(session, fetchFn = proxiedFetch, signal) {
|
|
318
334
|
const response = await fetchFn(GROK_CLI_MODELS_URL, {
|
|
319
335
|
headers: {
|
|
320
336
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -323,6 +339,7 @@ export async function fetchGrokCliCatalog(session, fetchFn = proxiedFetch) {
|
|
|
323
339
|
'accept': 'application/json',
|
|
324
340
|
...attributionHeaders(),
|
|
325
341
|
},
|
|
342
|
+
...signal === undefined ? {} : { signal },
|
|
326
343
|
});
|
|
327
344
|
if (!response.ok)
|
|
328
345
|
throw await oauthEndpointError(response, 'grok CLI catalog');
|
|
@@ -384,9 +401,10 @@ function grokPriorMeta(prior) {
|
|
|
384
401
|
* @param onWarn - warning sink for a failed CLI catalog fetch.
|
|
385
402
|
* @param previous - last-known catalog used to keep enrichment when the CLI
|
|
386
403
|
* catalog is down or omits a model.
|
|
404
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
387
405
|
* @returns discovered chat models in endpoint order.
|
|
388
406
|
*/
|
|
389
|
-
export async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, previous) {
|
|
407
|
+
export async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, previous, signal) {
|
|
390
408
|
const previousById = previous === undefined || previous.length === 0
|
|
391
409
|
? undefined
|
|
392
410
|
: new Map(previous.map(model => [model.id, model]));
|
|
@@ -397,8 +415,11 @@ export async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, p
|
|
|
397
415
|
'accept': 'application/json',
|
|
398
416
|
...attributionHeaders(),
|
|
399
417
|
},
|
|
418
|
+
...signal === undefined ? {} : { signal },
|
|
400
419
|
}),
|
|
401
|
-
fetchGrokCliCatalog(session, fetchFn).catch((error) => {
|
|
420
|
+
fetchGrokCliCatalog(session, fetchFn, signal).catch((error) => {
|
|
421
|
+
if (isDiscoveryAborted(error, signal))
|
|
422
|
+
throw error;
|
|
402
423
|
onWarn?.(previousById === undefined
|
|
403
424
|
? `grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`
|
|
404
425
|
: `grok CLI catalog fetch failed; keeping last-known reasoning efforts (${errorChain(error)})`);
|
|
@@ -436,14 +457,50 @@ export async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, p
|
|
|
436
457
|
export class GrokAdapter extends LlmAdapter {
|
|
437
458
|
options;
|
|
438
459
|
catalog;
|
|
460
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
461
|
+
accountCatalogs = new Map();
|
|
462
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
463
|
+
catalogOwner;
|
|
439
464
|
constructor(options) {
|
|
440
465
|
super();
|
|
441
466
|
this.options = options;
|
|
442
467
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
443
468
|
}
|
|
444
469
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
445
|
-
async fetchCatalog() {
|
|
446
|
-
|
|
470
|
+
async fetchCatalog(account, signal) {
|
|
471
|
+
const lastKnown = account === undefined || account === await this.options.tokens.defaultAccount()
|
|
472
|
+
? this.catalog.lastKnown()
|
|
473
|
+
: this.accountCatalogs.get(account)?.lastKnown();
|
|
474
|
+
return fetchGrokModels(await this.options.tokens.session(account), this.options.fetchFn, this.options.onWarn, lastKnown, signal);
|
|
475
|
+
}
|
|
476
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
477
|
+
clearAccountCatalog(account) {
|
|
478
|
+
if (account === undefined)
|
|
479
|
+
this.accountCatalogs.clear();
|
|
480
|
+
else
|
|
481
|
+
this.accountCatalogs.delete(account);
|
|
482
|
+
if (account === undefined || this.catalogOwner === account || this.catalogOwner === undefined) {
|
|
483
|
+
this.catalogOwner = undefined;
|
|
484
|
+
this.catalog.invalidate();
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
488
|
+
async catalogFor(account) {
|
|
489
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
490
|
+
const key = account ?? defaultKey;
|
|
491
|
+
if (key === undefined || key === defaultKey) {
|
|
492
|
+
if (this.catalogOwner !== undefined && this.catalogOwner !== defaultKey) {
|
|
493
|
+
this.catalog.invalidate();
|
|
494
|
+
}
|
|
495
|
+
this.catalogOwner = defaultKey;
|
|
496
|
+
return this.catalog;
|
|
497
|
+
}
|
|
498
|
+
let cache = this.accountCatalogs.get(key);
|
|
499
|
+
if (cache === undefined) {
|
|
500
|
+
cache = new ModelCatalogCache();
|
|
501
|
+
this.accountCatalogs.set(key, cache);
|
|
502
|
+
}
|
|
503
|
+
return cache;
|
|
447
504
|
}
|
|
448
505
|
listed(provider, discovered) {
|
|
449
506
|
return discovered.map(model => ({
|
|
@@ -457,6 +514,9 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
457
514
|
providerInfo(provider) {
|
|
458
515
|
return { id: provider, name: 'Grok (Subscription)' };
|
|
459
516
|
}
|
|
517
|
+
providerRetryPolicy(provider) {
|
|
518
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `grok: provider "${provider}" retryPolicy`);
|
|
519
|
+
}
|
|
460
520
|
staticModels(provider) {
|
|
461
521
|
return this.options.models.map(model => ({
|
|
462
522
|
provider,
|
|
@@ -466,19 +526,38 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
466
526
|
}));
|
|
467
527
|
}
|
|
468
528
|
async listModels(provider) {
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
if (
|
|
529
|
+
const own = await this.listOwnModels(provider);
|
|
530
|
+
const pool = this.options.pool?.();
|
|
531
|
+
if (pool === undefined)
|
|
532
|
+
return own;
|
|
533
|
+
const extra = await pool.modelsForProvider(provider);
|
|
534
|
+
const seen = new Set(own.map(model => model.id));
|
|
535
|
+
// Account pools reuse the catalog row; only configured tiers are extra.
|
|
536
|
+
return [...own, ...extra.filter(model => !seen.has(model.id))];
|
|
537
|
+
}
|
|
538
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
539
|
+
async listOwnModels(provider, account, signal) {
|
|
540
|
+
if (account === undefined) {
|
|
541
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
542
|
+
if (accounts.length === 0)
|
|
543
|
+
return [];
|
|
544
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), { timeoutMs: DISCOVERY_TIMEOUT_MS, ...signal === undefined ? {} : { signal } });
|
|
545
|
+
}
|
|
546
|
+
if (!await this.options.tokens.hasSession(account)) {
|
|
472
547
|
return [];
|
|
548
|
+
}
|
|
473
549
|
if (!this.options.discovery)
|
|
474
550
|
return this.staticModels(provider);
|
|
551
|
+
const catalog = await this.catalogFor(account);
|
|
475
552
|
try {
|
|
476
553
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
477
554
|
// through the refresh-aware path so an expired access token renews here
|
|
478
555
|
// instead of failing discovery into the static fallback.
|
|
479
|
-
return this.listed(provider, await discoverOrRetryAuth(force => this.options.tokens.session(force),
|
|
556
|
+
return this.listed(provider, await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal))));
|
|
480
557
|
}
|
|
481
558
|
catch (error) {
|
|
559
|
+
if (isDiscoveryAborted(error, signal))
|
|
560
|
+
throw error;
|
|
482
561
|
// A permanent refresh failure deletes the stored session: the provider
|
|
483
562
|
// is logged out, so hide it instead of showing a stale static catalog.
|
|
484
563
|
if (isMissingOrInvalidCredential(error))
|
|
@@ -498,12 +577,30 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
498
577
|
async discovered(model) {
|
|
499
578
|
if (!this.options.discovery)
|
|
500
579
|
return undefined;
|
|
501
|
-
const
|
|
502
|
-
return
|
|
580
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
581
|
+
return discoverAcrossAccounts(accounts, async (account) => {
|
|
582
|
+
const catalog = await this.catalogFor(account);
|
|
583
|
+
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
584
|
+
return models?.find(entry => entry.id === model);
|
|
585
|
+
});
|
|
503
586
|
}
|
|
504
587
|
async resolveModel(provider, model) {
|
|
588
|
+
const pool = this.options.pool?.();
|
|
589
|
+
if (pool !== undefined && await pool.owns(provider, model)) {
|
|
590
|
+
return pool.resolveModel(provider, model);
|
|
591
|
+
}
|
|
592
|
+
return this.resolveOwnModel(provider, model);
|
|
593
|
+
}
|
|
594
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
595
|
+
async resolveOwnModel(provider, model) {
|
|
505
596
|
const discovered = await this.discovered(model);
|
|
506
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);
|
|
507
604
|
return {
|
|
508
605
|
provider,
|
|
509
606
|
id: model,
|
|
@@ -512,24 +609,37 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
512
609
|
inputModalities: configured?.inputModalities ?? grokModalities(model),
|
|
513
610
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
514
611
|
defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
|
|
515
|
-
|
|
516
|
-
// cover expose none, so the harness rejects explicit efforts before
|
|
517
|
-
// provider I/O instead of the API 400ing.
|
|
518
|
-
...discovered?.reasoning === undefined ? {} : { reasoning: discovered.reasoning },
|
|
612
|
+
...reasoning === undefined ? {} : { reasoning },
|
|
519
613
|
};
|
|
520
614
|
}
|
|
521
615
|
async *stream(options) {
|
|
616
|
+
const pool = this.options.pool?.();
|
|
617
|
+
if (pool !== undefined && await pool.owns(options.provider, options.model)) {
|
|
618
|
+
yield* pool.stream(options);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
yield* this.streamCore(options);
|
|
622
|
+
}
|
|
623
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
624
|
+
streamAccount(options, account) {
|
|
625
|
+
return this.streamCore(options, account);
|
|
626
|
+
}
|
|
627
|
+
async *streamCore(options, account) {
|
|
522
628
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
523
629
|
try {
|
|
524
|
-
let session = await this.options.tokens.session();
|
|
630
|
+
let session = await this.options.tokens.session(account);
|
|
525
631
|
let response = await this.request(options, session, watchdog.signal);
|
|
526
632
|
if (response.status === 401) {
|
|
527
633
|
// One forced refresh + retry on an unexpired-but-rejected token.
|
|
528
|
-
session = await this.options.tokens.session(true);
|
|
634
|
+
session = await this.options.tokens.session(account, true);
|
|
529
635
|
response = await this.request(options, session, watchdog.signal);
|
|
530
636
|
}
|
|
531
|
-
if (!response.ok)
|
|
532
|
-
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
|
+
}
|
|
533
643
|
if (response.body === null) {
|
|
534
644
|
throw new LlmError('grok API returned no response body', EMPTY_RESPONSE_CODE);
|
|
535
645
|
}
|
|
@@ -560,6 +670,12 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
560
670
|
...options.reasoningEffort !== undefined
|
|
561
671
|
? { reasoning: { effort: String(options.reasoningEffort) } }
|
|
562
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) } : {},
|
|
563
679
|
store: false,
|
|
564
680
|
stream: true,
|
|
565
681
|
};
|