dsh-subscriptions 0.1.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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/auth/claude-code-creds.d.ts +23 -0
  5. package/lib/auth/claude-code-creds.js +172 -0
  6. package/lib/auth/device-flow.d.ts +55 -0
  7. package/lib/auth/device-flow.js +177 -0
  8. package/lib/auth/jwt.d.ts +10 -0
  9. package/lib/auth/jwt.js +25 -0
  10. package/lib/auth/oauth-flow.d.ts +91 -0
  11. package/lib/auth/oauth-flow.js +227 -0
  12. package/lib/auth/pkce.d.ts +31 -0
  13. package/lib/auth/pkce.js +35 -0
  14. package/lib/auth/rpc.d.ts +209 -0
  15. package/lib/auth/rpc.js +386 -0
  16. package/lib/auth/store.d.ts +196 -0
  17. package/lib/auth/store.js +468 -0
  18. package/lib/client/CursorCard.d.ts +9 -0
  19. package/lib/client/CursorCard.js +113 -0
  20. package/lib/client/ExternalUsageCards.d.ts +9 -0
  21. package/lib/client/ExternalUsageCards.js +72 -0
  22. package/lib/client/ImageGallery.d.ts +54 -0
  23. package/lib/client/ImageGallery.js +112 -0
  24. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  25. package/lib/client/ImageGenerateToolview.js +133 -0
  26. package/lib/client/ProviderAccountManager.d.ts +13 -0
  27. package/lib/client/ProviderAccountManager.js +147 -0
  28. package/lib/client/ProviderModelEditor.d.ts +39 -0
  29. package/lib/client/ProviderModelEditor.js +107 -0
  30. package/lib/client/SpeedSelect.d.ts +71 -0
  31. package/lib/client/SpeedSelect.js +177 -0
  32. package/lib/client/SubscriptionUsageBadge.d.ts +116 -0
  33. package/lib/client/SubscriptionUsageBadge.js +543 -0
  34. package/lib/client/SubscriptionsSection.d.ts +140 -0
  35. package/lib/client/SubscriptionsSection.js +544 -0
  36. package/lib/client/UsageBadgeDisplaySetting.d.ts +7 -0
  37. package/lib/client/UsageBadgeDisplaySetting.js +36 -0
  38. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  39. package/lib/client/VideoGenerateToolview.js +175 -0
  40. package/lib/client/account-preferences.d.ts +24 -0
  41. package/lib/client/account-preferences.js +21 -0
  42. package/lib/client/fast-command.d.ts +2 -0
  43. package/lib/client/fast-command.js +4 -0
  44. package/lib/client/host-contract.d.ts +90 -0
  45. package/lib/client/host-contract.js +101 -0
  46. package/lib/client/host-icons.d.ts +24 -0
  47. package/lib/client/host-icons.js +21 -0
  48. package/lib/client/index.d.ts +27 -0
  49. package/lib/client/index.js +136 -0
  50. package/lib/client/locales.d.ts +297 -0
  51. package/lib/client/locales.js +295 -0
  52. package/lib/client/subscription-card-styles.d.ts +3 -0
  53. package/lib/client/subscription-card-styles.js +53 -0
  54. package/lib/client/subscriptions-rpc.d.ts +14 -0
  55. package/lib/client/subscriptions-rpc.js +34 -0
  56. package/lib/client/usage-badge-preferences.d.ts +11 -0
  57. package/lib/client/usage-badge-preferences.js +41 -0
  58. package/lib/client.js +4472 -0
  59. package/lib/client.js.map +1 -0
  60. package/lib/compat.d.ts +36 -0
  61. package/lib/compat.js +20 -0
  62. package/lib/http.d.ts +6 -0
  63. package/lib/http.js +15 -0
  64. package/lib/index.d.ts +80 -0
  65. package/lib/index.js +14018 -0
  66. package/lib/model-defaults.d.ts +23 -0
  67. package/lib/model-defaults.js +237 -0
  68. package/lib/provider-settings.d.ts +39 -0
  69. package/lib/provider-settings.js +151 -0
  70. package/lib/providers/account-preferences.d.ts +41 -0
  71. package/lib/providers/account-preferences.js +153 -0
  72. package/lib/providers/accounts.d.ts +112 -0
  73. package/lib/providers/accounts.js +142 -0
  74. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  75. package/lib/providers/antigravity-oauth-client.js +7 -0
  76. package/lib/providers/antigravity.d.ts +110 -0
  77. package/lib/providers/antigravity.js +516 -0
  78. package/lib/providers/catalog-store.d.ts +48 -0
  79. package/lib/providers/catalog-store.js +243 -0
  80. package/lib/providers/claude-cli-version.d.ts +15 -0
  81. package/lib/providers/claude-cli-version.js +24 -0
  82. package/lib/providers/claude.d.ts +181 -0
  83. package/lib/providers/claude.js +680 -0
  84. package/lib/providers/codex-client-version.d.ts +8 -0
  85. package/lib/providers/codex-client-version.js +17 -0
  86. package/lib/providers/codex-search.d.ts +29 -0
  87. package/lib/providers/codex-search.js +198 -0
  88. package/lib/providers/codex.d.ts +229 -0
  89. package/lib/providers/codex.js +821 -0
  90. package/lib/providers/common.d.ts +389 -0
  91. package/lib/providers/common.js +656 -0
  92. package/lib/providers/copilot.d.ts +344 -0
  93. package/lib/providers/copilot.js +888 -0
  94. package/lib/providers/cursor-adapter.d.ts +15 -0
  95. package/lib/providers/cursor-adapter.js +39 -0
  96. package/lib/providers/cursor-auth.d.ts +43 -0
  97. package/lib/providers/cursor-auth.js +169 -0
  98. package/lib/providers/cursor-model-settings.d.ts +15 -0
  99. package/lib/providers/cursor-model-settings.js +51 -0
  100. package/lib/providers/cursor-usage.d.ts +9 -0
  101. package/lib/providers/cursor-usage.js +112 -0
  102. package/lib/providers/external-usage-controller.d.ts +19 -0
  103. package/lib/providers/external-usage-controller.js +32 -0
  104. package/lib/providers/external-usage.d.ts +8 -0
  105. package/lib/providers/external-usage.js +82 -0
  106. package/lib/providers/grok.d.ts +205 -0
  107. package/lib/providers/grok.js +713 -0
  108. package/lib/providers/image-pool.d.ts +30 -0
  109. package/lib/providers/image-pool.js +89 -0
  110. package/lib/providers/npm-cli-version.d.ts +56 -0
  111. package/lib/providers/npm-cli-version.js +98 -0
  112. package/lib/providers/pool-family.d.ts +56 -0
  113. package/lib/providers/pool-family.js +45 -0
  114. package/lib/providers/pool-health.d.ts +74 -0
  115. package/lib/providers/pool-health.js +150 -0
  116. package/lib/providers/pool-usage.d.ts +86 -0
  117. package/lib/providers/pool-usage.js +223 -0
  118. package/lib/providers/pool.d.ts +108 -0
  119. package/lib/providers/pool.js +368 -0
  120. package/lib/providers/rate-limit.d.ts +192 -0
  121. package/lib/providers/rate-limit.js +338 -0
  122. package/lib/tools/image-generate.d.ts +128 -0
  123. package/lib/tools/image-generate.js +438 -0
  124. package/lib/tools/registration.d.ts +15 -0
  125. package/lib/tools/registration.js +24 -0
  126. package/lib/tools/video-generate.d.ts +89 -0
  127. package/lib/tools/video-generate.js +257 -0
  128. package/lib/tools/x-search.d.ts +58 -0
  129. package/lib/tools/x-search.js +197 -0
  130. package/lib/translate/anthropic.d.ts +161 -0
  131. package/lib/translate/anthropic.js +515 -0
  132. package/lib/translate/antigravity-schema.d.ts +4 -0
  133. package/lib/translate/antigravity-schema.js +145 -0
  134. package/lib/translate/antigravity-thinking.d.ts +5 -0
  135. package/lib/translate/antigravity-thinking.js +44 -0
  136. package/lib/translate/antigravity.d.ts +114 -0
  137. package/lib/translate/antigravity.js +366 -0
  138. package/lib/translate/chat-completions.d.ts +120 -0
  139. package/lib/translate/chat-completions.js +382 -0
  140. package/lib/translate/resolved.d.ts +80 -0
  141. package/lib/translate/resolved.js +118 -0
  142. package/lib/translate/responses.d.ts +179 -0
  143. package/lib/translate/responses.js +408 -0
  144. package/lib/translate/sse.d.ts +21 -0
  145. package/lib/translate/sse.js +56 -0
  146. package/package.json +114 -0
  147. package/vendor/cursor/LICENSE +21 -0
  148. package/vendor/cursor/index.d.ts +18 -0
  149. package/vendor/cursor/index.js +3574 -0
  150. package/vendor/cursor/proto.js +254 -0
