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.
Files changed (60) hide show
  1. package/README.md +79 -5
  2. package/README.zh.md +78 -4
  3. package/lib/auth/rpc.d.ts +64 -13
  4. package/lib/auth/rpc.js +75 -10
  5. package/lib/auth/store.d.ts +75 -17
  6. package/lib/auth/store.js +148 -27
  7. package/lib/client/ImageGenerateToolview.d.ts +1 -1
  8. package/lib/client/SpeedSelect.d.ts +25 -2
  9. package/lib/client/SpeedSelect.js +10 -6
  10. package/lib/client/SubscriptionsSection.d.ts +83 -3
  11. package/lib/client/SubscriptionsSection.js +411 -62
  12. package/lib/client/VideoGenerateToolview.d.ts +1 -1
  13. package/lib/client/index.d.ts +1 -9
  14. package/lib/client/index.js +7 -4
  15. package/lib/client/locales.d.ts +46 -10
  16. package/lib/client/locales.js +46 -10
  17. package/lib/client.js +703 -132
  18. package/lib/client.js.map +1 -1
  19. package/lib/compat.d.ts +36 -0
  20. package/lib/compat.js +20 -0
  21. package/lib/index.d.ts +26 -1
  22. package/lib/index.js +2377 -309
  23. package/lib/model-defaults.d.ts +23 -0
  24. package/lib/model-defaults.js +237 -0
  25. package/lib/providers/accounts.d.ts +102 -0
  26. package/lib/providers/accounts.js +123 -0
  27. package/lib/providers/claude.d.ts +46 -7
  28. package/lib/providers/claude.js +125 -34
  29. package/lib/providers/codex.d.ts +45 -3
  30. package/lib/providers/codex.js +152 -26
  31. package/lib/providers/common.d.ts +87 -6
  32. package/lib/providers/common.js +185 -22
  33. package/lib/providers/copilot.d.ts +32 -3
  34. package/lib/providers/copilot.js +111 -19
  35. package/lib/providers/grok.d.ts +45 -4
  36. package/lib/providers/grok.js +136 -20
  37. package/lib/providers/pool-family.d.ts +56 -0
  38. package/lib/providers/pool-family.js +45 -0
  39. package/lib/providers/pool-health.d.ts +74 -0
  40. package/lib/providers/pool-health.js +148 -0
  41. package/lib/providers/pool-usage.d.ts +78 -0
  42. package/lib/providers/pool-usage.js +185 -0
  43. package/lib/providers/pool.d.ts +107 -0
  44. package/lib/providers/pool.js +371 -0
  45. package/lib/providers/rate-limit.d.ts +192 -0
  46. package/lib/providers/rate-limit.js +338 -0
  47. package/lib/tools/image-generate.d.ts +3 -3
  48. package/lib/tools/image-generate.js +2 -1
  49. package/lib/tools/video-generate.d.ts +2 -2
  50. package/lib/tools/video-generate.js +2 -1
  51. package/lib/tools/x-search.d.ts +2 -2
  52. package/lib/tools/x-search.js +2 -1
  53. package/lib/translate/anthropic.js +5 -4
  54. package/lib/translate/chat-completions.js +5 -4
  55. package/lib/translate/responses.js +5 -4
  56. package/package.json +21 -21
  57. package/lib/providers/antigravity.d.ts +0 -90
  58. package/lib/providers/antigravity.js +0 -392
  59. package/lib/translate/antigravity.d.ts +0 -110
  60. package/lib/translate/antigravity.js +0 -303
