@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.
@@ -1,5 +1,5 @@
1
1
  // src/bun-proxy.ts
2
- var PORT = parseInt(process.argv[2] || "0", 10);
2
+ var PORT = parseInt(process.argv[2] || "48372", 10);
3
3
  var server = Bun.serve({
4
4
  port: PORT,
5
5
  async fetch(req) {
@@ -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 __require() {
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" && typeof m.content === "string"
5567
+ (m) => m !== null && typeof m === "object" && m.role === "user"
5562
5568
  );
5563
5569
  if (firstUserMsg) {
5564
- const text = firstUserMsg.content;
5565
- const salt = "59cf53e54c78";
5566
- const picked = [4, 7, 20].map((i) => i < text.length ? text[i] : "0").join("");
5567
- const hash = createHash2("sha256").update(salt + picked + claudeCliVersion).digest("hex");
5568
- versionSuffix = `.${hash.slice(0, 3)}`;
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
- return true;
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
- async function ensureBunProxy() {
6404
- if (process.env.VITEST || process.env.NODE_ENV === "test") return null;
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, "0"], {
6416
- stdio: ["ignore", "pipe", "ignore"],
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 && !done) {
6424
- done = true;
6492
+ if (m) {
6425
6493
  proxyPort = parseInt(m[1], 10);
6426
- resolve2(proxyPort);
6494
+ healthCheckFails = 0;
6495
+ finish(proxyPort);
6427
6496
  }
6428
6497
  });
6429
6498
  child.on("error", () => {
6430
- if (!done) {
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
- if (!done) {
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
- return starting;
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) return fetch(input, init);
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
- return fetch(`http://127.0.0.1:${port}/`, {
6467
- method: init.method || "POST",
6468
- headers,
6469
- body: init.body
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vacbo/opencode-anthropic-fix",
3
- "version": "0.0.43",
3
+ "version": "0.0.45",
4
4
  "main": "./dist/opencode-anthropic-auth-plugin.js",
5
5
  "bin": {
6
6
  "opencode-anthropic-auth": "./dist/opencode-anthropic-auth-cli.mjs",
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
- return true;
49
+ _hasBun = true;
30
50
  } catch {
31
- return false;
51
+ _hasBun = false;
32
52
  }
53
+ return _hasBun;
33
54
  }
34
55
 
35
- export async function ensureBunProxy(): Promise<number | null> {
36
- // Skip proxy in test environments
37
- if (process.env.VITEST || process.env.NODE_ENV === "test") return null;
38
- if (proxyPort) return proxyPort;
39
- if (starting) return starting;
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
- starting = new Promise<number | null>((resolve) => {
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, "0"], {
51
- stdio: ["ignore", "pipe", "ignore"],
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 && !done) {
60
- done = true;
111
+ if (m) {
61
112
  proxyPort = parseInt(m[1], 10);
62
- resolve(proxyPort);
113
+ healthCheckFails = 0;
114
+ finish(proxyPort);
63
115
  }
64
116
  });
117
+
65
118
  child.on("error", () => {
66
- if (!done) { done = true; resolve(null); }
67
- proxyPort = null; proxyProcess = null; starting = null;
119
+ finish(null);
120
+ proxyPort = null;
121
+ proxyProcess = null;
122
+ starting = null;
68
123
  });
124
+
69
125
  child.on("exit", () => {
70
- proxyPort = null; proxyProcess = null; starting = null;
71
- if (!done) { done = true; resolve(null); }
126
+ proxyPort = null;
127
+ proxyProcess = null;
128
+ starting = null;
129
+ finish(null);
72
130
  });
73
- setTimeout(() => {
74
- if (!done) { done = true; resolve(null); }
75
- }, 5000);
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
- return starting;
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 { /* already dead */ }
88
- proxyProcess = null; proxyPort = null; starting = 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 Node fetch if Bun is unavailable.
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) return fetch(input, init as RequestInit);
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
- return fetch(`http://127.0.0.1:${port}/`, {
109
- method: init.method || "POST",
110
- headers,
111
- body: init.body,
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] || "0", 10);
4
+ const PORT = parseInt(process.argv[2] || "48372", 10);
5
5
 
6
6
  const server = Bun.serve({
7
7
  port: PORT,
@@ -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
- const text = firstUserMsg.content as string;
21
- const salt = "59cf53e54c78";
22
- const picked = [4, 7, 20].map((i) => (i < text.length ? text[i] : "0")).join("");
23
- const hash = createHash("sha256")
24
- .update(salt + picked + claudeCliVersion)
25
- .digest("hex");
26
- versionSuffix = `.${hash.slice(0, 3)}`;
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