opencode-anthropic-multi-account 0.2.4 → 0.2.6

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.
@@ -1,6 +1,9 @@
1
1
  /** Anthropic OAuth adapter config */
2
2
  export declare const ANTHROPIC_OAUTH_ADAPTER: import("opencode-multi-account-core").OAuthAdapter;
3
3
  export declare const ANTHROPIC_CLIENT_ID: string;
4
+ export declare const ANTHROPIC_AUTHORIZE_ENDPOINT: string;
5
+ export declare const ANTHROPIC_REDIRECT_URI: string;
6
+ export declare const ANTHROPIC_SCOPES: string;
4
7
  /** Token exchange / refresh endpoint */
5
8
  export declare const ANTHROPIC_TOKEN_ENDPOINT: string;
6
9
  /** OAuth usage stats endpoint */
package/dist/index.js CHANGED
@@ -1820,6 +1820,33 @@ async function confirm(message, defaultYes = false) {
1820
1820
  }
1821
1821
 
1822
1822
  // ../multi-account-core/src/adapters/anthropic.ts
1823
+ var ANTHROPIC_DEFAULT_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
1824
+ var ANTHROPIC_DEFAULT_CLI_VERSION = "2.1.80";
1825
+ var ANTHROPIC_DEFAULT_USER_AGENT = "claude-cli/2.1.2 (external, cli)";
1826
+ var ANTHROPIC_DEFAULT_AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
1827
+ var ANTHROPIC_DEFAULT_TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
1828
+ var ANTHROPIC_DEFAULT_REDIRECT_URI = "https://console.anthropic.com/oauth/code/callback";
1829
+ var ANTHROPIC_DEFAULT_SCOPES = "org:create_api_key user:profile user:inference";
1830
+ var ANTHROPIC_DEFAULT_BETA_FLAGS = "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,prompt-caching-scope-2026-01-05";
1831
+ function buildCliUserAgent(cliVersion) {
1832
+ return `claude-cli/${cliVersion} (external, cli)`;
1833
+ }
1834
+ function resolveAnthropicOAuthEnv(env = process.env) {
1835
+ const cliVersion = env.ANTHROPIC_CLI_VERSION || ANTHROPIC_DEFAULT_CLI_VERSION;
1836
+ const composedUserAgent = buildCliUserAgent(cliVersion);
1837
+ const userAgent = env.ANTHROPIC_USER_AGENT || (env.ANTHROPIC_CLI_VERSION ? composedUserAgent : "") || ANTHROPIC_DEFAULT_USER_AGENT;
1838
+ return {
1839
+ clientId: env.ANTHROPIC_CLIENT_ID || ANTHROPIC_DEFAULT_CLIENT_ID,
1840
+ cliVersion,
1841
+ userAgent,
1842
+ authorizeUrl: env.ANTHROPIC_AUTHORIZE_URL || ANTHROPIC_DEFAULT_AUTHORIZE_URL,
1843
+ tokenUrl: env.ANTHROPIC_TOKEN_URL || ANTHROPIC_DEFAULT_TOKEN_URL,
1844
+ redirectUri: env.ANTHROPIC_REDIRECT_URI || ANTHROPIC_DEFAULT_REDIRECT_URI,
1845
+ scopes: env.ANTHROPIC_SCOPES || ANTHROPIC_DEFAULT_SCOPES,
1846
+ betaFlags: env.ANTHROPIC_BETA_FLAGS || ANTHROPIC_DEFAULT_BETA_FLAGS
1847
+ };
1848
+ }
1849
+ var anthropicEnv = resolveAnthropicOAuthEnv();
1823
1850
  var anthropicOAuthAdapter = {
1824
1851
  id: "anthropic",
1825
1852
  authProviderId: "anthropic",
@@ -1827,14 +1854,14 @@ var anthropicOAuthAdapter = {
1827
1854
  statusToolName: "claude_multiauth_status",
1828
1855
  authMethodLabel: "Claude Pro/Max (Multi-Auth)",
1829
1856
  serviceLogName: "claude-multiauth",
1830
- oauthClientId: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
1831
- tokenEndpoint: "https://console.anthropic.com/v1/oauth/token",
1857
+ oauthClientId: anthropicEnv.clientId,
1858
+ tokenEndpoint: anthropicEnv.tokenUrl,
1832
1859
  usageEndpoint: "https://api.anthropic.com/api/oauth/usage",
1833
1860
  profileEndpoint: "https://api.anthropic.com/api/oauth/profile",
1834
1861
  oauthBetaHeader: "oauth-2025-04-20",
1835
- requestBetaHeader: "oauth-2025-04-20,interleaved-thinking-2025-05-14",
1836
- cliUserAgent: "claude-cli/2.1.2 (external, cli)",
1837
- cliVersion: "2.1.80",
1862
+ requestBetaHeader: anthropicEnv.betaFlags,
1863
+ cliUserAgent: anthropicEnv.userAgent,
1864
+ cliVersion: anthropicEnv.cliVersion,
1838
1865
  billingSalt: "59cf53e54c78",
1839
1866
  toolPrefix: "mcp_",
1840
1867
  accountStorageFilename: "anthropic-multi-account-accounts.json",
@@ -2183,8 +2210,12 @@ var CascadeStateManager = class {
2183
2210
  };
2184
2211
 
2185
2212
  // src/constants.ts
2213
+ var resolvedAnthropicOAuthEnv = resolveAnthropicOAuthEnv();
2186
2214
  var ANTHROPIC_OAUTH_ADAPTER = anthropicOAuthAdapter;
2187
2215
  var ANTHROPIC_CLIENT_ID = ANTHROPIC_OAUTH_ADAPTER.oauthClientId;
2216
+ var ANTHROPIC_AUTHORIZE_ENDPOINT = resolvedAnthropicOAuthEnv.authorizeUrl;
2217
+ var ANTHROPIC_REDIRECT_URI = resolvedAnthropicOAuthEnv.redirectUri;
2218
+ var ANTHROPIC_SCOPES = resolvedAnthropicOAuthEnv.scopes;
2188
2219
  var ANTHROPIC_TOKEN_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.tokenEndpoint;
2189
2220
  var ANTHROPIC_USAGE_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.usageEndpoint;
2190
2221
  var ANTHROPIC_PROFILE_ENDPOINT = ANTHROPIC_OAUTH_ADAPTER.profileEndpoint;
@@ -2194,9 +2225,100 @@ var TOOL_PREFIX = ANTHROPIC_OAUTH_ADAPTER.toolPrefix;
2194
2225
  var ACCOUNTS_FILENAME2 = ANTHROPIC_OAUTH_ADAPTER.accountStorageFilename;
2195
2226
  var PLAN_LABELS = ANTHROPIC_OAUTH_ADAPTER.planLabels;
2196
2227
  var TOKEN_EXPIRY_BUFFER_MS = 6e4;
2228
+ var TOKEN_REFRESH_TIMEOUT_MS = 3e4;
2197
2229
 
2198
2230
  // src/pi-ai-adapter.ts
2199
- import { loginAnthropic, refreshAnthropicToken } from "@mariozechner/pi-ai/oauth";
2231
+ import { AsyncLocalStorage } from "node:async_hooks";
2232
+ import * as piAiOauth from "@mariozechner/pi-ai/oauth";
2233
+
2234
+ // src/token-node-request.ts
2235
+ import * as childProcess from "node:child_process";
2236
+ function buildNodeTokenRequestScript() {
2237
+ return `
2238
+ const https = require("node:https");
2239
+ const endpoint = process.env.ANTHROPIC_REFRESH_ENDPOINT;
2240
+ const timeoutMs = Number(process.env.ANTHROPIC_REFRESH_TIMEOUT_MS || "30000");
2241
+ const payload = process.env.ANTHROPIC_REFRESH_REQUEST_BODY || "";
2242
+
2243
+ function printSuccess(body) {
2244
+ console.log(JSON.stringify({ ok: true, body }));
2245
+ }
2246
+
2247
+ function printFailure(error) {
2248
+ console.log(JSON.stringify({ ok: false, ...error }));
2249
+ }
2250
+
2251
+ const request = https.request(endpoint, {
2252
+ method: "POST",
2253
+ headers: {
2254
+ "Content-Type": "application/json",
2255
+ Accept: "application/json",
2256
+ "Content-Length": Buffer.byteLength(payload).toString(),
2257
+ },
2258
+ }, (response) => {
2259
+ let body = "";
2260
+ response.setEncoding("utf8");
2261
+ response.on("data", (chunk) => {
2262
+ body += chunk;
2263
+ });
2264
+ response.on("end", () => {
2265
+ const status = response.statusCode ?? 0;
2266
+ if (status < 200 || status >= 300) {
2267
+ printFailure({ status, body });
2268
+ return;
2269
+ }
2270
+
2271
+ printSuccess(body);
2272
+ });
2273
+ });
2274
+
2275
+ request.setTimeout(timeoutMs, () => {
2276
+ request.destroy(new Error("Request timed out after " + timeoutMs + "ms"));
2277
+ });
2278
+
2279
+ request.on("error", (error) => {
2280
+ printFailure({ error: error instanceof Error ? error.name + ": " + error.message : String(error) });
2281
+ });
2282
+
2283
+ request.write(payload);
2284
+ request.end();
2285
+ `;
2286
+ }
2287
+ async function defaultRunNodeTokenRequest(options) {
2288
+ const script = buildNodeTokenRequestScript();
2289
+ return await new Promise((resolve, reject) => {
2290
+ childProcess.execFile(
2291
+ options.executable,
2292
+ ["-e", script],
2293
+ {
2294
+ timeout: options.timeoutMs + 1e3,
2295
+ maxBuffer: 1024 * 1024,
2296
+ env: {
2297
+ ...process.env,
2298
+ ANTHROPIC_REFRESH_ENDPOINT: options.endpoint,
2299
+ ANTHROPIC_REFRESH_REQUEST_BODY: options.body,
2300
+ ANTHROPIC_REFRESH_TIMEOUT_MS: String(options.timeoutMs)
2301
+ }
2302
+ },
2303
+ (error, stdout, stderr) => {
2304
+ const trimmedStdout = stdout.trim();
2305
+ if (error) {
2306
+ reject(new Error(stderr.trim() || error.message));
2307
+ return;
2308
+ }
2309
+ if (!trimmedStdout) {
2310
+ reject(new Error("Empty response from Node refresh helper"));
2311
+ return;
2312
+ }
2313
+ resolve(trimmedStdout);
2314
+ }
2315
+ );
2316
+ });
2317
+ }
2318
+ var nodeTokenRequestRunner = defaultRunNodeTokenRequest;
2319
+ async function runNodeTokenRequest(options) {
2320
+ return await nodeTokenRequestRunner(options);
2321
+ }
2200
2322
 
2201
2323
  // src/config.ts
2202
2324
  initCoreConfig("claude-multiauth.json");
@@ -2378,19 +2500,204 @@ function fromPiAiCredentials(creds) {
2378
2500
  expiresAt: creds.expires
2379
2501
  };
2380
2502
  }
2503
+ var ANTHROPIC_REFRESH_ENDPOINT = ANTHROPIC_TOKEN_ENDPOINT;
2504
+ var LEGACY_ANTHROPIC_TOKEN_ENDPOINT = "https://platform.claude.com/v1/oauth/token";
2505
+ var REFRESH_NODE_EXECUTABLE = process.env.OPENCODE_REFRESH_NODE_EXECUTABLE || "node";
2506
+ var tokenProxyContext = new AsyncLocalStorage();
2507
+ var tokenProxyInstalled = false;
2508
+ var tokenProxyOriginalFetch = null;
2509
+ var refreshEndpointUrl = new URL(ANTHROPIC_REFRESH_ENDPOINT);
2510
+ var legacyRefreshEndpointUrl = new URL(LEGACY_ANTHROPIC_TOKEN_ENDPOINT);
2511
+ function buildRefreshRequestError(details) {
2512
+ return new Error(`Anthropic token refresh request failed. url=${ANTHROPIC_REFRESH_ENDPOINT}; details=${details}`);
2513
+ }
2514
+ function buildRefreshInvalidJsonError(body, details) {
2515
+ return new Error(`Anthropic token refresh returned invalid JSON. url=${ANTHROPIC_REFRESH_ENDPOINT}; body=${body}; details=${details}`);
2516
+ }
2517
+ function getRequestUrlString(input) {
2518
+ if (typeof input === "string") return input;
2519
+ if (input instanceof URL) return input.toString();
2520
+ return input.url;
2521
+ }
2522
+ function isAnthropicTokenEndpoint(input) {
2523
+ const rawUrl = getRequestUrlString(input);
2524
+ try {
2525
+ const url = new URL(rawUrl);
2526
+ const isConfiguredEndpoint = url.origin === refreshEndpointUrl.origin && url.pathname === refreshEndpointUrl.pathname;
2527
+ const isLegacyEndpoint = url.origin === legacyRefreshEndpointUrl.origin && url.pathname === legacyRefreshEndpointUrl.pathname;
2528
+ return isConfiguredEndpoint || isLegacyEndpoint;
2529
+ } catch {
2530
+ return rawUrl === ANTHROPIC_REFRESH_ENDPOINT || rawUrl === LEGACY_ANTHROPIC_TOKEN_ENDPOINT;
2531
+ }
2532
+ }
2533
+ function applyOverridesToTokenParams(params) {
2534
+ params.set("client_id", ANTHROPIC_CLIENT_ID);
2535
+ if (params.get("grant_type") === "authorization_code") {
2536
+ params.set("redirect_uri", ANTHROPIC_REDIRECT_URI);
2537
+ params.set("scope", ANTHROPIC_SCOPES);
2538
+ }
2539
+ }
2540
+ function isRecord(value) {
2541
+ return typeof value === "object" && value !== null;
2542
+ }
2543
+ function applyAnthropicTokenRequestOverrides(rawBody) {
2544
+ const trimmed = rawBody.trim();
2545
+ if (trimmed.length === 0) {
2546
+ return rawBody;
2547
+ }
2548
+ if (trimmed.startsWith("{")) {
2549
+ try {
2550
+ const parsed = JSON.parse(trimmed);
2551
+ if (!isRecord(parsed)) {
2552
+ return rawBody;
2553
+ }
2554
+ const grantType = typeof parsed.grant_type === "string" ? parsed.grant_type : "";
2555
+ parsed.client_id = ANTHROPIC_CLIENT_ID;
2556
+ if (grantType === "authorization_code") {
2557
+ parsed.redirect_uri = ANTHROPIC_REDIRECT_URI;
2558
+ parsed.scope = ANTHROPIC_SCOPES;
2559
+ }
2560
+ return JSON.stringify(parsed);
2561
+ } catch {
2562
+ return rawBody;
2563
+ }
2564
+ }
2565
+ const params = new URLSearchParams(rawBody);
2566
+ applyOverridesToTokenParams(params);
2567
+ return params.toString();
2568
+ }
2569
+ function rewriteAnthropicAuthUrl(rawUrl) {
2570
+ try {
2571
+ const configuredAuthorizeUrl = new URL(ANTHROPIC_AUTHORIZE_ENDPOINT);
2572
+ const rewritten = new URL(rawUrl);
2573
+ rewritten.protocol = configuredAuthorizeUrl.protocol;
2574
+ rewritten.host = configuredAuthorizeUrl.host;
2575
+ rewritten.pathname = configuredAuthorizeUrl.pathname;
2576
+ for (const [key, value] of configuredAuthorizeUrl.searchParams.entries()) {
2577
+ rewritten.searchParams.set(key, value);
2578
+ }
2579
+ rewritten.searchParams.set("client_id", ANTHROPIC_CLIENT_ID);
2580
+ rewritten.searchParams.set("redirect_uri", ANTHROPIC_REDIRECT_URI);
2581
+ rewritten.searchParams.set("scope", ANTHROPIC_SCOPES);
2582
+ return rewritten.toString();
2583
+ } catch {
2584
+ return rawUrl;
2585
+ }
2586
+ }
2587
+ function getRequestBodySource(input, init) {
2588
+ if (init?.body !== void 0) {
2589
+ return init.body;
2590
+ }
2591
+ if (input instanceof Request) {
2592
+ return input.body;
2593
+ }
2594
+ return void 0;
2595
+ }
2596
+ function stringifyBinaryBody(body) {
2597
+ if (body instanceof ArrayBuffer) {
2598
+ return Buffer.from(body).toString("utf8");
2599
+ }
2600
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength).toString("utf8");
2601
+ }
2602
+ async function getRequestBody(input, init) {
2603
+ const body = getRequestBodySource(input, init);
2604
+ if (typeof body === "string") return body;
2605
+ if (body instanceof URLSearchParams) return body.toString();
2606
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
2607
+ return stringifyBinaryBody(body);
2608
+ }
2609
+ if (typeof Blob !== "undefined" && body instanceof Blob) return await body.text();
2610
+ if (body instanceof ReadableStream) return await new Response(body).text();
2611
+ if (input instanceof Request && init?.body === void 0) return await input.clone().text();
2612
+ if (body == null) return "";
2613
+ throw buildRefreshRequestError(`Unsupported token request body type: ${Object.prototype.toString.call(body)}`);
2614
+ }
2615
+ function getRequestMethod(input, init) {
2616
+ return init?.method ?? (input instanceof Request ? input.method : "GET");
2617
+ }
2618
+ function shouldProxyTokenRequest(input) {
2619
+ return tokenProxyContext.getStore() === true && isAnthropicTokenEndpoint(input);
2620
+ }
2621
+ async function postAnthropicTokenViaNode(body) {
2622
+ const overriddenBody = applyAnthropicTokenRequestOverrides(body);
2623
+ let output;
2624
+ try {
2625
+ output = await runNodeTokenRequest({
2626
+ body: overriddenBody,
2627
+ endpoint: ANTHROPIC_REFRESH_ENDPOINT,
2628
+ executable: REFRESH_NODE_EXECUTABLE,
2629
+ timeoutMs: TOKEN_REFRESH_TIMEOUT_MS
2630
+ });
2631
+ } catch (error) {
2632
+ const details = error instanceof Error ? error.message : String(error);
2633
+ throw buildRefreshRequestError(details);
2634
+ }
2635
+ let parsed;
2636
+ try {
2637
+ parsed = JSON.parse(output);
2638
+ } catch (error) {
2639
+ const details = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
2640
+ throw buildRefreshInvalidJsonError(output, details);
2641
+ }
2642
+ const result = parsed;
2643
+ if (!result.ok) {
2644
+ if (result.error) {
2645
+ throw buildRefreshRequestError(result.error);
2646
+ }
2647
+ throw buildRefreshRequestError(`Error: HTTP request failed. status=${result.status ?? 0}; url=${ANTHROPIC_REFRESH_ENDPOINT}; body=${result.body ?? ""}`);
2648
+ }
2649
+ return new Response(result.body ?? "", {
2650
+ status: 200,
2651
+ headers: {
2652
+ "content-type": "application/json"
2653
+ }
2654
+ });
2655
+ }
2656
+ function createAnthropicTokenProxyFetch(originalFetch) {
2657
+ return (async (input, init) => {
2658
+ if (!shouldProxyTokenRequest(input)) {
2659
+ return originalFetch(input, init);
2660
+ }
2661
+ const method = getRequestMethod(input, init).toUpperCase();
2662
+ if (method !== "POST") {
2663
+ throw buildRefreshRequestError(`Unsupported token endpoint method: ${method}`);
2664
+ }
2665
+ return await postAnthropicTokenViaNode(await getRequestBody(input, init));
2666
+ });
2667
+ }
2668
+ function ensureAnthropicTokenProxyFetchInstalled() {
2669
+ if (tokenProxyInstalled) return;
2670
+ tokenProxyOriginalFetch = globalThis.fetch;
2671
+ globalThis.fetch = createAnthropicTokenProxyFetch(tokenProxyOriginalFetch);
2672
+ tokenProxyInstalled = true;
2673
+ }
2674
+ async function withAnthropicTokenProxyFetch(operation) {
2675
+ ensureAnthropicTokenProxyFetchInstalled();
2676
+ return await tokenProxyContext.run(true, operation);
2677
+ }
2678
+ async function fetchProfileWithSingleRetry(accessToken) {
2679
+ let profileResult = await fetchProfile(accessToken);
2680
+ if (profileResult.ok) {
2681
+ return profileResult;
2682
+ }
2683
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
2684
+ profileResult = await fetchProfile(accessToken);
2685
+ return profileResult;
2686
+ }
2381
2687
  async function loginWithPiAi(callbacks) {
2382
- const piCreds = await loginAnthropic({
2383
- onAuth: callbacks.onAuth,
2688
+ const piCreds = await withAnthropicTokenProxyFetch(() => piAiOauth.loginAnthropic({
2689
+ onAuth: (info) => {
2690
+ callbacks.onAuth({
2691
+ ...info,
2692
+ url: info.url ? rewriteAnthropicAuthUrl(info.url) : info.url
2693
+ });
2694
+ },
2384
2695
  onPrompt: callbacks.onPrompt,
2385
2696
  onProgress: callbacks.onProgress,
2386
2697
  onManualCodeInput: callbacks.onManualCodeInput
2387
- });
2698
+ }));
2388
2699
  const base = fromPiAiCredentials(piCreds);
2389
- let profileResult = await fetchProfile(piCreds.access);
2390
- if (!profileResult.ok) {
2391
- await new Promise((r) => setTimeout(r, 1e3));
2392
- profileResult = await fetchProfile(piCreds.access);
2393
- }
2700
+ const profileResult = await fetchProfileWithSingleRetry(piCreds.access);
2394
2701
  const profileData = profileResult.ok ? profileResult.data : void 0;
2395
2702
  return {
2396
2703
  ...base,
@@ -2401,7 +2708,7 @@ async function loginWithPiAi(callbacks) {
2401
2708
  };
2402
2709
  }
2403
2710
  async function refreshWithPiAi(currentRefreshToken) {
2404
- const piCreds = await refreshAnthropicToken(currentRefreshToken);
2711
+ const piCreds = await withAnthropicTokenProxyFetch(() => piAiOauth.refreshAnthropicToken(currentRefreshToken));
2405
2712
  return {
2406
2713
  accessToken: piCreds.access,
2407
2714
  refreshToken: piCreds.refresh,
@@ -2412,6 +2719,13 @@ var PI_AI_ADAPTER_SERVICE = ANTHROPIC_OAUTH_ADAPTER.serviceLogName;
2412
2719
 
2413
2720
  // src/token.ts
2414
2721
  var PERMANENT_FAILURE_HTTP_STATUSES = /* @__PURE__ */ new Set([400, 401, 403]);
2722
+ var PERMANENT_FAILURE_MESSAGE_PATTERNS = [
2723
+ /\binvalid_grant\b/i,
2724
+ /\binvalid_scope\b/i,
2725
+ /\bunauthorized_client\b/i,
2726
+ /\brefresh token\b.*\b(invalid|expired|revoked|no longer valid)\b/i,
2727
+ /\bauth(?:entication)?(?:[_\s-]+)?invalid\b/i
2728
+ ];
2415
2729
  var refreshMutexByAccountId = /* @__PURE__ */ new Map();
2416
2730
  function isTokenExpired(account) {
2417
2731
  if (!account.accessToken || !account.expiresAt) return true;
@@ -2428,7 +2742,9 @@ async function refreshToken(currentRefreshToken, accountId, client) {
2428
2742
  } catch (error) {
2429
2743
  const message = error instanceof Error ? error.message : String(error);
2430
2744
  const statusMatch = message.match(/\b(400|401|403)\b/);
2431
- const isPermanent = statusMatch !== null && PERMANENT_FAILURE_HTTP_STATUSES.has(Number(statusMatch[1]));
2745
+ const hasPermanentStatus = statusMatch !== null && PERMANENT_FAILURE_HTTP_STATUSES.has(Number(statusMatch[1]));
2746
+ const hasPermanentMessage = PERMANENT_FAILURE_MESSAGE_PATTERNS.some((pattern) => pattern.test(message));
2747
+ const isPermanent = hasPermanentStatus || hasPermanentMessage;
2432
2748
  await client.app.log({
2433
2749
  body: {
2434
2750
  service: ANTHROPIC_OAUTH_ADAPTER.serviceLogName,
@@ -2755,7 +3071,7 @@ function printUsageEntry(name, entry, isLast) {
2755
3071
  return;
2756
3072
  }
2757
3073
  const bar = createProgressBar(entry.utilization);
2758
- const reset = entry.utilization >= 100 && entry.resets_at ? formatResetTime(entry.resets_at) : "";
3074
+ const reset = entry.resets_at ? formatResetTime(entry.resets_at) : "";
2759
3075
  console.log(` ${connector} ${name.padEnd(16)} ${bar}${reset}`);
2760
3076
  }
2761
3077
  function printQuotaReport(account, usage) {
@@ -3039,7 +3355,14 @@ async function checkAccountQuota(manager, account, client) {
3039
3355
  }
3040
3356
  const refreshResult = await manager.ensureValidToken(account.uuid, client);
3041
3357
  if (!refreshResult.ok) {
3042
- printQuotaError(account, account.isAuthDisabled ? `${account.authDisabledReason ?? "Auth disabled"} (refresh failed)` : "Failed to refresh token");
3358
+ await manager.markAuthFailure(account.uuid, refreshResult);
3359
+ await manager.refresh();
3360
+ const updatedAccount = manager.getAccounts().find((candidate) => candidate.uuid === account.uuid);
3361
+ if (!updatedAccount) {
3362
+ printQuotaError(account, refreshResult.permanent ? "Refresh failed permanently; account removed" : "Failed to refresh token");
3363
+ return;
3364
+ }
3365
+ printQuotaError(updatedAccount, updatedAccount.isAuthDisabled ? `${updatedAccount.authDisabledReason ?? "Auth disabled"} (refresh failed)` : "Failed to refresh token");
3043
3366
  return;
3044
3367
  }
3045
3368
  await manager.refresh();
@@ -3307,11 +3630,30 @@ function buildBillingHeader(firstUserMessage) {
3307
3630
  if (!version || !salt) return "";
3308
3631
  const sampled = sampleCodeUnits(firstUserMessage, [4, 7, 20]);
3309
3632
  const hash = createHash("sha256").update(`${salt}${sampled}${version}`).digest("hex").slice(0, 3);
3310
- return `x-anthropic-billing-header: cc_version=${version}.${hash}; cc_entrypoint=cli; cch=00000;`;
3633
+ return `cc_version=${version}.${hash}; cc_entrypoint=cli; cch=00000;`;
3311
3634
  }
3312
3635
  var OPENCODE_CAMEL_RE = /OpenCode/g;
3313
3636
  var OPENCODE_LOWER_RE = /(?<!\/)opencode/gi;
3314
3637
  var TOOL_PREFIX_RESPONSE_RE = /"name"\s*:\s*"mcp_([^"]+)"/g;
3638
+ function extractFirstUserTextFromBody(body) {
3639
+ if (!body) return "";
3640
+ try {
3641
+ const parsed = JSON.parse(body);
3642
+ if (!Array.isArray(parsed.messages)) return "";
3643
+ for (const message of parsed.messages) {
3644
+ if (message.role !== "user") continue;
3645
+ if (typeof message.content === "string") return message.content;
3646
+ if (!Array.isArray(message.content)) continue;
3647
+ for (const block of message.content) {
3648
+ if (block.type === "text" && typeof block.text === "string") {
3649
+ return block.text;
3650
+ }
3651
+ }
3652
+ }
3653
+ } catch {
3654
+ }
3655
+ return "";
3656
+ }
3315
3657
  function addToolPrefix(name) {
3316
3658
  if (!ANTHROPIC_OAUTH_ADAPTER.transform.addToolPrefix) {
3317
3659
  return name;
@@ -3337,7 +3679,7 @@ function processCompleteLines(buffer) {
3337
3679
  `;
3338
3680
  return { output, remaining };
3339
3681
  }
3340
- function buildRequestHeaders(input, init, accessToken) {
3682
+ function buildRequestHeaders(input, init, accessToken, bodyString) {
3341
3683
  const headers = new Headers();
3342
3684
  if (input instanceof Request) {
3343
3685
  input.headers.forEach((value, key) => {
@@ -3369,6 +3711,13 @@ function buildRequestHeaders(input, init, accessToken) {
3369
3711
  headers.set("user-agent", CLAUDE_CLI_USER_AGENT);
3370
3712
  headers.set("anthropic-dangerous-direct-browser-access", "true");
3371
3713
  headers.set("x-app", "cli");
3714
+ const resolvedBody = bodyString ?? (typeof init?.body === "string" ? init.body : void 0);
3715
+ const billingHeader = buildBillingHeader(
3716
+ extractFirstUserTextFromBody(resolvedBody)
3717
+ );
3718
+ if (billingHeader) {
3719
+ headers.set("x-anthropic-billing-header", billingHeader);
3720
+ }
3372
3721
  headers.delete("x-api-key");
3373
3722
  return headers;
3374
3723
  }
@@ -3547,8 +3896,9 @@ var AccountRuntimeFactory = class {
3547
3896
  }
3548
3897
  async executeTransformedFetch(input, init, accessToken) {
3549
3898
  const transformedInput = transformRequestUrl(input);
3550
- const headers = buildRequestHeaders(transformedInput, init, accessToken);
3551
- const transformedBody = typeof init?.body === "string" ? transformRequestBody(init.body) : init?.body;
3899
+ const rawBody = typeof init?.body === "string" ? init.body : void 0;
3900
+ const headers = buildRequestHeaders(transformedInput, init, accessToken, rawBody);
3901
+ const transformedBody = rawBody !== void 0 ? transformRequestBody(rawBody) : init?.body;
3552
3902
  const response = await fetch(transformedInput, {
3553
3903
  ...init,
3554
3904
  headers,
@@ -3579,24 +3929,6 @@ var AccountRuntimeFactory = class {
3579
3929
  };
3580
3930
 
3581
3931
  // src/index.ts
3582
- function extractFirstUserText(input) {
3583
- try {
3584
- const raw = input;
3585
- const messages = raw.messages ?? raw.request?.messages;
3586
- if (!Array.isArray(messages)) return "";
3587
- for (const msg of messages) {
3588
- if (msg.role !== "user") continue;
3589
- if (typeof msg.content === "string") return msg.content;
3590
- if (Array.isArray(msg.content)) {
3591
- for (const block of msg.content) {
3592
- if (block.type === "text" && block.text) return block.text;
3593
- }
3594
- }
3595
- }
3596
- } catch {
3597
- }
3598
- return "";
3599
- }
3600
3932
  function injectSystemPrompt(output) {
3601
3933
  const systemPrompt = getSystemPrompt();
3602
3934
  if (!Array.isArray(output.system)) {
@@ -3618,12 +3950,8 @@ var ClaudeMultiAuthPlugin = async (ctx) => {
3618
3950
  let cascadeStateManager = null;
3619
3951
  let poolChainConfig = { pools: [], chains: [] };
3620
3952
  return {
3621
- "experimental.chat.system.transform": (input, output) => {
3953
+ "experimental.chat.system.transform": (_input, output) => {
3622
3954
  injectSystemPrompt(output);
3623
- const billingHeader = buildBillingHeader(extractFirstUserText(input));
3624
- if (billingHeader && !output.system?.includes(billingHeader)) {
3625
- output.system?.unshift(billingHeader);
3626
- }
3627
3955
  },
3628
3956
  tool: {
3629
3957
  [ANTHROPIC_OAUTH_ADAPTER.statusToolName]: tool({
@@ -11,6 +11,10 @@ export interface LoginWithPiAiCallbacks {
11
11
  onProgress?: (message: string) => void;
12
12
  onManualCodeInput?: () => Promise<string>;
13
13
  }
14
+ export declare function applyAnthropicTokenRequestOverrides(rawBody: string): string;
15
+ export declare function rewriteAnthropicAuthUrl(rawUrl: string): string;
16
+ export declare function withAnthropicTokenProxyFetch<T>(operation: () => Promise<T>): Promise<T>;
17
+ export declare function resetAnthropicTokenProxyStateForTest(): void;
14
18
  export declare function loginWithPiAi(callbacks: LoginWithPiAiCallbacks): Promise<Partial<StoredAccount>>;
15
19
  export declare function refreshWithPiAi(currentRefreshToken: string): Promise<CredentialRefreshPatch>;
16
20
  export declare const PI_AI_ADAPTER_SERVICE: string;
@@ -1,6 +1,6 @@
1
1
  export declare function getSystemPrompt(): string;
2
2
  export declare function buildBillingHeader(firstUserMessage: string): string;
3
- export declare function buildRequestHeaders(input: RequestInfo | URL, init: RequestInit | undefined, accessToken: string): Headers;
3
+ export declare function buildRequestHeaders(input: RequestInfo | URL, init: RequestInit | undefined, accessToken: string, bodyString?: string): Headers;
4
4
  export declare function transformRequestBody(body: string | undefined): string | undefined;
5
5
  export declare function transformRequestUrl(input: RequestInfo | URL): RequestInfo | URL;
6
6
  export declare function createResponseStreamTransform(response: Response): Response;
@@ -0,0 +1,10 @@
1
+ export interface NodeTokenRequestOptions {
2
+ body: string;
3
+ endpoint: string;
4
+ executable: string;
5
+ timeoutMs: number;
6
+ }
7
+ type NodeTokenRequestRunner = (options: NodeTokenRequestOptions) => Promise<string>;
8
+ export declare function runNodeTokenRequest(options: NodeTokenRequestOptions): Promise<string>;
9
+ export declare function setNodeTokenRequestRunnerForTest(runner: NodeTokenRequestRunner | null): void;
10
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-multi-account",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "OpenCode plugin for Anthropic multi-account management with automatic rate limit switching",
5
5
  "main": "./dist/index.js",
6
6
  "type": "module",
@@ -39,7 +39,7 @@
39
39
  "directory": "packages/anthropic-multi-account"
40
40
  },
41
41
  "dependencies": {
42
- "opencode-multi-account-core": "^0.2.4",
42
+ "opencode-multi-account-core": "^0.2.6",
43
43
  "@mariozechner/pi-ai": "^0.61.0",
44
44
  "valibot": "^1.2.0"
45
45
  },