@@ -0,0 +1,23 @@
1
+ import { type ProviderId } from './auth/store.js';
2
+ /** One model id → its configured default reasoning effort id. */
3
+ export type ModelDefaultMap = Readonly<Record<string, string>>;
4
+ /** Provider route → model defaults. */
5
+ export type ModelDefaults = Readonly<Partial<Record<ProviderId, ModelDefaultMap>>>;
6
+ /** Absolute path of the defaults file. */
7
+ export declare function modelDefaultsFilePath(): string;
8
+ /**
9
+ * The last load failure, or a warning about entries that were skipped while
10
+ * loading; consumers only use it for diagnostics.
11
+ */
12
+ export declare function modelDefaultsLoadError(): unknown;
13
+ /** A detached snapshot for the RPC surface (render + diffing). */
14
+ export declare function modelDefaultsSnapshot(): ModelDefaults;
15
+ /**
16
+ * Set or clear one model's configured default effort, then persist. The
17
+ * memory snapshot updates only after the atomic write succeeds, so a failed
18
+ * write never leaves the live state ahead of the file.
19
+ * @param provider - the subscription provider route.
20
+ * @param model - the wire model id.
21
+ * @param effort - the effort id, or undefined to clear the override.
22
+ */
23
+ export declare function setDefaultEffort(provider: ProviderId, model: string, effort: string | undefined): Promise<void>;
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Per-model default reasoning effort overrides — the durable half of the
3
+ * Settings page's per-model "default effort" pickers.
4
+ *
5
+ * The file lives at `~/.dsh/plugins/subscriptions/model-defaults.json`
6
+ * (mode 0600, atomic replace). Shape: `{ "<provider>": { "<model id>": "<effort>" } }`.
7
+ * An absent entry means "follow the provider's own default": the `Default`
8
+ * chip the model picker shows when the discovered catalog advertises no
9
+ * default at all.
10
+ *
11
+ * Writes are single-process and atomic, but *not* as serialised as the rest
12
+ * of the page: the Settings page disables only the row being saved, so two
13
+ * rows saved back to back can overlap. The write chain below serialises them,
14
+ * so no update is lost to a read-modify-write race. Every read comes from the
15
+ * in-memory snapshot, so the on-disk file only needs to survive a restart: a
16
+ * malformed file reads as empty and is rewritten on the next save, never
17
+ * taking the plugin down with it.
18
+ */
19
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
20
+ import { dirname } from 'node:path';
21
+ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
22
+ import { PROVIDER_IDS } from './auth/store.js';
23
+ /** Absolute path of the defaults file. */
24
+ export function modelDefaultsFilePath() {
25
+ return dshHomePath('plugins', 'subscriptions', 'model-defaults.json');
26
+ }
27
+ const EMPTY = Object.freeze({});
28
+ /** In-memory snapshot read by every consumer (adapters, RPC). */
29
+ let current = EMPTY;
30
+ /** One lazy load of the on-disk file (read once per process). */
31
+ let ready;
32
+ /** Last load failure, surfaced to callers that care; defaults stay empty. */
33
+ let loadError;
34
+ /**
35
+ * Serialises every write: the read-modify-write sequence must not interleave,
36
+ * or a fast second save would compute its snapshot from the stale `current`
37
+ * and silently drop the first update (the UI disables only the row being
38
+ * saved, so overlaps are reachable).
39
+ */
40
+ let writeChain = Promise.resolve();
41
+ /**
42
+ * Validate one persisted provider section: a string→string map, or undefined.
43
+ * Malformed *entries* are skipped, not the whole section: one bad value (a
44
+ * hand edit losing its quotes) must not silently un-configure every model in
45
+ * that provider. What was dropped is reported so the caller can surface it
46
+ * instead of the loss disappearing.
47
+ */
48
+ function sanitizeProvider(value, dropped) {
49
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
50
+ return undefined;
51
+ const entries = {};
52
+ for (const [model, effort] of Object.entries(value)) {
53
+ if (typeof effort !== 'string' || effort.length === 0) {
54
+ dropped.push(model);
55
+ continue;
56
+ }
57
+ entries[model] = effort;
58
+ }
59
+ if (Object.keys(entries).length === 0)
60
+ return undefined;
61
+ return Object.freeze(entries);
62
+ }
63
+ /** Validate the raw document: only known providers, malformed sections dropped. */
64
+ function sanitizeDefaults(value) {
65
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
66
+ return { defaults: EMPTY, dropped: [] };
67
+ const record = value;
68
+ const result = {};
69
+ const dropped = [];
70
+ for (const provider of PROVIDER_IDS) {
71
+ const section = sanitizeProvider(record[provider], dropped);
72
+ if (section !== undefined)
73
+ result[provider] = section;
74
+ }
75
+ return { defaults: Object.freeze(result), dropped };
76
+ }
77
+ /** Read and validate the on-disk file; a missing file reads as empty. */
78
+ async function loadFile(path) {
79
+ let text;
80
+ try {
81
+ text = await readFile(path, 'utf8');
82
+ }
83
+ catch (error) {
84
+ if (error.code === 'ENOENT')
85
+ return EMPTY;
86
+ throw error;
87
+ }
88
+ try {
89
+ const { defaults, dropped } = sanitizeDefaults(JSON.parse(text));
90
+ if (dropped.length > 0)
91
+ loadError = new Error(`subscriptions model defaults: ${dropped.length} malformed entr${dropped.length === 1 ? 'y' : 'ies'} skipped (${dropped.join(', ')}); fix or delete the file`);
92
+ return defaults;
93
+ }
94
+ catch {
95
+ throw new Error(`subscriptions model defaults at ${path} are not valid JSON; fix or delete the file`);
96
+ }
97
+ }
98
+ /** Resolve the module state once from disk; failures leave the defaults empty. */
99
+ async function ensureReady() {
100
+ ready ??= loadFile(modelDefaultsFilePath()).then((loaded) => {
101
+ current = loaded;
102
+ // loadFile itself sets loadError for skipped entries; do not clobber it.
103
+ }, (error) => {
104
+ loadError = error;
105
+ current = EMPTY;
106
+ });
107
+ return ready;
108
+ }
109
+ /** Persist a snapshot atomically with owner-only permissions. */
110
+ async function atomicPersist(defaults, path) {
111
+ await mkdir(dirname(path), { recursive: true });
112
+ const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
113
+ try {
114
+ await writeFile(tmp, JSON.stringify(defaults, null, 2), { mode: 0o600 });
115
+ await chmod(tmp, 0o600);
116
+ await rename(tmp, path);
117
+ }
118
+ catch (error) {
119
+ await rm(tmp, { force: true });
120
+ throw error;
121
+ }
122
+ }
123
+ let persistDefaults = atomicPersist;
124
+ /**
125
+ * Clone one provider section, or undefined when nothing is configured for it.
126
+ * The clone is prototype-less: model ids are provider-supplied catalog data
127
+ * used as object keys, and consumers index the section directly (the RPC
128
+ * catalog in index.ts does), so an id like `toString` would otherwise yield an
129
+ * inherited *function* where a string is declared.
130
+ */
131
+ function sectionOf(defaults, provider) {
132
+ const section = defaults[provider];
133
+ if (section === undefined)
134
+ return undefined;
135
+ return Object.assign(Object.create(null), section);
136
+ }
137
+ /**
138
+ * Ready the defaults store.
139
+ * @internal Exported for tests; index.ts calls it at apply time so every
140
+ * later synchronous read sees the persisted state.
141
+ */
142
+ export async function loadModelDefaults() {
143
+ await ensureReady();
144
+ }
145
+ /**
146
+ * The last load failure, or a warning about entries that were skipped while
147
+ * loading; consumers only use it for diagnostics.
148
+ */
149
+ export function modelDefaultsLoadError() {
150
+ return loadError;
151
+ }
152
+ /**
153
+ * The configured default effort for one model, or undefined when none (the
154
+ * picker then follows the provider's own default).
155
+ * @internal Exported for the adapters' `defaultEffortOf` options.
156
+ */
157
+ export function defaultEffortOf(provider, model) {
158
+ const section = current[provider];
159
+ if (section === undefined)
160
+ return undefined;
161
+ // Own-property lookup: a model id is provider-supplied catalog data, and a
162
+ // plain index would inherit from Object.prototype for names like
163
+ // `toString`, handing a *function* to mergeReasoning (which then throws and
164
+ // breaks that model's resolution).
165
+ return Object.prototype.hasOwnProperty.call(section, model) ? section[model] : undefined;
166
+ }
167
+ /** A detached snapshot for the RPC surface (render + diffing). */
168
+ export function modelDefaultsSnapshot() {
169
+ const result = {};
170
+ for (const provider of PROVIDER_IDS) {
171
+ const section = sectionOf(current, provider);
172
+ if (section !== undefined)
173
+ result[provider] = section;
174
+ }
175
+ return Object.freeze(result);
176
+ }
177
+ /**
178
+ * Set or clear one model's configured default effort, then persist. The
179
+ * memory snapshot updates only after the atomic write succeeds, so a failed
180
+ * write never leaves the live state ahead of the file.
181
+ * @param provider - the subscription provider route.
182
+ * @param model - the wire model id.
183
+ * @param effort - the effort id, or undefined to clear the override.
184
+ */
185
+ export function setDefaultEffort(provider, model, effort) {
186
+ // Chained behind every earlier write: the snapshot `current` is read inside
187
+ // the chain, so two overlapping saves cannot lose either update. The caller
188
+ // receives the promise of its own write (a rejection propagates), not the
189
+ // shared chain.
190
+ const run = writeChain.then(async () => {
191
+ await ensureReady();
192
+ const section = { ...sectionOf(current, provider) ?? {} };
193
+ if (effort === undefined) {
194
+ delete section[model];
195
+ }
196
+ else {
197
+ section[model] = effort;
198
+ }
199
+ const next = { ...current };
200
+ if (Object.keys(section).length === 0) {
201
+ delete next[provider];
202
+ }
203
+ else {
204
+ next[provider] = Object.freeze(section);
205
+ }
206
+ const frozen = Object.freeze(next);
207
+ await persistDefaults(frozen, modelDefaultsFilePath());
208
+ current = frozen;
209
+ });
210
+ // Keep the chain alive even when one write fails, or every later save would
211
+ // be stuck behind the rejected promise. The caller has already received the
212
+ // rejection through `run`.
213
+ writeChain = run.catch(() => undefined);
214
+ return run;
215
+ }
216
+ /**
217
+ * Drop the in-memory state and the cached load. Test-only: lets a suite
218
+ * unwind the lazy singleton before the next `loadModelDefaults`.
219
+ * @internal Exported for tests only; not part of the plugin's public surface.
220
+ */
221
+ export async function resetModelDefaultsForTests() {
222
+ current = EMPTY;
223
+ ready = undefined;
224
+ loadError = undefined;
225
+ writeChain = Promise.resolve();
226
+ persistDefaults = atomicPersist;
227
+ }
228
+ /**
229
+ * Test-only seam: replace the atomic persistence so a failure happens on the
230
+ * real write path. Proves a failed write propagates to the caller and does
231
+ * not wedge the write chain (resetModelDefaultsForTests restores the real
232
+ * implementation).
233
+ * @internal Exported for tests only.
234
+ */
235
+ export function overridePersistForTests(persist) {
236
+ persistDefaults = persist;
237
+ }
@@ -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,10 +7,12 @@ 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 { TokenManager } from './common.js';
13
+ import { AccountTokenManager } from './accounts.js';
13
14
  import type { CatalogPersistence, DiscoveredModel, FetchFn, ModelEntry, ProviderUsage } from './common.js';
