@vacbo/opencode-anthropic-fix 0.1.4 → 0.1.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.
@@ -3370,13 +3370,28 @@ var init_cli = __esm({
3370
3370
  });
3371
3371
 
3372
3372
  // src/index.ts
3373
- import { randomUUID as randomUUID2 } from "node:crypto";
3373
+ import { randomUUID as randomUUID3 } from "node:crypto";
3374
3374
 
3375
3375
  // src/backoff.ts
3376
3376
  var QUOTA_EXHAUSTED_BACKOFFS = [6e4, 3e5, 18e5, 72e5];
3377
3377
  var AUTH_FAILED_BACKOFF = 5e3;
3378
3378
  var RATE_LIMIT_EXCEEDED_BACKOFF = 3e4;
3379
3379
  var MIN_BACKOFF_MS = 2e3;
3380
+ var RETRIABLE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ECONNREFUSED", "EPIPE", "ETIMEDOUT", "UND_ERR_SOCKET"]);
3381
+ var NON_RETRIABLE_ERROR_NAMES = /* @__PURE__ */ new Set(["AbortError", "TimeoutError", "APIUserAbortError"]);
3382
+ var RETRIABLE_NETWORK_ERROR_MESSAGES = [
3383
+ "bun proxy upstream error",
3384
+ "connection reset by peer",
3385
+ "connection reset by server",
3386
+ "econnreset",
3387
+ "econnrefused",
3388
+ "epipe",
3389
+ "etimedout",
3390
+ "fetch failed",
3391
+ "network connection lost",
3392
+ "socket hang up",
3393
+ "und_err_socket"
3394
+ ];
3380
3395
  function parseRetryAfterHeader(response) {
3381
3396
  const header = response.headers.get("retry-after");
3382
3397
  if (!header) return null;
@@ -3463,6 +3478,54 @@ function bodyHasAccountError(body) {
3463
3478
  ];
3464
3479
  return typeSignals.some((signal) => errorType.includes(signal)) || messageSignals.some((signal) => message.includes(signal)) || messageSignals.some((signal) => text.includes(signal));
3465
3480
  }
