dsh-plugin-subscriptions 0.5.2 → 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 +36 -1
- package/README.zh.md +36 -1
- package/lib/auth/rpc.d.ts +29 -12
- package/lib/auth/rpc.js +29 -6
- package/lib/auth/store.d.ts +75 -17
- package/lib/auth/store.js +148 -27
- package/lib/client/SubscriptionsSection.d.ts +9 -3
- package/lib/client/SubscriptionsSection.js +93 -65
- package/lib/client/locales.d.ts +18 -10
- package/lib/client/locales.js +18 -10
- package/lib/client.js +250 -127
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +21 -0
- package/lib/index.js +1482 -168
- package/lib/providers/accounts.d.ts +102 -0
- package/lib/providers/accounts.js +123 -0
- package/lib/providers/claude.d.ts +22 -4
- package/lib/providers/claude.js +91 -11
- package/lib/providers/codex.d.ts +24 -3
- package/lib/providers/codex.js +116 -17
- package/lib/providers/common.d.ts +17 -0
- package/lib/providers/common.js +67 -3
- package/lib/providers/copilot.d.ts +22 -3
- package/lib/providers/copilot.js +91 -12
- package/lib/providers/grok.d.ts +24 -4
- package/lib/providers/grok.js +100 -14
- 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 +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/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-account token plumbing: one {@link AccountTokenManager} per provider
|
|
3
|
+
* owns a lazily-built {@link TokenManager} per account, so refresh coalescing
|
|
4
|
+
* (`inflight`) and permanent-failure removal stay scoped to ONE account —
|
|
5
|
+
* a revoked account deletes itself without touching its siblings.
|
|
6
|
+
*
|
|
7
|
+
* {@link AccountAwareAdapter} is the internal interface the pool uses to
|
|
8
|
+
* stream through a specific account. A catalog model listed by several
|
|
9
|
+
* accounts failovers; one listed by a single account is pinned to it.
|
|
10
|
+
*/
|
|
11
|
+
import { LlmAdapter } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
import type { GenerateOptions, LlmModelInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
13
|
+
import { TokenManager } from './common.js';
|
|
14
|
+
import type { TokenManagerOptions } from './common.js';
|
|
15
|
+
import type { AccountEntry, ProviderId } from '../auth/store.js';
|
|
16
|
+
export { DISCOVERY_TIMEOUT_MS } from './common.js';
|
|
17
|
+
/** Minimal session shape the token managers need (mirrors common.ts). */
|
|
18
|
+
interface TimedSession {
|
|
19
|
+
accessToken: string;
|
|
20
|
+
refreshToken: string;
|
|
21
|
+
expiresAt: number;
|
|
22
|
+
}
|
|
23
|
+
/** An adapter that can stream through a named account (the pool's seam). */
|
|
24
|
+
export interface AccountAwareAdapter extends LlmAdapter {
|
|
25
|
+
/** Stream using the given account's credentials instead of the default. */
|
|
26
|
+
streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
|
|
27
|
+
/**
|
|
28
|
+
* The provider's own catalog: one account when `account` is set, otherwise
|
|
29
|
+
* the union of every logged-in account (default first; later duplicates
|
|
30
|
+
* dropped). The picker uses the union; pool assembly lists each account.
|
|
31
|
+
*/
|
|
32
|
+
listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Capability resolution of the provider's OWN models, bypassing the pool
|
|
35
|
+
* delegation. The pool resolves its members through this — an account pool
|
|
36
|
+
* reuses the catalog wire id (e.g. `gpt-5.4`), so resolveModel would
|
|
37
|
+
* otherwise bounce straight back into the pool forever.
|
|
38
|
+
*/
|
|
39
|
+
resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
40
|
+
/** Drop cached catalogs: one account, or every account when omitted (login/logout). */
|
|
41
|
+
clearAccountCatalog(account?: string): void;
|
|
42
|
+
}
|
|
43
|
+
/** Options for {@link unionAccountCatalogs}. */
|
|
44
|
+
export interface UnionAccountCatalogsOptions {
|
|
45
|
+
/** Per-account bound; a hang sits that account out instead of blocking the picker. */
|
|
46
|
+
timeoutMs?: number;
|
|
47
|
+
/** Caller cancellation; aborting drops the whole union. */
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Merge per-account catalogs, keeping the first occurrence of each model id.
|
|
52
|
+
* Rows that carry a numeric `priority` (Codex discovery) are then ordered by
|
|
53
|
+
* it so a model only the second account lists — e.g. `gpt-5.6-sol` — still
|
|
54
|
+
* sits with its generation instead of being appended after the default
|
|
55
|
+
* account's older ids.
|
|
56
|
+
*/
|
|
57
|
+
export declare function unionAccountCatalogs(accounts: readonly string[], listOne: (account: string, signal?: AbortSignal) => Promise<readonly LlmModelInfo[]>, options?: UnionAccountCatalogsOptions): Promise<LlmModelInfo[]>;
|
|
58
|
+
/** Store I/O behind {@link AccountTokenManager} (injectable for tests). */
|
|
59
|
+
export interface AccountStoreIo<S> {
|
|
60
|
+
list(): Promise<AccountEntry<S>[]>;
|
|
61
|
+
get(account?: string): Promise<S | undefined>;
|
|
62
|
+
save(account: string, session: S): Promise<void>;
|
|
63
|
+
remove(account: string): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
export interface AccountTokenManagerOptions<S extends TimedSession> {
|
|
66
|
+
provider: ProviderId;
|
|
67
|
+
/** Human-readable provider name for error messages. */
|
|
68
|
+
displayName: string;
|
|
69
|
+
/** Provider hooks shared by every account (load/save/remove are bound per account). */
|
|
70
|
+
makeOptions: (account: string) => Omit<TokenManagerOptions<S>, 'load' | 'save' | 'remove' | 'onRemoved' | 'displayName'>;
|
|
71
|
+
/** Called after a permanent refresh failure deleted one account's session. */
|
|
72
|
+
onAccountRemoved?: (account: string) => void;
|
|
73
|
+
/** Store backend; defaults to the durable auth store. */
|
|
74
|
+
io?: AccountStoreIo<S>;
|
|
75
|
+
}
|
|
76
|
+
export declare class AccountTokenManager<S extends TimedSession> {
|
|
77
|
+
private readonly options;
|
|
78
|
+
private readonly managers;
|
|
79
|
+
private readonly io;
|
|
80
|
+
constructor(options: AccountTokenManagerOptions<S>);
|
|
81
|
+
/** The provider's accounts, default first (straight from the store). */
|
|
82
|
+
list(): Promise<AccountEntry<S>[]>;
|
|
83
|
+
/** The default account's key, or undefined when logged out. */
|
|
84
|
+
defaultAccount(): Promise<string | undefined>;
|
|
85
|
+
/**
|
|
86
|
+
* Resolve a usable session for one account (default when omitted),
|
|
87
|
+
* refreshing proactively or on demand.
|
|
88
|
+
* @param account - the account key; the default account when undefined.
|
|
89
|
+
* @param forceRefresh - refresh regardless of expiry (used after a 401).
|
|
90
|
+
* @returns the persisted session to send.
|
|
91
|
+
* @throws LlmError MISSING_CREDENTIAL when the account is not logged in.
|
|
92
|
+
*/
|
|
93
|
+
session(account?: string, forceRefresh?: boolean): Promise<S>;
|
|
94
|
+
/** Read an account's stored session without any refresh side effect. */
|
|
95
|
+
peek(account?: string): Promise<S | undefined>;
|
|
96
|
+
/** Whether a session is stored for the account (cheap; never refreshes). */
|
|
97
|
+
hasSession(account?: string): Promise<boolean>;
|
|
98
|
+
/** The TokenManager bound to one account (created lazily, then cached). */
|
|
99
|
+
tokensFor(account: string): TokenManager<S>;
|
|
100
|
+
/** The logged-out error, mirroring TokenManager's own message. */
|
|
101
|
+
private missingCredential;
|
|
102
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-account token plumbing: one {@link AccountTokenManager} per provider
|
|
3
|
+
* owns a lazily-built {@link TokenManager} per account, so refresh coalescing
|
|
4
|
+
* (`inflight`) and permanent-failure removal stay scoped to ONE account —
|
|
5
|
+
* a revoked account deletes itself without touching its siblings.
|
|
6
|
+
*
|
|
7
|
+
* {@link AccountAwareAdapter} is the internal interface the pool uses to
|
|
8
|
+
* stream through a specific account. A catalog model listed by several
|
|
9
|
+
* accounts failovers; one listed by a single account is pinned to it.
|
|
10
|
+
*/
|
|
11
|
+
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
import { TokenManager, withTimeout } from './common.js';
|
|
13
|
+
import { deleteAccountSession, getAccountSession, listAccounts, saveAccountSession, } from '../auth/store.js';
|
|
14
|
+
export { DISCOVERY_TIMEOUT_MS } from './common.js';
|
|
15
|
+
/** Catalog sort hint when the provider advertised one (Codex `priority`). */
|
|
16
|
+
function catalogPriority(model) {
|
|
17
|
+
const ranked = model;
|
|
18
|
+
return typeof ranked.priority === 'number' ? ranked.priority : Number.MAX_SAFE_INTEGER;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Merge per-account catalogs, keeping the first occurrence of each model id.
|
|
22
|
+
* Rows that carry a numeric `priority` (Codex discovery) are then ordered by
|
|
23
|
+
* it so a model only the second account lists — e.g. `gpt-5.6-sol` — still
|
|
24
|
+
* sits with its generation instead of being appended after the default
|
|
25
|
+
* account's older ids.
|
|
26
|
+
*/
|
|
27
|
+
export async function unionAccountCatalogs(accounts, listOne, options) {
|
|
28
|
+
const timeoutMs = options?.timeoutMs;
|
|
29
|
+
const caller = options?.signal;
|
|
30
|
+
const catalogs = await Promise.all(accounts.map(async (account) => {
|
|
31
|
+
try {
|
|
32
|
+
if (timeoutMs === undefined)
|
|
33
|
+
return await listOne(account, caller);
|
|
34
|
+
const models = await withTimeout(timeoutSignal => listOne(account, caller === undefined ? timeoutSignal : AbortSignal.any([timeoutSignal, caller])), timeoutMs);
|
|
35
|
+
return models ?? [];
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// One expired or failing account must not hide models the others list.
|
|
39
|
+
if (caller?.aborted === true)
|
|
40
|
+
throw error;
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
const models = [];
|
|
46
|
+
for (const catalog of catalogs) {
|
|
47
|
+
for (const model of catalog) {
|
|
48
|
+
if (seen.has(model.id))
|
|
49
|
+
continue;
|
|
50
|
+
seen.add(model.id);
|
|
51
|
+
models.push(model);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
models.sort((left, right) => catalogPriority(left) - catalogPriority(right));
|
|
55
|
+
return models;
|
|
56
|
+
}
|
|
57
|
+
export class AccountTokenManager {
|
|
58
|
+
options;
|
|
59
|
+
managers = new Map();
|
|
60
|
+
io;
|
|
61
|
+
constructor(options) {
|
|
62
|
+
this.options = options;
|
|
63
|
+
const provider = options.provider;
|
|
64
|
+
this.io = options.io ?? {
|
|
65
|
+
list: () => listAccounts(provider),
|
|
66
|
+
get: account => getAccountSession(provider, account),
|
|
67
|
+
save: (account, session) => saveAccountSession(provider, account, session),
|
|
68
|
+
remove: account => deleteAccountSession(provider, account),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** The provider's accounts, default first (straight from the store). */
|
|
72
|
+
list() {
|
|
73
|
+
return this.io.list();
|
|
74
|
+
}
|
|
75
|
+
/** The default account's key, or undefined when logged out. */
|
|
76
|
+
async defaultAccount() {
|
|
77
|
+
return (await this.list())[0]?.key;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a usable session for one account (default when omitted),
|
|
81
|
+
* refreshing proactively or on demand.
|
|
82
|
+
* @param account - the account key; the default account when undefined.
|
|
83
|
+
* @param forceRefresh - refresh regardless of expiry (used after a 401).
|
|
84
|
+
* @returns the persisted session to send.
|
|
85
|
+
* @throws LlmError MISSING_CREDENTIAL when the account is not logged in.
|
|
86
|
+
*/
|
|
87
|
+
async session(account, forceRefresh = false) {
|
|
88
|
+
const key = account ?? await this.defaultAccount();
|
|
89
|
+
if (key === undefined)
|
|
90
|
+
throw this.missingCredential();
|
|
91
|
+
return this.tokensFor(key).session(forceRefresh);
|
|
92
|
+
}
|
|
93
|
+
/** Read an account's stored session without any refresh side effect. */
|
|
94
|
+
peek(account) {
|
|
95
|
+
return this.io.get(account);
|
|
96
|
+
}
|
|
97
|
+
/** Whether a session is stored for the account (cheap; never refreshes). */
|
|
98
|
+
async hasSession(account) {
|
|
99
|
+
return (await this.peek(account)) !== undefined;
|
|
100
|
+
}
|
|
101
|
+
/** The TokenManager bound to one account (created lazily, then cached). */
|
|
102
|
+
tokensFor(account) {
|
|
103
|
+
let manager = this.managers.get(account);
|
|
104
|
+
if (manager === undefined) {
|
|
105
|
+
const io = this.io;
|
|
106
|
+
manager = new TokenManager({
|
|
107
|
+
displayName: this.options.displayName,
|
|
108
|
+
...this.options.makeOptions(account),
|
|
109
|
+
load: () => io.get(account),
|
|
110
|
+
save: session => io.save(account, session),
|
|
111
|
+
remove: () => io.remove(account),
|
|
112
|
+
onRemoved: () => { this.options.onAccountRemoved?.(account); },
|
|
113
|
+
});
|
|
114
|
+
this.managers.set(account, manager);
|
|
115
|
+
}
|
|
116
|
+
return manager;
|
|
117
|
+
}
|
|
118
|
+
/** The logged-out error, mirroring TokenManager's own message. */
|
|
119
|
+
missingCredential() {
|
|
120
|
+
return new LlmError(`dsh-plugin-subscriptions: not logged in to ${this.options.displayName}; `
|
|
121
|
+
+ 'log in via Settings → Subscriptions in the dsh web app', 'MISSING_CREDENTIAL');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -7,9 +7,10 @@ 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 { ClaudeSession } from '../auth/store.js';
|
|
10
|
+
import type { PoolAdapter } from './pool.js';
|
|
10
11
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
11
12
|
import type { TranslatableMessage } from '../translate/resolved.js';
|
|
12
|
-
import {
|
|
13
|
+
import { AccountTokenManager } from './accounts.js';
|
|
13
14
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
14
15
|
export declare const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
15
16
|
export declare const CLAUDE_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
@@ -64,13 +65,15 @@ export declare const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usa
|
|
|
64
65
|
* @returns the mapped usage snapshot.
|
|
65
66
|
*/
|
|
66
67
|
export declare function fetchClaudeUsage(session: ClaudeSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<ProviderUsage>;
|
|
67
|
-
/** Fetch the live model catalog from the subscription endpoint. */
|
|
68
|
-
export declare function fetchClaudeModels(session: ClaudeSession, fetchFn?: FetchFn): Promise<DiscoveredModel[]>;
|
|
68
|
+
/** Fetch the live model catalog from the subscription endpoint. `signal` cancels the request. */
|
|
69
|
+
export declare function fetchClaudeModels(session: ClaudeSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<DiscoveredModel[]>;
|
|
69
70
|
/** Constructor dependencies for {@link ClaudeAdapter}. */
|
|
70
71
|
export interface ClaudeAdapterOptions {
|
|
71
72
|
models: readonly ModelEntry[];
|
|
72
73
|
streamIdleTimeoutMs: number;
|
|
73
|
-
tokens:
|
|
74
|
+
tokens: AccountTokenManager<ClaudeSession>;
|
|
75
|
+
/** Late-bound pool facade (wired after adapter construction); pools list under their first member's provider. */
|
|
76
|
+
pool?: () => PoolAdapter | undefined;
|
|
74
77
|
/** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
|
|
75
78
|
discovery: boolean;
|
|
76
79
|
fetchFn?: FetchFn;
|
|
@@ -102,15 +105,30 @@ export declare function claudeRequestBody(options: GenerateOptions, messages: re
|
|
|
102
105
|
export declare class ClaudeAdapter extends LlmAdapter {
|
|
103
106
|
private readonly options;
|
|
104
107
|
private readonly catalog;
|
|
108
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
109
|
+
private readonly accountCatalogs;
|
|
110
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
111
|
+
private catalogOwner;
|
|
105
112
|
constructor(options: ClaudeAdapterOptions);
|
|
106
113
|
private fetchCatalog;
|
|
114
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
115
|
+
clearAccountCatalog(account?: string): void;
|
|
116
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
117
|
+
private catalogFor;
|
|
107
118
|
private discovered;
|
|
108
119
|
private staticModels;
|
|
109
120
|
providerInfo(provider: string): LlmProviderInfo;
|
|
110
121
|
providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy | undefined;
|
|
111
122
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
123
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
124
|
+
listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
|
|
112
125
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
126
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
127
|
+
resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
113
128
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
129
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
130
|
+
streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
|
|
131
|
+
private streamCore;
|
|
114
132
|
/**
|
|
115
133
|
* `display: 'summarized'` is set explicitly on both shapes: `adaptive`-type
|
|
116
134
|
* models default to `display: 'omitted'`, which returns thinking blocks with
|
package/lib/providers/claude.js
CHANGED
|
@@ -7,7 +7,8 @@ import { execFileSync } from 'node:child_process';
|
|
|
7
7
|
import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId, resolveRetryPolicy } from '@deepseek-ai/dsh-llm';
|
|
8
8
|
import { resolveImages } from '../translate/resolved.js';
|
|
9
9
|
import { markMessageCache, streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.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';
|
|
11
12
|
import { proxiedFetch } from '../http.js';
|
|
12
13
|
export const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
|
|
13
14
|
export const CLAUDE_AUTHORIZE_URL = 'https://claude.ai/oauth/authorize';
|
|
@@ -288,8 +289,8 @@ function claudeReasoning(capabilities) {
|
|
|
288
289
|
.map(level => ({ id: ReasoningEffortId(level), name: level[0].toUpperCase() + level.slice(1) }));
|
|
289
290
|
return efforts.length > 0 ? { efforts } : undefined;
|
|
290
291
|
}
|
|
291
|
-
/** Fetch the live model catalog from the subscription endpoint. */
|
|
292
|
-
export async function fetchClaudeModels(session, fetchFn = proxiedFetch) {
|
|
292
|
+
/** Fetch the live model catalog from the subscription endpoint. `signal` cancels the request. */
|
|
293
|
+
export async function fetchClaudeModels(session, fetchFn = proxiedFetch, signal) {
|
|
293
294
|
const response = await fetchFn(CLAUDE_MODELS_URL, {
|
|
294
295
|
headers: {
|
|
295
296
|
'authorization': `Bearer ${session.accessToken}`,
|
|
@@ -298,6 +299,7 @@ export async function fetchClaudeModels(session, fetchFn = proxiedFetch) {
|
|
|
298
299
|
'anthropic-dangerous-direct-browser-access': 'true',
|
|
299
300
|
'accept': 'application/json',
|
|
300
301
|
},
|
|
302
|
+
...signal === undefined ? {} : { signal },
|
|
301
303
|
});
|
|
302
304
|
if (!response.ok)
|
|
303
305
|
throw await httpLlmError(response, 'claude models API');
|
|
@@ -368,19 +370,56 @@ export function claudeRequestBody(options, messages, maxTokens, thinking, effort
|
|
|
368
370
|
export class ClaudeAdapter extends LlmAdapter {
|
|
369
371
|
options;
|
|
370
372
|
catalog;
|
|
373
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
374
|
+
accountCatalogs = new Map();
|
|
375
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
376
|
+
catalogOwner;
|
|
371
377
|
constructor(options) {
|
|
372
378
|
super();
|
|
373
379
|
this.options = options;
|
|
374
380
|
this.catalog = new ModelCatalogCache(options.catalogStore);
|
|
375
381
|
}
|
|
376
|
-
async fetchCatalog() {
|
|
377
|
-
return fetchClaudeModels(await this.options.tokens.session(), this.options.fetchFn);
|
|
382
|
+
async fetchCatalog(account, signal) {
|
|
383
|
+
return fetchClaudeModels(await this.options.tokens.session(account), this.options.fetchFn, signal);
|
|
384
|
+
}
|
|
385
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
386
|
+
clearAccountCatalog(account) {
|
|
387
|
+
if (account === undefined)
|
|
388
|
+
this.accountCatalogs.clear();
|
|
389
|
+
else
|
|
390
|
+
this.accountCatalogs.delete(account);
|
|
391
|
+
if (account === undefined || this.catalogOwner === account || this.catalogOwner === undefined) {
|
|
392
|
+
this.catalogOwner = undefined;
|
|
393
|
+
this.catalog.invalidate();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
397
|
+
async catalogFor(account) {
|
|
398
|
+
const defaultKey = await this.options.tokens.defaultAccount();
|
|
399
|
+
const key = account ?? defaultKey;
|
|
400
|
+
if (key === undefined || key === defaultKey) {
|
|
401
|
+
if (this.catalogOwner !== undefined && this.catalogOwner !== defaultKey) {
|
|
402
|
+
this.catalog.invalidate();
|
|
403
|
+
}
|
|
404
|
+
this.catalogOwner = defaultKey;
|
|
405
|
+
return this.catalog;
|
|
406
|
+
}
|
|
407
|
+
let cache = this.accountCatalogs.get(key);
|
|
408
|
+
if (cache === undefined) {
|
|
409
|
+
cache = new ModelCatalogCache();
|
|
410
|
+
this.accountCatalogs.set(key, cache);
|
|
411
|
+
}
|
|
412
|
+
return cache;
|
|
378
413
|
}
|
|
379
414
|
async discovered(model) {
|
|
380
415
|
if (!this.options.discovery)
|
|
381
416
|
return undefined;
|
|
382
|
-
const
|
|
383
|
-
return
|
|
417
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
418
|
+
return discoverAcrossAccounts(accounts, async (account) => {
|
|
419
|
+
const catalog = await this.catalogFor(account);
|
|
420
|
+
const models = await catalog.resolve(() => this.fetchCatalog(account));
|
|
421
|
+
return models?.find(entry => entry.id === model);
|
|
422
|
+
});
|
|
384
423
|
}
|
|
385
424
|
staticModels(provider) {
|
|
386
425
|
return this.options.models.map(model => ({
|
|
@@ -407,12 +446,31 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
407
446
|
}, `claude: provider "${provider}" retryPolicy`);
|
|
408
447
|
}
|
|
409
448
|
async listModels(provider) {
|
|
410
|
-
|
|
449
|
+
const own = await this.listOwnModels(provider);
|
|
450
|
+
const pool = this.options.pool?.();
|
|
451
|
+
if (pool === undefined)
|
|
452
|
+
return own;
|
|
453
|
+
const extra = await pool.modelsForProvider(provider);
|
|
454
|
+
const seen = new Set(own.map(model => model.id));
|
|
455
|
+
// Account pools reuse the catalog row; only configured tiers are extra.
|
|
456
|
+
return [...own, ...extra.filter(model => !seen.has(model.id))];
|
|
457
|
+
}
|
|
458
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
459
|
+
async listOwnModels(provider, account, signal) {
|
|
460
|
+
if (account === undefined) {
|
|
461
|
+
const accounts = (await this.options.tokens.list()).map(entry => entry.key);
|
|
462
|
+
if (accounts.length === 0)
|
|
463
|
+
return [];
|
|
464
|
+
return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), { timeoutMs: DISCOVERY_TIMEOUT_MS, ...signal === undefined ? {} : { signal } });
|
|
465
|
+
}
|
|
466
|
+
if (!await this.options.tokens.hasSession(account)) {
|
|
411
467
|
return [];
|
|
468
|
+
}
|
|
412
469
|
if (!this.options.discovery)
|
|
413
470
|
return this.staticModels(provider);
|
|
471
|
+
const catalog = await this.catalogFor(account);
|
|
414
472
|
try {
|
|
415
|
-
const models = await discoverOrRetryAuth(force => this.options.tokens.session(force),
|
|
473
|
+
const models = await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)));
|
|
416
474
|
return models.map(model => ({
|
|
417
475
|
provider,
|
|
418
476
|
id: model.id,
|
|
@@ -421,6 +479,8 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
421
479
|
}));
|
|
422
480
|
}
|
|
423
481
|
catch (error) {
|
|
482
|
+
if (isDiscoveryAborted(error, signal))
|
|
483
|
+
throw error;
|
|
424
484
|
if (isMissingOrInvalidCredential(error))
|
|
425
485
|
return [];
|
|
426
486
|
this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
|
|
@@ -428,6 +488,14 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
428
488
|
}
|
|
429
489
|
}
|
|
430
490
|
async resolveModel(provider, model) {
|
|
491
|
+
const pool = this.options.pool?.();
|
|
492
|
+
if (pool !== undefined && await pool.owns(provider, model)) {
|
|
493
|
+
return pool.resolveModel(provider, model);
|
|
494
|
+
}
|
|
495
|
+
return this.resolveOwnModel(provider, model);
|
|
496
|
+
}
|
|
497
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
498
|
+
async resolveOwnModel(provider, model) {
|
|
431
499
|
const disc = await this.discovered(model);
|
|
432
500
|
const configured = this.options.models.find(entry => entry.id === model);
|
|
433
501
|
const reasoning = disc?.reasoning;
|
|
@@ -444,12 +512,24 @@ export class ClaudeAdapter extends LlmAdapter {
|
|
|
444
512
|
};
|
|
445
513
|
}
|
|
446
514
|
async *stream(options) {
|
|
515
|
+
const pool = this.options.pool?.();
|
|
516
|
+
if (pool !== undefined && await pool.owns(options.provider, options.model)) {
|
|
517
|
+
yield* pool.stream(options);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
yield* this.streamCore(options);
|
|
521
|
+
}
|
|
522
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
523
|
+
streamAccount(options, account) {
|
|
524
|
+
return this.streamCore(options, account);
|
|
525
|
+
}
|
|
526
|
+
async *streamCore(options, account) {
|
|
447
527
|
const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
|
|
448
528
|
try {
|
|
449
|
-
let session = await this.options.tokens.session();
|
|
529
|
+
let session = await this.options.tokens.session(account);
|
|
450
530
|
let response = await this.request(options, session, watchdog.signal);
|
|
451
531
|
if (response.status === 401) {
|
|
452
|
-
session = await this.options.tokens.session(true);
|
|
532
|
+
session = await this.options.tokens.session(account, true);
|
|
453
533
|
response = await this.request(options, session, watchdog.signal);
|
|
454
534
|
}
|
|
455
535
|
if (!response.ok)
|
package/lib/providers/codex.d.ts
CHANGED
|
@@ -7,9 +7,10 @@ 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 { CodexSession } from '../auth/store.js';
|
|
10
|
+
import type { PoolAdapter } from './pool.js';
|
|
10
11
|
import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
|
|
11
12
|
import type { ResponsesRequestInput } from '../translate/responses.js';
|
|
12
|
-
import {
|
|
13
|
+
import { AccountTokenManager } from './accounts.js';
|
|
13
14
|
import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
|
|
14
15
|
export declare const CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
15
16
|
export declare const CODEX_AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
@@ -90,14 +91,17 @@ export declare const CODEX_CLIENT_VERSION = "0.147.0";
|
|
|
90
91
|
* Fetch the live codex model catalog with the session's auth headers.
|
|
91
92
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
92
93
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
94
|
+
* @param signal - caller cancellation (pool-assembly timeout).
|
|
93
95
|
* @returns discovered models: hidden entries dropped, sorted by priority.
|
|
94
96
|
*/
|
|
95
|
-
export declare function fetchCodexModels(session: CodexSession, fetchFn?: FetchFn): Promise<DiscoveredModel[]>;
|
|
97
|
+
export declare function fetchCodexModels(session: CodexSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<DiscoveredModel[]>;
|
|
96
98
|
/** Constructor dependencies for {@link CodexAdapter}. */
|
|
97
99
|
export interface CodexAdapterOptions {
|
|
98
100
|
models: readonly ModelEntry[];
|
|
99
101
|
streamIdleTimeoutMs: number;
|
|
100
|
-
tokens:
|
|
102
|
+
tokens: AccountTokenManager<CodexSession>;
|
|
103
|
+
/** Late-bound pool facade (wired after adapter construction); pools list under their first member's provider. */
|
|
104
|
+
pool?: () => PoolAdapter | undefined;
|
|
101
105
|
/** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
|
|
102
106
|
discovery: boolean;
|
|
103
107
|
/** Warning sink for discovery failures that fall back to the static catalog. */
|
|
@@ -108,6 +112,8 @@ export interface CodexAdapterOptions {
|
|
|
108
112
|
resolveAttachments?: () => AttachmentStore | undefined;
|
|
109
113
|
/** Durable catalog store seeding capability metadata across restarts. */
|
|
110
114
|
catalogStore?: CatalogPersistence;
|
|
115
|
+
/** Per-account catalog bound for the picker union (defaults to {@link DISCOVERY_TIMEOUT_MS}). */
|
|
116
|
+
discoveryTimeoutMs?: number;
|
|
111
117
|
/**
|
|
112
118
|
* Per-request speed lookup (the composer Speed toggle's host half). Returns
|
|
113
119
|
* whether this session's current choice sends the model on the fast tier;
|
|
@@ -126,12 +132,22 @@ export declare function codexRequestBody(options: GenerateOptions, resolved: Res
|
|
|
126
132
|
export declare class CodexAdapter extends LlmAdapter {
|
|
127
133
|
private readonly options;
|
|
128
134
|
private readonly catalog;
|
|
135
|
+
/** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
|
|
136
|
+
private readonly accountCatalogs;
|
|
137
|
+
/** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
|
|
138
|
+
private catalogOwner;
|
|
129
139
|
constructor(options: CodexAdapterOptions);
|
|
130
140
|
/** Discovery fetcher: resolves the session through the refresh-aware path. */
|
|
131
141
|
private fetchCatalog;
|
|
142
|
+
/** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
|
|
143
|
+
clearAccountCatalog(account?: string): void;
|
|
144
|
+
/** Persisted cache for the default account; a throwaway cache for any other. */
|
|
145
|
+
private catalogFor;
|
|
132
146
|
providerInfo(provider: string): LlmProviderInfo;
|
|
133
147
|
private staticModels;
|
|
134
148
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
149
|
+
/** The provider's own catalog: union of every account, or one account when named. */
|
|
150
|
+
listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
|
|
135
151
|
/**
|
|
136
152
|
* The discovered entry for one model. Resolved through the cache's
|
|
137
153
|
* stale-while-revalidate path so capability metadata stays stable across a
|
|
@@ -145,6 +161,11 @@ export declare class CodexAdapter extends LlmAdapter {
|
|
|
145
161
|
/** Ids of every discovered model with a fast tier (the Speed toggle's visibility list). */
|
|
146
162
|
fastCapableModels(): Promise<string[]>;
|
|
147
163
|
resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
164
|
+
/** Capability resolution of the provider's own models (the pool resolves members here). */
|
|
165
|
+
resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
|
|
148
166
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
167
|
+
/** Pool seam: stream through one specific account instead of the default. */
|
|
168
|
+
streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
|
|
169
|
+
private streamCore;
|
|
149
170
|
private request;
|
|
150
171
|
}
|