15
+ import type { RateLimitResetReader, RateLimitWait } from './rate-limit.js';
14
16
  export declare const CLAUDE_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
15
17
  export declare const CLAUDE_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
16
18
  export declare const CLAUDE_TOKEN_URL = "https://claude.ai/v1/oauth/token";
@@ -21,6 +23,20 @@ export declare const CLAUDE_SCOPE = "org:create_api_key user:profile user:infere
21
23
  export declare const CLAUDE_CALLBACK_PATH = "/callback";
22
24
  /** Refresh when the access token has less than this much life left. */
23
25
  export declare const CLAUDE_PREEMPT_MS: number;
26
+ /**
27
+ * Reads the reset instant of the Anthropic window that rejected a request.
28
+ *
29
+ * `anthropic-ratelimit-unified-*` is the subscription-plan family — the one
30
+ * Claude Code renders as "resets 3pm" — and is the only header that names the
31
+ * window which actually rejected this request. The per-bucket
32
+ * `anthropic-ratelimit-{requests,tokens,input-tokens,output-tokens}-reset`
33
+ * headers are deliberately not read: they are rollover snapshots attached to
34
+ * every response, so on a 429 they cannot say which bucket refused, and the
35
+ * earliest of them is typically the bucket that still had room — a wait that
36
+ * lands straight back in the closed window. They reach the operator through
37
+ * `rateLimitDiagnostics` instead.
38
+ */
39
+ export declare const claudeRateLimitReset: RateLimitResetReader;
24
40
  /**
25
41
  * The subscription endpoint only serves requests presenting as Claude Code,
26
42
  * so these headers impersonate the CLI; the harness attribution user-agent
@@ -64,23 +80,31 @@ export declare const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usa
64
80
  * @returns the mapped usage snapshot.
65
81
  */
