dsh-plugin-subscriptions 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +42 -1
  2. package/README.zh.md +42 -1
  3. package/lib/auth/device-flow.d.ts +0 -9
  4. package/lib/auth/device-flow.js +2 -1
  5. package/lib/auth/rpc.d.ts +44 -13
  6. package/lib/auth/rpc.js +127 -9
  7. package/lib/auth/store.d.ts +75 -17
  8. package/lib/auth/store.js +148 -27
  9. package/lib/client/SubscriptionsSection.d.ts +26 -3
  10. package/lib/client/SubscriptionsSection.js +263 -67
  11. package/lib/client/index.js +11 -0
  12. package/lib/client/locales.d.ts +82 -10
  13. package/lib/client/locales.js +82 -10
  14. package/lib/client.js +837 -223
  15. package/lib/client.js.map +1 -1
  16. package/lib/http.d.ts +114 -0
  17. package/lib/http.js +402 -0
  18. package/lib/index.d.ts +21 -0
  19. package/lib/index.js +1938 -208
  20. package/lib/providers/accounts.d.ts +102 -0
  21. package/lib/providers/accounts.js +123 -0
  22. package/lib/providers/antigravity.d.ts +90 -0
  23. package/lib/providers/antigravity.js +392 -0
  24. package/lib/providers/claude.d.ts +22 -4
  25. package/lib/providers/claude.js +97 -16
  26. package/lib/providers/codex.d.ts +24 -3
  27. package/lib/providers/codex.js +121 -21
  28. package/lib/providers/common.d.ts +17 -0
  29. package/lib/providers/common.js +67 -3
  30. package/lib/providers/copilot.d.ts +23 -4
  31. package/lib/providers/copilot.js +99 -19
  32. package/lib/providers/grok.d.ts +24 -4
  33. package/lib/providers/grok.js +106 -19
  34. package/lib/providers/pool-family.d.ts +56 -0
  35. package/lib/providers/pool-family.js +45 -0
  36. package/lib/providers/pool-health.d.ts +74 -0
  37. package/lib/providers/pool-health.js +148 -0
  38. package/lib/providers/pool-usage.d.ts +57 -0
  39. package/lib/providers/pool-usage.js +130 -0
  40. package/lib/providers/pool.d.ts +107 -0
  41. package/lib/providers/pool.js +371 -0
  42. package/lib/tools/image-generate.d.ts +3 -3
  43. package/lib/tools/image-generate.js +4 -2
  44. package/lib/tools/video-generate.d.ts +2 -2
  45. package/lib/tools/video-generate.js +4 -2
  46. package/lib/tools/x-search.d.ts +2 -2
  47. package/lib/tools/x-search.js +4 -2
  48. package/lib/translate/antigravity.d.ts +110 -0
  49. package/lib/translate/antigravity.js +303 -0
  50. package/package.json +14 -9
