@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.
- package/dist/opencode-anthropic-auth-plugin.js +258 -46
- package/package.json +3 -1
- package/src/__tests__/cc-comparison.test.ts +1 -1
- package/src/__tests__/cch-drift-checker.test.ts +61 -0
- package/src/__tests__/cch-native-style.test.ts +76 -0
- package/src/__tests__/fingerprint-regression.test.ts +2 -3
- package/src/backoff.test.ts +25 -0
- package/src/backoff.ts +83 -0
- package/src/bun-fetch.test.ts +103 -0
- package/src/bun-fetch.ts +42 -10
- package/src/bun-proxy.test.ts +37 -0
- package/src/bun-proxy.ts +11 -3
- package/src/drift/cch-constants.ts +133 -0
- package/src/headers/billing.ts +6 -33
- package/src/headers/cch.ts +120 -0
- package/src/index.ts +38 -11
- package/src/refresh-lock.test.ts +11 -3
- package/src/refresh-lock.ts +7 -7
- package/src/request/body.history.test.ts +9 -31
- package/src/request/body.ts +2 -1
- package/src/request/retry.test.ts +27 -0
- package/src/request/retry.ts +44 -9
- package/src/system-prompt/builder.ts +2 -1
|
@@ -3370,13 +3370,28 @@ var init_cli = __esm({
|
|
|
3370
3370
|
});
|
|
3371
3371
|
|
|
3372
3372
|
// src/index.ts
|
|
3373
|
-
import { randomUUID as
|
|
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
|
-
|
|
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,
|
|
6386
|
-
const resolvedConfig = { ...DEFAULT_RETRY_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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
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
|
-
|
|
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 (
|
|
7629
|
-
console.error(
|
|
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 =
|
|
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
|
-
|
|
8254
|
-
|
|
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:
|
|
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(
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
|
|
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.
|
|
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
|
|
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
|
|
179
|
+
it("contains the native placeholder cch before post-serialization replacement", () => {
|
|
180
180
|
const header = buildAnthropicBillingHeader(CC_VERSION, []);
|
|
181
|
-
expect(header).
|
|
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", () => {
|
package/src/backoff.test.ts
CHANGED
|
@@ -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
|
// ---------------------------------------------------------------------------
|