66
82
  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[]>;
83
+ /** Fetch the live model catalog from the subscription endpoint. `signal` cancels the request. */
84
+ export declare function fetchClaudeModels(session: ClaudeSession, fetchFn?: FetchFn, signal?: AbortSignal): Promise<DiscoveredModel[]>;
69
85
  /** Constructor dependencies for {@link ClaudeAdapter}. */
70
86
  export interface ClaudeAdapterOptions {
71
87
  models: readonly ModelEntry[];
72
88
  streamIdleTimeoutMs: number;
73
- tokens: TokenManager<ClaudeSession>;
89
+ tokens: AccountTokenManager<ClaudeSession>;
90
+ /** Late-bound pool facade (wired after adapter construction); pools list under their first member's provider. */
91
+ pool?: () => PoolAdapter | undefined;
74
92
  /** Whether to fetch the live catalog when logged in (false when config `models` overrides). */
75
93
  discovery: boolean;
76
94
  fetchFn?: FetchFn;
77
95
  onWarn?: (message: string) => void;
78
- /** Max retries on a retryable failure before giving up; matches Claude Code's own client-side retry count. Defaults to the dsh-llm default (2) when unset. */
79
- maxRetries?: number;
96
+ /** How long this route may hold a turn open waiting for a rate-limit window; defaults to waiting on, six-hour ceiling. */
97
+ rateLimit?: RateLimitWait;
80
98
  /** Resolve the attachment service per request; absent means image requests fail loudly. */
81
99
  resolveAttachments?: () => AttachmentStore | undefined;
82
100
  /** Durable catalog store seeding capability metadata across restarts. */
83
101
  catalogStore?: CatalogPersistence;
102
+ /**
103
+ * Per-model default reasoning effort override (the Settings page's picker).
104
+ * Returns the user-configured default for one model, or undefined to follow
105
+ * the provider's own default.
106
+ */
107
+ defaultEffortOf?: (model: string) => string | undefined;
84
108
  }
