@vacbo/opencode-anthropic-fix 0.0.43 → 0.0.45
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/bun-proxy.mjs +1 -1
- package/dist/opencode-anthropic-auth-plugin.js +175 -42
- package/package.json +1 -1
- package/src/bun-fetch.ts +173 -34
- package/src/bun-proxy.ts +1 -1
- package/src/headers/billing.ts +21 -9
package/dist/bun-proxy.mjs
CHANGED
|
@@ -4,10 +4,16 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __getProtoOf = Object.getPrototypeOf;
|
|
6
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
7
13
|
var __esm = (fn, res) => function __init() {
|
|
8
14
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
15
|
};
|
|
10
|
-
var __commonJS = (cb, mod) => function
|
|
16
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
11
17
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
12
18
|
};
|
|
13
19
|
var __export = (target, all) => {
|
|
@@ -5558,14 +5564,25 @@ function buildAnthropicBillingHeader(claudeCliVersion, messages) {
|
|
|
5558
5564
|
let versionSuffix = "";
|
|
5559
5565
|
if (Array.isArray(messages)) {
|
|
5560
5566
|
const firstUserMsg = messages.find(
|
|
5561
|
-
(m) => m !== null && typeof m === "object" && m.role === "user"
|
|
5567
|
+
(m) => m !== null && typeof m === "object" && m.role === "user"
|
|
5562
5568
|
);
|
|
5563
5569
|
if (firstUserMsg) {
|
|
5564
|
-
|
|
5565
|
-
const
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5570
|
+
let text = "";
|
|
5571
|
+
const content = firstUserMsg.content;
|
|
5572
|
+
if (typeof content === "string") {
|
|
5573
|
+
text = content;
|
|
5574
|
+
} else if (Array.isArray(content)) {
|
|
5575
|
+
const textBlock = content.find((b) => b.type === "text");
|
|
5576
|
+
if (textBlock && typeof textBlock.text === "string") {
|
|
5577
|
+
text = textBlock.text;
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
if (text) {
|
|
5581
|
+
const salt = "59cf53e54c78";
|
|
5582
|
+
const picked = [4, 7, 20].map((i) => i < text.length ? text[i] : "0").join("");
|
|
5583
|
+
const hash = createHash2("sha256").update(salt + picked + claudeCliVersion).digest("hex");
|
|
5584
|
+
versionSuffix = `.${hash.slice(0, 3)}`;
|
|
5585
|
+
}
|
|
5569
5586
|
}
|
|
5570
5587
|
}
|
|
5571
5588
|
const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT || "cli";
|
|
@@ -6375,12 +6392,28 @@ function formatSwitchReason(status, reason) {
|
|
|
6375
6392
|
|
|
6376
6393
|
// src/bun-fetch.ts
|
|
6377
6394
|
import { execFileSync, spawn } from "node:child_process";
|
|
6378
|
-
import { existsSync as existsSync5 } from "node:fs";
|
|
6395
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync } from "node:fs";
|
|
6379
6396
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
6397
|
+
import { tmpdir } from "node:os";
|
|
6380
6398
|
import { fileURLToPath } from "node:url";
|
|
6381
6399
|
var proxyPort = null;
|
|
6382
6400
|
var proxyProcess = null;
|
|
6383
6401
|
var starting = null;
|
|
6402
|
+
var healthCheckFails = 0;
|
|
6403
|
+
var FIXED_PORT = 48372;
|
|
6404
|
+
var PID_FILE = join6(tmpdir(), "opencode-bun-proxy.pid");
|
|
6405
|
+
var MAX_HEALTH_FAILS = 2;
|
|
6406
|
+
var exitHandlerRegistered = false;
|
|
6407
|
+
function registerExitHandler() {
|
|
6408
|
+
if (exitHandlerRegistered) return;
|
|
6409
|
+
exitHandlerRegistered = true;
|
|
6410
|
+
const cleanup = () => {
|
|
6411
|
+
stopBunProxy();
|
|
6412
|
+
};
|
|
6413
|
+
process.on("exit", cleanup);
|
|
6414
|
+
process.on("SIGINT", cleanup);
|
|
6415
|
+
process.on("SIGTERM", cleanup);
|
|
6416
|
+
}
|
|
6384
6417
|
function findProxyScript() {
|
|
6385
6418
|
const dir = typeof __dirname !== "undefined" ? __dirname : dirname4(fileURLToPath(import.meta.url));
|
|
6386
6419
|
for (const candidate of [
|
|
@@ -6392,45 +6425,78 @@ function findProxyScript() {
|
|
|
6392
6425
|
}
|
|
6393
6426
|
return null;
|
|
6394
6427
|
}
|
|
6428
|
+
var _hasBun = null;
|
|
6395
6429
|
function hasBun() {
|
|
6430
|
+
if (_hasBun !== null) return _hasBun;
|
|
6396
6431
|
try {
|
|
6397
6432
|
execFileSync("which", ["bun"], { stdio: "ignore" });
|
|
6398
|
-
|
|
6433
|
+
_hasBun = true;
|
|
6434
|
+
} catch {
|
|
6435
|
+
_hasBun = false;
|
|
6436
|
+
}
|
|
6437
|
+
return _hasBun;
|
|
6438
|
+
}
|
|
6439
|
+
function killStaleProxy() {
|
|
6440
|
+
try {
|
|
6441
|
+
const raw = readFileSync6(PID_FILE, "utf-8").trim();
|
|
6442
|
+
const pid = parseInt(raw, 10);
|
|
6443
|
+
if (pid > 0) {
|
|
6444
|
+
try {
|
|
6445
|
+
process.kill(pid, "SIGTERM");
|
|
6446
|
+
} catch {
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
unlinkSync(PID_FILE);
|
|
6450
|
+
} catch {
|
|
6451
|
+
}
|
|
6452
|
+
}
|
|
6453
|
+
async function isProxyHealthy(port) {
|
|
6454
|
+
try {
|
|
6455
|
+
const resp = await fetch(`http://127.0.0.1:${port}/__health`, {
|
|
6456
|
+
signal: AbortSignal.timeout(2e3)
|
|
6457
|
+
});
|
|
6458
|
+
return resp.ok;
|
|
6399
6459
|
} catch {
|
|
6400
6460
|
return false;
|
|
6401
6461
|
}
|
|
6402
6462
|
}
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
if (proxyPort) return proxyPort;
|
|
6406
|
-
if (starting) return starting;
|
|
6407
|
-
starting = new Promise((resolve2) => {
|
|
6463
|
+
function spawnProxy() {
|
|
6464
|
+
return new Promise((resolve2) => {
|
|
6408
6465
|
const script = findProxyScript();
|
|
6409
6466
|
if (!script || !hasBun()) {
|
|
6410
6467
|
resolve2(null);
|
|
6411
|
-
starting = null;
|
|
6412
6468
|
return;
|
|
6413
6469
|
}
|
|
6470
|
+
killStaleProxy();
|
|
6414
6471
|
try {
|
|
6415
|
-
const child = spawn("bun", ["run", script,
|
|
6416
|
-
stdio: ["ignore", "pipe", "
|
|
6472
|
+
const child = spawn("bun", ["run", script, String(FIXED_PORT)], {
|
|
6473
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6417
6474
|
detached: false
|
|
6418
6475
|
});
|
|
6419
6476
|
proxyProcess = child;
|
|
6477
|
+
registerExitHandler();
|
|
6420
6478
|
let done = false;
|
|
6479
|
+
const finish = (port) => {
|
|
6480
|
+
if (done) return;
|
|
6481
|
+
done = true;
|
|
6482
|
+
if (port && child.pid) {
|
|
6483
|
+
try {
|
|
6484
|
+
writeFileSync5(PID_FILE, String(child.pid));
|
|
6485
|
+
} catch {
|
|
6486
|
+
}
|
|
6487
|
+
}
|
|
6488
|
+
resolve2(port);
|
|
6489
|
+
};
|
|
6421
6490
|
child.stdout?.on("data", (chunk) => {
|
|
6422
6491
|
const m = chunk.toString().match(/BUN_PROXY_PORT=(\d+)/);
|
|
6423
|
-
if (m
|
|
6424
|
-
done = true;
|
|
6492
|
+
if (m) {
|
|
6425
6493
|
proxyPort = parseInt(m[1], 10);
|
|
6426
|
-
|
|
6494
|
+
healthCheckFails = 0;
|
|
6495
|
+
finish(proxyPort);
|
|
6427
6496
|
}
|
|
6428
6497
|
});
|
|
6429
6498
|
child.on("error", () => {
|
|
6430
|
-
|
|
6431
|
-
done = true;
|
|
6432
|
-
resolve2(null);
|
|
6433
|
-
}
|
|
6499
|
+
finish(null);
|
|
6434
6500
|
proxyPort = null;
|
|
6435
6501
|
proxyProcess = null;
|
|
6436
6502
|
starting = null;
|
|
@@ -6439,35 +6505,102 @@ async function ensureBunProxy() {
|
|
|
6439
6505
|
proxyPort = null;
|
|
6440
6506
|
proxyProcess = null;
|
|
6441
6507
|
starting = null;
|
|
6442
|
-
|
|
6443
|
-
done = true;
|
|
6444
|
-
resolve2(null);
|
|
6445
|
-
}
|
|
6508
|
+
finish(null);
|
|
6446
6509
|
});
|
|
6447
|
-
setTimeout(() =>
|
|
6448
|
-
if (!done) {
|
|
6449
|
-
done = true;
|
|
6450
|
-
resolve2(null);
|
|
6451
|
-
}
|
|
6452
|
-
}, 5e3);
|
|
6510
|
+
setTimeout(() => finish(null), 5e3);
|
|
6453
6511
|
} catch {
|
|
6454
6512
|
resolve2(null);
|
|
6455
|
-
starting = null;
|
|
6456
6513
|
}
|
|
6457
6514
|
});
|
|
6458
|
-
|
|
6515
|
+
}
|
|
6516
|
+
async function ensureBunProxy() {
|
|
6517
|
+
if (process.env.VITEST || process.env.NODE_ENV === "test") return null;
|
|
6518
|
+
if (proxyPort && proxyProcess && !proxyProcess.killed) {
|
|
6519
|
+
return proxyPort;
|
|
6520
|
+
}
|
|
6521
|
+
if (!proxyPort && await isProxyHealthy(FIXED_PORT)) {
|
|
6522
|
+
proxyPort = FIXED_PORT;
|
|
6523
|
+
console.error("[bun-fetch] Reusing existing Bun proxy on port", FIXED_PORT);
|
|
6524
|
+
return proxyPort;
|
|
6525
|
+
}
|
|
6526
|
+
if (proxyPort && (!proxyProcess || proxyProcess.killed)) {
|
|
6527
|
+
proxyPort = null;
|
|
6528
|
+
proxyProcess = null;
|
|
6529
|
+
starting = null;
|
|
6530
|
+
}
|
|
6531
|
+
if (starting) return starting;
|
|
6532
|
+
starting = spawnProxy();
|
|
6533
|
+
const port = await starting;
|
|
6534
|
+
starting = null;
|
|
6535
|
+
if (port) console.error("[bun-fetch] Bun proxy started on port", port);
|
|
6536
|
+
else console.error("[bun-fetch] Failed to start Bun proxy, falling back to Node.js fetch");
|
|
6537
|
+
return port;
|
|
6538
|
+
}
|
|
6539
|
+
function stopBunProxy() {
|
|
6540
|
+
if (proxyProcess) {
|
|
6541
|
+
try {
|
|
6542
|
+
proxyProcess.kill();
|
|
6543
|
+
} catch {
|
|
6544
|
+
}
|
|
6545
|
+
proxyProcess = null;
|
|
6546
|
+
}
|
|
6547
|
+
proxyPort = null;
|
|
6548
|
+
starting = null;
|
|
6549
|
+
killStaleProxy();
|
|
6459
6550
|
}
|
|
6460
6551
|
async function fetchViaBun(input, init) {
|
|
6461
6552
|
const port = await ensureBunProxy();
|
|
6462
6553
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
6463
|
-
if (!port)
|
|
6554
|
+
if (!port) {
|
|
6555
|
+
console.error("[bun-fetch] WARNING: No proxy available, using Node.js fetch (will route to extra usage!)");
|
|
6556
|
+
return fetch(input, init);
|
|
6557
|
+
}
|
|
6558
|
+
console.error(`[bun-fetch] Routing through Bun proxy at :${port} \u2192 ${url}`);
|
|
6559
|
+
if (init.body && url.includes("/v1/messages") && !url.includes("count_tokens")) {
|
|
6560
|
+
try {
|
|
6561
|
+
const { writeFileSync: writeFileSync6 } = __require("node:fs");
|
|
6562
|
+
writeFileSync6("/tmp/opencode-last-request.json", typeof init.body === "string" ? init.body : JSON.stringify(init.body));
|
|
6563
|
+
const hdrs = {};
|
|
6564
|
+
init.headers.forEach((v, k) => {
|
|
6565
|
+
hdrs[k] = k === "authorization" ? "Bearer ***" : v;
|
|
6566
|
+
});
|
|
6567
|
+
writeFileSync6("/tmp/opencode-last-headers.json", JSON.stringify(hdrs, null, 2));
|
|
6568
|
+
console.error("[bun-fetch] Dumped request to /tmp/opencode-last-request.json");
|
|
6569
|
+
} catch {
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6464
6572
|
const headers = new Headers(init.headers);
|
|
6465
6573
|
headers.set("x-proxy-url", url);
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6574
|
+
try {
|
|
6575
|
+
const resp = await fetch(`http://127.0.0.1:${port}/`, {
|
|
6576
|
+
method: init.method || "POST",
|
|
6577
|
+
headers,
|
|
6578
|
+
body: init.body
|
|
6579
|
+
});
|
|
6580
|
+
if (resp.status === 502) {
|
|
6581
|
+
const errText = await resp.text();
|
|
6582
|
+
throw new Error(`Bun proxy upstream error: ${errText}`);
|
|
6583
|
+
}
|
|
6584
|
+
healthCheckFails = 0;
|
|
6585
|
+
return resp;
|
|
6586
|
+
} catch (err) {
|
|
6587
|
+
healthCheckFails++;
|
|
6588
|
+
if (healthCheckFails >= MAX_HEALTH_FAILS) {
|
|
6589
|
+
stopBunProxy();
|
|
6590
|
+
const newPort = await ensureBunProxy();
|
|
6591
|
+
if (newPort) {
|
|
6592
|
+
healthCheckFails = 0;
|
|
6593
|
+
const retryHeaders = new Headers(init.headers);
|
|
6594
|
+
retryHeaders.set("x-proxy-url", url);
|
|
6595
|
+
return fetch(`http://127.0.0.1:${newPort}/`, {
|
|
6596
|
+
method: init.method || "POST",
|
|
6597
|
+
headers: retryHeaders,
|
|
6598
|
+
body: init.body
|
|
6599
|
+
});
|
|
6600
|
+
}
|
|
6601
|
+
}
|
|
6602
|
+
throw err;
|
|
6603
|
+
}
|
|
6471
6604
|
}
|
|
6472
6605
|
|
|
6473
6606
|
// src/index.ts
|
package/package.json
CHANGED
package/src/bun-fetch.ts
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// Bun TLS proxy manager — spawns a Bun subprocess for BoringSSL TLS.
|
|
2
|
+
// Bun TLS proxy manager — spawns a single Bun subprocess for BoringSSL TLS.
|
|
3
|
+
// Hardened: health checks, auto-restart, single-instance guarantee.
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
|
|
5
6
|
import { execFileSync, spawn } from "node:child_process";
|
|
6
|
-
import { existsSync } from "node:fs";
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
7
8
|
import { dirname, join } from "node:path";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
8
10
|
import { fileURLToPath } from "node:url";
|
|
9
11
|
|
|
10
12
|
let proxyPort: number | null = null;
|
|
11
13
|
let proxyProcess: ReturnType<typeof spawn> | null = null;
|
|
12
14
|
let starting: Promise<number | null> | null = null;
|
|
15
|
+
let healthCheckFails = 0;
|
|
16
|
+
|
|
17
|
+
const FIXED_PORT = 48372;
|
|
18
|
+
const PID_FILE = join(tmpdir(), "opencode-bun-proxy.pid");
|
|
19
|
+
const MAX_HEALTH_FAILS = 2;
|
|
20
|
+
|
|
21
|
+
// Kill proxy when parent process exits
|
|
22
|
+
let exitHandlerRegistered = false;
|
|
23
|
+
function registerExitHandler(): void {
|
|
24
|
+
if (exitHandlerRegistered) return;
|
|
25
|
+
exitHandlerRegistered = true;
|
|
26
|
+
const cleanup = () => { stopBunProxy(); };
|
|
27
|
+
process.on("exit", cleanup);
|
|
28
|
+
process.on("SIGINT", cleanup);
|
|
29
|
+
process.on("SIGTERM", cleanup);
|
|
30
|
+
}
|
|
13
31
|
|
|
14
32
|
function findProxyScript(): string | null {
|
|
15
33
|
const dir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
|
|
@@ -23,75 +41,147 @@ function findProxyScript(): string | null {
|
|
|
23
41
|
return null;
|
|
24
42
|
}
|
|
25
43
|
|
|
44
|
+
let _hasBun: boolean | null = null;
|
|
26
45
|
function hasBun(): boolean {
|
|
46
|
+
if (_hasBun !== null) return _hasBun;
|
|
27
47
|
try {
|
|
28
48
|
execFileSync("which", ["bun"], { stdio: "ignore" });
|
|
29
|
-
|
|
49
|
+
_hasBun = true;
|
|
30
50
|
} catch {
|
|
31
|
-
|
|
51
|
+
_hasBun = false;
|
|
32
52
|
}
|
|
53
|
+
return _hasBun;
|
|
33
54
|
}
|
|
34
55
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
function killStaleProxy(): void {
|
|
57
|
+
try {
|
|
58
|
+
const raw = readFileSync(PID_FILE, "utf-8").trim();
|
|
59
|
+
const pid = parseInt(raw, 10);
|
|
60
|
+
if (pid > 0) {
|
|
61
|
+
try { process.kill(pid, "SIGTERM"); } catch { /* already dead */ }
|
|
62
|
+
}
|
|
63
|
+
unlinkSync(PID_FILE);
|
|
64
|
+
} catch {
|
|
65
|
+
// No PID file or already cleaned
|
|
66
|
+
}
|
|
67
|
+
}
|
|
40
68
|
|
|
41
|
-
|
|
69
|
+
async function isProxyHealthy(port: number): Promise<boolean> {
|
|
70
|
+
try {
|
|
71
|
+
const resp = await fetch(`http://127.0.0.1:${port}/__health`, {
|
|
72
|
+
signal: AbortSignal.timeout(2000),
|
|
73
|
+
});
|
|
74
|
+
return resp.ok;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function spawnProxy(): Promise<number | null> {
|
|
81
|
+
return new Promise<number | null>((resolve) => {
|
|
42
82
|
const script = findProxyScript();
|
|
43
83
|
if (!script || !hasBun()) {
|
|
44
84
|
resolve(null);
|
|
45
|
-
starting = null;
|
|
46
85
|
return;
|
|
47
86
|
}
|
|
48
87
|
|
|
88
|
+
// Kill any stale instance first
|
|
89
|
+
killStaleProxy();
|
|
90
|
+
|
|
49
91
|
try {
|
|
50
|
-
const child = spawn("bun", ["run", script,
|
|
51
|
-
stdio: ["ignore", "pipe", "
|
|
92
|
+
const child = spawn("bun", ["run", script, String(FIXED_PORT)], {
|
|
93
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
52
94
|
detached: false,
|
|
53
95
|
});
|
|
54
96
|
proxyProcess = child;
|
|
97
|
+
registerExitHandler();
|
|
55
98
|
|
|
56
99
|
let done = false;
|
|
100
|
+
const finish = (port: number | null) => {
|
|
101
|
+
if (done) return;
|
|
102
|
+
done = true;
|
|
103
|
+
if (port && child.pid) {
|
|
104
|
+
try { writeFileSync(PID_FILE, String(child.pid)); } catch { /* ok */ }
|
|
105
|
+
}
|
|
106
|
+
resolve(port);
|
|
107
|
+
};
|
|
108
|
+
|
|
57
109
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
58
110
|
const m = chunk.toString().match(/BUN_PROXY_PORT=(\d+)/);
|
|
59
|
-
if (m
|
|
60
|
-
done = true;
|
|
111
|
+
if (m) {
|
|
61
112
|
proxyPort = parseInt(m[1], 10);
|
|
62
|
-
|
|
113
|
+
healthCheckFails = 0;
|
|
114
|
+
finish(proxyPort);
|
|
63
115
|
}
|
|
64
116
|
});
|
|
117
|
+
|
|
65
118
|
child.on("error", () => {
|
|
66
|
-
|
|
67
|
-
proxyPort = null;
|
|
119
|
+
finish(null);
|
|
120
|
+
proxyPort = null;
|
|
121
|
+
proxyProcess = null;
|
|
122
|
+
starting = null;
|
|
68
123
|
});
|
|
124
|
+
|
|
69
125
|
child.on("exit", () => {
|
|
70
|
-
proxyPort = null;
|
|
71
|
-
|
|
126
|
+
proxyPort = null;
|
|
127
|
+
proxyProcess = null;
|
|
128
|
+
starting = null;
|
|
129
|
+
finish(null);
|
|
72
130
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
131
|
+
|
|
132
|
+
// Timeout
|
|
133
|
+
setTimeout(() => finish(null), 5000);
|
|
76
134
|
} catch {
|
|
77
135
|
resolve(null);
|
|
78
|
-
starting = null;
|
|
79
136
|
}
|
|
80
137
|
});
|
|
138
|
+
}
|
|
81
139
|
|
|
82
|
-
|
|
140
|
+
export async function ensureBunProxy(): Promise<number | null> {
|
|
141
|
+
if (process.env.VITEST || process.env.NODE_ENV === "test") return null;
|
|
142
|
+
|
|
143
|
+
// Fast path: proxy already running and healthy
|
|
144
|
+
if (proxyPort && proxyProcess && !proxyProcess.killed) {
|
|
145
|
+
return proxyPort;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check if a proxy is already running on the fixed port (from previous session)
|
|
149
|
+
if (!proxyPort && await isProxyHealthy(FIXED_PORT)) {
|
|
150
|
+
proxyPort = FIXED_PORT;
|
|
151
|
+
console.error("[bun-fetch] Reusing existing Bun proxy on port", FIXED_PORT);
|
|
152
|
+
return proxyPort;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Restart if previous instance died
|
|
156
|
+
if (proxyPort && (!proxyProcess || proxyProcess.killed)) {
|
|
157
|
+
proxyPort = null;
|
|
158
|
+
proxyProcess = null;
|
|
159
|
+
starting = null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (starting) return starting;
|
|
163
|
+
|
|
164
|
+
starting = spawnProxy();
|
|
165
|
+
const port = await starting;
|
|
166
|
+
starting = null;
|
|
167
|
+
if (port) console.error("[bun-fetch] Bun proxy started on port", port);
|
|
168
|
+
else console.error("[bun-fetch] Failed to start Bun proxy, falling back to Node.js fetch");
|
|
169
|
+
return port;
|
|
83
170
|
}
|
|
84
171
|
|
|
85
172
|
export function stopBunProxy(): void {
|
|
86
173
|
if (proxyProcess) {
|
|
87
|
-
try { proxyProcess.kill(); } catch { /*
|
|
88
|
-
proxyProcess = null;
|
|
174
|
+
try { proxyProcess.kill(); } catch { /* */ }
|
|
175
|
+
proxyProcess = null;
|
|
89
176
|
}
|
|
177
|
+
proxyPort = null;
|
|
178
|
+
starting = null;
|
|
179
|
+
killStaleProxy();
|
|
90
180
|
}
|
|
91
181
|
|
|
92
182
|
/**
|
|
93
183
|
* Fetch via Bun proxy for BoringSSL TLS fingerprint.
|
|
94
|
-
* Falls back to native
|
|
184
|
+
* Auto-restarts proxy on failure. Falls back to native fetch only if Bun is unavailable.
|
|
95
185
|
*/
|
|
96
186
|
export async function fetchViaBun(
|
|
97
187
|
input: string | URL | Request,
|
|
@@ -100,14 +190,63 @@ export async function fetchViaBun(
|
|
|
100
190
|
const port = await ensureBunProxy();
|
|
101
191
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
102
192
|
|
|
103
|
-
if (!port)
|
|
193
|
+
if (!port) {
|
|
194
|
+
console.error("[bun-fetch] WARNING: No proxy available, using Node.js fetch (will route to extra usage!)");
|
|
195
|
+
return fetch(input, init as RequestInit);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.error(`[bun-fetch] Routing through Bun proxy at :${port} → ${url}`);
|
|
199
|
+
|
|
200
|
+
// Dump full request for debugging
|
|
201
|
+
if (init.body && url.includes("/v1/messages") && !url.includes("count_tokens")) {
|
|
202
|
+
try {
|
|
203
|
+
const { writeFileSync } = require("node:fs");
|
|
204
|
+
writeFileSync("/tmp/opencode-last-request.json", typeof init.body === "string" ? init.body : JSON.stringify(init.body));
|
|
205
|
+
const hdrs: Record<string, string> = {};
|
|
206
|
+
init.headers.forEach((v: string, k: string) => { hdrs[k] = k === "authorization" ? "Bearer ***" : v; });
|
|
207
|
+
writeFileSync("/tmp/opencode-last-headers.json", JSON.stringify(hdrs, null, 2));
|
|
208
|
+
console.error("[bun-fetch] Dumped request to /tmp/opencode-last-request.json");
|
|
209
|
+
} catch { /* ignore */ }
|
|
210
|
+
}
|
|
104
211
|
|
|
105
212
|
const headers = new Headers(init.headers);
|
|
106
213
|
headers.set("x-proxy-url", url);
|
|
107
214
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
215
|
+
try {
|
|
216
|
+
const resp = await fetch(`http://127.0.0.1:${port}/`, {
|
|
217
|
+
method: init.method || "POST",
|
|
218
|
+
headers,
|
|
219
|
+
body: init.body,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Proxy returned a 502 — Bun proxy couldn't reach Anthropic
|
|
223
|
+
if (resp.status === 502) {
|
|
224
|
+
const errText = await resp.text();
|
|
225
|
+
throw new Error(`Bun proxy upstream error: ${errText}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
healthCheckFails = 0;
|
|
229
|
+
return resp;
|
|
230
|
+
} catch (err) {
|
|
231
|
+
healthCheckFails++;
|
|
232
|
+
|
|
233
|
+
// If proxy seems dead, restart it and retry once
|
|
234
|
+
if (healthCheckFails >= MAX_HEALTH_FAILS) {
|
|
235
|
+
stopBunProxy();
|
|
236
|
+
const newPort = await ensureBunProxy();
|
|
237
|
+
if (newPort) {
|
|
238
|
+
healthCheckFails = 0;
|
|
239
|
+
const retryHeaders = new Headers(init.headers);
|
|
240
|
+
retryHeaders.set("x-proxy-url", url);
|
|
241
|
+
return fetch(`http://127.0.0.1:${newPort}/`, {
|
|
242
|
+
method: init.method || "POST",
|
|
243
|
+
headers: retryHeaders,
|
|
244
|
+
body: init.body,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Final fallback: native fetch (will use Node TLS — not ideal but better than failing)
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
113
252
|
}
|
package/src/bun-proxy.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Standalone Bun TLS proxy — run with: bun dist/bun-proxy.mjs [port]
|
|
2
2
|
// Forwards requests using Bun's native fetch (BoringSSL TLS fingerprint).
|
|
3
3
|
|
|
4
|
-
const PORT = parseInt(process.argv[2] || "
|
|
4
|
+
const PORT = parseInt(process.argv[2] || "48372", 10);
|
|
5
5
|
|
|
6
6
|
const server = Bun.serve({
|
|
7
7
|
port: PORT,
|
package/src/headers/billing.ts
CHANGED
|
@@ -9,21 +9,33 @@ export function buildAnthropicBillingHeader(claudeCliVersion: string, messages:
|
|
|
9
9
|
// the CLI version, then taking the first 3 hex chars of that combined string.
|
|
10
10
|
let versionSuffix = "";
|
|
11
11
|
if (Array.isArray(messages)) {
|
|
12
|
+
// Find first user message (CC uses first non-meta user turn)
|
|
12
13
|
const firstUserMsg = messages.find(
|
|
13
14
|
(m) =>
|
|
14
15
|
m !== null &&
|
|
15
16
|
typeof m === "object" &&
|
|
16
|
-
(m as Record<string, unknown>).role === "user"
|
|
17
|
-
typeof (m as Record<string, unknown>).content === "string",
|
|
17
|
+
(m as Record<string, unknown>).role === "user",
|
|
18
18
|
) as Record<string, unknown> | undefined;
|
|
19
19
|
if (firstUserMsg) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
// Extract text from string or content-block array
|
|
21
|
+
let text = "";
|
|
22
|
+
const content = firstUserMsg.content;
|
|
23
|
+
if (typeof content === "string") {
|
|
24
|
+
text = content;
|
|
25
|
+
} else if (Array.isArray(content)) {
|
|
26
|
+
const textBlock = (content as Array<Record<string, unknown>>).find((b) => b.type === "text");
|
|
27
|
+
if (textBlock && typeof textBlock.text === "string") {
|
|
28
|
+
text = textBlock.text;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (text) {
|
|
32
|
+
const salt = "59cf53e54c78";
|
|
33
|
+
const picked = [4, 7, 20].map((i) => (i < text.length ? text[i] : "0")).join("");
|
|
34
|
+
const hash = createHash("sha256")
|
|
35
|
+
.update(salt + picked + claudeCliVersion)
|
|
36
|
+
.digest("hex");
|
|
37
|
+
versionSuffix = `.${hash.slice(0, 3)}`;
|
|
38
|
+
}
|
|
27
39
|
}
|
|
28
40
|
}
|
|
29
41
|
|