@vacbo/opencode-anthropic-fix 0.0.45 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/bun-proxy.mjs +282 -55
- package/dist/opencode-anthropic-auth-cli.mjs +194 -55
- package/dist/opencode-anthropic-auth-plugin.js +1801 -613
- package/package.json +4 -4
- package/src/__tests__/billing-edge-cases.test.ts +84 -0
- package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
- package/src/__tests__/debug-gating.test.ts +76 -0
- package/src/__tests__/decomposition-smoke.test.ts +92 -0
- package/src/__tests__/fingerprint-regression.test.ts +1 -1
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
- package/src/__tests__/helpers/conversation-history.ts +376 -0
- package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
- package/src/__tests__/helpers/deferred.ts +122 -0
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
- package/src/__tests__/helpers/in-memory-storage.ts +152 -0
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
- package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
- package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
- package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
- package/src/__tests__/helpers/sse.ts +288 -0
- package/src/__tests__/index.parallel.test.ts +711 -0
- package/src/__tests__/sanitization-regex.test.ts +65 -0
- package/src/__tests__/state-bounds.test.ts +110 -0
- package/src/account-identity.test.ts +213 -0
- package/src/account-identity.ts +108 -0
- package/src/accounts.dedup.test.ts +696 -0
- package/src/accounts.test.ts +2 -1
- package/src/accounts.ts +485 -191
- package/src/bun-fetch.test.ts +379 -0
- package/src/bun-fetch.ts +447 -191
- package/src/bun-proxy.ts +289 -57
- package/src/circuit-breaker.test.ts +274 -0
- package/src/circuit-breaker.ts +235 -0
- package/src/cli.test.ts +1 -0
- package/src/cli.ts +37 -18
- package/src/commands/router.ts +25 -5
- package/src/env.ts +1 -0
- package/src/headers/billing.ts +11 -5
- package/src/index.ts +224 -247
- package/src/oauth.ts +7 -1
- package/src/parent-pid-watcher.test.ts +219 -0
- package/src/parent-pid-watcher.ts +99 -0
- package/src/plugin-helpers.ts +112 -0
- package/src/refresh-helpers.ts +169 -0
- package/src/refresh-lock.test.ts +36 -9
- package/src/refresh-lock.ts +2 -2
- package/src/request/body.history.test.ts +398 -0
- package/src/request/body.ts +200 -13
- package/src/request/metadata.ts +6 -2
- package/src/response/index.ts +1 -1
- package/src/response/mcp.ts +60 -31
- package/src/response/streaming.test.ts +382 -0
- package/src/response/streaming.ts +403 -76
- package/src/storage.test.ts +127 -104
- package/src/storage.ts +152 -62
- package/src/system-prompt/builder.ts +33 -3
- package/src/system-prompt/sanitize.ts +12 -2
- package/src/token-refresh.test.ts +84 -1
- package/src/token-refresh.ts +14 -8
package/src/bun-fetch.ts
CHANGED
|
@@ -1,252 +1,508 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// Hardened: health checks, auto-restart, single-instance guarantee.
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
|
|
6
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
8
3
|
import { dirname, join } from "node:path";
|
|
9
|
-
import
|
|
4
|
+
import * as readline from "node:readline";
|
|
10
5
|
import { fileURLToPath } from "node:url";
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
import type { CircuitState } from "./circuit-breaker.js";
|
|
8
|
+
import { createCircuitBreaker } from "./circuit-breaker.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PROXY_HOST = "127.0.0.1";
|
|
11
|
+
const DEFAULT_STARTUP_TIMEOUT_MS = 5_000;
|
|
12
|
+
const DEFAULT_BREAKER_FAILURE_THRESHOLD = 2;
|
|
13
|
+
const DEFAULT_BREAKER_RESET_TIMEOUT_MS = 10_000;
|
|
14
|
+
|
|
15
|
+
type FetchInput = string | URL | Request;
|
|
16
|
+
type ForwardFetch = (input: FetchInput, init?: RequestInit) => Promise<Response>;
|
|
17
|
+
|
|
18
|
+
type ProxyChildProcess = ChildProcess & {
|
|
19
|
+
stdout: NodeJS.ReadableStream | null;
|
|
20
|
+
stderr: NodeJS.ReadableStream | null;
|
|
21
|
+
forwardFetch?: ForwardFetch;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface BunFetchStatus {
|
|
25
|
+
status: "state" | "fallback";
|
|
26
|
+
mode: "native" | "starting" | "proxy";
|
|
27
|
+
port: number | null;
|
|
28
|
+
bunAvailable: boolean | null;
|
|
29
|
+
childPid: number | null;
|
|
30
|
+
circuitState: CircuitState;
|
|
31
|
+
circuitFailureCount: number;
|
|
32
|
+
reason: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BunFetchOptions {
|
|
36
|
+
debug?: boolean;
|
|
37
|
+
onProxyStatus?: (status: BunFetchStatus) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface BunFetchInstance {
|
|
41
|
+
fetch: (input: FetchInput, init?: RequestInit) => Promise<Response>;
|
|
42
|
+
shutdown: () => Promise<void>;
|
|
43
|
+
getStatus: () => BunFetchStatus;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface BunFetchInternal extends BunFetchInstance {
|
|
47
|
+
ensureProxy: (debugOverride?: boolean) => Promise<number | null>;
|
|
48
|
+
fetchWithDebug: (input: FetchInput, init?: RequestInit, debugOverride?: boolean) => Promise<Response>;
|
|
49
|
+
}
|
|
16
50
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
51
|
+
interface StartProxyResult {
|
|
52
|
+
child: ProxyChildProcess;
|
|
53
|
+
port: number;
|
|
54
|
+
}
|
|
20
55
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
56
|
+
interface InstanceState {
|
|
57
|
+
activeChild: ProxyChildProcess | null;
|
|
58
|
+
activePort: number | null;
|
|
59
|
+
startingChild: ProxyChildProcess | null;
|
|
60
|
+
startPromise: Promise<number | null> | null;
|
|
61
|
+
bunAvailable: boolean | null;
|
|
62
|
+
pendingFetches: Array<{
|
|
63
|
+
runProxy: (useForwardFetch: boolean) => void;
|
|
64
|
+
runNative: (reason: string) => void;
|
|
65
|
+
}>;
|
|
30
66
|
}
|
|
31
67
|
|
|
32
68
|
function findProxyScript(): string | null {
|
|
33
69
|
const dir = typeof __dirname !== "undefined" ? __dirname : dirname(fileURLToPath(import.meta.url));
|
|
70
|
+
|
|
34
71
|
for (const candidate of [
|
|
35
72
|
join(dir, "bun-proxy.mjs"),
|
|
36
73
|
join(dir, "..", "dist", "bun-proxy.mjs"),
|
|
37
74
|
join(dir, "bun-proxy.ts"),
|
|
38
75
|
]) {
|
|
39
|
-
if (existsSync(candidate))
|
|
76
|
+
if (existsSync(candidate)) {
|
|
77
|
+
return candidate;
|
|
78
|
+
}
|
|
40
79
|
}
|
|
80
|
+
|
|
41
81
|
return null;
|
|
42
82
|
}
|
|
43
83
|
|
|
44
|
-
|
|
45
|
-
function hasBun(): boolean {
|
|
46
|
-
if (_hasBun !== null) return _hasBun;
|
|
84
|
+
function detectBunAvailability(): boolean {
|
|
47
85
|
try {
|
|
48
|
-
execFileSync("
|
|
49
|
-
|
|
86
|
+
execFileSync("bun", ["--version"], { stdio: "ignore" });
|
|
87
|
+
return true;
|
|
50
88
|
} catch {
|
|
51
|
-
|
|
89
|
+
return false;
|
|
52
90
|
}
|
|
53
|
-
return _hasBun;
|
|
54
91
|
}
|
|
55
92
|
|
|
56
|
-
function
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
function toHeaders(headersInit?: RequestInit["headers"]): Headers {
|
|
94
|
+
return new Headers(headersInit ?? undefined);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function toRequestUrl(input: FetchInput): string {
|
|
98
|
+
return typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveProxySignal(input: FetchInput, init?: RequestInit): AbortSignal | undefined {
|
|
102
|
+
if (init?.signal) {
|
|
103
|
+
return init.signal;
|
|
66
104
|
}
|
|
105
|
+
|
|
106
|
+
return input instanceof Request ? input.signal : undefined;
|
|
67
107
|
}
|
|
68
108
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
109
|
+
function buildProxyRequestInit(input: FetchInput, init?: RequestInit): RequestInit {
|
|
110
|
+
const targetUrl = toRequestUrl(input);
|
|
111
|
+
const headers = toHeaders(init?.headers);
|
|
112
|
+
const signal = resolveProxySignal(input, init);
|
|
113
|
+
headers.set("x-proxy-url", targetUrl);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
...init,
|
|
117
|
+
headers,
|
|
118
|
+
...(signal ? { signal } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function writeDebugArtifacts(url: string, init: RequestInit): Promise<void> {
|
|
123
|
+
if (!init.body || !url.includes("/v1/messages") || url.includes("count_tokens")) {
|
|
124
|
+
return;
|
|
77
125
|
}
|
|
126
|
+
|
|
127
|
+
const { writeFileSync } = await import("node:fs");
|
|
128
|
+
writeFileSync(
|
|
129
|
+
"/tmp/opencode-last-request.json",
|
|
130
|
+
typeof init.body === "string" ? init.body : JSON.stringify(init.body),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const logHeaders: Record<string, string> = {};
|
|
134
|
+
toHeaders(init.headers).forEach((value, key) => {
|
|
135
|
+
logHeaders[key] = key === "authorization" ? "Bearer ***" : value;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
writeFileSync("/tmp/opencode-last-headers.json", JSON.stringify(logHeaders, null, 2));
|
|
78
139
|
}
|
|
79
140
|
|
|
80
|
-
function
|
|
81
|
-
|
|
141
|
+
export function createBunFetch(options: BunFetchOptions = {}): BunFetchInstance {
|
|
142
|
+
const breaker = createCircuitBreaker({
|
|
143
|
+
failureThreshold: DEFAULT_BREAKER_FAILURE_THRESHOLD,
|
|
144
|
+
resetTimeoutMs: DEFAULT_BREAKER_RESET_TIMEOUT_MS,
|
|
145
|
+
});
|
|
146
|
+
const closingChildren = new WeakSet<ProxyChildProcess>();
|
|
147
|
+
const defaultDebug = options.debug ?? false;
|
|
148
|
+
const onProxyStatus = options.onProxyStatus;
|
|
149
|
+
const state: InstanceState = {
|
|
150
|
+
activeChild: null,
|
|
151
|
+
activePort: null,
|
|
152
|
+
startingChild: null,
|
|
153
|
+
startPromise: null,
|
|
154
|
+
bunAvailable: null,
|
|
155
|
+
pendingFetches: [],
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const getStatus = (reason = "idle", status: BunFetchStatus["status"] = "state"): BunFetchStatus => ({
|
|
159
|
+
status,
|
|
160
|
+
mode: state.activePort !== null ? "proxy" : state.startPromise ? "starting" : "native",
|
|
161
|
+
port: state.activePort,
|
|
162
|
+
bunAvailable: state.bunAvailable,
|
|
163
|
+
childPid: state.activeChild?.pid ?? state.startingChild?.pid ?? null,
|
|
164
|
+
circuitState: breaker.getState(),
|
|
165
|
+
circuitFailureCount: breaker.getFailureCount(),
|
|
166
|
+
reason,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const reportStatus = (reason: string): void => {
|
|
170
|
+
onProxyStatus?.(getStatus(reason));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const reportFallback = (reason: string, _debugOverride?: boolean): void => {
|
|
174
|
+
onProxyStatus?.(getStatus(reason, "fallback"));
|
|
175
|
+
console.error(
|
|
176
|
+
`[opencode-anthropic-auth] Native fetch fallback engaged (${reason}); Bun proxy fingerprint mimicry disabled for this request`,
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const resolveDebug = (debugOverride?: boolean): boolean => debugOverride ?? defaultDebug;
|
|
181
|
+
|
|
182
|
+
const clearActiveProxy = (child: ProxyChildProcess | null): void => {
|
|
183
|
+
if (child && state.activeChild === child) {
|
|
184
|
+
state.activeChild = null;
|
|
185
|
+
state.activePort = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (child && state.startingChild === child) {
|
|
189
|
+
state.startingChild = null;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const flushPendingFetches = (mode: "proxy" | "native", reason = "proxy-unavailable"): void => {
|
|
194
|
+
const pendingFetches = state.pendingFetches.splice(0, state.pendingFetches.length);
|
|
195
|
+
const useForwardFetch = pendingFetches.length <= 2;
|
|
196
|
+
for (const pendingFetch of pendingFetches) {
|
|
197
|
+
if (mode === "proxy") {
|
|
198
|
+
pendingFetch.runProxy(useForwardFetch);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
pendingFetch.runNative(reason);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const startProxy = async (debugOverride?: boolean): Promise<number | null> => {
|
|
207
|
+
if (state.activeChild && state.activePort !== null && !state.activeChild.killed) {
|
|
208
|
+
return state.activePort;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (state.startPromise) {
|
|
212
|
+
return state.startPromise;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!breaker.canExecute()) {
|
|
216
|
+
reportStatus("breaker-open");
|
|
217
|
+
flushPendingFetches("native", "breaker-open");
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (state.bunAvailable === false) {
|
|
222
|
+
reportStatus("bun-unavailable");
|
|
223
|
+
flushPendingFetches("native", "bun-unavailable");
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
82
227
|
const script = findProxyScript();
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
228
|
+
state.bunAvailable = detectBunAvailability();
|
|
229
|
+
|
|
230
|
+
if (!script || !state.bunAvailable) {
|
|
231
|
+
breaker.recordFailure();
|
|
232
|
+
reportStatus(script ? "bun-unavailable" : "proxy-script-missing");
|
|
233
|
+
flushPendingFetches("native", script ? "bun-unavailable" : "proxy-script-missing");
|
|
234
|
+
return null;
|
|
86
235
|
}
|
|
87
236
|
|
|
88
|
-
|
|
89
|
-
|
|
237
|
+
state.startPromise = new Promise<number | null>((resolve) => {
|
|
238
|
+
const debugEnabled = resolveDebug(debugOverride);
|
|
239
|
+
|
|
240
|
+
let child: ProxyChildProcess;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
child = spawn("bun", ["run", script, "--parent-pid", String(process.pid)], {
|
|
244
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
245
|
+
env: {
|
|
246
|
+
...process.env,
|
|
247
|
+
OPENCODE_ANTHROPIC_DEBUG: debugEnabled ? "1" : "0",
|
|
248
|
+
},
|
|
249
|
+
}) as ProxyChildProcess;
|
|
250
|
+
} catch {
|
|
251
|
+
breaker.recordFailure();
|
|
252
|
+
reportStatus("spawn-failed");
|
|
253
|
+
flushPendingFetches("native", "spawn-failed");
|
|
254
|
+
resolve(null);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
90
257
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
258
|
+
state.startingChild = child;
|
|
259
|
+
reportStatus("starting");
|
|
260
|
+
|
|
261
|
+
const stdout = child.stdout;
|
|
262
|
+
if (!stdout) {
|
|
263
|
+
clearActiveProxy(child);
|
|
264
|
+
breaker.recordFailure();
|
|
265
|
+
reportStatus("stdout-missing");
|
|
266
|
+
flushPendingFetches("native", "stdout-missing");
|
|
267
|
+
resolve(null);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let settled = false;
|
|
272
|
+
const stdoutLines = readline.createInterface({ input: stdout });
|
|
273
|
+
const startupTimeout = setTimeout(() => {
|
|
274
|
+
finalize(null, "startup-timeout");
|
|
275
|
+
}, DEFAULT_STARTUP_TIMEOUT_MS);
|
|
276
|
+
|
|
277
|
+
startupTimeout.unref?.();
|
|
278
|
+
|
|
279
|
+
const cleanupStartupResources = (): void => {
|
|
280
|
+
clearTimeout(startupTimeout);
|
|
281
|
+
stdoutLines.close();
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const finalize = (result: StartProxyResult | null, reason: string): void => {
|
|
285
|
+
if (settled) {
|
|
286
|
+
return;
|
|
105
287
|
}
|
|
106
|
-
|
|
288
|
+
|
|
289
|
+
settled = true;
|
|
290
|
+
cleanupStartupResources();
|
|
291
|
+
|
|
292
|
+
if (result) {
|
|
293
|
+
state.startingChild = null;
|
|
294
|
+
state.activeChild = result.child;
|
|
295
|
+
state.activePort = result.port;
|
|
296
|
+
breaker.recordSuccess();
|
|
297
|
+
reportStatus(reason);
|
|
298
|
+
flushPendingFetches("proxy");
|
|
299
|
+
resolve(result.port);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
clearActiveProxy(child);
|
|
304
|
+
breaker.recordFailure();
|
|
305
|
+
reportStatus(reason);
|
|
306
|
+
flushPendingFetches("native", reason);
|
|
307
|
+
resolve(null);
|
|
107
308
|
};
|
|
108
309
|
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
healthCheckFails = 0;
|
|
114
|
-
finish(proxyPort);
|
|
310
|
+
stdoutLines.on("line", (line) => {
|
|
311
|
+
const match = line.match(/^BUN_PROXY_PORT=(\d+)$/);
|
|
312
|
+
if (!match) {
|
|
313
|
+
return;
|
|
115
314
|
}
|
|
315
|
+
|
|
316
|
+
finalize(
|
|
317
|
+
{
|
|
318
|
+
child,
|
|
319
|
+
port: Number.parseInt(match[1], 10),
|
|
320
|
+
},
|
|
321
|
+
"proxy-ready",
|
|
322
|
+
);
|
|
116
323
|
});
|
|
117
324
|
|
|
118
|
-
child.
|
|
119
|
-
|
|
120
|
-
proxyPort = null;
|
|
121
|
-
proxyProcess = null;
|
|
122
|
-
starting = null;
|
|
325
|
+
child.once("error", () => {
|
|
326
|
+
finalize(null, "child-error");
|
|
123
327
|
});
|
|
124
328
|
|
|
125
|
-
child.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
329
|
+
child.once("exit", () => {
|
|
330
|
+
const shutdownOwned = closingChildren.has(child);
|
|
331
|
+
const isCurrentChild = state.activeChild === child || state.startingChild === child;
|
|
332
|
+
|
|
333
|
+
clearActiveProxy(child);
|
|
334
|
+
|
|
335
|
+
if (!settled) {
|
|
336
|
+
finalize(null, shutdownOwned ? "shutdown-complete" : "child-exit-before-ready");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (!shutdownOwned && isCurrentChild) {
|
|
341
|
+
breaker.recordFailure();
|
|
342
|
+
reportStatus("child-exited");
|
|
343
|
+
}
|
|
130
344
|
});
|
|
345
|
+
}).finally(() => {
|
|
346
|
+
state.startPromise = null;
|
|
347
|
+
});
|
|
131
348
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
349
|
+
return state.startPromise;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const shutdown = async (): Promise<void> => {
|
|
353
|
+
const children = [state.startingChild, state.activeChild].filter(
|
|
354
|
+
(child): child is ProxyChildProcess => child !== null,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
state.startPromise = null;
|
|
358
|
+
state.startingChild = null;
|
|
359
|
+
state.activeChild = null;
|
|
360
|
+
state.activePort = null;
|
|
361
|
+
|
|
362
|
+
for (const child of children) {
|
|
363
|
+
closingChildren.add(child);
|
|
364
|
+
if (!child.killed) {
|
|
365
|
+
try {
|
|
366
|
+
child.kill("SIGTERM");
|
|
367
|
+
} catch {
|
|
368
|
+
// Process may have already exited; ignore kill failures
|
|
369
|
+
}
|
|
370
|
+
}
|
|
136
371
|
}
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
372
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
373
|
+
breaker.dispose();
|
|
374
|
+
reportStatus("shutdown-requested");
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const fetchThroughProxy = async (
|
|
378
|
+
input: FetchInput,
|
|
379
|
+
init: RequestInit | undefined,
|
|
380
|
+
debugOverride?: boolean,
|
|
381
|
+
): Promise<Response> => {
|
|
382
|
+
const url = toRequestUrl(input);
|
|
383
|
+
const fetchNative = async (reason: string): Promise<Response> => {
|
|
384
|
+
reportFallback(reason, debugOverride);
|
|
385
|
+
|
|
386
|
+
return globalThis.fetch(input, init);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const fetchFromActiveProxy = async (useForwardFetch: boolean): Promise<Response> => {
|
|
390
|
+
const port = state.activePort;
|
|
391
|
+
if (port === null) {
|
|
392
|
+
return fetchNative("proxy-port-missing");
|
|
393
|
+
}
|
|
147
394
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.error("[bun-fetch] Reusing existing Bun proxy on port", FIXED_PORT);
|
|
152
|
-
return proxyPort;
|
|
153
|
-
}
|
|
395
|
+
if (resolveDebug(debugOverride)) {
|
|
396
|
+
console.error(`[opencode-anthropic-auth] Routing through Bun proxy at :${port} → ${url}`);
|
|
397
|
+
}
|
|
154
398
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
399
|
+
if (resolveDebug(debugOverride)) {
|
|
400
|
+
try {
|
|
401
|
+
await writeDebugArtifacts(url, init ?? {});
|
|
402
|
+
if ((init?.body ?? null) !== null && url.includes("/v1/messages") && !url.includes("count_tokens")) {
|
|
403
|
+
console.error("[opencode-anthropic-auth] Dumped request to /tmp/opencode-last-request.json");
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error("[opencode-anthropic-auth] Failed to dump request:", error);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
161
409
|
|
|
162
|
-
|
|
410
|
+
const proxyInit = buildProxyRequestInit(input, init);
|
|
411
|
+
const forwardFetch = state.activeChild?.forwardFetch;
|
|
163
412
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
170
|
-
}
|
|
413
|
+
const response = await (useForwardFetch && typeof forwardFetch === "function"
|
|
414
|
+
? forwardFetch(`http://${DEFAULT_PROXY_HOST}:${port}/`, proxyInit)
|
|
415
|
+
: fetch(`http://${DEFAULT_PROXY_HOST}:${port}/`, proxyInit));
|
|
171
416
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
proxyPort = null;
|
|
178
|
-
starting = null;
|
|
179
|
-
killStaleProxy();
|
|
180
|
-
}
|
|
417
|
+
if (response.status === 502) {
|
|
418
|
+
const errorText = await response.text();
|
|
419
|
+
throw new Error(`Bun proxy upstream error: ${errorText}`);
|
|
420
|
+
}
|
|
181
421
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
* Auto-restarts proxy on failure. Falls back to native fetch only if Bun is unavailable.
|
|
185
|
-
*/
|
|
186
|
-
export async function fetchViaBun(
|
|
187
|
-
input: string | URL | Request,
|
|
188
|
-
init: { headers: Headers; body?: string | null; method?: string; [k: string]: unknown },
|
|
189
|
-
): Promise<Response> {
|
|
190
|
-
const port = await ensureBunProxy();
|
|
191
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
422
|
+
return response;
|
|
423
|
+
};
|
|
192
424
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
425
|
+
if (state.activeChild && state.activePort !== null && !state.activeChild.killed) {
|
|
426
|
+
return fetchFromActiveProxy(true);
|
|
427
|
+
}
|
|
197
428
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
429
|
+
return new Promise<Response>((resolve, reject) => {
|
|
430
|
+
let settled = false;
|
|
431
|
+
const pendingFetch: InstanceState["pendingFetches"][number] = {
|
|
432
|
+
runProxy: (useForwardFetch: boolean) => {
|
|
433
|
+
if (settled) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
settled = true;
|
|
438
|
+
void fetchFromActiveProxy(useForwardFetch).then(resolve, reject);
|
|
439
|
+
},
|
|
440
|
+
runNative: (reason: string) => {
|
|
441
|
+
if (settled) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
settled = true;
|
|
446
|
+
void fetchNative(reason).then(resolve, reject);
|
|
447
|
+
},
|
|
448
|
+
};
|
|
211
449
|
|
|
212
|
-
|
|
213
|
-
headers.set("x-proxy-url", url);
|
|
450
|
+
state.pendingFetches.push(pendingFetch);
|
|
214
451
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
body: init.body,
|
|
452
|
+
void startProxy(debugOverride).catch(() => {
|
|
453
|
+
state.pendingFetches = state.pendingFetches.filter((candidate) => candidate !== pendingFetch);
|
|
454
|
+
pendingFetch.runNative("proxy-start-error");
|
|
455
|
+
});
|
|
220
456
|
});
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const instance: BunFetchInternal = {
|
|
460
|
+
fetch(input, init) {
|
|
461
|
+
return fetchThroughProxy(input, init);
|
|
462
|
+
},
|
|
463
|
+
ensureProxy: startProxy,
|
|
464
|
+
fetchWithDebug: fetchThroughProxy,
|
|
465
|
+
shutdown,
|
|
466
|
+
getStatus: () => getStatus(),
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
return instance;
|
|
470
|
+
}
|
|
221
471
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const errText = await resp.text();
|
|
225
|
-
throw new Error(`Bun proxy upstream error: ${errText}`);
|
|
226
|
-
}
|
|
472
|
+
const defaultBunFetch = (() => {
|
|
473
|
+
let instance: BunFetchInternal | null = null;
|
|
227
474
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
});
|
|
475
|
+
return {
|
|
476
|
+
get(): BunFetchInternal {
|
|
477
|
+
if (!instance) {
|
|
478
|
+
instance = createBunFetch() as BunFetchInternal;
|
|
246
479
|
}
|
|
247
|
-
}
|
|
248
480
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
481
|
+
return instance;
|
|
482
|
+
},
|
|
483
|
+
async reset(): Promise<void> {
|
|
484
|
+
if (!instance) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await instance.shutdown();
|
|
489
|
+
instance = null;
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
})();
|
|
493
|
+
|
|
494
|
+
export async function ensureBunProxy(debug: boolean): Promise<number | null> {
|
|
495
|
+
return defaultBunFetch.get().ensureProxy(debug);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export const stopBunProxy = (): void => {
|
|
499
|
+
void defaultBunFetch.reset();
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
export async function fetchViaBun(
|
|
503
|
+
input: FetchInput,
|
|
504
|
+
init: { headers: Headers; body?: string | null; method?: string; [key: string]: unknown },
|
|
505
|
+
debug: boolean,
|
|
506
|
+
): Promise<Response> {
|
|
507
|
+
return defaultBunFetch.get().fetchWithDebug(input, init as RequestInit & { headers: Headers }, debug);
|
|
252
508
|
}
|