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,30 @@
1
+ import type { AccountTokenManager } from './accounts.js';
2
+ import type { RateLimitResetReader } from './rate-limit.js';
3
+ type ImageProvider = 'codex' | 'grok';
4
+ interface ImageSession {
5
+ accessToken: string;
6
+ refreshToken: string;
7
+ expiresAt: number;
8
+ }
9
+ export interface ImageAccountRequest<S extends ImageSession> {
10
+ provider: ImageProvider;
11
+ tokens: AccountTokenManager<S>;
12
+ signal: AbortSignal;
13
+ /** Session object identity provides bounded-lifetime affinity, also across generate/edit. */
14
+ owner?: object | undefined;
15
+ rateLimitReset: RateLimitResetReader;
16
+ send: (session: S) => Promise<Response>;
17
+ }
18
+ export declare class ImageAccountPool {
19
+ private readonly options;
20
+ private readonly health;
21
+ private sticky;
22
+ constructor(options?: {
23
+ enabled?: boolean;
24
+ onWarn?: (message: string) => void;
25
+ });
26
+ /** Login/logout clears cooling image members and stale session affinity. */
27
+ clear(provider: ImageProvider, account?: string): void;
28
+ request<S extends ImageSession>(request: ImageAccountRequest<S>): Promise<Response>;
29
+ }
30
+ export {};
@@ -0,0 +1,89 @@
1
+ /** Account scheduling shared by image generation and editing, independent of chat catalogs/quota. */
2
+ import { LlmError } from '@deepseek-ai/dsh-llm';
3
+ import { httpLlmError } from './common.js';
4
+ import { AUTH_COOLDOWN_MS, DEFAULT_QUOTA_COOLDOWN_MS, TRANSIENT_COOLDOWN_MS, PoolHealthRegistry, memberKey } from './pool-health.js';
5
+ export class ImageAccountPool {
6
+ options;
7
+ // Deliberately separate from chat health: image quotas/entitlements need not match chat.
8
+ health = new PoolHealthRegistry();
9
+ sticky = new WeakMap();
10
+ constructor(options = {}) {
11
+ this.options = options;
12
+ }
13
+ /** Login/logout clears cooling image members and stale session affinity. */
14
+ clear(provider, account) {
15
+ this.health.clear(provider, account);
16
+ this.sticky = new WeakMap();
17
+ }
18
+ async request(request) {
19
+ const { provider, tokens, signal, owner, send, rateLimitReset } = request;
20
+ signal.throwIfAborted();
21
+ const accounts = await tokens.list();
22
+ if (accounts.length === 0) {
23
+ await tokens.session(); // standard provider-specific login hint
24
+ throw new LlmError('image_generate: no image account is logged in', 'MISSING_CREDENTIAL');
25
+ }
26
+ const pooling = this.options.enabled !== false;
27
+ const members = (pooling ? accounts : accounts.slice(0, 1)).map(entry => entry.key);
28
+ const sticky = pooling && owner !== undefined ? this.sticky.get(owner)?.get(provider) : undefined;
29
+ const ordered = sticky !== undefined && members.includes(sticky)
30
+ ? [sticky, ...members.filter(key => key !== sticky)] : members;
31
+ let lastError;
32
+ for (const account of ordered) {
33
+ signal.throwIfAborted();
34
+ const key = memberKey(provider, account, 'images');
35
+ if (pooling && !this.health.isAvailable(key))
36
+ continue;
37
+ let failure;
38
+ try {
39
+ let session = await tokens.session(account);
40
+ signal.throwIfAborted();
41
+ let response = await send(session);
42
+ // Only a definitive unauthorized rejection permits retrying this account.
43
+ if (response.status === 401) {
44
+ await response.body?.cancel();
45
+ session = await tokens.session(account, true);
46
+ signal.throwIfAborted();
47
+ response = await send(session);
48
+ }
49
+ signal.throwIfAborted();
50
+ if (response.ok) {
51
+ if (pooling && owner !== undefined) {
52
+ let affinity = this.sticky.get(owner);
53
+ if (affinity === undefined) {
54
+ affinity = new Map();
55
+ this.sticky.set(owner, affinity);
56
+ }
57
+ affinity.set(provider, account);
58
+ }
59
+ return response;
60
+ }
61
+ // Images can be produced despite timeouts/server failures. Switch only on
62
+ // explicit auth/quota/entitlement rejection, never ambiguous transport/5xx.
63
+ failure = await httpLlmError(response, 'image_generate', { rateLimitReset });
64
+ if (![401, 402, 403, 404, 429].includes(response.status))
65
+ throw failure;
66
+ }
67
+ catch (error) {
68
+ signal.throwIfAborted();
69
+ if (!(error instanceof LlmError) || !['AUTH', 'INVALID_CREDENTIAL', 'MISSING_CREDENTIAL', 'RATE_LIMIT', 'HTTP_402', 'HTTP_404'].includes(error.code))
70
+ throw error;
71
+ failure = error;
72
+ }
73
+ if (!pooling)
74
+ throw failure;
75
+ const delay = failure.failure.providerRetryAfterMs ?? (failure.code === 'RATE_LIMIT'
76
+ ? DEFAULT_QUOTA_COOLDOWN_MS
77
+ : ['AUTH', 'INVALID_CREDENTIAL', 'MISSING_CREDENTIAL'].includes(failure.code) ? AUTH_COOLDOWN_MS : TRANSIENT_COOLDOWN_MS);
78
+ this.health.markUnavailable(key, delay, failure.code);
79
+ this.options.onWarn?.(`image pool ${provider}: account ${members.indexOf(account) + 1}/${members.length} rejected (${failure.code}); checking remaining accounts`);
80
+ lastError = failure;
81
+ }
82
+ if (lastError !== undefined)
83
+ throw lastError;
84
+ const recovery = this.health.earliestRecovery(new Set(members.map(account => memberKey(provider, account, 'images'))));
85
+ throw new LlmError(`image_generate: all ${provider} image accounts are cooling down`, 'RATE_LIMIT', {
86
+ ...recovery === undefined ? {} : { providerRetryAfterMs: Math.max(0, recovery - Date.now()) },
87
+ });
88
+ }
89
+ }
@@ -0,0 +1,56 @@
1
+ import type { FetchFn } from './common.js';
2
+ /**
3
+ * Compare two stable `major.minor.patch` versions numerically.
4
+ * @returns negative when `a` is older, positive when newer, 0 when equal.
5
+ */
6
+ export declare function compareVersions(a: string, b: string): number;
7
+ /** Where a presented CLI version came from; shown in Settings so a stale one is visible. */
8
+ export type CliVersionSource = 'npm' | 'local' | 'fallback' | 'config';
9
+ /** The CLI version a route presents, and where it came from. */
10
+ export interface CliVersion {
11
+ version: string;
12
+ source: CliVersionSource;
13
+ }
14
+ export interface NpmCliVersionOptions {
15
+ /** Registry metadata of the package's `latest` dist-tag; public, so never send credentials. */
16
+ url: string;
17
+ /** Names the CLI in lookup errors. */
18
+ label: string;
19
+ /**
20
+ * The version served until a lookup succeeds, and the floor a looked-up
21
+ * version may never go below. Read once, on first use, so a costly probe
22
+ * (a local CLI's `--version`) never runs for an unused provider.
23
+ */
24
+ floor: () => CliVersion;
25
+ fetchFn?: FetchFn;
26
+ now?: () => number;
27
+ timeoutMs?: number;
28
+ }
29
+ /**
30
+ * Lazy, shared-per-plugin lookup of an official CLI's newest stable version
31
+ * on npm. Subscription endpoints gate new models on the client version they
32
+ * see, so presenting the released CLI's version keeps new models usable
33
+ * without a plugin release for every CLI bump.
34
+ */
35
+ export declare class NpmCliVersionCache {
36
+ private readonly options;
37
+ private presented;
38
+ /** Whether a lookup has finished yet, successfully or not. */
39
+ private settled;
40
+ private expiresAt;
41
+ private pending;
42
+ private readonly fetchFn;
43
+ private readonly now;
44
+ private readonly timeoutMs;
45
+ constructor(options: NpmCliVersionOptions);
46
+ /** A manual catalog refresh also checks for a newly released CLI. */
47
+ invalidate(): void;
48
+ resolve(): Promise<string>;
49
+ /**
50
+ * The version presented right now, without waiting on the registry;
51
+ * undefined until the first lookup finishes, so a lookup still in flight
52
+ * is never reported as a failed one.
53
+ */
54
+ current(): CliVersion | undefined;
55
+ private refresh;
56
+ }
@@ -0,0 +1,98 @@
1
+ import { hostFetch } from '../http.js';
2
+ /** Stable `major.minor.patch` only: prerelease and platform tags are not client versions. */
3
+ const STABLE_VERSION = /^\d{1,6}\.\d{1,6}\.\d{1,6}$/;
4
+ /**
5
+ * Compare two stable `major.minor.patch` versions numerically.
6
+ * @returns negative when `a` is older, positive when newer, 0 when equal.
7
+ */
8
+ export function compareVersions(a, b) {
9
+ const left = a.split('.').map(Number);
10
+ const right = b.split('.').map(Number);
11
+ const different = left.findIndex((part, index) => part !== right[index]);
12
+ return different === -1 ? 0 : left[different] - right[different];
13
+ }
14
+ /**
15
+ * Lazy, shared-per-plugin lookup of an official CLI's newest stable version
16
+ * on npm. Subscription endpoints gate new models on the client version they
17
+ * see, so presenting the released CLI's version keeps new models usable
18
+ * without a plugin release for every CLI bump.
19
+ */
20
+ export class NpmCliVersionCache {
21
+ options;
22
+ presented;
23
+ /** Whether a lookup has finished yet, successfully or not. */
24
+ settled = false;
25
+ expiresAt = 0;
26
+ pending;
27
+ fetchFn;
28
+ now;
29
+ timeoutMs;
30
+ constructor(options) {
31
+ this.options = options;
32
+ this.fetchFn = options.fetchFn ?? hostFetch;
33
+ this.now = options.now ?? Date.now;
34
+ this.timeoutMs = options.timeoutMs ?? 5000;
35
+ }
36
+ /** A manual catalog refresh also checks for a newly released CLI. */
37
+ invalidate() { this.expiresAt = 0; }
38
+ resolve() {
39
+ if (this.pending !== undefined)
40
+ return this.pending;
41
+ if (this.presented !== undefined && this.now() < this.expiresAt)
42
+ return Promise.resolve(this.presented.version);
43
+ this.pending = this.refresh().finally(() => { this.pending = undefined; });
44
+ return this.pending;
45
+ }
46
+ /**
47
+ * The version presented right now, without waiting on the registry;
48
+ * undefined until the first lookup finishes, so a lookup still in flight
49
+ * is never reported as a failed one.
50
+ */
51
+ current() {
52
+ return this.settled ? this.presented : undefined;
53
+ }
54
+ async refresh() {
55
+ const current = (this.presented ??= this.options.floor()).version;
56
+ const { url, label } = this.options;
57
+ const controller = new AbortController();
58
+ let timer;
59
+ try {
60
+ const lookup = async () => {
61
+ const response = await this.fetchFn(url, {
62
+ headers: { accept: 'application/json' },
63
+ redirect: 'error',
64
+ signal: controller.signal,
65
+ });
66
+ if (!response.ok)
67
+ throw new Error(`${label} version lookup failed`);
68
+ const payload = await response.json();
69
+ const version = payload?.version;
70
+ // Ignore prerelease/platform tags and malformed or regressed metadata.
71
+ if (typeof version !== 'string' || !STABLE_VERSION.test(version)) {
72
+ throw new Error(`Invalid stable ${label} version`);
73
+ }
74
+ if (compareVersions(version, current) < 0)
75
+ throw new Error(`Older ${label} version`);
76
+ return version;
77
+ };
78
+ const timeout = new Promise((_, reject) => {
79
+ timer = setTimeout(() => {
80
+ controller.abort();
81
+ reject(new Error(`${label} version lookup timed out`));
82
+ }, this.timeoutMs);
83
+ });
84
+ // Also bounded when an injected transport ignores cancellation.
85
+ this.presented = { version: await Promise.race([lookup(), timeout]), source: 'npm' };
86
+ this.expiresAt = this.now() + 6 * 60 * 60_000;
87
+ }
88
+ catch {
89
+ // Retain last-known good; on first use this is the floor.
90
+ this.expiresAt = this.now() + 5 * 60_000;
91
+ }
92
+ finally {
93
+ clearTimeout(timer);
94
+ this.settled = true;
95
+ }
96
+ return this.presented.version;
97
+ }
98
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Same-subscription account routing: the picker is the union of every
3
+ * account's catalog (deduped by wire id). A model listed by two or more
4
+ * accounts failovers between them; a model listed by only one account is
5
+ * sent to that account. No extra pool identity, no cross-provider
6
+ * aggregation.
7
+ */
8
+ import type { LlmModelInfo } from '@deepseek-ai/dsh-llm';
9
+ import type { ProviderId } from '../auth/store.js';
10
+ /** One pool member: an exact provider/account/model route. */
11
+ export interface PoolMemberRef {
12
+ provider: ProviderId;
13
+ /** Account key; omitted in configured members to mean "the default account". */
14
+ account?: string;
15
+ model: string;
16
+ }
17
+ /** A member with its account resolved (no config indirection left). */
18
+ export type ConcretePoolMember = PoolMemberRef & {
19
+ account: string;
20
+ };
21
+ /** One pool: its members plus display metadata for the picker. */
22
+ export interface PoolDefinition {
23
+ members: PoolMemberRef[];
24
+ /** Display name of the catalog entry (account pools) or the pool id (extras). */
25
+ name?: string;
26
+ /** Description of the catalog entry, when the pool borrowed one. */
27
+ description?: string;
28
+ /**
29
+ * When true, the pool is an extra picker entry (a configured tier). Account
30
+ * pools leave this unset so they reuse the provider's existing catalog row.
31
+ */
32
+ extra?: boolean;
33
+ }
34
+ /** One account's catalog as seen through that account's credentials. */
35
+ export interface AccountCatalog {
36
+ account: string;
37
+ models: readonly LlmModelInfo[];
38
+ }
39
+ /** One provider's contribution to account-pool aggregation. */
40
+ export interface ProviderPoolSource {
41
+ /** Per-account catalogs, default first. A model pools only the accounts that list it. */
42
+ catalogs: readonly AccountCatalog[];
43
+ }
44
+ /** Map key for one provider's pool of one model (ids collide across providers). */
45
+ export declare function poolKey(provider: string, model: string): string;
46
+ /**
47
+ * Build per-provider account routes. Each model id becomes a definition of
48
+ * the accounts that list it: two or more fail over; one is pinned to that
49
+ * account (so a Max-only model is never sent to a Plus login). The picker
50
+ * unions these catalogs; a logout that drops a model to one account keeps
51
+ * the same id and pins it to whoever remains.
52
+ * @param sources - per-account catalogs (providers with no accounts list
53
+ * nothing and simply never join a pool).
54
+ * @returns `provider/model` → pool definition (not listed as an extra entry).
55
+ */
56
+ export declare function buildAccountPools(sources: Partial<Record<ProviderId, ProviderPoolSource>>): Map<string, PoolDefinition>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Same-subscription account routing: the picker is the union of every
3
+ * account's catalog (deduped by wire id). A model listed by two or more
4
+ * accounts failovers between them; a model listed by only one account is
5
+ * sent to that account. No extra pool identity, no cross-provider
6
+ * aggregation.
7
+ */
8
+ /** Map key for one provider's pool of one model (ids collide across providers). */
9
+ export function poolKey(provider, model) {
10
+ return `${provider}/${model}`;
11
+ }
12
+ /**
13
+ * Build per-provider account routes. Each model id becomes a definition of
14
+ * the accounts that list it: two or more fail over; one is pinned to that
15
+ * account (so a Max-only model is never sent to a Plus login). The picker
16
+ * unions these catalogs; a logout that drops a model to one account keeps
17
+ * the same id and pins it to whoever remains.
18
+ * @param sources - per-account catalogs (providers with no accounts list
19
+ * nothing and simply never join a pool).
20
+ * @returns `provider/model` → pool definition (not listed as an extra entry).
21
+ */
22
+ export function buildAccountPools(sources) {
23
+ const pools = new Map();
24
+ for (const [provider, source] of Object.entries(sources)) {
25
+ const byModel = new Map();
26
+ for (const catalog of source.catalogs) {
27
+ for (const model of catalog.models) {
28
+ let entry = byModel.get(model.id);
29
+ if (entry === undefined) {
30
+ entry = { members: [], info: model };
31
+ byModel.set(model.id, entry);
32
+ }
33
+ entry.members.push({ provider, account: catalog.account, model: model.id });
34
+ }
35
+ }
36
+ for (const [id, { members, info }] of byModel) {
37
+ pools.set(poolKey(provider, id), {
38
+ members,
39
+ ...info.name === undefined || info.name === id ? {} : { name: info.name },
40
+ ...info.description === undefined ? {} : { description: info.description },
41
+ });
42
+ }
43
+ }
44
+ return pools;
45
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Health bookkeeping for pool members: which `(provider, account, model)`
3
+ * member is cooling down after a failure, and for how long. Purely in-memory
4
+ * — a restart re-probes members naturally, so nothing here is persisted.
5
+ *
6
+ * The failure classifier maps the adapters' stable `LlmError` codes (see
7
+ * `httpLlmError`/`mapFetchFailure` in `common.ts`) to one of three actions:
8
+ * switch to another member with a cooldown, switch without recording a
9
+ * cooldown (transport blips say nothing about the account), or rethrow
10
+ * (the request itself is at fault and another account would fail alike).
11
+ */
12
+ import type { ProviderId } from '../auth/store.js';
13
+ /** Registry key for one pool member. */
14
+ export declare function memberKey(provider: ProviderId, account: string, model: string): string;
15
+ /** Registry key parking EVERY member of one account (account-level failures). */
16
+ export declare function accountKey(provider: ProviderId, account: string): string;
17
+ /** Default cooldown when a quota/rate failure carries no `retry-after`. */
18
+ export declare const DEFAULT_QUOTA_COOLDOWN_MS: number;
19
+ /** Auth failures recheck after a day; a re-login clears the record immediately. */
20
+ export declare const AUTH_COOLDOWN_MS: number;
21
+ /** Transient server-side failures cool down briefly. */
22
+ export declare const TRANSIENT_COOLDOWN_MS = 60000;
23
+ /** Whether a failure parks one member or the account's whole quota. */
24
+ export type PoolFailureScope = 'member' | 'account';
25
+ /** What the pool should do with a member that just failed. */
26
+ export type PoolFailureAction = {
27
+ action: 'switch';
28
+ cooldownMs: number;
29
+ reason: string;
30
+ scope: PoolFailureScope;
31
+ } | {
32
+ action: 'switch';
33
+ } | {
34
+ action: 'throw';
35
+ };
36
+ /**
37
+ * Classify a member failure. Quota and rate-limit failures cool down (using
38
+ * the provider's own `retry-after` when sent, which is more accurate than
39
+ * any fixed guess) — account-wide for account-metered providers, per-member
40
+ * for model-scoped ones; auth failures park the account until re-login
41
+ * (credentials are account-level); server/timeout failures get a short
42
+ * per-member cooldown; transport failures switch without a record;
43
+ * everything else — most importantly CONTEXT_WINDOW_EXCEEDED and ABORTED —
44
+ * is the request's own fault and is rethrown untouched.
45
+ * @param error - the failure thrown by a member adapter's stream.
46
+ * @param provider - the failing member's provider (decides the quota scope).
47
+ * @returns the action the pool should take.
48
+ */
49
+ export declare function classifyPoolFailure(error: unknown, provider: ProviderId): PoolFailureAction;
50
+ /**
51
+ * Cooldown registry keyed by {@link memberKey}. A member whose cooldown has
52
+ * expired is simply available again — recovery is proven by the next real
53
+ * request, not by a background probe.
54
+ */
55
+ export declare class PoolHealthRegistry {
56
+ private readonly records;
57
+ /** Whether a member may serve: neither it nor its whole account is cooling. */
58
+ isMemberAvailable(provider: ProviderId, account: string, model: string, now?: number): boolean;
59
+ /** Whether one registry key is clear right now. */
60
+ isAvailable(key: string, now?: number): boolean;
61
+ /** Park a member for `cooldownMs`; a longer existing cooldown wins. */
62
+ markUnavailable(key: string, cooldownMs: number, reason: string, now?: number): void;
63
+ /**
64
+ * Epoch ms at which the earliest cooling record among `keys` recovers;
65
+ * `undefined` when none of them is cooling. The registry is shared by
66
+ * every pool, so the caller passes the keys of ITS members (member and
67
+ * account keys alike) — an unrelated pool's cooldown must not shape this
68
+ * pool's retry hint. Feeds the pool-exhausted error's
69
+ * `providerRetryAfterMs`.
70
+ */
71
+ earliestRecovery(keys: ReadonlySet<string>, now?: number): number | undefined;
72
+ /** Drop records of one provider, or of a single account when given (auth changes). */
73
+ clear(provider: ProviderId, account?: string): void;
74
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Health bookkeeping for pool members: which `(provider, account, model)`
3
+ * member is cooling down after a failure, and for how long. Purely in-memory
4
+ * — a restart re-probes members naturally, so nothing here is persisted.
5
+ *
6
+ * The failure classifier maps the adapters' stable `LlmError` codes (see
7
+ * `httpLlmError`/`mapFetchFailure` in `common.ts`) to one of three actions:
8
+ * switch to another member with a cooldown, switch without recording a
9
+ * cooldown (transport blips say nothing about the account), or rethrow
10
+ * (the request itself is at fault and another account would fail alike).
11
+ */
12
+ import { CONTEXT_WINDOW_EXCEEDED_CODE, LlmError, QUOTA_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm';
13
+ /** Registry key for one pool member. */
14
+ export function memberKey(provider, account, model) {
15
+ return `${provider}/${account}/${model}`;
16
+ }
17
+ /** Registry key parking EVERY member of one account (account-level failures). */
18
+ export function accountKey(provider, account) {
19
+ return `${provider}/${account}/*`;
20
+ }
21
+ /** Default cooldown when a quota/rate failure carries no `retry-after`. */
22
+ export const DEFAULT_QUOTA_COOLDOWN_MS = 5 * 60_000;
23
+ /** Auth failures recheck after a day; a re-login clears the record immediately. */
24
+ export const AUTH_COOLDOWN_MS = 24 * 60 * 60_000;
25
+ /** Transient server-side failures cool down briefly. */
26
+ export const TRANSIENT_COOLDOWN_MS = 60_000;
27
+ /**
28
+ * Providers whose quota windows are model-scoped, so a quota failure on one
29
+ * model says nothing about its siblings (Claude's Opus/Sonnet lanes;
30
+ * Antigravity's per-model quotas, whose 429 names the exhausted model and
31
+ * can disclose a reset 35 hours out that must not park its Gemini siblings).
32
+ * Every other provider meters the account as a whole: one member hitting the
33
+ * wall means its siblings on the SAME account would too, so the cooldown
34
+ * parks the account (other accounts of the provider are unaffected).
35
+ */
36
+ const MODEL_SCOPED_QUOTA_PROVIDERS = new Set(['claude', 'antigravity']);
37
+ /** The `retry-after` an adapter propagated through `httpLlmError`, when any. */
38
+ function retryAfterMs(error) {
39
+ return error.failure.providerRetryAfterMs;
40
+ }
41
+ /**
42
+ * Classify a member failure. Quota and rate-limit failures cool down (using
43
+ * the provider's own `retry-after` when sent, which is more accurate than
44
+ * any fixed guess) — account-wide for account-metered providers, per-member
45
+ * for model-scoped ones; auth failures park the account until re-login
46
+ * (credentials are account-level); server/timeout failures get a short
47
+ * per-member cooldown; transport failures switch without a record;
48
+ * everything else — most importantly CONTEXT_WINDOW_EXCEEDED and ABORTED —
49
+ * is the request's own fault and is rethrown untouched.
50
+ * @param error - the failure thrown by a member adapter's stream.
51
+ * @param provider - the failing member's provider (decides the quota scope).
52
+ * @returns the action the pool should take.
53
+ */
54
+ export function classifyPoolFailure(error, provider) {
55
+ if (!(error instanceof LlmError))
56
+ return { action: 'throw' };
57
+ switch (error.code) {
58
+ case QUOTA_EXCEEDED_CODE:
59
+ case 'RATE_LIMIT':
60
+ return {
61
+ action: 'switch',
62
+ cooldownMs: retryAfterMs(error) ?? DEFAULT_QUOTA_COOLDOWN_MS,
63
+ reason: error.code,
64
+ scope: MODEL_SCOPED_QUOTA_PROVIDERS.has(provider) ? 'member' : 'account',
65
+ };
66
+ case 'AUTH':
67
+ case 'INVALID_CREDENTIAL':
68
+ case 'MISSING_CREDENTIAL':
69
+ return { action: 'switch', cooldownMs: AUTH_COOLDOWN_MS, reason: error.code, scope: 'account' };
70
+ case 'SERVER':
71
+ case 'TIMEOUT':
72
+ case 'EMPTY_RESPONSE':
73
+ return { action: 'switch', cooldownMs: TRANSIENT_COOLDOWN_MS, reason: error.code, scope: 'member' };
74
+ case 'TRANSPORT':
75
+ return { action: 'switch' };
76
+ case 'HTTP_402':
77
+ case 'HTTP_404':
78
+ // Plan/model availability is account-shaped: another account of the
79
+ // same subscription may still serve. A 400 that is not a context-window
80
+ // error stays HTTP_400 and throws (the request itself is at fault).
81
+ return { action: 'switch', cooldownMs: TRANSIENT_COOLDOWN_MS, reason: error.code, scope: 'member' };
82
+ case CONTEXT_WINDOW_EXCEEDED_CODE:
83
+ case 'ABORTED':
84
+ default:
85
+ return { action: 'throw' };
86
+ }
87
+ }
88
+ /**
89
+ * Cooldown registry keyed by {@link memberKey}. A member whose cooldown has
90
+ * expired is simply available again — recovery is proven by the next real
91
+ * request, not by a background probe.
92
+ */
93
+ export class PoolHealthRegistry {
94
+ records = new Map();
95
+ /** Whether a member may serve: neither it nor its whole account is cooling. */
96
+ isMemberAvailable(provider, account, model, now = Date.now()) {
97
+ return this.isAvailable(accountKey(provider, account), now)
98
+ && this.isAvailable(memberKey(provider, account, model), now);
99
+ }
100
+ /** Whether one registry key is clear right now. */
101
+ isAvailable(key, now = Date.now()) {
102
+ const record = this.records.get(key);
103
+ if (record === undefined)
104
+ return true;
105
+ if (record.unavailableUntil <= now) {
106
+ this.records.delete(key);
107
+ return true;
108
+ }
109
+ return false;
110
+ }
111
+ /** Park a member for `cooldownMs`; a longer existing cooldown wins. */
112
+ markUnavailable(key, cooldownMs, reason, now = Date.now()) {
113
+ const until = now + cooldownMs;
114
+ const existing = this.records.get(key);
115
+ if (existing !== undefined && existing.unavailableUntil > until)
116
+ return;
117
+ this.records.set(key, { unavailableUntil: until, reason });
118
+ }
119
+ /**
120
+ * Epoch ms at which the earliest cooling record among `keys` recovers;
121
+ * `undefined` when none of them is cooling. The registry is shared by
122
+ * every pool, so the caller passes the keys of ITS members (member and
123
+ * account keys alike) — an unrelated pool's cooldown must not shape this
124
+ * pool's retry hint. Feeds the pool-exhausted error's
125
+ * `providerRetryAfterMs`.
126
+ */
127
+ earliestRecovery(keys, now = Date.now()) {
128
+ let earliest;
129
+ for (const [key, record] of this.records) {
130
+ if (record.unavailableUntil <= now) {
131
+ this.records.delete(key);
132
+ continue;
133
+ }
134
+ if (!keys.has(key))
135
+ continue;
136
+ if (earliest === undefined || record.unavailableUntil < earliest) {
137
+ earliest = record.unavailableUntil;
138
+ }
139
+ }
140
+ return earliest;
141
+ }
142
+ /** Drop records of one provider, or of a single account when given (auth changes). */
143
+ clear(provider, account) {
144
+ const prefix = account === undefined ? `${provider}/` : `${provider}/${account}/`;
145
+ for (const key of [...this.records.keys()]) {
146
+ if (key.startsWith(prefix))
147
+ this.records.delete(key);
148
+ }
149
+ }
150
+ }