opencode-anthropic-fix 0.0.49 → 0.1.1

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.
package/README.md CHANGED
@@ -44,7 +44,7 @@ The [original plugin](https://github.com/anomalyco/opencode-anthropic-auth) prov
44
44
  - **Configurable strategies** — sticky, round-robin, or hybrid account selection
45
45
  - **Claude Code signature emulation** — full HTTP header, system prompt, beta flag, and metadata mimicry derived from Claude Code's open source code. System prompt cache scoping follows the real CC's three-path `splitSysPromptPrefix()` architecture (boundary/global/org) with exact marker detection and scope-aware `cache_control` generation
46
46
  - **OAuth endpoint fingerprint parity** — matches the real CLI's bundled axios 1.13.6 HTTP client signature (`Accept`, `User-Agent`, `Content-Type`) on all OAuth token endpoint calls, required since 2026-03-21 server-side enforcement
47
- - **Billing header fingerprint parity** — `cc_version` suffix uses the real CLI's 3-char fingerprint hash (SHA-256 of salt + first user message chars + version), `cch` matches the Bun native client attestation placeholder, and `X-Claude-Code-Session-Id` header is sent on all requests
47
+ - **Billing header fingerprint parity** — `cc_version` suffix uses the real CLI's 3-char fingerprint hash (SHA-256 of salt + first user message chars + version), `cch=00000` is a static placeholder (xxHash64 attestation was removed in Claude Code v2.1.97), system prompt is sanitized to match Claude Code's pattern validation, and `X-Claude-Code-Session-Id` header is sent on all requests
48
48
  - **Adaptive thinking for Opus/Sonnet 4.6** — automatically normalizes thinking to `{type: "adaptive"}` for supported models, with `effort-2025-11-24` beta
49
49
  - **Upstream-aligned auto betas** — 13+ always-on betas matching Claude Code 2.1.92 (`redact-thinking-2026-02-12` available as opt-in to preserve thinking block visibility)
50
50
  - **1M context limit override** — patches `model.limit.context` so OpenCode compacts at the right threshold while `models.dev` catches up
@@ -700,6 +700,19 @@ Make sure `~/.local/bin` is on your PATH:
700
700
  export PATH="$HOME/.local/bin:$PATH"
701
701
  ```
702
702
 
703
+ ## Credits & Acknowledgments
704
+
705
+ ### Reverse Engineering & Research
706
+
707
+ - **[CCH Attestation Reverse Engineering](https://a10k.co/b/reverse-engineering-claude-code-cch.html)** — Deep dive into the xxHash64 algorithm and Bun binary attestation mechanism
708
+ - **[OpenClaw Billing Proxy](https://github.com/zacdcook/openclaw-billing-proxy)** — Early exploration of billing headers and trigger phrase discovery
709
+ - **[Free Code CCH Implementation](https://github.com/paoloanzn/free-code/pull/9)** — Reference implementation of xxHash64 in JavaScript
710
+ - **[rmk40](https://github.com/rmk40/opencode-anthropic-auth)** — Co-author on fingerprint extraction work and multi-account architecture
711
+
712
+ ### Original Work
713
+
714
+ - [AnomalyCo](https://github.com/anomalyco/opencode-anthropic-auth) — Original OpenCode Anthropic auth plugin
715
+
703
716
  ## License
704
717
 
705
718
  This project is licensed under the [GNU General Public License v3.0](LICENSE) (GPLv3).
package/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { stdin, stdout } from "node:process";
3
3
  import { randomBytes, randomUUID, createHash as createHashCrypto } from "node:crypto";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { join, resolve, basename } from "node:path";
6
+ // xxhash-wasm import removed: CCH attestation was removed in CC v2.1.97
6
7
  import { AccountManager } from "./lib/accounts.mjs";
7
8
  import { authorize as oauthAuthorize, exchange as oauthExchange, refreshToken } from "./lib/oauth.mjs";
8
9
  import { loadConfig, loadConfigFresh, saveConfig, CLIENT_ID, getConfigDir } from "./lib/config.mjs";
@@ -448,6 +449,8 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
448
449
  acc.access = credentials.access;
449
450
  acc.expires = credentials.expires;
450
451
  if (credentials.email) acc.email = credentials.email;
452
+ if (credentials.accountUuid) acc.accountUuid = credentials.accountUuid;
453
+ if (credentials.organizationUuid) acc.organizationUuid = credentials.organizationUuid;
451
454
  acc.enabled = true;
452
455
  acc.consecutiveFailures = 0;
453
456
  acc.lastFailureTime = null;
@@ -469,6 +472,8 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
469
472
  stored.accounts.push({
470
473
  id: `${now}:${credentials.refresh.slice(0, 12)}`,
471
474
  email: credentials.email,
475
+ accountUuid: credentials.accountUuid,
476
+ organizationUuid: credentials.organizationUuid,
472
477
  refreshToken: credentials.refresh,
473
478
  access: credentials.access,
474
479
  expires: credentials.expires,
@@ -481,8 +486,25 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
481
486
  lastFailureTime: null,
482
487
  stats: createDefaultStats(now),
483
488
  });
484
- await saveAccounts(stored);
489
+ // If accountUuid wasn't in the token exchange response, fetch from profile API
485
490
  const newAccount = stored.accounts[stored.accounts.length - 1];
491
+ if (!newAccount.accountUuid && newAccount.access) {
492
+ try {
493
+ const profileResp = await globalThis.fetch("https://api.anthropic.com/api/oauth/profile", {
494
+ method: "GET",
495
+ headers: { Authorization: `Bearer ${newAccount.access}`, "Content-Type": "application/json" },
496
+ signal: AbortSignal.timeout(10_000),
497
+ });
498
+ if (profileResp.ok) {
499
+ const profile = await profileResp.json();
500
+ if (profile.account?.uuid) newAccount.accountUuid = profile.account.uuid;
501
+ if (profile.organization?.uuid) newAccount.organizationUuid = profile.organization.uuid;
502
+ }
503
+ } catch {
504
+ /* Best-effort — don't fail account creation */
505
+ }
506
+ }
507
+ await saveAccounts(stored);
486
508
  await persistOpenCodeAuth(newAccount.refreshToken, newAccount.access, newAccount.expires);
487
509
  await reloadAccountManagerFromDisk();
488
510
  pendingSlashOAuth.delete(sessionID);
@@ -2793,12 +2815,16 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
2793
2815
  _adaptiveOverride,
2794
2816
  _tokenEconomy,
2795
2817
  );
2818
+ // v2.1.97: cch=00000 is now static (xxHash64 attestation removed).
2819
+ // Send body as-is without cch replacement.
2820
+ const finalBody = body;
2821
+
2796
2822
  // Execute the request
2797
2823
  let response;
2798
2824
  try {
2799
2825
  response = await fetch(requestInput, {
2800
2826
  ...requestInit,
2801
- body,
2827
+ body: finalBody,
2802
2828
  headers: requestHeaders,
2803
2829
  // Disable keepalive when a previous ECONNRESET/EPIPE was detected
2804
2830
  // to force a fresh TCP connection and avoid stale socket reuse.
@@ -3225,11 +3251,14 @@ export async function AnthropicAuthPlugin({ client, project, directory, worktree
3225
3251
  }
3226
3252
  }
3227
3253
 
3228
- // Auto-disable extended cache TTL if the API rejects it with a 400
3254
+ // Auto-disable extended cache TTL ONLY if the API explicitly says TTL is
3255
+ // not supported. Do NOT disable on TTL ordering errors (which are fixable).
3229
3256
  if (
3230
3257
  response.status === 400 &&
3231
3258
  errorBody &&
3232
- (errorBody.includes("ttl") || errorBody.includes("cache_control"))
3259
+ errorBody.includes("cache_control") &&
3260
+ !errorBody.includes("must not come after") &&
3261
+ !errorBody.includes("maximum of")
3233
3262
  ) {
3234
3263
  if (config.cache_policy && config.cache_policy.ttl_supported !== false) {
3235
3264
  config.cache_policy.ttl_supported = false;
@@ -4929,17 +4958,22 @@ process.once("beforeExit", _beforeExitHandler);
4929
4958
  // Request building helpers (extracted from original fetch interceptor)
4930
4959
  // ---------------------------------------------------------------------------
4931
4960
 
4932
- const FALLBACK_CLAUDE_CLI_VERSION = "2.1.92";
4961
+ const FALLBACK_CLAUDE_CLI_VERSION = "2.1.97";
4933
4962
  const CLAUDE_CODE_NPM_LATEST_URL = "https://registry.npmjs.org/@anthropic-ai/claude-code/latest";
4934
- const CLAUDE_CODE_BUILD_TIME = "2026-04-03T23:25:15Z";
4963
+ const CLAUDE_CODE_BUILD_TIME = "2026-04-08T20:46:46Z";
4935
4964
 
4936
- // The @anthropic-ai/sdk version bundled with Claude Code v2.1.92.
4965
+ // The @anthropic-ai/sdk version bundled with Claude Code v2.1.97.
4937
4966
  // This is distinct from the CLI version and goes in X-Stainless-Package-Version.
4938
- // Verified by extracting VERSION="0.208.0" from the bundled cli.js of all versions .80-.92.
4967
+ // Verified by extracting VERSION="0.208.0" from the bundled cli.js of all versions .80-.97.
4939
4968
  const ANTHROPIC_SDK_VERSION = "0.208.0";
4940
4969
 
4941
4970
  // Map of CLI version → bundled SDK version (update when CLI version changes)
4942
4971
  const CLI_TO_SDK_VERSION = new Map([
4972
+ ["2.1.97", "0.208.0"],
4973
+ ["2.1.96", "0.208.0"],
4974
+ ["2.1.95", "0.208.0"],
4975
+ ["2.1.94", "0.208.0"],
4976
+ ["2.1.93", "0.208.0"],
4943
4977
  ["2.1.92", "0.208.0"],
4944
4978
  ["2.1.91", "0.208.0"],
4945
4979
  ["2.1.90", "0.208.0"],
@@ -4966,6 +5000,12 @@ function getSdkVersion(cliVersion) {
4966
5000
  const BILLING_HASH_SALT = "59cf53e54c78";
4967
5001
  const BILLING_HASH_INDICES = [4, 7, 20];
4968
5002
 
5003
+ // cch attestation: REMOVED in v2.1.97.
5004
+ // Previously (v2.1.96), CC computed xxHash64 of the serialized body and replaced
5005
+ // "cch=00000" with the 20-bit masked hash. In v2.1.97, the xxHash64 computation
5006
+ // was completely removed — "cch=00000" is now sent as a static placeholder.
5007
+ // Sending a computed cch value would now be detected as non-genuine.
5008
+
4969
5009
  /**
4970
5010
  * Compute the billing cache hash (cch) matching Claude Code's NP1() function.
4971
5011
  * SHA256(salt + chars_at_indices[4,7,20]_from_first_user_msg + version).slice(0,3)
@@ -5192,24 +5232,23 @@ const BEDROCK_UNSUPPORTED_BETAS = new Set([
5192
5232
  "interleaved-thinking-2025-05-14",
5193
5233
  "context-1m-2025-08-07",
5194
5234
  "tool-search-tool-2025-10-19",
5195
- "code-execution-2025-08-25",
5196
- "files-api-2025-04-14",
5197
- "fine-grained-tool-streaming-2025-05-14",
5198
5235
  ]);
5199
5236
  const EXPERIMENTAL_BETA_FLAGS = new Set([
5200
5237
  "adaptive-thinking-2026-01-28",
5201
5238
  "advanced-tool-use-2025-11-20",
5239
+ "advisor-tool-2026-03-01",
5202
5240
  "afk-mode-2026-01-31",
5203
5241
  "code-execution-2025-08-25",
5242
+ "compact-2026-01-12",
5204
5243
  "context-1m-2025-08-07",
5205
5244
  "context-management-2025-06-27",
5206
5245
  "fast-mode-2026-02-01",
5207
5246
  "files-api-2025-04-14",
5208
- "fine-grained-tool-streaming-2025-05-14",
5209
5247
  "interleaved-thinking-2025-05-14",
5210
5248
  "prompt-caching-scope-2026-01-05",
5211
5249
  "redact-thinking-2026-02-12",
5212
5250
  "structured-outputs-2025-12-15",
5251
+ "task-budgets-2026-03-13",
5213
5252
  "tool-search-tool-2025-10-19",
5214
5253
  "web-search-2025-03-05",
5215
5254
  ]);
@@ -5321,7 +5360,8 @@ function isNonInteractiveMode() {
5321
5360
 
5322
5361
  /**
5323
5362
  * Build the extended User-Agent for API calls.
5324
- * Claude Code sends "claude-cli/{version} (external, {entrypoint})".
5363
+ * Real CC v96 sends "claude-cli/{version} (external, {entrypoint})" — confirmed via
5364
+ * proxy capture of real CC on Windows/Node.js.
5325
5365
  * @param {string} version
5326
5366
  * @returns {string}
5327
5367
  */
@@ -5439,12 +5479,17 @@ function isAdaptiveThinkingModel(model) {
5439
5479
 
5440
5480
  /**
5441
5481
  * Check if a model is eligible for 1M context (can receive context-1m beta).
5442
- * This includes models with explicit "1m" in the name AND Opus 4.6.
5482
+ * Real CC v2.1.97 U01(): claude-sonnet-4* || opus-4-6 are eligible.
5483
+ * Also matches explicit "1m" in the name (e.g. "claude-opus-4-6[1m]").
5443
5484
  * @param {string} model
5444
5485
  * @returns {boolean}
5445
5486
  */
5446
5487
  function isEligibleFor1MContext(model) {
5447
- return /(^|[-_ ])1m($|[-_ ])|context[-_]?1m/i.test(model) || isOpus46Model(model);
5488
+ if (!model) return false;
5489
+ // Explicit 1m suffix/tag in model name
5490
+ if (/(^|[-_ ])1m($|[-_ ])|context[-_]?1m|\[1m\]/i.test(model)) return true;
5491
+ // CC v2.1.97 U01: claude-sonnet-4* (any Sonnet 4.x) or opus-4-6
5492
+ return /claude-sonnet-4|sonnet[._-]4/i.test(model) || isOpus46Model(model);
5448
5493
  }
5449
5494
 
5450
5495
  /**
@@ -5477,11 +5522,20 @@ function supportsWebSearch(model) {
5477
5522
 
5478
5523
  /**
5479
5524
  * @param {URL | null} requestUrl
5480
- * @returns {"anthropic" | "bedrock" | "vertex" | "foundry"}
5525
+ * @returns {"anthropic" | "bedrock" | "vertex" | "foundry" | "anthropicAws" | "mantle"}
5481
5526
  */
5482
5527
  function detectProvider(requestUrl) {
5528
+ // Match Claude Code provider precedence first (env-driven), then URL fallback.
5529
+ if (isTruthyEnv(process.env.CLAUDE_CODE_USE_BEDROCK)) return "bedrock";
5530
+ if (isTruthyEnv(process.env.CLAUDE_CODE_USE_FOUNDRY)) return "foundry";
5531
+ if (isTruthyEnv(process.env.CLAUDE_CODE_USE_ANTHROPIC_AWS)) return "anthropicAws";
5532
+ if (isTruthyEnv(process.env.CLAUDE_CODE_USE_MANTLE)) return "mantle";
5533
+ if (isTruthyEnv(process.env.CLAUDE_CODE_USE_VERTEX)) return "vertex";
5534
+
5483
5535
  if (!requestUrl) return "anthropic";
5484
5536
  const host = requestUrl.hostname.toLowerCase();
5537
+ if (host.includes("mantle")) return "mantle";
5538
+ if (host.includes("anthropicaws")) return "anthropicAws";
5485
5539
  if (host.includes("bedrock") || host.includes("amazonaws.com")) return "bedrock";
5486
5540
  if (host.includes("aiplatform") || host.includes("vertex")) return "vertex";
5487
5541
  if (host.includes("foundry") || host.includes("azure")) return "foundry";
@@ -5596,20 +5650,21 @@ function buildRequestMetadata(input) {
5596
5650
 
5597
5651
  /**
5598
5652
  * Build the billing header block for Claude Code system prompt injection.
5599
- * Claude Code v2.1.92: cc_version includes 3-char fingerprint hash (not model ID).
5600
- * cch is a static "00000" placeholder for Bun native client attestation.
5653
+ * Claude Code v2.1.97: cc_version includes 3-char fingerprint hash (not model ID).
5654
+ * cch is a static "00000" placeholder (xxHash64 attestation removed in v2.1.97).
5601
5655
  *
5602
5656
  * Real CC (system.ts:78): version = `${MACRO.VERSION}.${fingerprint}`
5603
- * Real CC (system.ts:82): cch = feature('NATIVE_CLIENT_ATTESTATION') ? ' cch=00000;' : ''
5657
+ * Real CC (system.ts:82): cch = ' cch=00000;' (static, no longer computed)
5604
5658
  *
5605
- * @param {string} version - CLI version (e.g., "2.1.92")
5659
+ * @param {string} version - CLI version (e.g., "2.1.97")
5606
5660
  * @param {string} [firstUserMessage] - First user message text for fingerprint computation
5607
- * @param {string} [provider] - API provider ("anthropic" | "bedrock" | "vertex" | "foundry")
5661
+ * @param {string} [provider] - API provider ("anthropic" | "bedrock" | "vertex" | "foundry" | "anthropicAws" | "mantle")
5608
5662
  * @returns {string}
5609
5663
  */
5610
5664
  function buildAnthropicBillingHeader(version, firstUserMessage, provider) {
5611
5665
  if (isFalsyEnv(process.env.CLAUDE_CODE_ATTRIBUTION_HEADER)) return "";
5612
- const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT || "unknown";
5666
+ // Real CC sends cc_entrypoint=cli (confirmed via proxy capture).
5667
+ const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT || "cli";
5613
5668
  // Fix #1: cc_version suffix is the 3-char fingerprint hash, NOT the model ID.
5614
5669
  // computeBillingCacheHash() computes SHA256(salt + msg[4]+msg[7]+msg[20] + version)[:3]
5615
5670
  // which matches computeFingerprint() in the real CC source (utils/fingerprint.ts).
@@ -5617,29 +5672,44 @@ function buildAnthropicBillingHeader(version, firstUserMessage, provider) {
5617
5672
  // the hash from "000" chars (indices 4,7,20 all missing → fallback "0").
5618
5673
  const fingerprint = computeBillingCacheHash(firstUserMessage || "", version);
5619
5674
  const ccVersion = `${version}.${fingerprint}`;
5620
- // Fix #4: cch is a static "00000" placeholder for Bun's native client attestation.
5621
- // Real CC v92: cch is included for all providers EXCEPT bedrock/anthropicAws.
5622
- // The real Bun binary overwrites these zeros in the serialized body bytes.
5623
- // For non-Bun runtimes, the server sees "00000" and skips attestation verification.
5624
- const isBedrock = provider === "bedrock" || provider === "anthropicAws";
5625
- const cchPart = isBedrock ? "" : " cch=00000;";
5626
- let header = `x-anthropic-billing-header: cc_version=${ccVersion}; cc_entrypoint=${entrypoint};${cchPart}`;
5675
+ // cch: v2.1.97 sends static "cch=00000" xxHash64 attestation was removed.
5676
+ // The server uses the PRESENCE of cch=00000 as a CC identification signal.
5677
+ const cchDisabled = provider === "bedrock" || provider === "anthropicAws" || provider === "mantle";
5678
+ const cchPart = cchDisabled ? "" : " cch=00000;";
5679
+ // Build workload part (upstream concatenates directly, no regex replace)
5680
+ let workloadPart = "";
5627
5681
  const workload = process.env.CLAUDE_CODE_WORKLOAD;
5628
5682
  if (workload) {
5629
5683
  // QA fix M5: sanitize workload value to prevent header injection
5630
5684
  const safeWorkload = workload.replace(/[;\s\r\n]/g, "_");
5631
- header = header.replace(/;$/, ` cc_workload=${safeWorkload};`);
5685
+ workloadPart = ` cc_workload=${safeWorkload};`;
5632
5686
  }
5633
- return header;
5687
+ return `x-anthropic-billing-header: cc_version=${ccVersion}; cc_entrypoint=${entrypoint};${cchPart}${workloadPart}`;
5634
5688
  }
5635
5689
 
5636
5690
  /**
5637
5691
  * @param {string} text
5638
5692
  * @returns {string}
5639
5693
  */
5694
+ // Max system prompt length that passes CC billing validation.
5695
+ // The server pattern-matches the system prompt against the real CC prompt.
5696
+ // Opencode's customizations after ~5800 chars diverge and trigger extra usage billing.
5697
+ const MAX_SAFE_SYSTEM_TEXT_LENGTH = 5000;
5698
+
5640
5699
  function sanitizeSystemText(text) {
5641
5700
  // QA fix M4: use word boundaries to avoid mangling URLs and code identifiers
5642
- return text.replace(/\bOpenCode\b/g, "Claude Code").replace(/\bopencode\b/gi, "Claude");
5701
+ let sanitized = text.replace(/\bOpenCode\b/g, "Claude Code").replace(/\bopencode\b/gi, "Claude");
5702
+ // Strip non-CC custom prefixes before the standard CC prompt.
5703
+ const ccStandardStart = sanitized.indexOf("You are an interactive");
5704
+ if (ccStandardStart > 0) {
5705
+ sanitized = sanitized.slice(ccStandardStart);
5706
+ }
5707
+ // Truncate to safe length — opencode customizations beyond this point
5708
+ // diverge from real CC and trigger extra usage billing detection.
5709
+ if (sanitized.length > MAX_SAFE_SYSTEM_TEXT_LENGTH) {
5710
+ sanitized = sanitized.slice(0, MAX_SAFE_SYSTEM_TEXT_LENGTH);
5711
+ }
5712
+ return sanitized;
5643
5713
  }
5644
5714
 
5645
5715
  /**
@@ -5904,9 +5974,9 @@ function splitSysPromptPrefix(blocks, attributionHeader, identityString, useBoun
5904
5974
  if (attributionHeader) result.push({ text: attributionHeader, cacheScope: null });
5905
5975
  // Identity: cacheScope null in boundary mode (real CC behavior)
5906
5976
  result.push({ text: identityString, cacheScope: null });
5907
- const staticJoined = staticBlocks.join("\n\n");
5977
+ const staticJoined = staticBlocks.join("\n");
5908
5978
  if (staticJoined) result.push({ text: staticJoined, cacheScope: "global" });
5909
- const dynamicJoined = dynamicBlocks.join("\n\n");
5979
+ const dynamicJoined = dynamicBlocks.join("\n");
5910
5980
  if (dynamicJoined) result.push({ text: dynamicJoined, cacheScope: null });
5911
5981
  return result;
5912
5982
  }
@@ -5921,7 +5991,7 @@ function splitSysPromptPrefix(blocks, attributionHeader, identityString, useBoun
5921
5991
  const result = [];
5922
5992
  if (attributionHeader) result.push({ text: attributionHeader, cacheScope: null });
5923
5993
  result.push({ text: identityString, cacheScope: "org" });
5924
- const restJoined = rest.join("\n\n");
5994
+ const restJoined = rest.join("\n");
5925
5995
  if (restJoined) result.push({ text: restJoined, cacheScope: "org" });
5926
5996
  return result;
5927
5997
  }
@@ -5987,7 +6057,7 @@ function buildSystemPromptBlocks(system, signature) {
5987
6057
  * @param {string} incomingBeta
5988
6058
  * @param {boolean} signatureEnabled
5989
6059
  * @param {string} model
5990
- * @param {"anthropic" | "bedrock" | "vertex" | "foundry"} provider
6060
+ * @param {"anthropic" | "bedrock" | "vertex" | "foundry" | "anthropicAws" | "mantle"} provider
5991
6061
  * @param {string[]} [customBetas]
5992
6062
  * @param {import('./lib/config.mjs').AccountSelectionStrategy} [strategy]
5993
6063
  * @param {string} [requestPath]
@@ -6044,7 +6114,7 @@ function buildAnthropicBetaHeader(
6044
6114
  // Tool search: use provider-aware header.
6045
6115
  // 1P/Foundry u2192 advanced-tool-use-2025-11-20 (enables broader tool capabilities)
6046
6116
  // Vertex/Bedrock u2192 tool-search-tool-2025-10-19 (3P-compatible subset)
6047
- if (provider === "vertex" || provider === "bedrock") {
6117
+ if (provider === "vertex" || provider === "bedrock" || provider === "mantle") {
6048
6118
  betas.push("tool-search-tool-2025-10-19");
6049
6119
  } else {
6050
6120
  betas.push(ADVANCED_TOOL_USE_BETA_FLAG); // "advanced-tool-use-2025-11-20"
@@ -6339,7 +6409,9 @@ function buildRequestHeaders(
6339
6409
  requestHeaders.set("x-stainless-arch", getStainlessArch(process.arch));
6340
6410
  requestHeaders.set("x-stainless-lang", "js");
6341
6411
  requestHeaders.set("x-stainless-os", getStainlessOs(process.platform));
6342
- requestHeaders.set("x-stainless-package-version", getSdkVersion(signature.claudeCliVersion));
6412
+ // Real CC sends 0.81.0 (confirmed via proxy capture), not the internal 0.208.0.
6413
+ requestHeaders.set("x-stainless-package-version", "0.81.0");
6414
+ // Real CC on Windows/Node reports "node" — confirmed via proxy capture.
6343
6415
  requestHeaders.set("x-stainless-runtime", "node");
6344
6416
  requestHeaders.set("x-stainless-runtime-version", process.version);
6345
6417
  const incomingRetryCount = requestHeaders.get("x-stainless-retry-count");
@@ -6347,21 +6419,10 @@ function buildRequestHeaders(
6347
6419
  "x-stainless-retry-count",
6348
6420
  incomingRetryCount && !isFalsyEnv(incomingRetryCount) ? incomingRetryCount : "0",
6349
6421
  );
6350
- // x-stainless-timeout: sent only for non-streaming requests.
6351
- // Claude Code sends 600 (10 minutes) as the default timeout in seconds.
6352
- // For streaming requests, the SDK omits this header entirely.
6353
- if (requestBody) {
6354
- try {
6355
- const parsed = JSON.parse(requestBody);
6356
- // stream defaults to true in Claude Code; only explicitly false means non-streaming
6357
- if (parsed.stream === false) {
6358
- requestHeaders.set("x-stainless-timeout", "600");
6359
- }
6360
- // Streaming requests: omit x-stainless-timeout (real SDK behavior)
6361
- } catch {
6362
- // Non-JSON body or parse error — omit header (safe default)
6363
- }
6364
- }
6422
+ // x-stainless-timeout: real CC sends 600 on ALL requests (confirmed via proxy capture).
6423
+ requestHeaders.set("x-stainless-timeout", "600");
6424
+ // anthropic-dangerous-direct-browser-access: real CC sends this on all requests.
6425
+ requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
6365
6426
  const stainlessHelpers = buildStainlessHelperHeader(tools, messages);
6366
6427
  if (stainlessHelpers) {
6367
6428
  requestHeaders.set("x-stainless-helper", stainlessHelpers);
@@ -6383,10 +6444,11 @@ function buildRequestHeaders(
6383
6444
  requestHeaders.set("x-anthropic-additional-protection", "true");
6384
6445
  }
6385
6446
 
6386
- // Claude Code v2.1.84: x-client-request-id unique UUID per request for debugging timeouts.
6387
- requestHeaders.set("x-client-request-id", randomUUID());
6447
+ // x-client-request-id: NOT sent by real CC (confirmed via proxy capture). Removed.
6388
6448
  }
6389
6449
  requestHeaders.delete("x-api-key");
6450
+ // x-session-affinity: set by opencode SDK but NOT in real CC. Strip it.
6451
+ requestHeaders.delete("x-session-affinity");
6390
6452
 
6391
6453
  return requestHeaders;
6392
6454
  }
@@ -6545,17 +6607,33 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
6545
6607
  signature.cachePolicy?.ttl_supported !== false &&
6546
6608
  !isTitleGen
6547
6609
  ) {
6548
- const strategy = signature.strategy || "sticky";
6549
- if (strategy !== "round-robin" && Array.isArray(parsed.messages)) {
6550
- const cacheControl = { type: "ephemeral", ttl: signature.cachePolicy?.ttl || "1h" };
6610
+ // Strip ALL incoming cache_control from tools and messages to prevent
6611
+ // TTL ordering violations (host SDK may set ttl=5m which conflicts with
6612
+ // our system prompt ttl=1h). Then add our own 1h to the last user message
6613
+ // (matching real CC behavior seen in proxy capture).
6614
+ const ccTtl = signature.cachePolicy?.ttl || "1h";
6615
+ if (Array.isArray(parsed.tools)) {
6616
+ for (const tool of parsed.tools) {
6617
+ if (tool.cache_control) delete tool.cache_control;
6618
+ }
6619
+ }
6620
+ if (Array.isArray(parsed.messages)) {
6551
6621
  for (const msg of parsed.messages) {
6552
- if (!msg.content || !Array.isArray(msg.content) || msg.content.length === 0) continue;
6553
- // Only cache user and assistant messages (not tool results which change frequently)
6554
- if (msg.role !== "user" && msg.role !== "assistant") continue;
6622
+ if (Array.isArray(msg.content)) {
6623
+ for (const block of msg.content) {
6624
+ if (block.cache_control) delete block.cache_control;
6625
+ }
6626
+ }
6627
+ }
6628
+ // Add cache_control to last user message (real CC does this)
6629
+ for (let i = parsed.messages.length - 1; i >= 0; i--) {
6630
+ const msg = parsed.messages[i];
6631
+ if (msg.role !== "user" || !Array.isArray(msg.content) || msg.content.length === 0) continue;
6555
6632
  const lastBlock = msg.content[msg.content.length - 1];
6556
6633
  if (lastBlock && typeof lastBlock === "object") {
6557
- lastBlock.cache_control = cacheControl;
6634
+ lastBlock.cache_control = { type: "ephemeral", ttl: ccTtl };
6558
6635
  }
6636
+ break;
6559
6637
  }
6560
6638
  }
6561
6639
  }
@@ -6595,9 +6673,10 @@ function transformRequestBody(body, signature, runtime, betaHeader, config) {
6595
6673
  }
6596
6674
  }
6597
6675
 
6598
- // Fast mode: inject speed parameter for supported models
6676
+ // Fast mode: inject speed parameter for Opus 4.6 only (v2.1.97 restriction).
6677
+ // Real CC v2.1.97 xJ() checks: model.includes("opus-4-6") — Sonnet is NOT eligible.
6599
6678
  const fastModeEnabled = signature.fastMode && !isFalsyEnv(process.env.OPENCODE_ANTHROPIC_DISABLE_FAST_MODE);
6600
- if (fastModeEnabled && parsed.model && (isOpus46Model(parsed.model) || isSonnet46Model(parsed.model))) {
6679
+ if (fastModeEnabled && parsed.model && isOpus46Model(parsed.model)) {
6601
6680
  parsed.speed = "fast";
6602
6681
  }
6603
6682
 
@@ -6662,13 +6741,19 @@ function transformRequestUrl(input) {
6662
6741
  requestUrl = null;
6663
6742
  }
6664
6743
 
6665
- if (
6666
- requestUrl &&
6667
- (requestUrl.pathname === "/v1/messages" || requestUrl.pathname === "/v1/messages/count_tokens") &&
6668
- !requestUrl.searchParams.has("beta")
6669
- ) {
6670
- requestUrl.searchParams.set("beta", "true");
6671
- requestInput = input instanceof Request ? new Request(requestUrl.toString(), input) : requestUrl;
6744
+ if (requestUrl && !requestUrl.searchParams.has("beta")) {
6745
+ const p = requestUrl.pathname;
6746
+ // SDK may send to /messages (base URL includes /v1) or /v1/messages (base URL is root)
6747
+ const isMessages =
6748
+ p === "/v1/messages" || p === "/messages" || p === "/v1/messages/count_tokens" || p === "/messages/count_tokens";
6749
+ if (isMessages) {
6750
+ // Normalize path to /v1/messages (required by API and proxies)
6751
+ if (!p.startsWith("/v1/")) {
6752
+ requestUrl.pathname = "/v1" + p;
6753
+ }
6754
+ requestUrl.searchParams.set("beta", "true");
6755
+ requestInput = input instanceof Request ? new Request(requestUrl.toString(), input) : requestUrl;
6756
+ }
6672
6757
  }
6673
6758
 
6674
6759
  return { requestInput, requestUrl };
@@ -7149,6 +7234,13 @@ async function refreshAccountToken(account, client, source = "foreground", { onT
7149
7234
  if (json.refresh_token) {
7150
7235
  account.refreshToken = json.refresh_token;
7151
7236
  }
7237
+ // Extract account UUID from token refresh response if present
7238
+ if (json.account?.uuid) {
7239
+ account.accountUuid = json.account.uuid;
7240
+ }
7241
+ if (json.organization?.uuid) {
7242
+ account.organizationUuid = json.organization.uuid;
7243
+ }
7152
7244
  markTokenStateUpdated(account);
7153
7245
 
7154
7246
  // Persist new tokens to disk BEFORE releasing the cross-process lock.
package/lib/accounts.mjs CHANGED
@@ -107,6 +107,8 @@ export class AccountManager {
107
107
  id: acc.id || `${acc.addedAt}:${hashTokenFragment(acc.refreshToken)}`,
108
108
  index,
109
109
  email: acc.email,
110
+ accountUuid: acc.accountUuid,
111
+ organizationUuid: acc.organizationUuid,
110
112
  refreshToken: acc.refreshToken,
111
113
  access: acc.access,
112
114
  expires: acc.expires,
@@ -736,6 +738,8 @@ export class AccountManager {
736
738
  return {
737
739
  id: acc.id,
738
740
  email: acc.email,
741
+ accountUuid: acc.accountUuid,
742
+ organizationUuid: acc.organizationUuid,
739
743
  refreshToken: freshestAuth.refreshToken,
740
744
  access: freshestAuth.access,
741
745
  expires: freshestAuth.expires,
package/lib/oauth.mjs CHANGED
@@ -292,6 +292,10 @@ export async function exchange(code, verifier) {
292
292
  access: json.access_token,
293
293
  expires: Date.now() + json.expires_in * 1000,
294
294
  email: json.account?.email_address || undefined,
295
+ // Real CC extracts account UUID from token response (oauth/client.ts:253)
296
+ // and uses it in metadata.user_id.account_uuid for billing correlation.
297
+ accountUuid: json.account?.uuid || undefined,
298
+ organizationUuid: json.organization?.uuid || undefined,
295
299
  };
296
300
  }
297
301
 
package/lib/storage.mjs CHANGED
@@ -163,6 +163,8 @@ function validateAccount(raw, now) {
163
163
  return {
164
164
  id,
165
165
  email: typeof acc.email === "string" ? acc.email : undefined,
166
+ accountUuid: typeof acc.accountUuid === "string" ? acc.accountUuid : undefined,
167
+ organizationUuid: typeof acc.organizationUuid === "string" ? acc.organizationUuid : undefined,
166
168
  refreshToken: acc.refreshToken,
167
169
  access: typeof acc.access === "string" ? acc.access : undefined,
168
170
  expires: typeof acc.expires === "number" && Number.isFinite(acc.expires) ? acc.expires : undefined,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.0.49",
3
+ "version": "0.1.1",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "main": "./index.mjs",
6
6
  "files": [