package/lib/http.d.ts ADDED
@@ -0,0 +1,114 @@
1
+ /** Stored proxy configuration (the proxy.json shape). */
2
+ export interface ProxyConfig {
3
+ /** Whether outbound subscription requests route through {@link url}. */
4
+ enabled: boolean;
5
+ /** Proxy origin: `http://host:port` or `https://host:port`. */
6
+ url: string;
7
+ /** Optional proxy user for basic auth. */
8
+ username?: string;
9
+ /** Optional proxy password for basic auth; never sent back to the client. */
10
+ password?: string;
11
+ /** Hostnames (exact, suffix, or `*.example.com`) that stay direct. */
12
+ bypass: string[];
13
+ }
14
+ /** The proxy config as served to the client: secrets replaced by a flag. */
15
+ export interface ProxyConfigView {
16
+ enabled: boolean;
17
+ url: string;
18
+ username?: string;
19
+ /** Whether a password is stored (the password itself never leaves the host). */
20
+ passwordSet: boolean;
21
+ bypass: string[];
22
+ /** Last load/apply failure, when the stored config is unusable. */
23
+ error?: string;
24
+ }
25
+ /** One `proxySet` payload. */
26
+ export interface ProxyInput {
27
+ enabled: boolean;
28
+ url: string;
29
+ username?: string;
30
+ /** `undefined` keeps the stored password, `null`/`''` clears it. */
31
+ password?: string | null;
32
+ bypass?: string[];
33
+ }
34
+ /** One `proxyTest` result. */
35
+ export interface ProxyTestResult {
36
+ /** Whether the destination answered with an HTTP status. */
37
+ ok: boolean;
38
+ /** Whether the request actually went through the proxy (bypass/direct otherwise). */
39
+ viaProxy: boolean;
40
+ /** Status of the answered request, when one was received. */
41
+ status?: number;
42
+ /** Round-trip latency in milliseconds. */
43
+ latencyMs?: number;
44
+ /** Failure message, when no response was received. */
45
+ error?: string;
46
+ }
47
+ /** A draft proxy for one test probe (never persisted). */
48
+ export interface ProxyDraft {
49
+ url: string;
50
+ username?: string;
51
+ password?: string;
52
+ }
53
+ /** Destination the `proxyTest` endpoint probes when none is given. */
54
+ export declare const DEFAULT_PROXY_TEST_URL = "https://api.x.ai/v1/models";
55
+ /** Probe deadline; a hung proxy must not pin the Settings dialog forever. */
56
+ export declare const DEFAULT_PROXY_TEST_TIMEOUT_MS = 15000;
57
+ /** Absolute path of the proxy config file. */
58
+ export declare function proxyFilePath(): string;
59
+ /**
60
+ * Flatten a fetch failure into a readable message: undici wraps the true
61
+ * cause (`connect ECONNREFUSED ...`) behind a bare "fetch failed", so walk
62
+ * the cause chain and append each distinct layer (up to four, cycle-safe).
63
+ * A hostname resolving to several addresses (e.g. `localhost` → ::1 and
64
+ * 127.0.0.1) fails as an `AggregateError` with an empty message, so its
65
+ * per-address `errors` entries are folded in too.
66
+ */
67
+ export declare function describeFetchError(error: unknown): string;
68
+ /**
69
+ * Parse and validate a proxy URL. Only HTTP(S) proxies are supported because
70
+ * the undici dispatcher speaks CONNECT over HTTP; socks5 is not supported.
71
+ * @param raw - the URL the user configured.
72
+ * @returns the parsed URL (credentials attached by the caller).
73
+ */
74
+ export declare function parseProxyUrl(raw: string): URL;
75
+ /**
76
+ * Whether a request hostname bypasses the proxy.
77
+ * @param hostname - the request's hostname.
78
+ * @param entries - configured bypass entries: exact host, plain suffix
79
+ * (`example.com` also matches `api.example.com`), or `*.example.com`.
80
+ */
81
+ export declare function matchesBypass(hostname: string, entries: readonly string[]): boolean;
82
+ /**
83
+ * Current proxy config as served to the client (secrets omitted).
84
+ * @returns the view; {@link ProxyConfigView.error} carries the last
85
+ * load/apply failure when the stored config is unusable.
86
+ */
87
+ export declare function proxyGetConfig(): Promise<ProxyConfigView>;
88
+ /**
89
+ * Validate, persist, and apply one proxy config. A `password` of `undefined`
90
+ * keeps the stored value; `null` or `''` clears it.
91
+ * @param input - the client's payload.
92
+ * @returns the resulting view (secrets omitted).
93
+ */
94
+ export declare function proxySetConfig(input: ProxyInput): Promise<ProxyConfigView>;
95
+ /**
96
+ * The fetch caller all subscription code uses: routes through the configured
97
+ * proxy unless the host bypasses it. Identity-passthrough otherwise.
98
+ *
99
+ * Proxied requests run on undici's own fetch (not the global one) so the
100
+ * ProxyAgent dispatcher always comes from the same undici build the request
101
+ * is issued with — a mismatched dispatcher can be silently ignored by the
102
+ * host's global fetch.
103
+ */
104
+ export declare function proxiedFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
105
+ /**
106
+ * Probe a destination through a proxy, answering with the HTTP status or a
107
+ * flattened transport error. The probe uses `draft` when given (the dialog's
108
+ * current inputs, without saving) and the stored config otherwise.
109
+ * @param target - `http(s)` URL to fetch; defaults to {@link DEFAULT_PROXY_TEST_URL}.
110
+ * @param draft - unsaved proxy inputs to test; absent means the stored config.
111
+ * @returns the result; any HTTP status counts as a successful connection,
112
+ * only a transport failure is an error.
113
+ */
114
+ export declare function proxyTestConnection(target?: string, draft?: ProxyDraft): Promise<ProxyTestResult>;
package/lib/http.js ADDED
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Proxy routing for every outbound subscription request. When a proxy is
3
+ * configured, {@link proxiedFetch} attaches an undici {@link ProxyAgent} as the
4
+ * fetch `dispatcher`, so token exchanges, model-API streams, usage lookups,
5
+ * model discovery, and the `x_search` / `image_generate` / `video_generate`
6
+ * tools all leave through the proxy without touching their call sites.
7
+ *
8
+ * The config lives at `~/.dsh/plugins/subscriptions/proxy.json` (mode 0600,
9
+ * it may carry a password), sibling to the auth store. The `proxyGet` /
10
+ * `proxySet` / `proxyTest` RPC endpoints drive it from the web Settings page;
11
+ * a saved config applies immediately to subsequent requests.
12
+ *
13
+ * The OAuth authorize step opens in the user's browser, which uses the
14
+ * browser/system proxy and is outside this module's reach.
15
+ */
16
+ import { ProxyAgent, fetch as undiciFetch } from 'undici';
17
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
18
+ import { dirname } from 'node:path';
19
+ import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
20
+ /**
21
+ * undici's own fetch, typed to the DOM fetch signature: its bundled types are
22
+ * stricter (Request requires `duplex`, `RequestInit.body` is non-null) and
23
+ * incompatible with the DOM shapes the provider code passes. The runtime
24
+ * object is the same Web-fetch implementation Node uses.
25
+ */
26
+ const dispatchFetch = undiciFetch;
27
+ /** Destination the `proxyTest` endpoint probes when none is given. */
28
+ export const DEFAULT_PROXY_TEST_URL = 'https://api.x.ai/v1/models';
29
+ /** Probe deadline; a hung proxy must not pin the Settings dialog forever. */
30
+ export const DEFAULT_PROXY_TEST_TIMEOUT_MS = 15_000;
31
+ /** Disabled configuration: the module state before the first load. */
32
+ const DISABLED = { enabled: false, url: '', bypass: [] };
33
+ /** Current config; updated by every load/apply/save. */
34
+ let current = DISABLED;
35
+ /** The live dispatcher, or undefined when proxies are off/errored. */
36
+ let agent;
37
+ /** Last load/apply failure, surfaced by the config view. */
38
+ let configError;
39
+ /** One lazy load of the on-disk config (module-import cheap; file read once). */
40
+ let ready;
41
+ /** Absolute path of the proxy config file. */
42
+ export function proxyFilePath() {
43
+ return dshHomePath('plugins', 'subscriptions', 'proxy.json');
44
+ }
45
+ function errorMessage(error) {
46
+ return error instanceof Error ? error.message : String(error);
47
+ }
48
+ /**
49
+ * Flatten a fetch failure into a readable message: undici wraps the true
50
+ * cause (`connect ECONNREFUSED ...`) behind a bare "fetch failed", so walk
51
+ * the cause chain and append each distinct layer (up to four, cycle-safe).
52
+ * A hostname resolving to several addresses (e.g. `localhost` → ::1 and
53
+ * 127.0.0.1) fails as an `AggregateError` with an empty message, so its
54
+ * per-address `errors` entries are folded in too.
55
+ */
56
+ export function describeFetchError(error) {
57
+ const parts = [];
58
+ let node = error;
59
+ for (let depth = 0; depth < 4 && node !== undefined && node !== null; depth += 1) {
60
+ const layer = node;
61
+ if (Array.isArray(layer.errors)) {
62
+ for (const child of layer.errors) {
63
+ const childText = child instanceof Error && child.message !== '' ? child.message : String(child);
64
+ if (childText !== '' && !parts.includes(childText))
65
+ parts.push(childText);
66
+ }
67
+ }
68
+ let text = layer instanceof Error ? layer.message : String(node);
69
+ const code = layer.code;
70
+ if (typeof code === 'string' && code !== '') {
71
+ if (text === '')
72
+ text = code;
73
+ else if (!text.includes(code))
74
+ text = `${text} (${code})`;
75
+ }
76
+ if (text !== '' && !parts.includes(text))
77
+ parts.push(text);
78
+ const next = layer.cause;
79
+ if (next === undefined || next === null || next === node)
80
+ break;
81
+ node = next;
82
+ }
83
+ return parts.join(' → ');
84
+ }
85
+ function withError(error) {
86
+ configError = errorMessage(error);
87
+ }
88
+ /**
89
+ * Parse and validate a proxy URL. Only HTTP(S) proxies are supported because
90
+ * the undici dispatcher speaks CONNECT over HTTP; socks5 is not supported.
91
+ * @param raw - the URL the user configured.
92
+ * @returns the parsed URL (credentials attached by the caller).
93
+ */
94
+ export function parseProxyUrl(raw) {
95
+ let url;
96
+ try {
97
+ url = new URL(raw);
98
+ }
99
+ catch {
100
+ throw new Error(`proxy URL "${raw}" is not a valid URL`);
101
+ }
102
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
103
+ throw new Error(`proxy URL must use the http:// or https:// scheme (got "${raw}")`);
104
+ }
105
+ if (url.hostname === '')
106
+ throw new Error('proxy URL must include a host');
107
+ return url;
108
+ }
109
+ /**
110
+ * Whether a request hostname bypasses the proxy.
111
+ * @param hostname - the request's hostname.
112
+ * @param entries - configured bypass entries: exact host, plain suffix
113
+ * (`example.com` also matches `api.example.com`), or `*.example.com`.
114
+ */
115
+ export function matchesBypass(hostname, entries) {
116
+ const host = hostname.toLowerCase();
117
+ for (const raw of entries) {
118
+ let entry = raw.trim().toLowerCase();
119
+ if (entry === '')
120
+ continue;
121
+ if (entry.includes('://')) {
122
+ try {
123
+ entry = new URL(entry).hostname;
124
+ }
125
+ catch {
126
+ continue;
127
+ }
128
+ }
129
+ entry = entry.replace(/:\d+$/, '');
130
+ if (entry === '' || entry === '*')
131
+ continue;
132
+ if (entry.startsWith('*.')) {
133
+ if (host.endsWith(entry.slice(1)))
134
+ return true;
135
+ }
136
+ else if (host === entry || host.endsWith(`.${entry}`)) {
137
+ return true;
138
+ }
139
+ }
140
+ return false;
141
+ }
142
+ /** Validate and normalize one config (throws with a user-facing message). */
143
+ function normalizeConfig(input) {
144
+ const url = input.url.trim();
145
+ if (input.enabled && url === '') {
146
+ throw new Error('a proxy URL is required when the proxy is enabled');
147
+ }
148
+ if (url !== '')
149
+ parseProxyUrl(url);
150
+ const bypass = Array.from(new Set((input.bypass ?? [])
151
+ .map(entry => entry.trim())
152
+ .filter(entry => entry !== '')));
153
+ return {
154
+ enabled: input.enabled,
155
+ url,
156
+ ...input.username !== undefined && input.username !== '' ? { username: input.username.trim() } : {},
157
+ ...input.password !== undefined && input.password !== '' && input.password !== null ? { password: input.password } : {},
158
+ bypass,
159
+ };
160
+ }
161
+ /** Build the undici agent for a config (throws on an unusable URL). */
162
+ function buildAgent(cfg) {
163
+ if (!cfg.enabled || cfg.url === '')
164
+ return undefined;
165
+ const url = parseProxyUrl(cfg.url);
166
+ if (cfg.username !== undefined)
167
+ url.username = cfg.username;
168
+ if (cfg.password !== undefined)
169
+ url.password = cfg.password;
170
+ return new ProxyAgent(url.toString());
171
+ }
172
+ /** Swap in a config and its agent; a failed agent keeps the requests direct. */
173
+ async function applyConfig(cfg) {
174
+ let next;
175
+ if (cfg !== undefined) {
176
+ configError = undefined;
177
+ try {
178
+ next = buildAgent(cfg);
179
+ }
180
+ catch (error) {
181
+ withError(error);
182
+ next = undefined;
183
+ }
184
+ current = cfg;
185
+ }
186
+ const previous = agent;
187
+ agent = next;
188
+ if (previous !== undefined)
189
+ void previous.close().catch(() => undefined);
190
+ }
191
+ /** Read the on-disk config. A missing file is the disabled default. */
192
+ async function loadConfigFile(path) {
193
+ let text;
194
+ try {
195
+ text = await readFile(path, 'utf8');
196
+ }
197
+ catch (error) {
198
+ if (error.code === 'ENOENT')
199
+ return { ...DISABLED, bypass: [] };
200
+ throw error;
201
+ }
202
+ let parsed;
203
+ try {
204
+ parsed = JSON.parse(text);
205
+ }
206
+ catch {
207
+ throw new Error(`subscriptions proxy config at ${path} is not valid JSON; fix or delete the file`);
208
+ }
209
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
210
+ throw new Error('subscriptions proxy config must be a JSON object');
211
+ }
212
+ const record = parsed;
213
+ const enabled = record.enabled === true;
214
+ const url = typeof record.url === 'string' ? record.url : '';
215
+ const username = typeof record.username === 'string' ? record.username : undefined;
216
+ const password = typeof record.password === 'string' ? record.password : undefined;
217
+ const bypass = Array.isArray(record.bypass)
218
+ ? record.bypass.filter((entry) => typeof entry === 'string')
219
+ : [];
220
+ return normalizeConfig({
221
+ enabled,
222
+ url,
223
+ ...username === undefined ? {} : { username },
224
+ ...password === undefined ? {} : { password },
225
+ bypass,
226
+ });
227
+ }
228
+ /** Resolve the module state once from disk; failures disable the proxy. */
229
+ async function ensureReady() {
230
+ ready ??= loadConfigFile(proxyFilePath()).then(async (cfg) => {
231
+ await applyConfig(cfg);
232
+ return current;
233
+ }, async (error) => {
234
+ withError(error);
235
+ await applyConfig(undefined);
236
+ return current;
237
+ });
238
+ return ready;
239
+ }
240
+ /** Persist a config atomically with owner-only permissions, then apply it. */
241
+ async function persistConfig(cfg, path) {
242
+ await mkdir(dirname(path), { recursive: true });
243
+ const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
244
+ try {
245
+ await writeFile(tmp, JSON.stringify(cfg, null, 2), { mode: 0o600 });
246
+ await chmod(tmp, 0o600);
247
+ await rename(tmp, path);
248
+ }
249
+ catch (error) {
250
+ await rm(tmp, { force: true });
251
+ throw error;
252
+ }
253
+ }
254
+ /**
255
+ * Close the live agent and drop the cached config. Test-only: lets a suite
256
+ * unwind the agent's keep-alive sockets before the process exits.
257
+ * @internal Exported for tests only; not part of the plugin's public surface.
258
+ */
259
+ export async function resetProxyForTests() {
260
+ const previous = agent;
261
+ agent = undefined;
262
+ current = { ...DISABLED, bypass: [] };
263
+ ready = undefined;
264
+ configError = undefined;
265
+ if (previous !== undefined)
266
+ await previous.close().catch(() => undefined);
267
+ }
268
+ /**
269
+ * Current proxy config as served to the client (secrets omitted).
270
+ * @returns the view; {@link ProxyConfigView.error} carries the last
271
+ * load/apply failure when the stored config is unusable.
272
+ */
273
+ export async function proxyGetConfig() {
274
+ await ensureReady();
275
+ return {
276
+ enabled: current.enabled,
277
+ url: current.url,
278
+ ...current.username === undefined ? {} : { username: current.username },
279
+ passwordSet: current.password !== undefined && current.password !== '',
280
+ bypass: [...current.bypass],
281
+ ...configError === undefined ? {} : { error: configError },
282
+ };
283
+ }
284
+ /**
285
+ * Validate, persist, and apply one proxy config. A `password` of `undefined`
286
+ * keeps the stored value; `null` or `''` clears it.
287
+ * @param input - the client's payload.
288
+ * @returns the resulting view (secrets omitted).
289
+ */
290
+ export async function proxySetConfig(input) {
291
+ await ensureReady();
292
+ const password = input.password === undefined
293
+ ? current.password
294
+ : input.password === null || input.password === ''
295
+ ? undefined
296
+ : input.password;
297
+ const next = normalizeConfig({
298
+ enabled: input.enabled,
299
+ url: input.url,
300
+ ...input.username === undefined ? {} : { username: input.username },
301
+ ...password === undefined ? {} : { password },
302
+ bypass: input.bypass ?? current.bypass,
303
+ });
304
+ await persistConfig(next, proxyFilePath());
305
+ await applyConfig(next);
306
+ return proxyGetConfig();
307
+ }
308
+ /**
309
+ * The fetch caller all subscription code uses: routes through the configured
310
+ * proxy unless the host bypasses it. Identity-passthrough otherwise.
311
+ *
312
+ * Proxied requests run on undici's own fetch (not the global one) so the
313
+ * ProxyAgent dispatcher always comes from the same undici build the request
314
+ * is issued with — a mismatched dispatcher can be silently ignored by the
315
+ * host's global fetch.
316
+ */
317
+ export async function proxiedFetch(input, init = {}) {
318
+ await ensureReady();
319
+ let dispatcher;
320
+ if (current.enabled && agent !== undefined) {
321
+ let hostname = '';
322
+ try {
323
+ const url = typeof input === 'string' ? new URL(input) : input instanceof URL ? input : new URL(input.url);
324
+ hostname = url.hostname;
325
+ }
326
+ catch {
327
+ hostname = '';
328
+ }
329
+ if (!matchesBypass(hostname, current.bypass))
330
+ dispatcher = agent;
331
+ }
332
+ if (dispatcher === undefined)
333
+ return fetch(input, init);
334
+ const proxied = { ...init, dispatcher };
335
+ return dispatchFetch(input, proxied);
336
+ }
337
+ /**
338
+ * Probe a destination through a proxy, answering with the HTTP status or a
339
+ * flattened transport error. The probe uses `draft` when given (the dialog's
340
+ * current inputs, without saving) and the stored config otherwise.
341
+ * @param target - `http(s)` URL to fetch; defaults to {@link DEFAULT_PROXY_TEST_URL}.
342
+ * @param draft - unsaved proxy inputs to test; absent means the stored config.
343
+ * @returns the result; any HTTP status counts as a successful connection,
344
+ * only a transport failure is an error.
345
+ */
346
+ export async function proxyTestConnection(target = DEFAULT_PROXY_TEST_URL, draft) {
347
+ let parsed;
348
+ try {
349
+ parsed = new URL(target);
350
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
351
+ return { ok: false, viaProxy: false, error: `test destination must be http or https (got "${parsed.protocol}//")` };
352
+ }
353
+ }
354
+ catch (error) {
355
+ return { ok: false, viaProxy: false, error: errorMessage(error) };
356
+ }
357
+ await ensureReady();
358
+ let probeAgent;
359
+ let viaProxy;
360
+ let closeProbe = false;
361
+ if (draft !== undefined) {
362
+ // Test the typed values: a throw here is a config problem, not a route one.
363
+ try {
364
+ probeAgent = buildAgent(normalizeConfig({
365
+ enabled: true,
366
+ url: draft.url,
367
+ ...draft.username === undefined || draft.username === '' ? {} : { username: draft.username },
368
+ ...draft.password === undefined || draft.password === '' ? {} : { password: draft.password },
369
+ bypass: [],
370
+ }));
371
+ viaProxy = probeAgent !== undefined;
372
+ closeProbe = true;
373
+ }
374
+ catch (error) {
375
+ return { ok: false, viaProxy: false, error: errorMessage(error) };
376
+ }
377
+ }
378
+ else {
379
+ viaProxy = current.enabled && agent !== undefined && !matchesBypass(parsed.hostname, current.bypass);
380
+ probeAgent = viaProxy ? agent : undefined;
381
+ }
382
+ const started = Date.now();
383
+ try {
384
+ const init = probeAgent !== undefined
385
+ ? { method: 'GET', dispatcher: probeAgent, signal: AbortSignal.timeout(DEFAULT_PROXY_TEST_TIMEOUT_MS) }
386
+ : { method: 'GET', signal: AbortSignal.timeout(DEFAULT_PROXY_TEST_TIMEOUT_MS) };
387
+ const response = probeAgent !== undefined
388
+ ? await dispatchFetch(parsed.toString(), init)
389
+ : await fetch(parsed.toString(), init);
390
+ // Drain so the connection can be released; the body is irrelevant.
391
+ void response.arrayBuffer().catch(() => undefined);
392
+ return { ok: true, viaProxy, status: response.status, latencyMs: Date.now() - started };
393
+ }
394
+ catch (error) {
395
+ return { ok: false, viaProxy, latencyMs: Date.now() - started, error: describeFetchError(error) };
396
+ }
397
+ finally {
398
+ if (closeProbe && probeAgent !== undefined) {
399
+ await probeAgent.close().catch(() => undefined);
400
+ }
401
+ }
402
+ }
package/lib/index.d.ts CHANGED
@@ -10,6 +10,7 @@ import type { Context } from '@deepseek-ai/cordis';
10
10
  import z from '@deepseek-ai/schemastery';
