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
@@ -1,10 +1,16 @@
1
1
  /**
2
2
  * On-disk OAuth session store at `~/.dsh/plugins/subscriptions/auth.json`.
3
3
  *
4
- * The file is a JSON object keyed by provider id. Writes are atomic
5
- * (tmp file + rename) with mode 0600 because they carry bearer tokens.
6
- * Session shapes live here (not in the provider modules) because this file
7
- * owns the durable format.
4
+ * The file is a JSON object keyed by provider id, each entry holding that
5
+ * provider's ACCOUNTS: a map of account key session plus the default
6
+ * account's key. Writes are atomic (tmp file + rename) with mode 0600
7
+ * because they carry bearer tokens. Session shapes live here (not in the
8
+ * provider modules) because this file owns the durable format.
9
+ *
10
+ * Backward compatibility: entries written by single-account versions hold
11
+ * the session fields directly (no `accounts` wrapper); reads migrate them
12
+ * in memory, and the next write persists the new shape — existing logins
13
+ * survive the upgrade untouched.
8
14
  */
9
15
  /** Provider routes this plugin can serve. */
10
16
  export type ProviderId = 'codex' | 'claude' | 'grok' | 'copilot';
@@ -34,6 +40,11 @@ export interface ClaudeSession {
34
40
  scopes: string;
35
41
  emailAddress?: string;
36
42
  subscriptionType?: string;
43
+ /**
44
+ * True when this account was imported from Claude Code's own credential
45
+ * store (Keychain/file): only bound accounts sync refreshes back to it.
46
+ */
47
+ keychainBound?: boolean;
37
48
  }
38
49
  /** Stored Grok (X Premium / xAI) subscription session. */