85
109
  /**
86
110
  * Assemble the Anthropic request body.
@@ -102,15 +126,30 @@ export declare function claudeRequestBody(options: GenerateOptions, messages: re
102
126
  export declare class ClaudeAdapter extends LlmAdapter {
103
127
  private readonly options;
104
128
  private readonly catalog;
129
+ /** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
130
+ private readonly accountCatalogs;
131
+ /** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
132
+ private catalogOwner;
105
133
  constructor(options: ClaudeAdapterOptions);
106
134
  private fetchCatalog;
135
+ /** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
136
+ clearAccountCatalog(account?: string): void;
137
+ /** Persisted cache for the default account; a throwaway cache for any other. */
138
+ private catalogFor;
107
139
  private discovered;
108
140
  private staticModels;
109
141
  providerInfo(provider: string): LlmProviderInfo;
110
- providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy | undefined;
142
+ providerRetryPolicy(provider: string): import("@deepseek-ai/dsh-llm").ResolvedRetryPolicy;
111
143
  listModels(provider: string): Promise<readonly LlmModelInfo[]>;
144
+ /** The provider's own catalog: union of every account, or one account when named. */
145
+ listOwnModels(provider: string, account?: string, signal?: AbortSignal): Promise<readonly LlmModelInfo[]>;
112
146
  resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
147
+ /** Capability resolution of the provider's own models (the pool resolves members here). */
148
+ resolveOwnModel(provider: string, model: string): Promise<LlmResolvedModelInfo>;
113
149
  stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
150
+ /** Pool seam: stream through one specific account instead of the default. */
151
+ streamAccount(options: GenerateOptions, account: string): AsyncIterable<StreamChunk>;
152
+ private streamCore;
114
153
  /**
115
154
  * `display: 'summarized'` is set explicitly on both shapes: `adaptive`-type
116
155
  * models default to `display: 'omitted'`, which returns thinking blocks with