3481
+ function collectErrorChain(error) {
3482
+ const queue = [error];
3483
+ const visited = /* @__PURE__ */ new Set();
3484
+ const chain = [];
3485
+ while (queue.length > 0) {
3486
+ const candidate = queue.shift();
3487
+ if (candidate == null || visited.has(candidate)) {
3488
+ continue;
3489
+ }
3490
+ visited.add(candidate);
3491
+ if (candidate instanceof Error) {
3492
+ const typedCandidate = candidate;
3493
+ chain.push(typedCandidate);
3494
+ if (typedCandidate.cause !== void 0) {
3495
+ queue.push(typedCandidate.cause);
3496
+ }
3497
+ continue;
3498
+ }
3499
+ if (typeof candidate === "object" && "cause" in candidate) {
3500
+ queue.push(candidate.cause);
3501
+ }
3502
+ }
3503
+ return chain;
3504
+ }
3505
+ function isRetriableNetworkError(error) {
3506
+ if (typeof error === "string") {
3507
+ const text = error.toLowerCase();
3508
+ return RETRIABLE_NETWORK_ERROR_MESSAGES.some((signal) => text.includes(signal));
3509
+ }
3510
+ const chain = collectErrorChain(error);
3511
+ if (chain.length === 0) {
3512
+ return false;
3513
+ }
3514
+ for (const candidate of chain) {
3515
+ if (NON_RETRIABLE_ERROR_NAMES.has(candidate.name)) {
3516
+ return false;
3517
+ }
3518
+ const code = candidate.code?.toUpperCase();
3519
+ if (code && RETRIABLE_NETWORK_ERROR_CODES.has(code)) {
3520
+ return true;
3521
+ }
3522
+ const message = candidate.message.toLowerCase();
3523
+ if (RETRIABLE_NETWORK_ERROR_MESSAGES.some((signal) => message.includes(signal))) {
3524
+ return true;
3525
+ }
3526
+ }
3527
+ return false;
3528
+ }
3466
3529
  function isAccountSpecificError(status, body) {
3467
3530
  if (status === 429) return true;
3468
3531
  if (status === 401) return true;
@@ -5947,6 +6010,107 @@ function buildRequestHeaders(input, requestInit, accessToken, requestBody, reque
5947
6010
  // src/index.ts
5948
6011
  init_oauth();
5949
6012
 
6013
+ // src/headers/cch.ts
6014
+ var MASK64 = 0xffffffffffffffffn;
6015
+ var CCH_MASK = 0x0fffffn;
6016
+ var PRIME1 = 0x9e3779b185ebca87n;
6017
+ var PRIME2 = 0xc2b2ae3d27d4eb4fn;
6018
+ var PRIME3 = 0x165667b19e3779f9n;
6019
+ var PRIME4 = 0x85ebca77c2b2ae63n;
6020
+ var PRIME5 = 0x27d4eb2f165667c5n;
6021
+ var CCH_FIELD_PREFIX = "cch=";
6022
+ var CCH_PLACEHOLDER = "00000";
6023
+ var CCH_SEED = 0x6e52736ac806831en;
6024
+ var encoder = new TextEncoder();
6025
+ function toUint64(value) {
6026
+ return value & MASK64;
6027
+ }
6028
+ function rotateLeft64(value, bits) {
6029
+ const shift = BigInt(bits);
6030
+ return toUint64(value << shift | value >> 64n - shift);
6031
+ }
6032
+ function readUint32LE(view, offset) {
6033
+ return BigInt(view.getUint32(offset, true));
6034
+ }
6035
+ function readUint64LE(view, offset) {
6036
+ return view.getBigUint64(offset, true);
6037
+ }
6038
+ function round64(acc, input) {
6039
+ const mixed = toUint64(acc + toUint64(input * PRIME2));
6040
+ return toUint64(rotateLeft64(mixed, 31) * PRIME1);
6041
+ }
6042
+ function mergeRound64(acc, value) {
6043
+ const mixed = acc ^ round64(0n, value);
6044
+ return toUint64(toUint64(mixed) * PRIME1 + PRIME4);
6045
+ }
6046
+ function avalanche64(hash) {
6047
+ let mixed = hash ^ hash >> 33n;
6048
+ mixed = toUint64(mixed * PRIME2);
6049
+ mixed ^= mixed >> 29n;
6050
+ mixed = toUint64(mixed * PRIME3);
6051
+ mixed ^= mixed >> 32n;
6052
+ return toUint64(mixed);
6053
+ }
6054
+ function xxHash64(input, seed = CCH_SEED) {
6055
+ const view = new DataView(input.buffer, input.byteOffset, input.byteLength);
6056
+ const length = input.byteLength;
6057
+ let offset = 0;
6058
+ let hash;
6059
+ if (length >= 32) {
6060
+ let v1 = toUint64(seed + PRIME1 + PRIME2);
6061
+ let v2 = toUint64(seed + PRIME2);
6062
+ let v3 = toUint64(seed);
6063
+ let v4 = toUint64(seed - PRIME1);
6064
+ while (offset <= length - 32) {
6065
+ v1 = round64(v1, readUint64LE(view, offset));
6066
+ v2 = round64(v2, readUint64LE(view, offset + 8));
6067
+ v3 = round64(v3, readUint64LE(view, offset + 16));
6068
+ v4 = round64(v4, readUint64LE(view, offset + 24));
6069
+ offset += 32;
6070
+ }
6071
+ hash = toUint64(rotateLeft64(v1, 1) + rotateLeft64(v2, 7) + rotateLeft64(v3, 12) + rotateLeft64(v4, 18));
6072
+ hash = mergeRound64(hash, v1);
6073
+ hash = mergeRound64(hash, v2);
6074
+ hash = mergeRound64(hash, v3);
6075
+ hash = mergeRound64(hash, v4);
6076
+ } else {
6077
+ hash = toUint64(seed + PRIME5);
6078
+ }
6079
+ hash = toUint64(hash + BigInt(length));
6080
+ while (offset <= length - 8) {
6081
+ const lane = round64(0n, readUint64LE(view, offset));
6082
+ hash ^= lane;
6083
+ hash = toUint64(rotateLeft64(hash, 27) * PRIME1 + PRIME4);
6084
+ offset += 8;
6085
+ }
6086
+ if (offset <= length - 4) {
6087
+ hash ^= toUint64(readUint32LE(view, offset) * PRIME1);
6088
+ hash = toUint64(rotateLeft64(hash, 23) * PRIME2 + PRIME3);
6089
+ offset += 4;
6090
+ }
6091
+ while (offset < length) {
6092
+ hash ^= toUint64(BigInt(view.getUint8(offset)) * PRIME5);
6093
+ hash = toUint64(rotateLeft64(hash, 11) * PRIME1);
6094
+ offset += 1;
6095
+ }
6096
+ return avalanche64(hash);
6097
+ }
6098
+ function computeNativeStyleCch(serializedBody) {
6099
+ const hash = xxHash64(encoder.encode(serializedBody), CCH_SEED);
6100
+ return (hash & CCH_MASK).toString(16).padStart(5, "0");
6101
+ }
6102
+ function replaceNativeStyleCch(serializedBody) {
6103
+ const sentinel = `${CCH_FIELD_PREFIX}${CCH_PLACEHOLDER}`;
6104
+ const fieldIndex = serializedBody.indexOf(sentinel);
6105
+ if (fieldIndex === -1) {
6106
+ return serializedBody;
6107
+ }
6108
+ const valueStart = fieldIndex + CCH_FIELD_PREFIX.length;
6109
+ const valueEnd = valueStart + CCH_PLACEHOLDER.length;
6110
+ const cch = computeNativeStyleCch(serializedBody);
6111
+ return `${serializedBody.slice(0, valueStart)}${cch}${serializedBody.slice(valueEnd)}`;
6112
+ }
6113
+
5950
6114
  // src/headers/billing.ts
5951
6115
  import { createHash as createHash2 } from "node:crypto";
5952
6116
  function buildAnthropicBillingHeader(claudeCliVersion, messages) {
@@ -5976,16 +6140,7 @@ function buildAnthropicBillingHeader(claudeCliVersion, messages) {
5976
6140
  }
5977
6141
  }
5978
6142
  const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT ?? "cli";
5979
- let cchValue;
5980
- if (Array.isArray(messages) && messages.length > 0) {
5981
- const bodyHint = JSON.stringify(messages).slice(0, 512);
5982
- const cchHash = createHash2("sha256").update(bodyHint + claudeCliVersion + Date.now().toString(36)).digest("hex");
5983
- cchValue = cchHash.slice(0, 5);
5984
- } else {
5985
- const buf = createHash2("sha256").update(Date.now().toString(36) + Math.random().toString(36)).digest("hex");
5986
- cchValue = buf.slice(0, 5);
5987
- }
5988
- return `x-anthropic-billing-header: cc_version=${claudeCliVersion}${versionSuffix}; cc_entrypoint=${entrypoint}; cch=${cchValue};`;
6143
+ return `x-anthropic-billing-header: cc_version=${claudeCliVersion}${versionSuffix}; cc_entrypoint=${entrypoint}; cch=${CCH_PLACEHOLDER};`;
5989
6144
  }
5990
6145
 
5991
6146
  // src/system-prompt/normalize.ts
@@ -6352,7 +6507,7 @@ function transformRequestBody(body, signature, runtime, relocateThirdPartyPrompt
6352
6507
  return msg;
6353
6508
  });