11
11
  import type { ProviderId } from './auth/store.js';
12
12
  import type { ModelEntry } from './providers/common.js';
13
+ import type { PoolMemberRef } from './providers/pool-family.js';
13
14
  export type { ModelEntry, ProviderUsage, UsageWindow } from './providers/common.js';
14
15
  export type { ProviderStatus } from './auth/rpc.js';
15
16
  export type { ClaudeSession, CodexSession, CopilotSession, GrokSession, ProviderId } from './auth/store.js';
@@ -17,6 +18,9 @@ export declare const name = "dsh-plugin-subscriptions";
17
18
  export declare const inject: string[];
18
19
  /** Default maximum provider idle time while one stream read is outstanding. */
19
20
  export declare const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300000;
21
+ /** Bound on one pool quota poll — member selection must not hang on a usage endpoint. */
22
+ export declare const POOL_USAGE_TIMEOUT_MS = 10000;
23
+ export { withTimeout } from './providers/common.js';
20
24
  /** Plugin config, validated by the same-named schemastery schema. */
21
25
  export interface Config {
22
26
  /** Provider routes to register; defaults to all three. */
@@ -30,6 +34,23 @@ export interface Config {
30
34
  grok?: ModelEntry[];
31
35
  copilot?: ModelEntry[];
32
36
  };
37
+ /** Same-subscription account pools (and optional extra tier models). */
38
+ pool?: {
39
+ /** Enable account pooling (default true; needs ≥2 accounts of one provider). */
40
+ enabled?: boolean;
41
+ /** Member selection: plain priority failover, or quota-aware urgency scheduling. */
42
+ strategy?: 'priority' | 'quota_aware';
43
+ /** A challenger must out-score the sticky member by this factor to take over (default 2). */
44
+ switchMargin?: number;
45
+ /** Auto-pool every catalog model across a provider's logged-in accounts (default true). */
46
+ autoAccounts?: boolean;
47
+ /** @deprecated Use {@link autoAccounts}. */
48
+ autoFamilies?: boolean;
49
+ /** Explicit account lists for one catalog model (same provider); replaces the auto pool. */
50
+ families?: Record<string, PoolMemberRef[]>;
51
+ /** Extra picker entries with heterogeneous fallbacks, listed under the first member's provider. */
52
+ tiers?: Record<string, PoolMemberRef[]>;
53
+ };
33
54
  }
34
55
  export declare const Config: z<Config>;
35
56
  export declare function apply(ctx: Context, config: Config): void;