39
50
  export interface GrokSession {
@@ -64,15 +75,42 @@ export interface CopilotSession {
64
75
  /** GitHub login name, for the status display. */
65
76
  account?: string;
66
77
  }
67
- /** The durable store shape: one optional session per provider. */
78
+ /** One provider's accounts: account key session, plus the default account. */
79
+ export interface ProviderAccounts<S> {
80
+ /** Key of the account direct (non-pool) routes serve; the first login wins. */
81
+ default?: string;
82
+ accounts: Record<string, S>;
83
+ }
84
+ /** The durable store shape: per provider, its accounts. */
68
85
  export interface SessionMap {
69
- codex?: CodexSession;
70
- claude?: ClaudeSession;
71
- grok?: GrokSession;
72
- copilot?: CopilotSession;
86
+ codex?: ProviderAccounts<CodexSession>;
87
+ claude?: ProviderAccounts<ClaudeSession>;
88
+ grok?: ProviderAccounts<GrokSession>;
89
+ copilot?: ProviderAccounts<CopilotSession>;
73
90
  }
74
91
  /** Any stored session, for provider-agnostic plumbing. */
75
92
  export type StoredSession = CodexSession | ClaudeSession | GrokSession | CopilotSession;
93
+ /** The session type one provider stores. */
94
+ export type SessionOf<K extends ProviderId> = NonNullable<SessionMap[K]>['accounts'][string];
95
+ /** One account entry as returned by {@link listAccounts} (default first). */
96
+ export interface AccountEntry<S> {
97
+ key: string;
98
+ session: S;
99
+ }
100
+ /**
101
+ * The stable identity of one session's account: codex keys on the always
102
+ * present `accountId` claim, the others on their display identity, falling
103
+ * back to a refresh-token hash for sessions stored before identity fields
104
+ * existed. Logging the same account in again lands on the same key, so a
105
+ * re-login updates in place instead of duplicating. (The hash fallback can
106
+ * miss that dedup once for a legacy session re-logged with a now-known
107
+ * identity — the duplicate is visible on the Settings page and can simply
108
+ * be logged out.)
109
+ * @param provider - the provider route.
110
+ * @param session - the session to key.
111
+ * @returns the account map key.
112
+ */
113
+ export declare function accountKeyOf(provider: ProviderId, session: StoredSession): string;
76
114
  /**
77
115
  * Absolute path of the auth store file.
78
116
  * @returns `dshHomePath('plugins', 'subscriptions', 'auth.json')`.
@@ -81,28 +119,48 @@ export declare function authFilePath(): string;
81
119
  /**
82
120
  * Read the whole store. A missing file is an empty store; malformed JSON or a
83
121
  * malformed entry throws, because silently discarding tokens would strand the
84
- * user without a diagnosis.
122
+ * user without a diagnosis. Single-account entries are migrated in memory;
123
+ * the next write persists the new shape.
85
124
  * @param path - store file path; defaults to {@link authFilePath}.
86
125
  * @returns the parsed session map.
87
126
  */
88
127
  export declare function loadStore(path?: string): Promise<SessionMap>;
89
128
  /**
90
- * Read one provider's session.
129
+ * List one provider's accounts, default first.
130
+ * @param provider - the provider route.
131
+ * @param path - store file path; defaults to {@link authFilePath}.
132
+ * @returns the account entries in stable order (empty when logged out).
133
+ */
134
+ export declare function listAccounts<K extends ProviderId>(provider: K, path?: string): Promise<AccountEntry<SessionOf<K>>[]>;
135
+ /**
136
+ * Read one account's session.
91
137
  * @param provider - the provider route.
138
+ * @param account - the account key; defaults to the provider's default account.
92
139
  * @param path - store file path; defaults to {@link authFilePath}.
93
- * @returns the stored session, or `undefined` when logged out.
140
+ * @returns the stored session, or `undefined` when absent.
94
141
  */
95
- export declare function getSession<K extends ProviderId>(provider: K, path?: string): Promise<SessionMap[K] | undefined>;
142
+ export declare function getAccountSession<K extends ProviderId>(provider: K, account?: string, path?: string): Promise<SessionOf<K> | undefined>;
96
143
  /**
97
- * Write one provider's session, preserving the others.
144
+ * Write one account's session, preserving the others. The first account of a
145
+ * provider becomes its default.
98
146
  * @param provider - the provider route.
147
+ * @param account - the account key (see {@link accountKeyOf}).
99
148
  * @param session - the fresh session from a login or refresh.
100
149
  * @param path - store file path; defaults to {@link authFilePath}.
101
150
  */
102
- export declare function saveSession<K extends ProviderId>(provider: K, session: NonNullable<SessionMap[K]>, path?: string): Promise<void>;
151
+ export declare function saveAccountSession<K extends ProviderId>(provider: K, account: string, session: SessionOf<K>, path?: string): Promise<void>;
152
+ /**
153
+ * Delete one account's session (logout). Deleting the default moves the badge
154
+ * to the next remaining account.
155
+ * @param provider - the provider route.
156
+ * @param account - the account key.
157
+ * @param path - store file path; defaults to {@link authFilePath}.
158
+ */
159
+ export declare function deleteAccountSession(provider: ProviderId, account: string, path?: string): Promise<void>;
103
160
  /**
104
- * Delete one provider's session (logout).
161
+ * Pin the account direct (non-pool) routes serve.
105
162
  * @param provider - the provider route.
163
+ * @param account - the account key; must exist.
106
164
  * @param path - store file path; defaults to {@link authFilePath}.
107
165
  */
108
- export declare function deleteSession(provider: ProviderId, path?: string): Promise<void>;
166
+ export declare function setDefaultAccount(provider: ProviderId, account: string, path?: string): Promise<void>;
package/lib/auth/store.js CHANGED
@@ -1,16 +1,52 @@
1
1
  /**
2
2
  * On-disk OAuth session store at `~/.dsh/plugins/subscriptions/auth.json`.
3
3
  *
4
- * The file is a JSON object keyed by provider id. Writes are atomic
5
- * (tmp file + rename) with mode 0600 because they carry bearer tokens.
6
- * Session shapes live here (not in the provider modules) because this file
7
- * owns the durable format.
4
+ * The file is a JSON object keyed by provider id, each entry holding that
5
+ * provider's ACCOUNTS: a map of account key session plus the default
6
+ * account's key. Writes are atomic (tmp file + rename) with mode 0600
7
+ * because they carry bearer tokens. Session shapes live here (not in the
8
+ * provider modules) because this file owns the durable format.
9
+ *
10
+ * Backward compatibility: entries written by single-account versions hold
11
+ * the session fields directly (no `accounts` wrapper); reads migrate them
12
+ * in memory, and the next write persists the new shape — existing logins
13
+ * survive the upgrade untouched.
8
14
  */
15
+ import { createHash } from 'node:crypto';
9
16
  import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
10
17
  import { dirname } from 'node:path';
11
18
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
12
19
  /** Every provider route, in display order. */
13
20
  export const PROVIDER_IDS = ['codex', 'claude', 'grok', 'copilot'];
21
+ /**
22
+ * The stable identity of one session's account: codex keys on the always
23
+ * present `accountId` claim, the others on their display identity, falling
24
+ * back to a refresh-token hash for sessions stored before identity fields
25
+ * existed. Logging the same account in again lands on the same key, so a
26
+ * re-login updates in place instead of duplicating. (The hash fallback can
27
+ * miss that dedup once for a legacy session re-logged with a now-known
28
+ * identity — the duplicate is visible on the Settings page and can simply
29
+ * be logged out.)
30
+ * @param provider - the provider route.
31
+ * @param session - the session to key.
32
+ * @returns the account map key.
33
+ */
34
+ export function accountKeyOf(provider, session) {
35
+ switch (provider) {
36
+ case 'codex':
37
+ return session.accountId;
38
+ case 'claude':
39
+ return session.emailAddress ?? tokenHash(session.refreshToken);
40
+ case 'grok':
41
+ return session.account ?? tokenHash(session.refreshToken);
42
+ case 'copilot':
43
+ return session.account ?? tokenHash(session.refreshToken);
44
+ }
45
+ }
46
+ /** Short stable hash for sessions without an identity field. */
47
+ function tokenHash(refreshToken) {
48
+ return `token-${createHash('sha256').update(refreshToken).digest('hex').slice(0, 16)}`;
49
+ }
14
50
  /**
15
51
  * Absolute path of the auth store file.
16
52
  * @returns `dshHomePath('plugins', 'subscriptions', 'auth.json')`.
@@ -22,22 +58,23 @@ export function authFilePath() {
22
58
  function legacyAuthFilePath() {
23
59
  return dshHomePath('plugins', 'router', 'auth.json');
24
60
  }
25
- /** Check that one durable entry carries the fields every session needs. */
26
- function assertSessionShape(provider, value) {
61
+ /** Check that one durable session carries the fields every session needs. */
62
+ function assertSessionShape(provider, account, value) {
27
63
  if (typeof value !== 'object' || value === null) {
28
- throw new Error(`subscriptions auth store: entry "${provider}" is not an object; fix or delete the store file`);
64
+ throw new Error(`subscriptions auth store: entry "${provider}/${account}" is not an object; fix or delete the store file`);
29
65
  }
30
66
  const entry = value;
31
67
  if (typeof entry.accessToken !== 'string' || entry.accessToken.length === 0
32
68
  || typeof entry.refreshToken !== 'string' || entry.refreshToken.length === 0
33
69
  || typeof entry.expiresAt !== 'number' || !Number.isFinite(entry.expiresAt)) {
34
- throw new Error(`subscriptions auth store: entry "${provider}" is missing accessToken/refreshToken/expiresAt; fix or delete the store file`);
70
+ throw new Error(`subscriptions auth store: entry "${provider}/${account}" is missing accessToken/refreshToken/expiresAt; fix or delete the store file`);
35
71
  }
36
72
  }
37
73
  /**
38
74
  * Read the whole store. A missing file is an empty store; malformed JSON or a
39
75
  * malformed entry throws, because silently discarding tokens would strand the
40
- * user without a diagnosis.
76
+ * user without a diagnosis. Single-account entries are migrated in memory;
77
+ * the next write persists the new shape.
41
78
  * @param path - store file path; defaults to {@link authFilePath}.
42
79
  * @returns the parsed session map.
43
80
  */
@@ -67,7 +104,7 @@ export async function loadStore(path = authFilePath()) {
67
104
  }
68
105
  return parseStore(text, path);
69
106
  }
70
- /** Parse and validate store JSON read from `path`. */
107
+ /** Parse, validate, and migrate store JSON read from `path`. */
71
108
  function parseStore(text, path) {
72
109
  let parsed;
73
110
  try {
@@ -79,11 +116,36 @@ function parseStore(text, path) {
79
116
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
80
117
  throw new Error(`subscriptions auth store at ${path} must be a JSON object keyed by provider; fix or delete the file`);
81
118
  }
82
- const store = parsed;
119
+ const raw = parsed;
120
+ const store = {};
83
121
  for (const provider of PROVIDER_IDS) {
84
- const entry = store[provider];
85
- if (entry !== undefined)
86
- assertSessionShape(provider, entry);
122
+ const entry = raw[provider];
123
+ if (entry === undefined)
124
+ continue;
125
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
126
+ throw new Error(`subscriptions auth store: entry "${provider}" is not an object; fix or delete the store file`);
127
+ }
128
+ const record = entry;
129
+ if (typeof record.accessToken === 'string') {
130
+ // Single-account format: wrap the bare session, preserving every field.
131
+ assertSessionShape(provider, '(legacy)', record);
132
+ const session = record;
133
+ const key = accountKeyOf(provider, session);
134
+ store[provider] = { default: key, accounts: { [key]: session } };
135
+ continue;
136
+ }
137
+ const accounts = record.accounts;
138
+ if (typeof accounts !== 'object' || accounts === null || Array.isArray(accounts)) {
139
+ throw new Error(`subscriptions auth store: entry "${provider}" has no accounts map; fix or delete the store file`);
140
+ }
141
+ if (record.default !== undefined && typeof record.default !== 'string') {
142
+ throw new Error(`subscriptions auth store: entry "${provider}" default is not a string; fix or delete the store file`);
143
+ }
144
+ for (const [account, session] of Object.entries(accounts)) {
145
+ assertSessionShape(provider, account, session);
146
+ }
147
+ ;
148
+ store[provider] = record;
87
149
  }
88
150
  return store;
89
151
  }
@@ -106,8 +168,8 @@ async function writeStore(store, path) {
106
168
  /**
107
169
  * One write chain per store path. Every mutation is a read-modify-write of a
108
170
  * single JSON file, and the plugin has several independent writers — a login,
109
- * a logout, and one token refresh per provider adapter, each on its own
110
- * schedule. Overlapping them unserialized costs whichever provider read the
171
+ * a logout, and one token refresh per provider account, each on its own
172
+ * schedule. Overlapping them unserialized costs whichever account read the
111
173
  * store first its entry.
112
174
  *
113
175
  * A chain is dropped once nothing is queued behind it, so the map holds an
@@ -136,38 +198,97 @@ async function serialize(path, action) {
136
198
  }
137
199
  }
138
200
  /**
139
- * Read one provider's session.
201
+ * List one provider's accounts, default first.
140
202
  * @param provider - the provider route.
141
203
  * @param path - store file path; defaults to {@link authFilePath}.
142
- * @returns the stored session, or `undefined` when logged out.
204
+ * @returns the account entries in stable order (empty when logged out).
143
205
  */
144
- export async function getSession(provider, path = authFilePath()) {
145
- return (await loadStore(path))[provider];
206
+ export async function listAccounts(provider, path = authFilePath()) {
207
+ const entry = (await loadStore(path))[provider];
208
+ if (entry === undefined)
209
+ return [];
210
+ const accounts = Object.entries(entry.accounts).map(([key, session]) => ({ key, session }));
211
+ accounts.sort((a, b) => Number(b.key === entry.default) - Number(a.key === entry.default));
212
+ return accounts;
146
213
  }
147
214
  /**
148
- * Write one provider's session, preserving the others.
215
+ * Read one account's session.
149
216
  * @param provider - the provider route.
217
+ * @param account - the account key; defaults to the provider's default account.
218
+ * @param path - store file path; defaults to {@link authFilePath}.
219
+ * @returns the stored session, or `undefined` when absent.
220
+ */
221
+ export async function getAccountSession(provider, account, path = authFilePath()) {
222
+ const entry = (await loadStore(path))[provider];
223
+ if (entry === undefined)
224
+ return undefined;
225
+ const key = account ?? entry.default;
226
+ if (key === undefined)
227
+ return undefined;
228
+ return entry.accounts[key];
229
+ }
230
+ /**
231
+ * Write one account's session, preserving the others. The first account of a
232
+ * provider becomes its default.
233
+ * @param provider - the provider route.
234
+ * @param account - the account key (see {@link accountKeyOf}).
150
235
  * @param session - the fresh session from a login or refresh.
151
236
  * @param path - store file path; defaults to {@link authFilePath}.
152
237
  */
153
- export async function saveSession(provider, session, path = authFilePath()) {
238
+ export async function saveAccountSession(provider, account, session, path = authFilePath()) {
154
239
  return serialize(path, async () => {
155
240
  const store = await loadStore(path);
156
- store[provider] = session;
241
+ const entry = store[provider];
242
+ store[provider] = {
243
+ default: entry?.default ?? account,
244
+ accounts: { ...entry?.accounts, [account]: session },
245
+ };
157
246
  await writeStore(store, path);
158
247
  });
159
248
  }
160
249
  /**
161
- * Delete one provider's session (logout).
250
+ * Delete one account's session (logout). Deleting the default moves the badge
251
+ * to the next remaining account.
162
252
  * @param provider - the provider route.
253
+ * @param account - the account key.
163
254
  * @param path - store file path; defaults to {@link authFilePath}.
164
255
  */
165
- export async function deleteSession(provider, path = authFilePath()) {
256
+ export async function deleteAccountSession(provider, account, path = authFilePath()) {
166
257
  return serialize(path, async () => {
167
258
  const store = await loadStore(path);
168
- if (store[provider] === undefined)
259
+ const entry = store[provider];
260
+ if (entry === undefined || !(account in entry.accounts))
169
261
  return;
170
- delete store[provider];
262
+ const accounts = { ...entry.accounts };
263
+ delete accounts[account];
264
+ if (Object.keys(accounts).length === 0) {
265
+ delete store[provider];
266
+ }
267
+ else {
268
+ ;
269
+ store[provider] = {
270
+ ...entry.default === account ? { default: Object.keys(accounts)[0] } : { default: entry.default },
271
+ accounts,
272
+ };
273
+ }
274
+ await writeStore(store, path);
275
+ });
276
+ }
277
+ /**
278
+ * Pin the account direct (non-pool) routes serve.
279
+ * @param provider - the provider route.
280
+ * @param account - the account key; must exist.
281
+ * @param path - store file path; defaults to {@link authFilePath}.
282
+ */
283
+ export async function setDefaultAccount(provider, account, path = authFilePath()) {
284
+ return serialize(path, async () => {
285
+ const store = await loadStore(path);
286
+ const entry = store[provider];
287
+ if (entry === undefined || !(account in entry.accounts)) {
288
+ throw new Error(`no ${provider} account "${account}" is logged in`);
289
+ }
290
+ ;
291
+ store[provider] = { ...entry, default: account };
171
292
  await writeStore(store, path);
172
293
  });
173
294
  }
@@ -1,5 +1,5 @@
1
1
  import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
- import type { ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client';
2
+ import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
3
3
  import type { ImageLoader } from './ImageGallery.js';
4
4
  import type { SubscriptionsKey } from './locales.js';
5
5
  /** Mirror of ui-tool's ToolCallOwnerProps (see the module header). */
@@ -12,6 +12,25 @@ export interface SpeedSelectState {
12
12
  visible: boolean;
13
13
  tier: SpeedTier;
14
14
  }
15
+ /**
16
+ * Minimal structural face of ui-model-selection's `ctx.modelDirectories`
17
+ * service (the sanctioned cross-plugin channel: cordis services, not value
18
+ * imports — same mirroring discipline as ToolCallOwnerProps). Both dsh lines
19
+ * ship it with this shape; it replaces rc.2's `connection.api.sessions.models`
20
+ * read, which the 0.1.2-alpha removed along with the whole `.api` face.
21
+ */
22
+ export interface ModelDirectoriesLike {
23
+ /** Resolve one session's shared model directory (throws for unknown sessions). */
24
+ directoryFor(sessionId: string): {
25
+ /** Load the directory; `current` is the session's effective model selection. */
26
+ load(): Promise<{
27
+ current: {
28
+ provider: string;
29
+ model: string;
30
+ } | null;
31
+ }>;
32
+ };
33
+ }
15
34
  /** Injected dependencies of {@link SpeedSelect} (slot `inject`, session-bound). */
16
35
  export interface SpeedSelectInjected {
17
36
  /** Load the session's speed state; `visible` false keeps the control hidden. */
@@ -32,9 +51,13 @@ export type SpeedSelectProps = PropsRuntime<'conversation.input.right'> & Partia
32
51
  * known state, so a transient failure never locks the toggle away.
33
52
  *
34
53
  * `sessionId` is a plain string: slot and command contexts brand it through
35
- * different dsh-session copies, and only the API-client boundary needs one.
54
+ * different dsh-session copies, and only the service boundary needs one.
55
+ *
56
+ * `models` resolves lazily per call: the ui-model-selection service may
57
+ * register after this plugin applies, and a shell without it (no model seat
58
+ * at all) simply keeps the toggle hidden.
36
59
  */
37
- export declare function createSpeedLoader(connection: ConnectionHandle, sessionId: string): SpeedSelectInjected['loadSpeed'];
60
+ export declare function createSpeedLoader(connection: ConnectionHandle, models: () => ModelDirectoriesLike | undefined, sessionId: string): SpeedSelectInjected['loadSpeed'];
38
61
  /** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
39
62
  export declare function createSpeedSetter(connection: ConnectionHandle, sessionId: string): SpeedSelectInjected['setSpeed'];
40
63
  /**
@@ -22,15 +22,19 @@ import { en } from './locales.js';
22
22
  * known state, so a transient failure never locks the toggle away.
23
23
  *
24
24
  * `sessionId` is a plain string: slot and command contexts brand it through
25
- * different dsh-session copies, and only the API-client boundary needs one.
25
+ * different dsh-session copies, and only the service boundary needs one.
26
+ *
27
+ * `models` resolves lazily per call: the ui-model-selection service may
28
+ * register after this plugin applies, and a shell without it (no model seat
29
+ * at all) simply keeps the toggle hidden.
26
30
  */
27
- export function createSpeedLoader(connection, sessionId) {
31
+ export function createSpeedLoader(connection, models, sessionId) {
28
32
  return async () => {
29
33
  const state = await callSubscriptionsAuth(connection.rpc, 'speed', { sessionId });
30
- const { result } = await connection.api.sessions.models({ sessionId: sessionId });
31
- if (!result.ok)
32
- throw new Error(`session.models failed: ${result.error.code}: ${result.error.message}`);
33
- const current = result.value.current;
34
+ const directories = models();
35
+ if (directories === undefined)
36
+ return { visible: false, tier: state.tier };
37
+ const { current } = await directories.directoryFor(sessionId).load();
34
38
  const visible = current !== null && current.provider === 'codex'
35
39
  && state.fastModels.includes(current.model);
36
40
  return { visible, tier: state.tier };
@@ -2,12 +2,18 @@ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
2
  import type { SubscriptionsKey } from './locales.js';
3
3
  /** Subscription provider ids, fixed by the node half's OAuth adapters. */
4
4
  export type SubscriptionProvider = 'codex' | 'claude' | 'grok' | 'copilot';
5
+ /** One logged-in account as answered by the `status` endpoint. */
6
+ export interface AccountStatus {
7
+ key: string;
8
+ account?: string;
9
+ expiresAt?: number;
10
+ plan?: string;
11
+ isDefault: boolean;
12
+ }
5
13
  /** One provider's login state as answered by the `status` endpoint. */
6
14
  export interface ProviderStatus {
7
- loggedIn: boolean;
8
15
  busy: boolean;
9
- expiresAt?: number;
10
- account?: string;
16
+ accounts: AccountStatus[];
11
17
  detail?: string;
12
18
  }
13
19
  /** One rate-limit window as answered by the `usage` endpoint. */
@@ -23,6 +29,23 @@ export interface ProviderUsage {
23
29
  windows?: UsageWindow[];
24
30
  plan?: string;
25
31
  }
32
+ /** One model's default-effort picker state as answered by `modelDefaults`. */
33
+ export interface ModelDefaultView {
34
+ id: string;
35
+ name: string;
36
+ /** Advertised effort levels, in catalog order (empty when the model has no reasoning). */
37
+ efforts: {
38
+ id: string;
39
+ name: string;
40
+ }[];
41
+ /** The user-configured default effort, when set. */
42
+ configured?: string;
43
+ }
44
+ /** `modelDefaults` endpoint value: one provider's picker state. */
45
+ export interface ModelDefaultsCatalog {
46
+ provider: SubscriptionProvider;
47
+ models: ModelDefaultView[];
48
+ }
26
49
  /** `proxyGet` endpoint value: the node half owns this shape (no secrets). */
27
50
  export interface ProxyConfigView {
28
51
  enabled: boolean;
@@ -61,6 +84,63 @@ export type SubscriptionsSectionProps = Partial<SubscriptionsSectionInjected>;
61
84
  * @returns the success value, cast by the caller to the endpoint's shape.
62
85
  */
63
86
  export declare function callSubscriptionsAuth<T>(rpc: ConnectionHandle['rpc'], endpoint: string, payload: unknown): Promise<T>;
87
+ /** What one provider's collapsible default-effort section renders. */
88
+ export interface ModelDefaultsView {
89
+ /** Models with reasoning levels, after the name filter — one row each. */
90
+ shown: ModelDefaultView[];
91
+ /** Models with reasoning levels before filtering (the header total). */
92
+ total: number;
93
+ /** How many of those carry a user override (the header count). */
94
+ overridden: number;
95
+ /** Models without reasoning levels: one count line, never a row each. */
96
+ withoutEfforts: number;
97
+ /** Whether the list is long enough to deserve a filter box. */
98
+ showFilter: boolean;
99
+ }
100
+ /**
101
+ * Derive one provider's default-effort section from its catalog and filter.
102
+ * Pure so the collapsed-header counts and the filter stay testable without a
103
+ * DOM: rows come only from models that advertise levels, the count of the rest
104
+ * rides as one line, and the filter matches display name or model id.
105
+ * @param models - the provider's catalog models, or undefined while loading.
106
+ * @param filter - the raw filter input (trimmed and lowercased here).
107
+ * @returns the section's rows and header counts.
108
+ */
109
+ export declare function deriveModelDefaultsView(models: readonly ModelDefaultView[] | undefined, filter: string): ModelDefaultsView;
110
+ /** Inputs of the default-effort fetch decision (see {@link shouldFetchModelDefaults}). */
111
+ export interface ModelDefaultsFetchInput {
112
+ /** Providers that currently have at least one account. */
113
+ loggedIn: readonly SubscriptionProvider[];
114
+ /** Providers whose disclosure is open. */
115
+ open: readonly SubscriptionProvider[];
116
+ /** The account signature the last completed fetch was answered for. */
117
+ loadedFor: string | undefined;
118
+ /** The account signature of the current status snapshot. */
119
+ signature: string;
120
+ /** Whether the last attempt failed (a failure latches until Retry). */
121
+ failed: boolean;
122
+ }
123
+ /**
124
+ * Whether the default-effort catalog needs (re)fetching.
125
+ *
126
+ * Fetching is gated on an *attempt* signature rather than on the payload
127
+ * being empty: an empty answer is a legitimate result (a narrowed
128
+ * `config.providers`, or a catalog that is momentarily unavailable), and
129
+ * treating it as "not loaded yet" re-ran this effect forever. The signature
130
+ * also covers the accounts, so logging a second provider in refetches
131
+ * instead of leaving that card on the previous answer.
132
+ * @param input - the decision inputs.
133
+ * @returns true when the caller should start a fetch.
134
+ */
135
+ export declare function shouldFetchModelDefaults(input: ModelDefaultsFetchInput): boolean;
136
+ /**
137
+ * Stable signature of the accounts a catalog answer depends on. A change
138
+ * means a previous answer is stale (an account arrived or left), so the next
139
+ * open disclosure refetches.
140
+ * @param statuses - the per-provider status snapshot.
141
+ * @returns a signature string, stable across renders with equal accounts.
142
+ */
143
+ export declare function modelDefaultsSignature(statuses: Partial<Record<SubscriptionProvider, ProviderStatus>>): string;
64
144
  /**
65
145
  * The Subscriptions settings page component.
66
146
  * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).