dsh-plugin-subscriptions 0.5.1 → 0.5.3
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 +42 -1
- package/README.zh.md +42 -1
- package/lib/auth/device-flow.d.ts +0 -9
- package/lib/auth/device-flow.js +2 -1
- package/lib/auth/rpc.d.ts +44 -13
- package/lib/auth/rpc.js +127 -9
- package/lib/auth/store.d.ts +75 -17
- package/lib/auth/store.js +148 -27
- package/lib/client/SubscriptionsSection.d.ts +26 -3
- package/lib/client/SubscriptionsSection.js +263 -67
- package/lib/client/index.js +11 -0
- package/lib/client/locales.d.ts +82 -10
- package/lib/client/locales.js +82 -10
- package/lib/client.js +837 -223
- package/lib/client.js.map +1 -1
- package/lib/http.d.ts +114 -0
- package/lib/http.js +402 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.js +1938 -208
- package/lib/providers/accounts.d.ts +102 -0
- package/lib/providers/accounts.js +123 -0
- package/lib/providers/antigravity.d.ts +90 -0
- package/lib/providers/antigravity.js +392 -0
- package/lib/providers/claude.d.ts +22 -4
- package/lib/providers/claude.js +97 -16
- package/lib/providers/codex.d.ts +24 -3
- package/lib/providers/codex.js +121 -21
- package/lib/providers/common.d.ts +17 -0
- package/lib/providers/common.js +67 -3
- package/lib/providers/copilot.d.ts +23 -4
- package/lib/providers/copilot.js +99 -19
- package/lib/providers/grok.d.ts +24 -4
- package/lib/providers/grok.js +106 -19
- 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 +57 -0
- package/lib/providers/pool-usage.js +130 -0
- package/lib/providers/pool.d.ts +107 -0
- package/lib/providers/pool.js +371 -0
- package/lib/tools/image-generate.d.ts +3 -3
- package/lib/tools/image-generate.js +4 -2
- package/lib/tools/video-generate.d.ts +2 -2
- package/lib/tools/video-generate.js +4 -2
- package/lib/tools/x-search.d.ts +2 -2
- package/lib/tools/x-search.js +4 -2
- package/lib/translate/antigravity.d.ts +110 -0
- package/lib/translate/antigravity.js +303 -0
- package/package.json +14 -9
package/lib/providers/copilot.js
CHANGED
|
@@ -17,7 +17,9 @@ 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, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
21
|
+
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
22
|
+
import { proxiedFetch } from '../http.js';
|
|
21
23
|
/**
|
|
22
24
|
* Client id of the VS Code Copilot Chat GitHub App (pi-mono and
|
|
23
25
|
* copilot2api-go use the same value): the app is pre-authorized for the
|
|
@@ -57,7 +59,7 @@ let vscodeVersionInflight;
|
|
|
57
59
|
* @param forceRefresh - bypass the cache (a 401 `IDE token expired` retry).
|
|
58
60
|
* @returns a `major.minor.patch` version string.
|
|
59
61
|
*/
|
|
60
|
-
export async function latestVsCodeVersion(fetchFn =
|
|
62
|
+
export async function latestVsCodeVersion(fetchFn = proxiedFetch, forceRefresh = false) {
|
|
61
63
|
if (!forceRefresh && vscodeVersionCache !== undefined
|
|
62
64
|
&& Date.now() - vscodeVersionCache.at < VSCODE_VERSION_TTL_MS) {
|
|
63
65
|
return vscodeVersionCache.version;
|
|
@@ -122,7 +124,7 @@ export function copilotHeaders(hasVision = false, vscodeVersion = FALLBACK_VSCOD
|
|
|
122
124
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
123
125
|
* @returns the Copilot API token and its expiry.
|
|
124
126
|
*/
|
|
125
|
-
export async function exchangeCopilotToken(githubToken, fetchFn =
|
|
127
|
+
export async function exchangeCopilotToken(githubToken, fetchFn = proxiedFetch) {
|
|
126
128
|
const response = await fetchFn(COPILOT_TOKEN_URL, {
|
|
127
129
|
headers: {
|
|
128
130
|
'authorization': `Bearer ${githubToken}`,
|
|
@@ -152,7 +154,7 @@ export async function exchangeCopilotToken(githubToken, fetchFn = fetch) {
|
|
|
152
154
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
153
155
|
* @returns the session to store.
|
|
154
156
|
*/
|
|
155
|
-
export async function completeCopilotLogin(githubToken, fetchFn =
|
|
157
|
+
export async function completeCopilotLogin(githubToken, fetchFn = proxiedFetch) {
|
|
156
158
|
const pair = await exchangeCopilotToken(githubToken, fetchFn);
|
|
157
159
|
let account;
|
|
158
160
|
try {
|
|
@@ -187,7 +189,7 @@ export async function completeCopilotLogin(githubToken, fetchFn = fetch) {
|
|
|
187
189
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
188
190
|
* @returns the fresh session to store.
|
|
189
191
|
*/
|
|
190
|
-
export async function refreshCopilot(session, fetchFn =
|
|
192
|
+
export async function refreshCopilot(session, fetchFn = proxiedFetch) {
|
|
191
193
|
const pair = await exchangeCopilotToken(session.refreshToken, fetchFn);
|
|
192
194
|
return {
|
|
193
195
|
accessToken: pair.accessToken,
|
|
@@ -242,15 +244,17 @@ function copilotReasoning(entry) {
|
|
|
242
244
|
* reasoning efforts (the endpoint discloses no default, so none is claimed).
|
|
243
245
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
244
246
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
247
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
245
248
|
* @returns discovered chat models in endpoint order.
|
|
246
249
|
*/
|
|
247
|
-
export async function fetchCopilotModels(session, fetchFn =
|
|
250
|
+
export async function fetchCopilotModels(session, fetchFn = proxiedFetch, signal) {
|
|
248
251
|
const response = await fetchFn(COPILOT_MODELS_URL, {
|
|
249
252
|
headers: {
|
|
250
253
|
'authorization': `Bearer ${session.accessToken}`,
|
|
251
254
|
'accept': 'application/json',
|
|
252
255
|
...copilotHeaders(false, await latestVsCodeVersion(fetchFn)),
|
|
253
256
|
},
|
|
257
|
+
...signal === undefined ? {} : { signal },
|
|
254
258
|
});
|
|
255
259
|
if (!response.ok)
|
|
256
260
|
throw await oauthEndpointError(response, 'copilot models');
|
|
@@ -509,6 +513,10 @@ export class CopilotResponsesItemNormalizer {
|
|
|
509
513
|
export class CopilotAdapter extends LlmAdapter {
|
|
510
514
|
options;
|
|
511
515
|
catalog;
|
|
516
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
517
|
+
accountCatalogs = new Map();
|
|
518
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
519
|
+
catalogOwner;
|
|
512
520
|
/**
|
|
513
521
|
* [2026-08-23]-[a reasoning model continuing a tool chain must get its
|
|
514
522
|
* reasoning back or it restarts from scratch every tool round trip; the
|
|
@@ -531,8 +539,37 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
531
539
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
532
540
|
}
|
|
533
541
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
534
|
-
async fetchCatalog() {
|
|
535
|
-
return fetchCopilotModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
542
|
+
async fetchCatalog(account, signal) {
|
|
543
|
+
return fetchCopilotModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
544
|
+
}
|
|
545
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
546
|
+
clearAccountCatalog(account) {
|
|
547
|
+
if (account === undefined)
|
|
548
|
+
this.accountCatalogs.clear();
|
|
549
|
+
else
|
|
550
|
+
this.accountCatalogs.delete(account);
|
|
551
|
+
if (account === undefined || this.catalogOwner === account || this.catalogOwner === undefined) {
|
|
552
|
+
this.catalogOwner = undefined;
|
|
553
|
+
this.catalog.invalidate();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
557
|
+
async catalogFor(account) {
|
|
558
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
559
|
+
const key = account ?? defaultKey;
|
|
560
|
+
if (key === undefined || key === defaultKey) {
|
|
561
|
+
if (this.catalogOwner !== undefined && this.catalogOwner !== defaultKey) {
|
|
562
|
+
this.catalog.invalidate();
|
|
563
|
+
}
|
|
564
|
+
this.catalogOwner = defaultKey;
|
|
565
|
+
return this.catalog;
|
|
566
|
+
}
|
|
567
|
+
let cache = this.accountCatalogs.get(key);
|
|
568
|
+
if (cache === undefined) {
|
|
569
|
+
cache = new ModelCatalogCache();
|
|
570
|
+
this.accountCatalogs.set(key, cache);
|
|
571
|
+
}
|
|
572
|
+
return cache;
|
|
536
573
|
}
|
|
537
574
|
providerInfo(provider) {
|
|
538
575
|
return { id: provider, name: 'GitHub Copilot' };
|
|
@@ -546,17 +583,34 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
546
583
|
}));
|
|
547
584
|
}
|
|
548
585
|
async listModels(provider) {
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
if (
|
|
586
|
+
const own = await this.listOwnModels(provider);
|
|
587
|
+
const pool = this.options.pool?.();
|
|
588
|
+
if (pool === undefined)
|
|
589
|
+
return own;
|
|
590
|
+
const extra = await pool.modelsForProvider(provider);
|
|
591
|
+
const seen = new Set(own.map(model => model.id));
|
|
592
|
+
// Account pools reuse the catalog row; only configured tiers are extra.
|
|
593
|
+
return [...own, ...extra.filter(model => !seen.has(model.id))];
|
|
594
|
+
}
|
|
595
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
596
|
+
async listOwnModels(provider, account, signal) {
|
|
597
|
+
if (account === undefined) {
|
|
598
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
599
|
+
if (accounts.length === 0)
|
|
600
|
+
return [];
|
|
601
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), { timeoutMs: DISCOVERY_TIMEOUT_MS, ...signal === undefined ? {} : { signal } });
|
|
602
|
+
}
|
|
603
|
+
if (!await this.options.tokens.hasSession(account)) {
|
|
552
604
|
return [];
|
|
605
|
+
}
|
|
553
606
|
if (!this.options.discovery)
|
|
554
607
|
return this.staticModels(provider);
|
|
608
|
+
const catalog = await this.catalogFor(account);
|
|
555
609
|
try {
|
|
556
610
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
557
611
|
// through the refresh-aware path so an expired access token renews here
|
|
558
612
|
// instead of failing discovery into the static fallback.
|
|
559
|
-
const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(force),
|
|
613
|
+
const discovered = await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)));
|
|
560
614
|
return discovered.map(model => ({
|
|
561
615
|
provider,
|
|
562
616
|
id: model.id,
|
|
@@ -566,6 +620,8 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
566
620
|
}));
|
|
567
621
|
}
|
|
568
622
|
catch (error) {
|
|
623
|
+
if (isDiscoveryAborted(error, signal))
|
|
624
|
+
throw error;
|
|
569
625
|
// A permanent refresh failure deletes the stored session: the provider
|
|
570
626
|
// is logged out, so hide it instead of showing a stale static catalog.
|
|
571
627
|
if (isMissingOrInvalidCredential(error))
|
|
@@ -583,8 +639,12 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
583
639
|
async discovered(model) {
|
|
584
640
|
if (!this.options.discovery)
|
|
585
641
|
return undefined;
|
|
586
|
-
const
|
|
587
|
-
return
|
|
642
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
643
|
+
return discoverAcrossAccounts(accounts, async (account) => {
|
|
644
|
+
const catalog = await this.catalogFor(account);
|
|
645
|
+
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
646
|
+
return models?.find(entry => entry.id === model);
|
|
647
|
+
});
|
|
588
648
|
}
|
|
589
649
|
/**
|
|
590
650
|
* [2026-08-23]-[a manually configured responses-only model combined with
|
|
@@ -698,6 +758,14 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
698
758
|
this.replayByScope.clear();
|
|
699
759
|
}
|
|
700
760
|
async resolveModel(provider, model) {
|
|
761
|
+
const pool = this.options.pool?.();
|
|
762
|
+
if (pool !== undefined && await pool.owns(provider, model)) {
|
|
763
|
+
return pool.resolveModel(provider, model);
|
|
764
|
+
}
|
|
765
|
+
return this.resolveOwnModel(provider, model);
|
|
766
|
+
}
|
|
767
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
768
|
+
async resolveOwnModel(provider, model) {
|
|
701
769
|
const discovered = await this.discovered(model);
|
|
702
770
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
703
771
|
return {
|
|
@@ -716,6 +784,18 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
716
784
|
};
|
|
717
785
|
}
|
|
718
786
|
async *stream(options) {
|
|
787
|
+
const pool = this.options.pool?.();
|
|
788
|
+
if (pool !== undefined && await pool.owns(options.provider, options.model)) {
|
|
789
|
+
yield* pool.stream(options);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
yield* this.streamCore(options);
|
|
793
|
+
}
|
|
794
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
795
|
+
streamAccount(options, account) {
|
|
796
|
+
return this.streamCore(options, account);
|
|
797
|
+
}
|
|
798
|
+
async *streamCore(options, account) {
|
|
719
799
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
720
800
|
try {
|
|
721
801
|
// The discovered catalog decides the protocol: `/responses`-only model
|
|
@@ -724,7 +804,7 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
724
804
|
// tools with a reasoning effort (gpt-5.4 400s on the chat wire then).
|
|
725
805
|
// A configured `wire` outranks the catalog (see configuredWireEntry).
|
|
726
806
|
const wire = copilotRequestWire(this.configuredWireEntry(options.model) ?? await this.discovered(options.model), options);
|
|
727
|
-
let session = await this.options.tokens.session();
|
|
807
|
+
let session = await this.options.tokens.session(account);
|
|
728
808
|
// Replay scope: account identity × conversation × model (see
|
|
729
809
|
// replayScope); a Copilot-token refresh preserves the GitHub token, so
|
|
730
810
|
// the 401 retry below reuses it too.
|
|
@@ -735,8 +815,8 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
735
815
|
// editor version is force-refreshed too: a 401 `IDE token expired`
|
|
736
816
|
// means GitHub raised its minimum VS Code version, and only a fresh
|
|
737
817
|
// Editor-Version header fixes that (a new token does not).
|
|
738
|
-
await latestVsCodeVersion(this.options.fetchFn ??
|
|
739
|
-
session = await this.options.tokens.session(true);
|
|
818
|
+
await latestVsCodeVersion(this.options.fetchFn ?? proxiedFetch, true);
|
|
819
|
+
session = await this.options.tokens.session(account, true);
|
|
740
820
|
response = await this.request(options, session, watchdog.signal, wire, scope);
|
|
741
821
|
}
|
|
742
822
|
if (!response.ok)
|
|
@@ -771,13 +851,13 @@ export class CopilotAdapter extends LlmAdapter {
|
|
|
771
851
|
// Captured completed reasoning replays ahead of its tool call.
|
|
772
852
|
callId => this.replayFor(replayScopeKey, callId)))
|
|
773
853
|
: copilotChatRequestBody(options, toChatMessages(messages, options.system));
|
|
774
|
-
return
|
|
854
|
+
return proxiedFetch(wire === 'responses' ? COPILOT_RESPONSES_URL : COPILOT_API_URL, {
|
|
775
855
|
method: 'POST',
|
|
776
856
|
headers: {
|
|
777
857
|
'authorization': `Bearer ${session.accessToken}`,
|
|
778
858
|
'accept': 'text/event-stream',
|
|
779
859
|
'content-type': 'application/json',
|
|
780
|
-
...copilotHeaders(hasVision, await latestVsCodeVersion(this.options.fetchFn ??
|
|
860
|
+
...copilotHeaders(hasVision, await latestVsCodeVersion(this.options.fetchFn ?? proxiedFetch)),
|
|
781
861
|
},
|
|
782
862
|
body: JSON.stringify(body),
|
|
783
863
|
signal,
|
package/lib/providers/grok.d.ts
CHANGED
|
@@ -7,8 +7,9 @@ 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';
|
|
13
14
|
export declare const GROK_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
|
|
14
15
|
export declare const GROK_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration";
|
|
@@ -93,9 +94,10 @@ type GrokCliModelMeta = Partial<Pick<DiscoveredModel, 'name' | 'description' | '
|
|
|
93
94
|
* Fetch the CLI catalog and index its per-model metadata by model id.
|
|
94
95
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
95
96
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
97
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
96
98
|
* @returns model id → contributed metadata.
|
|
97
99
|
*/
|
|
98
|
-
export declare function fetchGrokCliCatalog(session: GrokSession, fetchFn?: FetchFn): Promise<Map<string, GrokCliModelMeta>>;
|
|
100
|
+
export declare function fetchGrokCliCatalog(session: GrokSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<Map<string, GrokCliModelMeta>>;
|
|
99
101
|
/**
|
|
100
102
|
* Fetch the live grok model list, enriched with the CLI catalog's per-model
|
|
101
103
|
* metadata (display name, context window, reasoning efforts). The api.x.ai
|
|
@@ -109,14 +111,17 @@ export declare function fetchGrokCliCatalog(session: GrokSession, fetchFn?: Fetc
|
|
|
109
111
|
* @param onWarn - warning sink for a failed CLI catalog fetch.
|
|
110
112
|
* @param previous - last-known catalog used to keep enrichment when the CLI
|
|
111
113
|
* catalog is down or omits a model.
|
|
114
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
112
115
|
* @returns discovered chat models in endpoint order.
|
|
113
116
|
*/
|
|
114
|
-
export declare function fetchGrokModels(session: GrokSession, fetchFn?: FetchFn, onWarn?: (message: string) => void, previous?: readonly DiscoveredModel[]): Promise<DiscoveredModel[]>;
|
|
117
|
+
export declare function fetchGrokModels(session: GrokSession, fetchFn?: FetchFn, onWarn?: (message: string) => void, previous?: readonly DiscoveredModel[], signal?: AbortSignal): Promise<DiscoveredModel[]>;
|
|
115
118
|
/** Constructor dependencies for {@link GrokAdapter}. */
|
|
116
119
|
export interface GrokAdapterOptions {
|
|
117
120
|
models: readonly ModelEntry[];
|
|
118
121
|
streamIdleTimeoutMs: number;
|
|
119
|
-
tokens:
|
|
122
|
+
tokens: AccountTokenManager<GrokSession>;
|
|
123
|
+
/** Late-bound pool facade (wired after adapter construction); pools list under their first member's provider. */
|
|
124
|
+
pool?: () => PoolAdapter | undefined;
|
|
120
125
|
/** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
|
|
121
126
|
discovery: boolean;
|
|
122
127
|
/** Warning sink for discovery failures that fall back to the static catalog. */
|
|
@@ -132,13 +137,23 @@ export interface GrokAdapterOptions {
|
|
|
132
137
|
export declare class GrokAdapter extends LlmAdapter {
|
|
133
138
|
private readonly options;
|
|
134
139
|
private readonly catalog;
|
|
140
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
141
|
+
private readonly accountCatalogs;
|
|
142
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
143
|
+
private catalogOwner;
|
|
135
144
|
constructor(options: GrokAdapterOptions);
|
|
136
145
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
137
146
|
private fetchCatalog;
|
|
147
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
148
|
+
clearAccountCatalog(account?: string): void;
|
|
149
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
150
|
+
private catalogFor;
|
|
138
151
|
private listed;
|
|
139
152
|
providerInfo(provider: string): LlmProviderInfo;
|
|
140
153
|
private staticModels;
|
|
141
154
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
155
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
156
|
+
listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
|
|
142
157
|
/**
|
|
143
158
|
* The discovered entry for one model. Resolved through the cache's
|
|
144
159
|
* stale-while-revalidate path: capability metadata must stay stable across
|
|
@@ -149,7 +164,12 @@ export declare class GrokAdapter extends LlmAdapter {
|
|
|
149
164
|
*/
|
|
150
165
|
private discovered;
|
|
151
166
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
167
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
168
|
+
resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
152
169
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
170
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
171
|
+
streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
|
|
172
|
+
private streamCore;
|
|
153
173
|
private request;
|
|
154
174
|
}
|
|
155
175
|
export {};
|
package/lib/providers/grok.js
CHANGED
|
@@ -7,7 +7,9 @@ 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, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
|
|
11
|
+
import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
|
|
12
|
+
import { proxiedFetch } from '../http.js';
|
|
11
13
|
export const GROK_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
|
|
12
14
|
export const GROK_DISCOVERY_URL = 'https://auth.x.ai/.well-known/openid-configuration';
|
|
13
15
|
export const GROK_API_URL = 'https://api.x.ai/v1/responses';
|
|
@@ -40,7 +42,7 @@ let discoveryCache;
|
|
|
40
42
|
export async function grokDiscovery() {
|
|
41
43
|
if (discoveryCache !== undefined)
|
|
42
44
|
return discoveryCache;
|
|
43
|
-
const response = await
|
|
45
|
+
const response = await proxiedFetch(GROK_DISCOVERY_URL);
|
|
44
46
|
if (!response.ok)
|
|
45
47
|
throw await oauthEndpointError(response, 'grok OIDC discovery');
|
|
46
48
|
const document = await response.json();
|
|
@@ -147,7 +149,7 @@ function grokSession(tokens, tokenEndpoint, fallbackRefreshToken) {
|
|
|
147
149
|
*/
|
|
148
150
|
export async function exchangeGrokCode(code, verifier, redirectUri, challenge) {
|
|
149
151
|
const discovery = await grokDiscovery();
|
|
150
|
-
const response = await
|
|
152
|
+
const response = await proxiedFetch(discovery.tokenEndpoint, {
|
|
151
153
|
method: 'POST',
|
|
152
154
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
153
155
|
body: new URLSearchParams({
|
|
@@ -174,7 +176,7 @@ export async function exchangeGrokCode(code, verifier, redirectUri, challenge) {
|
|
|
174
176
|
* @returns the fresh session to store.
|
|
175
177
|
*/
|
|
176
178
|
export async function refreshGrok(session) {
|
|
177
|
-
const response = await
|
|
179
|
+
const response = await proxiedFetch(session.tokenEndpoint, {
|
|
178
180
|
method: 'POST',
|
|
179
181
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
180
182
|
body: new URLSearchParams({
|
|
@@ -223,7 +225,7 @@ function grokResetsAt(value) {
|
|
|
223
225
|
* @param signal - caller cancellation from the RPC transport.
|
|
224
226
|
* @returns the mapped usage snapshot.
|
|
225
227
|
*/
|
|
226
|
-
export async function fetchGrokUsage(session, fetchFn =
|
|
228
|
+
export async function fetchGrokUsage(session, fetchFn = proxiedFetch, signal) {
|
|
227
229
|
const response = await fetchFn(GROK_BILLING_URL, {
|
|
228
230
|
headers: {
|
|
229
231
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -311,9 +313,10 @@ function grokCliReasoning(entry) {
|
|
|
311
313
|
* Fetch the CLI catalog and index its per-model metadata by model id.
|
|
312
314
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
313
315
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
316
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
314
317
|
* @returns model id → contributed metadata.
|
|
315
318
|
*/
|
|
316
|
-
export async function fetchGrokCliCatalog(session, fetchFn =
|
|
319
|
+
export async function fetchGrokCliCatalog(session, fetchFn = proxiedFetch, signal) {
|
|
317
320
|
const response = await fetchFn(GROK_CLI_MODELS_URL, {
|
|
318
321
|
headers: {
|
|
319
322
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -322,6 +325,7 @@ export async function fetchGrokCliCatalog(session, fetchFn = fetch) {
|
|
|
322
325
|
'accept': 'application/json',
|
|
323
326
|
...attributionHeaders(),
|
|
324
327
|
},
|
|
328
|
+
...signal === undefined ? {} : { signal },
|
|
325
329
|
});
|
|
326
330
|
if (!response.ok)
|
|
327
331
|
throw await oauthEndpointError(response, 'grok CLI catalog');
|
|
@@ -383,9 +387,10 @@ function grokPriorMeta(prior) {
|
|
|
383
387
|
* @param onWarn - warning sink for a failed CLI catalog fetch.
|
|
384
388
|
* @param previous - last-known catalog used to keep enrichment when the CLI
|
|
385
389
|
* catalog is down or omits a model.
|
|
390
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
386
391
|
* @returns discovered chat models in endpoint order.
|
|
387
392
|
*/
|
|
388
|
-
export async function fetchGrokModels(session, fetchFn =
|
|
393
|
+
export async function fetchGrokModels(session, fetchFn = proxiedFetch, onWarn, previous, signal) {
|
|
389
394
|
const previousById = previous === undefined || previous.length === 0
|
|
390
395
|
? undefined
|
|
391
396
|
: new Map(previous.map(model => [model.id, model]));
|
|
@@ -396,8 +401,11 @@ export async function fetchGrokModels(session, fetchFn = fetch, onWarn, previous
|
|
|
396
401
|
'accept': 'application/json',
|
|
397
402
|
...attributionHeaders(),
|
|
398
403
|
},
|
|
404
|
+
...signal === undefined ? {} : { signal },
|
|
399
405
|
}),
|
|
400
|
-
fetchGrokCliCatalog(session, fetchFn).catch((error) => {
|
|
406
|
+
fetchGrokCliCatalog(session, fetchFn, signal).catch((error) => {
|
|
407
|
+
if (isDiscoveryAborted(error, signal))
|
|
408
|
+
throw error;
|
|
401
409
|
onWarn?.(previousById === undefined
|
|
402
410
|
? `grok CLI catalog fetch failed; reasoning efforts are unavailable (${errorChain(error)})`
|
|
403
411
|
: `grok CLI catalog fetch failed; keeping last-known reasoning efforts (${errorChain(error)})`);
|
|
@@ -435,14 +443,50 @@ export async function fetchGrokModels(session, fetchFn = fetch, onWarn, previous
|
|
|
435
443
|
export class GrokAdapter extends LlmAdapter {
|
|
436
444
|
options;
|
|
437
445
|
catalog;
|
|
446
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
447
|
+
accountCatalogs = new Map();
|
|
448
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
449
|
+
catalogOwner;
|
|
438
450
|
constructor(options) {
|
|
439
451
|
super();
|
|
440
452
|
this.options = options;
|
|
441
453
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
442
454
|
}
|
|
443
455
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
444
|
-
async fetchCatalog() {
|
|
445
|
-
|
|
456
|
+
async fetchCatalog(account, signal) {
|
|
457
|
+
const lastKnown = account === undefined || account === await this.options.tokens.defaultAccount()
|
|
458
|
+
? this.catalog.lastKnown()
|
|
459
|
+
: this.accountCatalogs.get(account)?.lastKnown();
|
|
460
|
+
return fetchGrokModels(await this.options.tokens.session(account), this.options.fetchFn, this.options.onWarn, lastKnown, signal);
|
|
461
|
+
}
|
|
462
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
463
|
+
clearAccountCatalog(account) {
|
|
464
|
+
if (account === undefined)
|
|
465
|
+
this.accountCatalogs.clear();
|
|
466
|
+
else
|
|
467
|
+
this.accountCatalogs.delete(account);
|
|
468
|
+
if (account === undefined || this.catalogOwner === account || this.catalogOwner === undefined) {
|
|
469
|
+
this.catalogOwner = undefined;
|
|
470
|
+
this.catalog.invalidate();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
474
|
+
async catalogFor(account) {
|
|
475
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
476
|
+
const key = account ?? defaultKey;
|
|
477
|
+
if (key === undefined || key === defaultKey) {
|
|
478
|
+
if (this.catalogOwner !== undefined && this.catalogOwner !== defaultKey) {
|
|
479
|
+
this.catalog.invalidate();
|
|
480
|
+
}
|
|
481
|
+
this.catalogOwner = defaultKey;
|
|
482
|
+
return this.catalog;
|
|
483
|
+
}
|
|
484
|
+
let cache = this.accountCatalogs.get(key);
|
|
485
|
+
if (cache === undefined) {
|
|
486
|
+
cache = new ModelCatalogCache();
|
|
487
|
+
this.accountCatalogs.set(key, cache);
|
|
488
|
+
}
|
|
489
|
+
return cache;
|
|
446
490
|
}
|
|
447
491
|
listed(provider, discovered) {
|
|
448
492
|
return discovered.map(model => ({
|
|
@@ -465,19 +509,38 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
465
509
|
}));
|
|
466
510
|
}
|
|
467
511
|
async listModels(provider) {
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
if (
|
|
512
|
+
const own = await this.listOwnModels(provider);
|
|
513
|
+
const pool = this.options.pool?.();
|
|
514
|
+
if (pool === undefined)
|
|
515
|
+
return own;
|
|
516
|
+
const extra = await pool.modelsForProvider(provider);
|
|
517
|
+
const seen = new Set(own.map(model => model.id));
|
|
518
|
+
// Account pools reuse the catalog row; only configured tiers are extra.
|
|
519
|
+
return [...own, ...extra.filter(model => !seen.has(model.id))];
|
|
520
|
+
}
|
|
521
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
522
|
+
async listOwnModels(provider, account, signal) {
|
|
523
|
+
if (account === undefined) {
|
|
524
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
525
|
+
if (accounts.length === 0)
|
|
526
|
+
return [];
|
|
527
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), { timeoutMs: DISCOVERY_TIMEOUT_MS, ...signal === undefined ? {} : { signal } });
|
|
528
|
+
}
|
|
529
|
+
if (!await this.options.tokens.hasSession(account)) {
|
|
471
530
|
return [];
|
|
531
|
+
}
|
|
472
532
|
if (!this.options.discovery)
|
|
473
533
|
return this.staticModels(provider);
|
|
534
|
+
const catalog = await this.catalogFor(account);
|
|
474
535
|
try {
|
|
475
536
|
// The fetcher runs only on a cache miss, and resolves the session
|
|
476
537
|
// through the refresh-aware path so an expired access token renews here
|
|
477
538
|
// instead of failing discovery into the static fallback.
|
|
478
|
-
return this.listed(provider, await discoverOrRetryAuth(force => this.options.tokens.session(force),
|
|
539
|
+
return this.listed(provider, await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal))));
|
|
479
540
|
}
|
|
480
541
|
catch (error) {
|
|
542
|
+
if (isDiscoveryAborted(error, signal))
|
|
543
|
+
throw error;
|
|
481
544
|
// A permanent refresh failure deletes the stored session: the provider
|
|
482
545
|
// is logged out, so hide it instead of showing a stale static catalog.
|
|
483
546
|
if (isMissingOrInvalidCredential(error))
|
|
@@ -497,10 +560,22 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
497
560
|
async discovered(model) {
|
|
498
561
|
if (!this.options.discovery)
|
|
499
562
|
return undefined;
|
|
500
|
-
const
|
|
501
|
-
return
|
|
563
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
564
|
+
return discoverAcrossAccounts(accounts, async (account) => {
|
|
565
|
+
const catalog = await this.catalogFor(account);
|
|
566
|
+
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
567
|
+
return models?.find(entry => entry.id === model);
|
|
568
|
+
});
|
|
502
569
|
}
|
|
503
570
|
async resolveModel(provider, model) {
|
|
571
|
+
const pool = this.options.pool?.();
|
|
572
|
+
if (pool !== undefined && await pool.owns(provider, model)) {
|
|
573
|
+
return pool.resolveModel(provider, model);
|
|
574
|
+
}
|
|
575
|
+
return this.resolveOwnModel(provider, model);
|
|
576
|
+
}
|
|
577
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
578
|
+
async resolveOwnModel(provider, model) {
|
|
504
579
|
const discovered = await this.discovered(model);
|
|
505
580
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
506
581
|
return {
|
|
@@ -518,13 +593,25 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
518
593
|
};
|
|
519
594
|
}
|
|
520
595
|
async *stream(options) {
|
|
596
|
+
const pool = this.options.pool?.();
|
|
597
|
+
if (pool !== undefined && await pool.owns(options.provider, options.model)) {
|
|
598
|
+
yield* pool.stream(options);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
yield* this.streamCore(options);
|
|
602
|
+
}
|
|
603
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
604
|
+
streamAccount(options, account) {
|
|
605
|
+
return this.streamCore(options, account);
|
|
606
|
+
}
|
|
607
|
+
async *streamCore(options, account) {
|
|
521
608
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
522
609
|
try {
|
|
523
|
-
let session = await this.options.tokens.session();
|
|
610
|
+
let session = await this.options.tokens.session(account);
|
|
524
611
|
let response = await this.request(options, session, watchdog.signal);
|
|
525
612
|
if (response.status === 401) {
|
|
526
613
|
// One forced refresh + retry on an unexpired-but-rejected token.
|
|
527
|
-
session = await this.options.tokens.session(true);
|
|
614
|
+
session = await this.options.tokens.session(account, true);
|
|
528
615
|
response = await this.request(options, session, watchdog.signal);
|
|
529
616
|
}
|
|
530
617
|
if (!response.ok)
|
|
@@ -562,7 +649,7 @@ export class GrokAdapter extends LlmAdapter {
|
|
|
562
649
|
store: false,
|
|
563
650
|
stream: true,
|
|
564
651
|
};
|
|
565
|
-
return
|
|
652
|
+
return proxiedFetch(GROK_API_URL, {
|
|
566
653
|
method: 'POST',
|
|
567
654
|
headers: {
|
|
568
655
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-subscription account routing: the picker is the union of every
|
|
3
|
+
* account's catalog (deduped by wire id). A model listed by two or more
|
|
4
|
+
* accounts failovers between them; a model listed by only one account is
|
|
5
|
+
* sent to that account. No extra pool identity, no cross-provider
|
|
6
|
+
* aggregation.
|
|
7
|
+
*/
|
|
8
|
+
import type { LlmModelInfo } from '@deepseek-ai/dsh-llm';
|
|
9
|
+
import type { ProviderId } from '../auth/store.js';
|
|
10
|
+
/** One pool member: an exact provider/account/model route. */
|
|
11
|
+
export interface PoolMemberRef {
|
|
12
|
+
provider: ProviderId;
|
|
13
|
+
/** Account key; omitted in configured members to mean "the default account". */
|
|
14
|
+
account?: string;
|
|
15
|
+
model: string;
|
|
16
|
+
}
|
|
17
|
+
/** A member with its account resolved (no config indirection left). */
|
|
18
|
+
export type ConcretePoolMember = PoolMemberRef & {
|
|
19
|
+
account: string;
|
|
20
|
+
};
|
|
21
|
+
/** One pool: its members plus display metadata for the picker. */
|
|
22
|
+
export interface PoolDefinition {
|
|
23
|
+
members: PoolMemberRef[];
|
|
24
|
+
/** Display name of the catalog entry (account pools) or the pool id (extras). */
|
|
25
|
+
name?: string;
|
|
26
|
+
/** Description of the catalog entry, when the pool borrowed one. */
|
|
27
|
+
description?: string;
|
|
28
|
+
/**
|
|
29
|
+
* When true, the pool is an extra picker entry (a configured tier). Account
|
|
30
|
+
* pools leave this unset so they reuse the provider's existing catalog row.
|
|
31
|
+
*/
|
|
32
|
+
extra?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/** One account's catalog as seen through that account's credentials. */
|
|
35
|
+
export interface AccountCatalog {
|
|
36
|
+
account: string;
|
|
37
|
+
models: readonly LlmModelInfo[];
|
|
38
|
+
}
|
|
39
|
+
/** One provider's contribution to account-pool aggregation. */
|
|
40
|
+
export interface ProviderPoolSource {
|
|
41
|
+
/** Per-account catalogs, default first. A model pools only the accounts that list it. */
|
|
42
|
+
catalogs: readonly AccountCatalog[];
|
|
43
|
+
}
|
|
44
|
+
/** Map key for one provider's pool of one model (ids collide across providers). */
|
|
45
|
+
export declare function poolKey(provider: string, model: string): string;
|
|
46
|
+
/**
|
|
47
|
+
* Build per-provider account routes. Each model id becomes a definition of
|
|
48
|
+
* the accounts that list it: two or more fail over; one is pinned to that
|
|
49
|
+
* account (so a Max-only model is never sent to a Plus login). The picker
|
|
50
|
+
* unions these catalogs; a logout that drops a model to one account keeps
|
|
51
|
+
* the same id and pins it to whoever remains.
|
|
52
|
+
* @param sources - per-account catalogs (providers with no accounts list
|
|
53
|
+
* nothing and simply never join a pool).
|
|
54
|
+
* @returns `provider/model` → pool definition (not listed as an extra entry).
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildAccountPools(sources: Partial<Record<ProviderId, ProviderPoolSource>>): Map<string, PoolDefinition>;
|