6354
6509
  }
6355
- return JSON.stringify(parsed);
6510
+ return replaceNativeStyleCch(JSON.stringify(parsed));
6356
6511
  } catch (err) {
6357
6512
  if (err instanceof SyntaxError) {
6358
6513
  debugLog?.("body parse failed:", err.message);
@@ -6382,20 +6537,36 @@ function shouldRetryStatus(status, shouldRetryHeader) {
6382
6537
  if (shouldRetryHeader === false) return false;
6383
6538
  return status === 408 || status === 409 || status === 429 || status >= 500;
6384
6539
  }
6385
- async function fetchWithRetry(doFetch, config = {}) {
6386
- const resolvedConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
6540
+ async function fetchWithRetry(doFetch, options = {}) {
6541
+ const resolvedConfig = { ...DEFAULT_RETRY_CONFIG, ...options };
6542
+ const shouldRetryError = options.shouldRetryError ?? isRetriableNetworkError;
6543
+ const shouldRetryResponse = options.shouldRetryResponse ?? ((response) => {
6544
+ const shouldRetryHeader = parseShouldRetryHeader(response);
6545
+ return shouldRetryStatus(response.status, shouldRetryHeader);
6546
+ });
6547
+ let forceFreshConnection = false;
6387
6548
  for (let attempt = 0; ; attempt++) {
6388
- const response = await doFetch();
6549
+ let response;
6550
+ try {
6551
+ response = await doFetch({ attempt, forceFreshConnection });
6552
+ } catch (error) {
6553
+ if (!shouldRetryError(error) || attempt >= resolvedConfig.maxRetries) {
6554
+ throw error;
6555
+ }
6556
+ const delayMs2 = calculateRetryDelay(attempt, resolvedConfig);
6557
+ await waitFor(delayMs2);
6558
+ forceFreshConnection = true;
6559
+ continue;
6560
+ }
6389
6561
  if (response.ok) {
6390
6562
  return response;
6391
6563
  }
6392
- const shouldRetryHeader = parseShouldRetryHeader(response);
6393
- const shouldRetry = shouldRetryStatus(response.status, shouldRetryHeader);
6394
- if (!shouldRetry || attempt >= resolvedConfig.maxRetries) {
6564
+ if (!shouldRetryResponse(response) || attempt >= resolvedConfig.maxRetries) {
6395
6565
  return response;
6396
6566
  }
6397
6567
  const delayMs = parseRetryAfterMsHeader(response) ?? parseRetryAfterHeader(response) ?? calculateRetryDelay(attempt, resolvedConfig);
6398
6568
  await waitFor(delayMs);
6569
+ forceFreshConnection = false;
6399
6570
  }
6400
6571
  }
6401
6572
 
@@ -6730,7 +6901,7 @@ function transformResponse(response, onUsage, onAccountError, onStreamError) {
6730
6901
  if (!response.body || !isEventStreamResponse(response)) return response;
6731
6902
  const reader = response.body.getReader();
6732
6903
  const decoder = new TextDecoder("utf-8", { fatal: true });
6733
- const encoder = new TextEncoder();
6904
+ const encoder2 = new TextEncoder();
6734
6905
  const stats = {
6735
6906
  inputTokens: 0,
6736
6907
  outputTokens: 0,
@@ -6788,7 +6959,7 @@ function transformResponse(response, onUsage, onAccountError, onStreamError) {
6788
6959
  if (eventType === "error") {
6789
6960
  hasSeenError = true;
6790
6961
  }
6791
- controller.enqueue(encoder.encode(formatSSEEventBlock(eventType, parsedRecord, strictEventValidation)));
6962
+ controller.enqueue(encoder2.encode(formatSSEEventBlock(eventType, parsedRecord, strictEventValidation)));
6792
6963
  if (eventType === "error" && strictEventValidation) {
6793
6964
  throw new Error(getErrorMessage(parsedRecord));
6794
6965
  }
@@ -6927,7 +7098,7 @@ async function acquireRefreshLock(accountId, options = {}) {
6927
7098
  const handle = await fs2.open(lockPath, "wx", 384);
6928
7099
  try {
6929
7100
  await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt: Date.now(), owner }), "utf-8");
6930
- const stat = await handle.stat();
7101
+ const stat = await handle.stat({ bigint: true });
6931
7102
  return { acquired: true, lockPath, owner, lockInode: stat.ino };
6932
7103
  } finally {
6933
7104
  await handle.close();
@@ -6938,8 +7109,8 @@ async function acquireRefreshLock(accountId, options = {}) {
6938
7109
  throw error;
6939
7110
  }
6940
7111
  try {
6941
- const stat = await fs2.stat(lockPath);
6942
- if (Date.now() - stat.mtimeMs > staleMs) {
7112
+ const stat = await fs2.stat(lockPath, { bigint: true });
7113
+ if (Date.now() - Number(stat.mtimeMs) > staleMs) {
6943
7114
  await fs2.unlink(lockPath);
6944
7115
  continue;
6945
7116
  }
@@ -6956,7 +7127,7 @@ async function acquireRefreshLock(accountId, options = {}) {
6956
7127
  async function releaseRefreshLock(lock) {
6957
7128
  const lockPath = typeof lock === "string" || lock === null ? lock : lock.lockPath;
6958
7129
  const owner = typeof lock === "object" && lock ? lock.owner || null : null;
6959
- const lockInode = typeof lock === "object" && lock ? lock.lockInode || null : null;
7130
+ const lockInode = typeof lock === "object" && lock ? lock.lockInode ?? null : null;
6960
7131
  if (!lockPath) return;
6961
7132
  if (owner) {
6962
7133
  try {
@@ -6966,7 +7137,7 @@ async function releaseRefreshLock(lock) {
6966
7137
  return;
6967
7138
  }
6968
7139
  if (lockInode) {
6969
- const stat = await fs2.stat(lockPath);
7140
+ const stat = await fs2.stat(lockPath, { bigint: true });
6970
7141
  if (stat.ino !== lockInode) {
6971
7142
  return;
6972
7143
  }
@@ -7178,6 +7349,7 @@ function formatSwitchReason(status, reason) {
7178
7349
 
7179
7350
  // src/bun-fetch.ts
7180
7351
  import { execFileSync, spawn } from "node:child_process";
7352
+ import { randomUUID as randomUUID2 } from "node:crypto";
7181
7353
  import { existsSync as existsSync5 } from "node:fs";
7182
7354
  import { dirname as dirname4, join as join6 } from "node:path";
7183
7355
  import * as readline from "node:readline";
@@ -7397,20 +7569,38 @@ function buildProxyRequestInit(input, init) {
7397
7569
  ...signal ? { signal } : {}
7398
7570
  };
7399
7571
  }
7572
+ var DEBUG_DUMP_DIR = "/tmp";
7573
+ var DEBUG_LATEST_REQUEST_PATH = `${DEBUG_DUMP_DIR}/opencode-last-request.json`;
7574
+ var DEBUG_LATEST_HEADERS_PATH = `${DEBUG_DUMP_DIR}/opencode-last-headers.json`;
7575
+ function makeDebugDumpId() {
7576
+ const filesystemSafeTimestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7577
+ const subMillisecondCollisionGuard = randomUUID2().slice(0, 8);
7578
+ return `${filesystemSafeTimestamp}-${subMillisecondCollisionGuard}`;
7579
+ }
7400
7580
  async function writeDebugArtifacts(url, init) {
7401
7581
  if (!init.body || !url.includes("/v1/messages") || url.includes("count_tokens")) {
7402
- return;
7582
+ return null;
7403
7583
  }
7404
7584
  const { writeFileSync: writeFileSync5 } = await import("node:fs");
7405
- writeFileSync5(
7406
- "/tmp/opencode-last-request.json",
7407
- typeof init.body === "string" ? init.body : JSON.stringify(init.body)
7408
- );
7585
+ const id = makeDebugDumpId();
7586
+ const requestPath = `${DEBUG_DUMP_DIR}/opencode-request-${id}.json`;
7587
+ const headersPath = `${DEBUG_DUMP_DIR}/opencode-headers-${id}.json`;
7588
+ const bodyText = typeof init.body === "string" ? init.body : JSON.stringify(init.body);
7409
7589
  const logHeaders = {};
7410
7590
  toHeaders(init.headers).forEach((value, key) => {
7411
7591
  logHeaders[key] = key === "authorization" ? "Bearer ***" : value;
7412
7592
  });
7413
- writeFileSync5("/tmp/opencode-last-headers.json", JSON.stringify(logHeaders, null, 2));
7593
+ const headersText = JSON.stringify(logHeaders, null, 2);
7594
+ writeFileSync5(requestPath, bodyText);
7595
+ writeFileSync5(headersPath, headersText);
7596
+ writeFileSync5(DEBUG_LATEST_REQUEST_PATH, bodyText);
7597
+ writeFileSync5(DEBUG_LATEST_HEADERS_PATH, headersText);
7598
+ return {
7599
+ requestPath,
7600
+ headersPath,
7601
+ latestRequestPath: DEBUG_LATEST_REQUEST_PATH,
7602
+ latestHeadersPath: DEBUG_LATEST_HEADERS_PATH
7603
+ };
7414
7604
  }
7415
7605
  function createBunFetch(options = {}) {
7416
7606
  const breaker = createCircuitBreaker({
@@ -7624,9 +7814,11 @@ function createBunFetch(options = {}) {
7624
7814
  }
7625
7815
  if (resolveDebug(debugOverride)) {
7626
7816
  try {
7627
- await writeDebugArtifacts(url, init ?? {});
7628
- if ((init?.body ?? null) !== null && url.includes("/v1/messages") && !url.includes("count_tokens")) {
7629
- console.error("[opencode-anthropic-auth] Dumped request to /tmp/opencode-last-request.json");
7817
+ const dumped = await writeDebugArtifacts(url, init ?? {});
7818
+ if (dumped) {
7819
+ console.error(
7820
+ `[opencode-anthropic-auth] Dumped request to ${dumped.requestPath} (latest alias: ${dumped.latestRequestPath})`
7821
+ );
7630
7822
  }
7631
7823
  } catch (error) {
7632
7824
  console.error("[opencode-anthropic-auth] Failed to dump request:", error);
@@ -7944,7 +8136,7 @@ async function AnthropicAuthPlugin({
7944
8136
  return bunFetchInstance.fetch(input, init);
7945
8137
  };
7946
8138
  let claudeCliVersion = FALLBACK_CLAUDE_CLI_VERSION;
7947
- const signatureSessionId = randomUUID2();
8139
+ const signatureSessionId = randomUUID3();
7948
8140
  const signatureUserId = getOrCreateSignatureUserId();
7949
8141
  if (shouldFetchClaudeCodeVersion) {
7950
8142
  fetchLatestClaudeCodeVersion().then((version) => {
@@ -8250,12 +8442,33 @@ ${message}`);
8250
8442
  }
8251
8443
  let response;
8252
8444
  const fetchInput = requestInput;
8253
- try {
8254
- response = await fetchWithTransport(fetchInput, {
8445
+ const buildTransportRequestInit = (headers, requestBody, forceFreshConnection) => {
8446
+ const requestHeadersForTransport = new Headers(headers);
8447
+ if (forceFreshConnection) {
8448
+ requestHeadersForTransport.set("connection", "close");
8449
+ requestHeadersForTransport.set("x-proxy-disable-keepalive", "true");
8450
+ } else {
8451
+ requestHeadersForTransport.delete("connection");
8452
+ requestHeadersForTransport.delete("x-proxy-disable-keepalive");
8453
+ }
8454
+ return {
8255
8455
  ...requestInit,
8256
- body,
8257
- headers: requestHeaders
8258
- });
8456
+ body: requestBody,
8457
+ headers: requestHeadersForTransport,
8458
+ ...forceFreshConnection ? { keepalive: false } : {}
8459
+ };
8460
+ };
8461
+ try {
8462
+ response = await fetchWithRetry(
8463
+ async ({ forceFreshConnection }) => fetchWithTransport(
8464
+ fetchInput,
8465
+ buildTransportRequestInit(requestHeaders, body, forceFreshConnection)
8466
+ ),
8467
+ {
8468
+ maxRetries: 2,
8469
+ shouldRetryResponse: () => false
8470
+ }
8471
+ );
8259
8472
  } catch (err) {
8260
8473
  const fetchError = err instanceof Error ? err : new Error(String(err));
8261
8474
  if (accountManager && account) {
@@ -8308,7 +8521,7 @@ ${message}`);
8308
8521
  });
8309
8522
  let retryCount = 0;
8310
8523
  const retried = await fetchWithRetry(
8311
- async () => {
8524
+ async ({ forceFreshConnection }) => {
8312
8525
  if (retryCount === 0) {
8313
8526
  retryCount += 1;
8314
8527
  return response;
@@ -8318,11 +8531,10 @@ ${message}`);
8318
8531
  retryCount += 1;
8319
8532
  const retryUrl = fetchInput instanceof Request ? fetchInput.url : fetchInput.toString();
8320
8533
  const retryBody = requestContext.preparedBody === void 0 ? void 0 : cloneBodyForRetry(requestContext.preparedBody);
8321
- return fetchWithTransport(retryUrl, {
8322
- ...requestInit,
8323
- body: retryBody,
8324
- headers: headersForRetry
8325
- });
8534
+ return fetchWithTransport(
8535
+ retryUrl,
8536
+ buildTransportRequestInit(headersForRetry, retryBody, forceFreshConnection)
8537
+ );
8326
8538
  },
8327
8539
  { maxRetries: 2 }
8328
8540
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vacbo/opencode-anthropic-fix",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "main": "dist/opencode-anthropic-auth-plugin.js",
5
5
  "bin": {
6
6
  "opencode-anthropic-auth": "dist/opencode-anthropic-auth-cli.mjs",
@@ -20,6 +20,8 @@
20
20
  "build": "bun scripts/build.ts",
21
21
  "prepublishOnly": "npm run build && npm test",
22
22
  "test": "vitest run",
23
+ "check:cch-drift": "bun scripts/drift-checker/check-cch-constants.ts --fail-on-drift",
24
+ "check:cch-drift:npm": "bun scripts/drift-checker/check-cch-constants.ts --npm-version latest --fail-on-drift",
23
25
  "test:watch": "vitest",
24
26
  "lint": "eslint .",
25
27
  "lint:fix": "eslint --fix .",
@@ -57,7 +57,7 @@ describe("CC 2.1.98 — Full request fingerprint comparison", () => {
57
57
  console.log(`║ Axios version: 1.13.6 (token endpoint UA)`);
58
58
  console.log(`║ anthropic-ver: 2023-06-01`);
59
59
  console.log(`║ x-app: cli`);
60
- console.log(`║ cch: 00000 (placeholder, not attested on Node.js)`);
60
+ console.log(`║ cch: 00000 placeholder xxHash64(serialized body, seed 0x6E52736AC806831E)`);
61
61
  console.log(`║ Identity: You are Claude Code, Anthropic's official CLI for Claude.`);
62
62
  console.log(`║ Identity cache: {"type":"ephemeral","scope":"global","ttl":"1h"}`);
63
63
  console.log(`║ Client ID: 9d1c250a-e61b-44d9-88ed-5944d1962f5e`);
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ EXPECTED_CCH_PLACEHOLDER,
5
+ EXPECTED_CCH_SALT,
6
+ EXPECTED_CCH_SEED,
7
+ EXPECTED_XXHASH64_PRIMES,
8
+ bigintToLittleEndianBytes,
9
+ findAllOccurrences,
10
+ scanCchConstants,
11
+ } from "../drift/cch-constants.js";
12
+
13
+ describe("cch drift checker helpers", () => {
14
+ it("encodes bigint constants as little-endian bytes", () => {
15
+ expect(Array.from(bigintToLittleEndianBytes(EXPECTED_CCH_SEED))).toEqual([
16
+ 0x1e, 0x83, 0x06, 0xc8, 0x6a, 0x73, 0x52, 0x6e,
17
+ ]);
18
+ });
19
+
20
+ it("finds all occurrences of a byte pattern", () => {
21
+ const haystack = new Uint8Array([1, 2, 3, 1, 2, 3, 4]);
22
+ const needle = new Uint8Array([1, 2, 3]);
23
+ expect(findAllOccurrences(haystack, needle)).toEqual([0, 3]);
24
+ });
25
+ });
26
+
27
+ describe("scanCchConstants", () => {
28
+ it("passes when all standalone constants are present", () => {
29
+ const bytes = new Uint8Array([
30
+ ...new TextEncoder().encode(`xx cch=${EXPECTED_CCH_PLACEHOLDER} yy ${EXPECTED_CCH_SALT} zz`),
31
+ ...bigintToLittleEndianBytes(EXPECTED_CCH_SEED),
32
+ ...EXPECTED_XXHASH64_PRIMES.flatMap((prime) => Array.from(bigintToLittleEndianBytes(prime))),
33
+ ]);
34
+
35
+ const report = scanCchConstants(bytes, "synthetic-standalone", "standalone");
36
+ expect(report.passed).toBe(true);
37
+ expect(report.findings).toEqual([]);
38
+ expect(report.checked.placeholder).toBeGreaterThan(0);
39
+ expect(report.checked.salt).toBeGreaterThan(0);
40
+ expect(report.checked.seed).toBeGreaterThan(0);
41
+ expect(report.checked.primes.every((count) => count > 0)).toBe(true);
42
+ });
43
+
44
+ it("fails critically when placeholder and seed are missing", () => {
45
+ const bytes = new TextEncoder().encode(`missing ${EXPECTED_CCH_SALT}`);
46
+ const report = scanCchConstants(bytes, "broken-standalone", "standalone");
47
+
48
+ expect(report.passed).toBe(false);
49
+ expect(report.findings.map((finding) => finding.name)).toContain("cch placeholder");
50
+ expect(report.findings.map((finding) => finding.name)).toContain("native cch seed");
51
+ });
52
+
53
+ it("only requires placeholder and salt for npm cli bundles", () => {
54
+ const bytes = new TextEncoder().encode(`prefix cch=${EXPECTED_CCH_PLACEHOLDER}; ${EXPECTED_CCH_SALT} suffix`);
55
+ const report = scanCchConstants(bytes, "synthetic-bundle", "bundle");
56
+
57
+ expect(report.passed).toBe(true);
58
+ expect(report.checked.seed).toBe(0);
59
+ expect(report.findings).toEqual([]);
60
+ });
61
+ });
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { buildAnthropicBillingHeader } from "../headers/billing.js";
4
+ import { CCH_PLACEHOLDER, computeNativeStyleCch, replaceNativeStyleCch, xxHash64 } from "../headers/cch.js";
5
+ import { transformRequestBody } from "../request/body.js";
6
+
7
+ describe("native-style cch", () => {
8
+ it("matches independently verified xxHash64 reference values", () => {
9
+ expect(xxHash64(new TextEncoder().encode(""))).toBe(0x6e798575d82647edn);
10
+ expect(xxHash64(new TextEncoder().encode("a"))).toBe(0x8404a7f0a8a6bcaen);
11
+ expect(xxHash64(new TextEncoder().encode("hello"))).toBe(0x95ab2f66e009922an);
12
+ });
13
+
14
+ it("derives the expected 5-char cch from independently verified vectors", () => {
15
+ expect(computeNativeStyleCch("")).toBe("647ed");
16
+ expect(computeNativeStyleCch("cch=00000")).toBe("1d7f8");
17
+ expect(
18
+ computeNativeStyleCch("x-anthropic-billing-header: cc_version=2.1.101.0e7; cc_entrypoint=cli; cch=00000;"),
19
+ ).toBe("101fb");
20
+ });
21
+
22
+ it("replaces the placeholder in a serialized body", () => {
23
+ const serialized =
24
+ '{"system":[{"type":"text","text":"x-anthropic-billing-header: cc_version=2.1.101.0e7; cc_entrypoint=cli; cch=00000;"}],"messages":[{"role":"user","content":"Reply with the single word: OK"}]}';
25
+
26
+ expect(replaceNativeStyleCch(serialized)).toContain("cch=0ca30");
27
+ expect(replaceNativeStyleCch(serialized)).not.toContain(`cch=${CCH_PLACEHOLDER}`);
28
+ });
29
+
30
+ it("leaves strings without the placeholder unchanged", () => {
31
+ const serialized = '{"system":[{"type":"text","text":"no billing block here"}]}';
32
+ expect(replaceNativeStyleCch(serialized)).toBe(serialized);
33
+ });
34
+ });
35
+
36
+ describe("billing header placeholder + transformRequestBody replacement", () => {
37
+ const signature = {
38
+ enabled: true,
39
+ claudeCliVersion: "2.1.101",
40
+ promptCompactionMode: "minimal" as const,
41
+ sanitizeSystemPrompt: false,
42
+ };
43
+ const runtime = {
44
+ persistentUserId: "0".repeat(64),
45
+ accountId: "acct-1",
46
+ sessionId: "session-1",
47
+ };
48
+
49
+ it("buildAnthropicBillingHeader emits the native placeholder", () => {
50
+ process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = "true";
51
+ const header = buildAnthropicBillingHeader("2.1.101", [{ role: "user", content: "Hello world from a test" }]);
52
+ expect(header).toContain(`cch=${CCH_PLACEHOLDER};`);
53
+ });
54
+
55
+ it("transformRequestBody replaces the placeholder after serialization", () => {
56
+ const body = JSON.stringify({
57
+ model: "claude-sonnet-4-5",
58
+ max_tokens: 1024,
59
+ stream: false,
60
+ messages: [{ role: "user", content: "Reply with the single word: OK" }],
61
+ system: "You are a helpful assistant.",
62
+ });
63
+
64
+ const transformed = transformRequestBody(body, signature, runtime);
65
+ expect(transformed).toBeDefined();
66
+ expect(transformed).not.toContain(`cch=${CCH_PLACEHOLDER}`);
67
+
68
+ const actualCch = transformed?.match(/cch=([0-9a-f]{5})/)?.[1];
69
+ const placeholderBody = transformed?.replace(/cch=[0-9a-f]{5}/, `cch=${CCH_PLACEHOLDER}`);
70
+
71
+ expect(actualCch).toBeDefined();
72
+ expect(placeholderBody).toBeDefined();
73
+ expect(actualCch).toBe(computeNativeStyleCch(placeholderBody!));
74
+ expect(transformed).toBe(replaceNativeStyleCch(placeholderBody!));
75
+ });
76
+ });
@@ -176,10 +176,9 @@ describe("CC 2.1.98 — Billing header", () => {
176
176
  restoreEnv(originalEnv);
177
177
  });
178
178
 
179
- it("contains a 5-char hex cch value (not the literal 00000 placeholder)", () => {
179
+ it("contains the native placeholder cch before post-serialization replacement", () => {
180
180
  const header = buildAnthropicBillingHeader(CC_VERSION, []);
181
- expect(header).toMatch(/cch=[0-9a-f]{5};/);
182
- expect(header).not.toContain("cch=00000;");
181
+ expect(header).toContain("cch=00000;");
183
182
  });
184
183
 
185
184
  it("contains the correct cc_version", () => {
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  calculateBackoffMs,
4
4
  isAccountSpecificError,
5
+ isRetriableNetworkError,
5
6
  parseRateLimitReason,
6
7
  parseRetryAfterHeader,
7
8
  parseRetryAfterMsHeader,
@@ -235,6 +236,30 @@ describe("parseRateLimitReason", () => {
235
236
  });
236
237
  });
237
238
 
239
+ // ---------------------------------------------------------------------------
240
+ // isRetriableNetworkError
241
+ // ---------------------------------------------------------------------------
242
+
243
+ describe("isRetriableNetworkError", () => {
244
+ it("returns true for retryable connection reset codes", () => {
245
+ const error = Object.assign(new Error("socket died"), {
246
+ code: "ECONNRESET",
247
+ });
248
+
249
+ expect(isRetriableNetworkError(error)).toBe(true);
250
+ });
251
+
252
+ it("returns true for Bun proxy upstream reset messages", () => {
253
+ expect(isRetriableNetworkError(new Error("Bun proxy upstream error: Connection reset by server"))).toBe(true);
254
+ });
255
+
256
+ it("returns false for user abort errors", () => {
257
+ const error = new DOMException("The operation was aborted", "AbortError");
258
+
259
+ expect(isRetriableNetworkError(error)).toBe(false);
260
+ });
261
+ });
262
+
238
263
  // ---------------------------------------------------------------------------
239
264
  // calculateBackoffMs
240
265
  // ---------------------------------------------------------------------------