@@ -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,39 @@
1
+ import { type ProviderId } from './auth/store.js';
2
+ export declare const PROVIDER_TOOLS: {
3
+ readonly codex: readonly ["image_generate", "web_search"];
4
+ readonly claude: readonly [];
5
+ readonly grok: readonly ["image_generate", "video_generate", "x_search"];
6
+ readonly copilot: readonly [];
7
+ readonly antigravity: readonly [];
8
+ };
9
+ export type SubscriptionTool = 'image_generate' | 'video_generate' | 'x_search' | 'web_search';
10
+ export interface AccountPreferences {
11
+ alias?: string;
12
+ /** Absent means included. */
13
+ poolEnabled?: boolean;
14
+ /** Absent means no separate picker entries. */
15
+ independentEntry?: boolean;
16
+ /** Absent allows every model; [] allows none. */
17
+ poolModels?: string[];
18
+ }
19
+ export interface ProviderPreferences {
20
+ accounts?: Record<string, AccountPreferences>;
21
+ /** Absent follows discovery; an explicit selection hides newly discovered models. */
22
+ visibleModels?: string[];
23
+ contextWindows?: Record<string, number>;
24
+ tools?: Partial<Record<SubscriptionTool, boolean>>;
25
+ }
26
+ export declare function validatePreferences(provider: ProviderId, input: unknown): ProviderPreferences;
27
+ /** Durable user preferences, independent of expiring discovery caches. */
28
+ export declare class ProviderSettingsStore {
29
+ private current;
30
+ private writes;
31
+ readonly path: string;
32
+ constructor(path?: string);
33
+ get(provider: ProviderId): ProviderPreferences;
34
+ visible(provider: ProviderId, model: string): boolean;
35
+ contextWindow(model: string): number | undefined;
36
+ /** Creation-time policy survives restarts and never changes an existing session. */
37
+ toolEnabled(provider: ProviderId, tool: SubscriptionTool, createdAt?: number): boolean;
38
+ set(provider: ProviderId, input: unknown): Promise<void>;
39
+ }
@@ -0,0 +1,151 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { dirname } from 'node:path';
4
+ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
5
+ import { PROVIDER_IDS } from './auth/store.js';
6
+ export const PROVIDER_TOOLS = {
7
+ codex: ['image_generate', 'web_search'],
8
+ claude: [],
9
+ grok: ['image_generate', 'video_generate', 'x_search'],
10
+ copilot: [],
11
+ antigravity: [],
12
+ };
13
+ export function validatePreferences(provider, input) {
14
+ if (!input || typeof input !== 'object' || Array.isArray(input))
15
+ throw new Error('settings must be an object');
16
+ const raw = input;
17
+ const result = {};
18
+ if (raw.accounts !== undefined) {
19
+ if (!raw.accounts || typeof raw.accounts !== 'object' || Array.isArray(raw.accounts))
20
+ throw new Error('accounts must be an account-to-preferences map');
21
+ result.accounts = Object.create(null);
22
+ for (const [key, value] of Object.entries(raw.accounts)) {
23
+ if (!key.trim() || !value || typeof value !== 'object' || Array.isArray(value))
24
+ throw new Error('invalid account preferences');
25
+ const account = value;
26
+ const parsed = {};
27
+ if (account.alias !== undefined) {
28
+ if (typeof account.alias !== 'string')
29
+ throw new Error('account alias must be a string');
30
+ parsed.alias = account.alias.trim();
31
+ }
32
+ for (const field of ['poolEnabled', 'independentEntry']) {
33
+ if (account[field] !== undefined) {
34
+ if (typeof account[field] !== 'boolean')
35
+ throw new Error(`${field} must be a boolean`);
36
+ parsed[field] = account[field];
37
+ }
38
+ }
39
+ if (account.poolModels !== undefined) {
40
+ if (!Array.isArray(account.poolModels) || account.poolModels.some(id => typeof id !== 'string' || !id.trim()))
41
+ throw new Error('poolModels must be an array of model ids');
42
+ parsed.poolModels = [...new Set(account.poolModels)];
43
+ }
44
+ result.accounts[key] = parsed;
45
+ }
46
+ }
47
+ if (raw.visibleModels !== undefined) {
48
+ if (!Array.isArray(raw.visibleModels) || raw.visibleModels.some(id => typeof id !== 'string' || !id.trim())) {
49
+ throw new Error('visibleModels must be an array of model ids');
50
+ }
51
+ result.visibleModels = [...new Set(raw.visibleModels)];
52
+ }
53
+ if (raw.contextWindows !== undefined) {
54
+ if (provider !== 'codex')
55
+ throw new Error('context window overrides are currently supported only for Codex');
56
+ if (!raw.contextWindows || typeof raw.contextWindows !== 'object' || Array.isArray(raw.contextWindows)) {
57
+ throw new Error('contextWindows must be a model-to-token map');
58
+ }
59
+ result.contextWindows = Object.create(null);
60
+ for (const [model, value] of Object.entries(raw.contextWindows)) {
61
+ if (!model.trim() || typeof value !== 'number' || !Number.isSafeInteger(value) || value <= 0) {
62
+ throw new Error('context windows must be positive safe integers');
63
+ }
64
+ result.contextWindows[model] = value;
65
+ }
66
+ }
67
+ if (raw.tools !== undefined) {
68
+ if (!raw.tools || typeof raw.tools !== 'object' || Array.isArray(raw.tools))
69
+ throw new Error('tools must be an object');
70
+ result.tools = {};
71
+ for (const [tool, enabled] of Object.entries(raw.tools)) {
72
+ if (!PROVIDER_TOOLS[provider].includes(tool) || typeof enabled !== 'boolean') {
73
+ throw new Error(`unsupported tool setting: ${provider}/${tool}`);
74
+ }
75
+ result.tools[tool] = enabled;
76
+ }
77
+ }
78
+ return result;
79
+ }
80
+ /** Durable user preferences, independent of expiring discovery caches. */
81
+ export class ProviderSettingsStore {
82
+ current = { providers: {}, toolHistory: [] };
83
+ writes = Promise.resolve();
84
+ path;
85
+ constructor(path = dshHomePath('plugins', 'subscriptions', 'provider-settings.json')) {
86
+ this.path = path;
87
+ try {
88
+ const raw = JSON.parse(readFileSync(path, 'utf8'));
89
+ if (!raw.providers || !Array.isArray(raw.toolHistory))
90
+ throw new Error('invalid settings document');
91
+ for (const provider of PROVIDER_IDS) {
92
+ if (Object.hasOwn(raw.providers, provider))
93
+ this.current.providers[provider] = validatePreferences(provider, raw.providers[provider]);
94
+ }
95
+ this.current.toolHistory = raw.toolHistory.map(revision => {
96
+ if (!PROVIDER_IDS.includes(revision.provider) || !Number.isSafeInteger(revision.at) || revision.at < 0)
97
+ throw new Error('invalid tool history');
98
+ const tools = validatePreferences(revision.provider, { tools: revision.tools }).tools ?? {};
99
+ return { at: revision.at, provider: revision.provider, tools };
100
+ }).sort((a, b) => a.at - b.at);
101
+ }
102
+ catch (error) {
103
+ if (error.code !== 'ENOENT')
104
+ throw new Error(`Cannot read provider settings at ${path}`, { cause: error });
105
+ }
106
+ }
107
+ get(provider) {
108
+ return structuredClone(this.current.providers[provider] ?? {});
109
+ }
110
+ visible(provider, model) {
111
+ return this.current.providers[provider]?.visibleModels?.includes(model) ?? true;
112
+ }
113
+ contextWindow(model) {
114
+ const windows = this.current.providers.codex?.contextWindows;
115
+ return windows && Object.hasOwn(windows, model) ? windows[model] : undefined;
116
+ }
117
+ /** Creation-time policy survives restarts and never changes an existing session. */
118
+ toolEnabled(provider, tool, createdAt = Date.now()) {
119
+ let enabled = true;
120
+ for (const revision of this.current.toolHistory) {
121
+ if (revision.at > createdAt)
122
+ break;
123
+ if (revision.provider === provider)
124
+ enabled = revision.tools[tool] !== false;
125
+ }
126
+ return enabled;
127
+ }
128
+ set(provider, input) {
129
+ const preferences = validatePreferences(provider, input);
130
+ const run = this.writes.then(async () => {
131
+ const next = structuredClone(this.current);
132
+ const before = next.providers[provider]?.tools ?? {};
133
+ next.providers[provider] = preferences;
134
+ if (PROVIDER_TOOLS[provider].some(tool => (before[tool] !== false) !== (preferences.tools?.[tool] !== false))) {
135
+ next.toolHistory.push({ at: Date.now(), provider, tools: preferences.tools ?? {} });
136
+ }
137
+ await mkdir(dirname(this.path), { recursive: true });
138
+ const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
139
+ try {
140
+ await writeFile(temporary, JSON.stringify(next, null, 2), { mode: 0o600 });
141
+ await rename(temporary, this.path);
142
+ }
143
+ finally {
144
+ await rm(temporary, { force: true });
145
+ }
146
+ this.current = next;
147
+ });
148
+ this.writes = run.catch(() => undefined);
149
+ return run;
150
+ }
151
+ }
@@ -0,0 +1,41 @@
1
+ import { LlmAdapter } from '@deepseek-ai/dsh-llm';
2
+ import type { GenerateOptions, LlmModelInfo, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm';
3
+ import type { ProviderId } from '../auth/store.js';
4
+ import type { ProviderSettingsStore, AccountPreferences } from '../provider-settings.js';
5
+ import type { AccountAwareAdapter } from './accounts.js';
6
+ import type { PoolAdapter } from './pool.js';
7
+ /** Reserved namespace, recognized even when malformed or no longer enabled. */
8
+ export declare const ACCOUNT_MODEL_PREFIX = "~account:";
9
+ export declare function accountModelId(account: string, model: string): string;
10
+ export declare function parseAccountModelId(id: string): {
11
+ account: string;
12
+ model: string;
13
+ } | undefined;
14
+ export declare function accountAllowsPool(preferences: AccountPreferences | undefined, model: string): boolean;
15
+ interface Options {
16
+ provider: ProviderId;
17
+ adapter: AccountAwareAdapter;
18
+ settings: ProviderSettingsStore;
19
+ accounts: () => Promise<readonly {
20
+ key: string;
21
+ label: string;
22
+ }[]>;
23
+ pool: () => PoolAdapter | undefined;
24
+ /** Per-account discovery bound (defaults to {@link DISCOVERY_TIMEOUT_MS}; tests shorten it). */
25
+ discoveryTimeoutMs?: number;
26
+ }
27
+ /** Keeps the registered route separate from raw adapters and pool member seams. */
28
+ export declare class AccountPreferencesAdapter extends LlmAdapter {
29
+ private readonly options;
30
+ constructor(options: Options);
31
+ private preference;
32
+ private models;
33
+ private requireAccount;
34
+ private fallback;
35
+ /** Pool-only facade: explicit families/tiers must obey the same policy as auto pools. */
36
+ poolMember(): AccountAwareAdapter;
37
+ listModels(provider: string): Promise<readonly LlmModelInfo[]>;
38
+ resolveModel(provider: string, id: string): Promise<LlmResolvedModelInfo>;
39
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
40
+ }
41
+ export {};
@@ -0,0 +1,153 @@
1
+ import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm';
2
+ import { DISCOVERY_TIMEOUT_MS, withTimeout } from './common.js';
3
+ /** Reserved namespace, recognized even when malformed or no longer enabled. */
4
+ export const ACCOUNT_MODEL_PREFIX = '~account:';
5
+ export function accountModelId(account, model) {
6
+ return `${ACCOUNT_MODEL_PREFIX}${encodeURIComponent(account)}:${encodeURIComponent(model)}`;
7
+ }
8
+ export function parseAccountModelId(id) {
9
+ if (!id.startsWith(ACCOUNT_MODEL_PREFIX))
10
+ return undefined;
11
+ const parts = id.slice(ACCOUNT_MODEL_PREFIX.length).split(':');
12
+ try {
13
+ if (parts.length !== 2)
14
+ throw new Error();
15
+ const account = decodeURIComponent(parts[0]);
16
+ const model = decodeURIComponent(parts[1]);
17
+ if (!account || !model || accountModelId(account, model) !== id)
18
+ throw new Error();
19
+ return { account, model };
20
+ }
21
+ catch {
22
+ throw new LlmError('Invalid independent account model id', 'NO_ADAPTER');
23
+ }
24
+ }
25
+ export function accountAllowsPool(preferences, model) {
26
+ return preferences?.poolEnabled !== false && (preferences?.poolModels?.includes(model) ?? true);
27
+ }
28
+ /** Keeps the registered route separate from raw adapters and pool member seams. */
29
+ export class AccountPreferencesAdapter extends LlmAdapter {
30
+ options;
31
+ constructor(options) {
32
+ super();
33
+ this.options = options;
34
+ }
35
+ preference(account) {
36
+ const accounts = this.options.settings.get(this.options.provider).accounts;
37
+ return accounts && Object.hasOwn(accounts, account) ? accounts[account] : undefined;
38
+ }
39
+ async models(account) {
40
+ const models = await withTimeout(signal => this.options.adapter.listOwnModels(this.options.provider, account, signal), this.options.discoveryTimeoutMs ?? DISCOVERY_TIMEOUT_MS);
41
+ if (models !== undefined)
42
+ return models;
43
+ // Discovery timed out: the catalog this account listed last time beats
44
+ // "no models", which would fail the turn with "No eligible account".
45
+ return await this.options.adapter.lastKnownOwnModels?.(this.options.provider, account) ?? [];
46
+ }
47
+ async requireAccount(account, model, independent) {
48
+ if (!(await this.options.accounts()).some(entry => entry.key === account)
49
+ || (independent ? this.preference(account)?.independentEntry !== true : !accountAllowsPool(this.preference(account), model))
50
+ || !(await this.models(account)).some(entry => entry.id === model)) {
51
+ throw new LlmError(`Account route unavailable: ${this.options.provider}/${account}/${model}`, 'NO_ADAPTER');
52
+ }
53
+ }
54
+ async fallback(model) {
55
+ for (const { key } of await this.options.accounts()) {
56
+ if (!accountAllowsPool(this.preference(key), model))
57
+ continue;
58
+ try {
59
+ await this.requireAccount(key, model, false);
60
+ return key;
61
+ }
62
+ catch { /* unavailable catalog */ }
63
+ }
64
+ throw new LlmError(`No eligible account for ${this.options.provider}/${model}`, 'NO_ADAPTER');
65
+ }
66
+ /** Pool-only facade: explicit families/tiers must obey the same policy as auto pools. */
67
+ poolMember() {
68
+ const raw = this.options.adapter;
69
+ const keyFor = async (account, model) => {
70
+ if (model.startsWith(ACCOUNT_MODEL_PREFIX))
71
+ throw new LlmError('Independent entries cannot be pool members', 'NO_ADAPTER');
72
+ const key = account ?? (await this.options.accounts())[0]?.key;
73
+ if (!key)
74
+ throw new LlmError('No account available', 'NO_ADAPTER');
75
+ await this.requireAccount(key, model, false);
76
+ return key;
77
+ };
78
+ return new Proxy(raw, { get: (target, property) => {
79
+ if (property === 'streamAccount')
80
+ return async function* (options, account) {
81
+ let key;
82
+ try {
83
+ key = await keyFor(account, options.model);
84
+ }
85
+ catch (cause) {
86
+ // Skip policy-excluded members without poisoning account health. The
87
+ // pool treats TRANSPORT as switch-without-cooldown; raw is never called.
88
+ throw new LlmError('Pool member unavailable under account preferences', 'TRANSPORT', { cause });
89
+ }
90
+ yield* raw.streamAccount(options, key);
91
+ };
92
+ if (property === 'resolveOwnModel')
93
+ return async (provider, model, account) => raw.resolveOwnModel(provider, model, await keyFor(account, model));
94
+ const value = Reflect.get(target, property);
95
+ return typeof value === 'function' ? value.bind(target) : value;
96
+ } });
97
+ }
98
+ async listModels(provider) {
99
+ const result = new Map();
100
+ const catalogs = await Promise.all((await this.options.accounts()).map(async (account) => ({ ...account, models: await this.models(account.key).catch(() => []) })));
101
+ for (const { key, label, models } of catalogs) {
102
+ const preferences = this.preference(key);
103
+ for (const model of models) {
104
+ if (model.id.startsWith(ACCOUNT_MODEL_PREFIX))
105
+ continue;
106
+ if (accountAllowsPool(preferences, model.id) && this.options.settings.visible(this.options.provider, model.id) && !result.has(model.id))
107
+ result.set(model.id, model);
108
+ if (preferences?.independentEntry === true && this.options.settings.visible(this.options.provider, model.id)) {
109
+ const id = accountModelId(key, model.id);
110
+ result.set(id, { ...model, id, name: `${preferences.alias || label} · ${model.name}` });
111
+ }
112
+ }
113
+ }
114
+ for (const model of await this.options.pool()?.modelsForProvider(this.options.provider) ?? []) {
115
+ if (model.id.startsWith(ACCOUNT_MODEL_PREFIX) || !this.options.settings.visible(this.options.provider, model.id))
116
+ continue;
117
+ try {
118
+ await this.options.pool().resolveModel(provider, model.id);
119
+ result.set(model.id, model);
120
+ }
121
+ catch { /* excluded tier */ }
122
+ }
123
+ const priority = (model) => model.priority ?? Number.MAX_SAFE_INTEGER;
124
+ return [...result.values()].sort((left, right) => priority(left) - priority(right));
125
+ }
126
+ async resolveModel(provider, id) {
127
+ const independent = parseAccountModelId(id);
128
+ if (independent) {
129
+ await this.requireAccount(independent.account, independent.model, true);
130
+ const info = await this.options.adapter.resolveOwnModel(provider, independent.model, independent.account);
131
+ const label = (await this.options.accounts()).find(entry => entry.key === independent.account)?.label ?? independent.account;
132
+ return { ...info, id, name: `${this.preference(independent.account)?.alias || label} · ${info.name}` };
133
+ }
134
+ const pool = this.options.pool();
135
+ if (pool && await pool.owns(this.options.provider, id))
136
+ return pool.resolveModel(provider, id);
137
+ return this.options.adapter.resolveOwnModel(provider, id, await this.fallback(id));
138
+ }
139
+ async *stream(options) {
140
+ const independent = parseAccountModelId(options.model);
141
+ if (independent) {
142
+ await this.requireAccount(independent.account, independent.model, true);
143
+ yield* this.options.adapter.streamAccount({ ...options, model: independent.model }, independent.account);
144
+ return;
145
+ }
146
+ const pool = this.options.pool();
147
+ if (pool && await pool.owns(this.options.provider, options.model)) {
148
+ yield* pool.stream(options);
149
+ return;
150
+ }
151
+ yield* this.options.adapter.streamAccount(options, await this.fallback(options.model));
152
+ }
153
+ }