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,680 @@
1
+ /**
2
+ * Claude Pro/Max subscription provider: OAuth against claude.ai /
3
+ * platform.claude.com with the Claude Code client id, and streaming against
4
+ * the Anthropic Messages API with the Claude Code identity headers.
5
+ */
6
+ import { execFileSync } from 'node:child_process';
7
+ import { EMPTY_RESPONSE_CODE, errorChain, LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
8
+ import { resolveImages } from '../translate/resolved.js';
9
+ import { markMessageCache, streamAnthropic, toAnthropicMessages, toAnthropicSystem, toAnthropicTools, } from '../translate/anthropic.js';
10
+ import { httpLlmError, idleWatchdog, mapFetchFailure, mergeReasoning, ModelCatalogCache, discoverAcrossAccounts, discoverOrRetryAuth, isDiscoveryAborted, isMissingOrInvalidCredential, oauthEndpointError, OAuthEndpointError, } from './common.js';
11
+ import { AccountTokenManager, DISCOVERY_TIMEOUT_MS, unionAccountCatalogs } from './accounts.js';
12
+ import { hostFetch } from '../http.js';
13
+ import { compareVersions } from './npm-cli-version.js';
14
+ import { DEFAULT_RATE_LIMIT_WAIT, DEFAULT_RETRY, earliestReset, jsonBody, resetFromFields, resetInstantFromHeader, subscriptionRetryPolicy, } from './rate-limit.js';
15
+ export const CLAUDE_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
16
+ export const CLAUDE_AUTHORIZE_URL = 'https://claude.ai/oauth/authorize';
17
+ export const CLAUDE_TOKEN_URL = 'https://claude.ai/v1/oauth/token';
18
+ export const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages?beta=true';
19
+ export const CLAUDE_PROFILE_URL = 'https://api.anthropic.com/api/oauth/profile';
20
+ export const CLAUDE_MODELS_URL = 'https://api.anthropic.com/v1/models?beta=true';
21
+ export const CLAUDE_SCOPE = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload';
22
+ export const CLAUDE_CALLBACK_PATH = '/callback';
23
+ // Fallbacks only when discovery is unavailable or the model omits its limits.
24
+ const CLAUDE_CONTEXT_WINDOW = 200_000;
25
+ const CLAUDE_DEFAULT_MAX_TOKENS = 32_000;
26
+ /** Refresh when the access token has less than this much life left. */
27
+ export const CLAUDE_PREEMPT_MS = 5 * 60_000;
28
+ /**
29
+ * Body fields Anthropic uses to name a reset instant, read when the unified
30
+ * headers are absent.
31
+ */
32
+ const CLAUDE_RESET_FIELDS = ['resets_at', 'resetsAt', 'reset_at', 'retry_after'];
33
+ /**
34
+ * Reads the reset instant of the Anthropic window that rejected a request.
35
+ *
36
+ * `anthropic-ratelimit-unified-*` is the subscription-plan family — the one
37
+ * Claude Code renders as "resets 3pm" — and is the only header that names the
38
+ * window which actually rejected this request. The per-bucket
39
+ * `anthropic-ratelimit-{requests,tokens,input-tokens,output-tokens}-reset`
40
+ * headers are deliberately not read: they are rollover snapshots attached to
41
+ * every response, so on a 429 they cannot say which bucket refused, and the
42
+ * earliest of them is typically the bucket that still had room — a wait that
43
+ * lands straight back in the closed window. They reach the operator through
44
+ * `rateLimitDiagnostics` instead.
45
+ */
46
+ export const claudeRateLimitReset = (response, body, now) => {
47
+ const unified = earliestReset(resetInstantFromHeader(response, 'anthropic-ratelimit-unified-reset', now), resetInstantFromHeader(response, 'anthropic-ratelimit-unified-fallback-reset', now));
48
+ if (unified !== undefined)
49
+ return unified;
50
+ return resetFromFields(jsonBody(body), CLAUDE_RESET_FIELDS, now);
51
+ };
52
+ /**
53
+ * The subscription endpoint only serves requests presenting as Claude Code,
54
+ * so these headers impersonate the CLI; the harness attribution user-agent
55
+ * cannot be sent here (one user-agent slot, and the CLI's wins). The endpoint
56
+ * also gates new models on the CLI version, so the version normally comes
57
+ * from npm (`ClaudeCliVersionCache`); this is only the offline floor.
58
+ */
59
+ export const CLAUDE_CLI_FALLBACK_VERSION = '2.1.283';
60
+ /**
61
+ * Candidate invocations, in order of preference. Windows npm installs expose
62
+ * Claude Code as a `.cmd` shim, which must run through a shell.
63
+ */
64
+ const CLAUDE_VERSION_PROBES = process.platform === 'win32'
65
+ ? [
66
+ ['claude --version', [], { shell: true }],
67
+ ['claude.cmd --version', [], { shell: true }],
68
+ ]
69
+ : [['claude', ['--version'], {}]];
70
+ export function detectClaudeVersion() {
71
+ let lastError;
72
+ for (const [command, args, options] of CLAUDE_VERSION_PROBES) {
73
+ try {
74
+ const raw = execFileSync(command, [...args], {
75
+ timeout: 10_000,
76
+ encoding: 'utf8',
77
+ stdio: ['ignore', 'pipe', 'ignore'],
78
+ ...options,
79
+ });
80
+ const match = raw.match(/(\d+\.\d+\.\d+)/);
81
+ if (match)
82
+ return match[1];
83
+ }
84
+ catch (error) {
85
+ lastError = error;
86
+ }
87
+ }
88
+ // This is debug-only: the fallback is expected when Claude Code is absent.
89
+ if (lastError !== undefined) {
90
+ console.debug('dsh-subscriptions: Claude CLI version detection fell back', lastError);
91
+ }
92
+ return CLAUDE_CLI_FALLBACK_VERSION;
93
+ }
94
+ /**
95
+ * The lowest version to present: the local CLI or the bundled fallback,
96
+ * whichever is newer. An outdated local install must not pin requests to a
97
+ * version the endpoint already rejects for new models.
98
+ * @param detect - local CLI probe (injectable for tests).
99
+ */
100
+ export function claudeCliVersionFloor(detect = detectClaudeVersion) {
101
+ const local = detect();
102
+ return compareVersions(local, CLAUDE_CLI_FALLBACK_VERSION) > 0
103
+ ? { version: local, source: 'local' }
104
+ : { version: CLAUDE_CLI_FALLBACK_VERSION, source: 'fallback' };
105
+ }
106
+ /** The User-Agent Claude Code sends at `version`. */
107
+ export function claudeCliUserAgent(version) {
108
+ return `claude-cli/${version} (external, cli)`;
109
+ }
110
+ // Lazy + memoized: the floor shells out to `claude --version`, so this must
111
+ // not run at module-evaluation time (it would fire for every consumer of this
112
+ // module regardless of whether Claude is a configured provider). Used when no
113
+ // npm-backed resolver is wired in.
114
+ let localCliVersion;
115
+ async function localClaudeCliVersion() {
116
+ localCliVersion ??= claudeCliVersionFloor().version;
117
+ return localCliVersion;
118
+ }
119
+ export const CLAUDE_BETA_FALLBACK = [
120
+ 'claude-code-20250219',
121
+ 'oauth-2025-04-20',
122
+ 'interleaved-thinking-2025-05-14',
123
+ 'context-management-2025-06-27',
124
+ 'effort-2025-11-24',
125
+ 'compact-2026-01-12',
126
+ 'files-api-2025-04-14',
127
+ ].join(',');
128
+ const CLAUDE_BETA_FLAGS = CLAUDE_BETA_FALLBACK;
129
+ /** Static claude flow facts for the OAuth flow engine. */
130
+ export const claudeFlow = {
131
+ callbackPath: CLAUDE_CALLBACK_PATH,
132
+ // The redirect URI embeds the port, so it must be an ephemeral one.
133
+ listen: { host: 'localhost', ports: [0] },
134
+ buildAuthorizeUrl({ redirectUri, state, pkce }) {
135
+ const params = new URLSearchParams({
136
+ code: 'true',
137
+ client_id: CLAUDE_CLIENT_ID,
138
+ response_type: 'code',
139
+ redirect_uri: redirectUri,
140
+ scope: CLAUDE_SCOPE,
141
+ code_challenge: pkce.challenge,
142
+ code_challenge_method: 'S256',
143
+ state,
144
+ });
145
+ return `${CLAUDE_AUTHORIZE_URL}?${params.toString()}`;
146
+ },
147
+ };
148
+ /** Best-effort account profile; login must not fail when this does. */
149
+ async function fetchClaudeProfile(accessToken) {
150
+ try {
151
+ const response = await hostFetch(CLAUDE_PROFILE_URL, {
152
+ headers: { authorization: `Bearer ${accessToken}` },
153
+ });
154
+ if (!response.ok)
155
+ return {};
156
+ const profile = await response.json();
157
+ const account = typeof profile.account === 'object' && profile.account !== null
158
+ ? profile.account
159
+ : {};
160
+ const email = profile.emailAddress ?? profile.email ?? account.email_address ?? account.email;
161
+ const subscription = profile.subscriptionType ?? profile.subscription_type ?? account.subscription_type;
162
+ return {
163
+ ...typeof email === 'string' && email.length > 0 ? { emailAddress: email } : {},
164
+ ...typeof subscription === 'string' && subscription.length > 0 ? { subscriptionType: subscription } : {},
165
+ };
166
+ }
167
+ catch {
168
+ // Profile lookup is decorative; only the token exchange owns login success.
169
+ return {};
170
+ }
171
+ }
172
+ /** Build a session from a token response. */
173
+ async function claudeSession(tokens, fallbackRefreshToken, withProfile) {
174
+ if (typeof tokens.access_token !== 'string' || tokens.access_token.length === 0) {
175
+ throw new Error('claude token endpoint returned no access token');
176
+ }
177
+ const refreshToken = tokens.refresh_token ?? fallbackRefreshToken;
178
+ if (refreshToken === undefined)
179
+ throw new Error('claude token endpoint returned no refresh token');
180
+ if (typeof tokens.expires_in !== 'number' || tokens.expires_in <= 0) {
181
+ throw new Error('claude token endpoint returned no usable expiry');
182
+ }
183
+ const profile = withProfile ? await fetchClaudeProfile(tokens.access_token) : {};
184
+ return {
185
+ accessToken: tokens.access_token,
186
+ refreshToken,
187
+ expiresAt: Date.now() + tokens.expires_in * 1000,
188
+ scopes: tokens.scope ?? CLAUDE_SCOPE,
189
+ ...profile,
190
+ };
191
+ }
192
+ /**
193
+ * Exchange an authorization code for a claude session (JSON grant).
194
+ * @param code - the authorization code from the callback.
195
+ * @param verifier - the PKCE verifier minted for the attempt.
196
+ * @param redirectUri - the attempt's redirect URI.
197
+ * @param state - the attempt's state (echoed to the token endpoint).
198
+ * @returns the session to store.
199
+ */
200
+ export async function exchangeClaudeCode(code, verifier, redirectUri, state) {
201
+ const response = await hostFetch(CLAUDE_TOKEN_URL, {
202
+ method: 'POST',
203
+ headers: { 'content-type': 'application/json' },
204
+ body: JSON.stringify({
205
+ grant_type: 'authorization_code',
206
+ code,
207
+ redirect_uri: redirectUri,
208
+ client_id: CLAUDE_CLIENT_ID,
209
+ code_verifier: verifier,
210
+ state,
211
+ }),
212
+ });
213
+ if (!response.ok)
214
+ throw await oauthEndpointError(response, 'claude');
215
+ return claudeSession(await response.json(), undefined, true);
216
+ }
217
+ /**
218
+ * Refresh a claude session (JSON grant echoing the issued scope).
219
+ * @param session - the stored session.
220
+ * @returns the fresh session to store.
221
+ */
222
+ export async function refreshClaude(session) {
223
+ const response = await hostFetch(CLAUDE_TOKEN_URL, {
224
+ method: 'POST',
225
+ headers: { 'content-type': 'application/json' },
226
+ body: JSON.stringify({
227
+ grant_type: 'refresh_token',
228
+ refresh_token: session.refreshToken,
229
+ client_id: CLAUDE_CLIENT_ID,
230
+ scope: session.scopes,
231
+ }),
232
+ });
233
+ if (!response.ok)
234
+ throw await oauthEndpointError(response, 'claude');
235
+ const next = await claudeSession(await response.json(), session.refreshToken, false);
236
+ return {
237
+ ...next,
238
+ ...session.emailAddress === undefined ? {} : { emailAddress: session.emailAddress },
239
+ ...session.subscriptionType === undefined ? {} : { subscriptionType: session.subscriptionType },
240
+ };
241
+ }
242
+ /**
243
+ * Whether a claude refresh failure means the login is permanently gone.
244
+ * @param error - the thrown refresh error.
245
+ * @returns true when re-login is the only fix.
246
+ */
247
+ export function isClaudePermanentRefreshError(error) {
248
+ return error instanceof OAuthEndpointError
249
+ && (error.oauthCode === 'invalid_grant' || error.oauthCode === 'invalid_token');
250
+ }
251
+ export const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
252
+ /** RFC3339 `resets_at` value → epoch ms, or undefined when absent/unparsable. */
253
+ function claudeResetsAt(value) {
254
+ if (typeof value !== 'string' || value.length === 0)
255
+ return undefined;
256
+ const parsed = Date.parse(value);
257
+ return Number.isFinite(parsed) ? parsed : undefined;
258
+ }
259
+ /** Map one legacy `{utilization, resets_at}` bucket; undefined when null or unusable. */
260
+ function claudeLegacyWindow(value, kind, scope) {
261
+ if (typeof value !== 'object' || value === null)
262
+ return undefined;
263
+ const bucket = value;
264
+ if (typeof bucket.utilization !== 'number' || !Number.isFinite(bucket.utilization))
265
+ return undefined;
266
+ const resetsAt = claudeResetsAt(bucket.resets_at);
267
+ return {
268
+ kind,
269
+ ...scope === undefined ? {} : { scope },
270
+ usedPercent: bucket.utilization,
271
+ ...resetsAt === undefined ? {} : { resetsAt },
272
+ };
273
+ }
274
+ /** Map the modern `limits` array; empty when absent or carrying nothing usable. */
275
+ function claudeLimitsWindows(value) {
276
+ if (!Array.isArray(value))
277
+ return [];
278
+ const windows = [];
279
+ for (const raw of value) {
280
+ if (typeof raw !== 'object' || raw === null)
281
+ continue;
282
+ const entry = raw;
283
+ if (typeof entry.percent !== 'number' || !Number.isFinite(entry.percent))
284
+ continue;
285
+ const kind = entry.kind === 'session'
286
+ ? 'session'
287
+ : entry.kind === 'weekly_all' || entry.kind === 'weekly_scoped' ? 'weekly' : 'other';
288
+ const scope = entry.scope?.model?.display_name;
289
+ const resetsAt = claudeResetsAt(entry.resets_at);
290
+ windows.push({
291
+ kind,
292
+ ...typeof scope === 'string' && scope.length > 0 ? { scope } : {},
293
+ usedPercent: entry.percent,
294
+ ...resetsAt === undefined ? {} : { resetsAt },
295
+ });
296
+ }
297
+ return windows;
298
+ }
299
+ /**
300
+ * Fetch the claude subscription usage from the OAuth usage endpoint (the
301
+ * source of Claude Code's `/usage` screen). Newer responses carry a
302
+ * structured `limits` array; older ones the flat `five_hour`/`seven_day*`
303
+ * buckets — both shapes are read, the array winning when it has entries.
304
+ * @param session - the stored session (used as-is; never refreshed here).
305
+ * @param fetchFn - fetch implementation (injectable for tests).
306
+ * @param signal - caller cancellation from the RPC transport.
307
+ * @param cliVersion - resolves the Claude Code version to present; defaults to the local floor.
308
+ * @returns the mapped usage snapshot.
309
+ */
310
+ export async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion = localClaudeCliVersion) {
311
+ const userAgent = claudeCliUserAgent(await cliVersion());
312
+ const response = await fetchFn(CLAUDE_USAGE_URL, {
313
+ headers: {
314
+ 'authorization': `Bearer ${session.accessToken}`,
315
+ 'anthropic-beta': 'oauth-2025-04-20',
316
+ // Unrecognized clients are aggressively rate-limited on this endpoint,
317
+ // so it presents as the CLI like every other subscription request.
318
+ 'user-agent': userAgent,
319
+ 'accept': 'application/json',
320
+ },
321
+ ...signal === undefined ? {} : { signal },
322
+ });
323
+ if (!response.ok)
324
+ throw await oauthEndpointError(response, 'claude usage');
325
+ const payload = await response.json();
326
+ const modern = claudeLimitsWindows(payload.limits);
327
+ if (modern.length > 0)
328
+ return { supported: true, windows: modern };
329
+ const windows = [];
330
+ const legacy = [
331
+ claudeLegacyWindow(payload.five_hour, 'session'),
332
+ claudeLegacyWindow(payload.seven_day, 'weekly'),
333
+ claudeLegacyWindow(payload.seven_day_opus, 'weekly', 'Opus'),
334
+ claudeLegacyWindow(payload.seven_day_sonnet, 'weekly', 'Sonnet'),
335
+ ];
336
+ for (const window of legacy) {
337
+ if (window !== undefined)
338
+ windows.push(window);
339
+ }
340
+ return { supported: true, windows };
341
+ }
342
+ function claudeThinkingType(capabilities) {
343
+ const types = capabilities?.thinking?.types;
344
+ if (types?.enabled?.supported === true)
345
+ return 'enabled';
346
+ if (types?.adaptive?.supported === true)
347
+ return 'adaptive';
348
+ return undefined;
349
+ }
350
+ /** Effort levels in display order; a model exposes only the ones it advertises as supported. */
351
+ const CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
352
+ function claudeReasoning(capabilities) {
353
+ const effort = capabilities?.effort;
354
+ if (effort?.supported !== true)
355
+ return undefined;
356
+ const efforts = CLAUDE_EFFORT_LEVELS
357
+ .filter(level => effort[level]?.supported === true)
358
+ .map(level => ({ id: ReasoningEffortId(level), name: level[0].toUpperCase() + level.slice(1) }));
359
+ return efforts.length > 0 ? { efforts } : undefined;
360
+ }
361
+ /** A token limit the endpoint disclosed, or undefined when absent or malformed. */
362
+ function positiveTokenCount(value) {
363
+ return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined;
364
+ }
365
+ /**
366
+ * Fetch the live model catalog from the subscription endpoint. `signal`
367
+ * cancels the request; `cliVersion` resolves the Claude Code version to
368
+ * present (defaults to the local floor).
369
+ */
370
+ export async function fetchClaudeModels(session, fetchFn = hostFetch, signal, cliVersion = localClaudeCliVersion) {
371
+ const userAgent = claudeCliUserAgent(await cliVersion());
372
+ const response = await fetchFn(CLAUDE_MODELS_URL, {
373
+ headers: {
374
+ 'authorization': `Bearer ${session.accessToken}`,
375
+ 'anthropic-version': '2023-06-01',
376
+ 'user-agent': userAgent,
377
+ 'anthropic-dangerous-direct-browser-access': 'true',
378
+ 'accept': 'application/json',
379
+ },
380
+ ...signal === undefined ? {} : { signal },
381
+ });
382
+ if (!response.ok)
383
+ throw await httpLlmError(response, 'claude models API');
384
+ const payload = await response.json();
385
+ if (!Array.isArray(payload.data)) {
386
+ throw new Error('claude models API returned an invalid catalog');
387
+ }
388
+ const models = payload.data
389
+ .filter((m) => typeof m.id === 'string')
390
+ .map((m) => {
391
+ const thinkingType = claudeThinkingType(m.capabilities);
392
+ const reasoning = claudeReasoning(m.capabilities);
393
+ // The endpoint advertises each model's limits; carrying them through keeps
394
+ // the route current for models newer than the bundled catalog (#101).
395
+ const contextWindow = positiveTokenCount(m.max_input_tokens);
396
+ const maxOutputTokens = positiveTokenCount(m.max_tokens);
397
+ return {
398
+ id: m.id,
399
+ name: m.display_name ?? m.id,
400
+ ...contextWindow === undefined ? {} : { contextWindow },
401
+ ...maxOutputTokens === undefined ? {} : { maxOutputTokens },
402
+ ...thinkingType === undefined ? {} : { thinkingType },
403
+ ...reasoning === undefined ? {} : { reasoning },
404
+ };
405
+ });
406
+ if (models.length === 0) {
407
+ throw new Error('claude models API returned an empty catalog');
408
+ }
409
+ return models;
410
+ }
411
+ /**
412
+ * The output cap for one model: configuration may lower the default, but never
413
+ * exceeds a server-advertised ceiling; the built-in constant is the last resort.
414
+ */
415
+ function claudeMaxTokens(configured, disc) {
416
+ const outputLimit = disc?.maxOutputTokens;
417
+ const preferred = configured?.maxTokens ?? outputLimit ?? CLAUDE_DEFAULT_MAX_TOKENS;
418
+ return outputLimit === undefined ? preferred : Math.min(preferred, outputLimit);
419
+ }
420
+ /**
421
+ * Anthropic rejects any image edge over 2000px once a request carries many
422
+ * images, and every later turn resends the history, so one oversized
423
+ * screenshot would fail the session for good (#110). Images are sent within
424
+ * this cap; 3.75MB encoded stays under the 5MB base64 per-image limit.
425
+ */
426
+ const CLAUDE_IMAGE_LIMIT = { maxEdge: 2000, maxBytes: 3_750_000 };
427
+ /** The Claude 4.5 family accepts image input. */
428
+ const CLAUDE_MODALITIES = ['text', 'image'];
429
+ /**
430
+ * Assemble the Anthropic request body.
431
+ *
432
+ * Extracted from the adapter so the wire shape — cache breakpoints above all —
433
+ * is testable without a network round trip. The message array is marked before
434
+ * it is placed so the breakpoints land on the blocks the body ships: one on the
435
+ * last `system` block (covering `tools` + `system`, which render ahead of it)
436
+ * and up to three across the history, Anthropic's four-slot maximum.
437
+ * @param options - the generate request.
438
+ * @param messages - conversation messages with images already resolved.
439
+ * @param maxTokens - the resolved output cap.
440
+ * @param thinking - the thinking parameter, when the model takes one.
441
+ * @param effort - the reasoning effort, when the model advertises efforts.
442
+ * @returns the JSON body to POST.
443
+ */
444
+ export function claudeRequestBody(options, messages, maxTokens, thinking, effort) {
445
+ const anthropicMessages = toAnthropicMessages(messages);
446
+ markMessageCache(anthropicMessages);
447
+ return {
448
+ model: options.model,
449
+ max_tokens: maxTokens,
450
+ system: toAnthropicSystem(options.system, messages),
451
+ messages: anthropicMessages,
452
+ ...options.tools !== undefined && options.tools.length > 0
453
+ ? { tools: toAnthropicTools(options.tools) }
454
+ : {},
455
+ ...thinking === undefined ? {} : { thinking },
456
+ ...effort === undefined ? {} : { output_config: { effort } },
457
+ stream: true,
458
+ ...options.sessionId !== undefined ? { metadata: { user_id: String(options.sessionId) } } : {},
459
+ };
460
+ }
461
+ /** Claude wire adapter: one instance serves the `claude` provider route. */
462
+ export class ClaudeAdapter extends LlmAdapter {
463
+ options;
464
+ catalog;
465
+ /** In-memory catalogs for non-default accounts (the persisted cache is the default's). */
466
+ accountCatalogs = new Map();
467
+ /** Account whose snapshot currently lives in {@link catalog}; cleared on default change. */
468
+ catalogOwner;
469
+ constructor(options) {
470
+ super();
471
+ this.options = options;
472
+ this.catalog = new ModelCatalogCache(options.catalogStore);
473
+ }
474
+ async fetchCatalog(account, signal) {
475
+ return fetchClaudeModels(await this.options.tokens.session(account), this.options.fetchFn, signal, this.options.resolveCliVersion);
476
+ }
477
+ /** Drop cached catalogs after login/logout so the next list does not reuse a stale plan. */
478
+ clearAccountCatalog(account) {
479
+ if (account === undefined)
480
+ this.accountCatalogs.clear();
481
+ else
482
+ this.accountCatalogs.delete(account);
483
+ if (account === undefined || this.catalogOwner === account || this.catalogOwner === undefined) {
484
+ this.catalogOwner = undefined;
485
+ this.catalog.invalidate();
486
+ }
487
+ }
488
+ /** Persisted cache for the default account; a throwaway cache for any other. */
489
+ async catalogFor(account) {
490
+ const defaultKey = await this.options.tokens.defaultAccount();
491
+ const key = account ?? defaultKey;
492
+ if (key === undefined || key === defaultKey) {
493
+ if (this.catalogOwner !== undefined && this.catalogOwner !== defaultKey) {
494
+ this.catalog.invalidate();
495
+ }
496
+ this.catalogOwner = defaultKey;
497
+ return this.catalog;
498
+ }
499
+ let cache = this.accountCatalogs.get(key);
500
+ if (cache === undefined) {
501
+ cache = new ModelCatalogCache();
502
+ this.accountCatalogs.set(key, cache);
503
+ }
504
+ return cache;
505
+ }
506
+ async discovered(model) {
507
+ if (!this.options.discovery)
508
+ return undefined;
509
+ const accounts = (await this.options.tokens.list()).map(entry => entry.key);
510
+ return discoverAcrossAccounts(accounts, async (account) => {
511
+ const catalog = await this.catalogFor(account);
512
+ const models = await catalog.resolve(() => this.fetchCatalog(account));
513
+ return models?.find(entry => entry.id === model);
514
+ });
515
+ }
516
+ staticModels(provider) {
517
+ return this.options.models.map(model => ({
518
+ provider,
519
+ id: model.id,
520
+ name: model.name ?? model.id,
521
+ inputModalities: model.inputModalities ?? CLAUDE_MODALITIES,
522
+ }));
523
+ }
524
+ providerInfo(provider) {
525
+ return { id: provider, name: 'Claude (Subscription)' };
526
+ }
527
+ providerRetryPolicy(provider) {
528
+ return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `claude: provider "${provider}" retryPolicy`);
529
+ }
530
+ async listModels(provider) {
531
+ const own = await this.listOwnModels(provider);
532
+ const pool = this.options.pool?.();
533
+ if (pool === undefined)
534
+ return own;
535
+ const extra = await pool.modelsForProvider(provider);
536
+ const seen = new Set(own.map(model => model.id));
537
+ // Account pools reuse the catalog row; only configured tiers are extra.
538
+ return [...own, ...extra.filter(model => !seen.has(model.id))];
539
+ }
540
+ /** The provider's own catalog: union of every account, or one account when named. */
541
+ async listOwnModels(provider, account, signal) {
542
+ if (account === undefined) {
543
+ const accounts = (await this.options.tokens.list()).map(entry => entry.key);
544
+ if (accounts.length === 0)
545
+ return [];
546
+ return unionAccountCatalogs(accounts, (key, accountSignal) => this.listOwnModels(provider, key, accountSignal), { timeoutMs: DISCOVERY_TIMEOUT_MS, ...signal === undefined ? {} : { signal } });
547
+ }
548
+ if (!await this.options.tokens.hasSession(account)) {
549
+ return [];
550
+ }
551
+ if (!this.options.discovery)
552
+ return this.staticModels(provider);
553
+ const catalog = await this.catalogFor(account);
554
+ try {
555
+ const models = await discoverOrRetryAuth(force => this.options.tokens.session(account, force), catalog, () => catalog.get(() => this.fetchCatalog(account, signal)));
556
+ return models.map(model => ({
557
+ provider,
558
+ id: model.id,
559
+ name: model.name,
560
+ inputModalities: CLAUDE_MODALITIES,
561
+ }));
562
+ }
563
+ catch (error) {
564
+ if (isDiscoveryAborted(error, signal))
565
+ throw error;
566
+ if (isMissingOrInvalidCredential(error))
567
+ return [];
568
+ this.options.onWarn?.(`claude model discovery failed; using the built-in catalog (${errorChain(error)})`);
569
+ return this.staticModels(provider);
570
+ }
571
+ }
572
+ async resolveModel(provider, model) {
573
+ const pool = this.options.pool?.();
574
+ if (pool !== undefined && await pool.owns(provider, model)) {
575
+ return pool.resolveModel(provider, model);
576
+ }
577
+ return this.resolveOwnModel(provider, model);
578
+ }
579
+ /** Capability resolution of the provider's own models (the pool resolves members here). */
580
+ async resolveOwnModel(provider, model) {
581
+ const disc = await this.discovered(model);
582
+ const configured = this.options.models.find(entry => entry.id === model);
583
+ const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), disc?.reasoning);
584
+ return {
585
+ provider,
586
+ id: model,
587
+ name: disc?.name ?? configured?.name ?? model,
588
+ inputModalities: configured?.inputModalities ?? CLAUDE_MODALITIES,
589
+ context: {
590
+ contextWindow: disc?.contextWindow ?? configured?.contextWindow ?? CLAUDE_CONTEXT_WINDOW,
591
+ },
592
+ defaultMaxTokens: claudeMaxTokens(configured, disc),
593
+ ...(reasoning === undefined ? {} : { reasoning }),
594
+ };
595
+ }
596
+ async *stream(options) {
597
+ const pool = this.options.pool?.();
598
+ if (pool !== undefined && await pool.owns(options.provider, options.model)) {
599
+ yield* pool.stream(options);
600
+ return;
601
+ }
602
+ yield* this.streamCore(options);
603
+ }
604
+ /** Pool seam: stream through one specific account instead of the default. */
605
+ streamAccount(options, account) {
606
+ return this.streamCore(options, account);
607
+ }
608
+ async *streamCore(options, account) {
609
+ const watchdog = idleWatchdog(options.signal, this.options.streamIdleTimeoutMs);
610
+ try {
611
+ let session = await this.options.tokens.session(account);
612
+ let response = await this.request(options, session, watchdog.signal);
613
+ if (response.status === 401) {
614
+ session = await this.options.tokens.session(account, true);
615
+ response = await this.request(options, session, watchdog.signal);
616
+ }
617
+ if (!response.ok) {
618
+ throw await httpLlmError(response, 'claude API', {
619
+ rateLimitReset: claudeRateLimitReset,
620
+ ...this.options.onWarn === undefined ? {} : { onWarn: this.options.onWarn },
621
+ });
622
+ }
623
+ if (response.body === null) {
624
+ throw new LlmError('claude API returned no response body', EMPTY_RESPONSE_CODE);
625
+ }
626
+ yield* streamAnthropic(response.body, () => { watchdog.pulse(); });
627
+ }
628
+ catch (error) {
629
+ throw mapFetchFailure('claude API', error, watchdog, options.signal);
630
+ }
631
+ finally {
632
+ watchdog.stop();
633
+ }
634
+ }
635
+ /**
636
+ * `display: 'summarized'` is set explicitly on both shapes: `adaptive`-type
637
+ * models default to `display: 'omitted'`, which returns thinking blocks with
638
+ * an empty `thinking` field — without this override the "Think" panel would
639
+ * always render empty even though real reasoning (and billed thinking_tokens)
640
+ * ran.
641
+ */
642
+ thinkingParam(thinkingType, maxTokens) {
643
+ if (thinkingType === 'adaptive')
644
+ return { type: 'adaptive', display: 'summarized' };
645
+ if (thinkingType === 'enabled') {
646
+ const budget = Math.min(Math.max(1_024, Math.floor(maxTokens * 0.5)), maxTokens - 100);
647
+ if (budget < 1_024)
648
+ return undefined;
649
+ return { type: 'enabled', budget_tokens: budget, display: 'summarized' };
650
+ }
651
+ return undefined;
652
+ }
653
+ async request(options, session, signal) {
654
+ const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal, CLAUDE_IMAGE_LIMIT);
655
+ const disc = await this.discovered(options.model);
656
+ const maxTokens = options.maxTokens
657
+ ?? claudeMaxTokens(this.options.models.find(entry => entry.id === options.model), disc);
658
+ const thinking = this.thinkingParam(disc?.thinkingType, maxTokens);
659
+ const effort = options.reasoningEffort !== undefined && disc?.reasoning !== undefined
660
+ ? String(options.reasoningEffort)
661
+ : undefined;
662
+ const body = claudeRequestBody(options, messages, maxTokens, thinking, effort);
663
+ const cliVersion = await (this.options.resolveCliVersion ?? localClaudeCliVersion)();
664
+ return hostFetch(CLAUDE_API_URL, {
665
+ method: 'POST',
666
+ headers: {
667
+ 'authorization': `Bearer ${session.accessToken}`,
668
+ 'anthropic-version': '2023-06-01',
669
+ 'anthropic-beta': CLAUDE_BETA_FLAGS,
670
+ 'user-agent': claudeCliUserAgent(cliVersion),
671
+ 'x-app': 'cli',
672
+ 'anthropic-dangerous-direct-browser-access': 'true',
673
+ 'accept': 'text/event-stream',
674
+ 'content-type': 'application/json',
675
+ },
676
+ body: JSON.stringify(body),
677
+ signal,
678
+ });
679
+ }
680
+ }
@@ -0,0 +1,8 @@
1
+ import type { FetchFn } from './common.js';
2
+ import { NpmCliVersionCache } from './npm-cli-version.js';
3
+ /** Public metadata only: never send subscription credentials to this endpoint. */
4
+ export declare const CODEX_VERSION_URL = "https://registry.npmjs.org/@openai%2fcodex/latest";
5
+ /** Lazy, shared-per-plugin lookup of the official CLI's stable version. */
6
+ export declare class CodexClientVersionCache extends NpmCliVersionCache {
7
+ constructor(fetchFn?: FetchFn, now?: () => number, timeoutMs?: number);
8
+ }