@vacbo/opencode-anthropic-fix 0.0.44 → 0.1.1
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 +1816 -594
- package/package.json +1 -1
- 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 -174
- 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 +31 -13
- 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,235 +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);
|
|
90
239
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
|
257
|
+
|
|
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
|
+
});
|
|
348
|
+
|
|
349
|
+
return state.startPromise;
|
|
350
|
+
};
|
|
131
351
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
373
|
+
breaker.dispose();
|
|
374
|
+
reportStatus("shutdown-requested");
|
|
375
|
+
};
|
|
142
376
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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);
|
|
147
385
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
proxyPort = FIXED_PORT;
|
|
151
|
-
console.error("[bun-fetch] Reusing existing Bun proxy on port", FIXED_PORT);
|
|
152
|
-
return proxyPort;
|
|
153
|
-
}
|
|
386
|
+
return globalThis.fetch(input, init);
|
|
387
|
+
};
|
|
154
388
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
389
|
+
const fetchFromActiveProxy = async (useForwardFetch: boolean): Promise<Response> => {
|
|
390
|
+
const port = state.activePort;
|
|
391
|
+
if (port === null) {
|
|
392
|
+
return fetchNative("proxy-port-missing");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (resolveDebug(debugOverride)) {
|
|
396
|
+
console.error(`[opencode-anthropic-auth] Routing through Bun proxy at :${port} → ${url}`);
|
|
397
|
+
}
|
|
161
398
|
|
|
162
|
-
|
|
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
|
+
}
|
|
163
409
|
|
|
164
|
-
|
|
165
|
-
|
|
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;
|
|
170
|
-
}
|
|
410
|
+
const proxyInit = buildProxyRequestInit(input, init);
|
|
411
|
+
const forwardFetch = state.activeChild?.forwardFetch;
|
|
171
412
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
proxyProcess = null;
|
|
176
|
-
}
|
|
177
|
-
proxyPort = null;
|
|
178
|
-
starting = null;
|
|
179
|
-
killStaleProxy();
|
|
180
|
-
}
|
|
413
|
+
const response = await (useForwardFetch && typeof forwardFetch === "function"
|
|
414
|
+
? forwardFetch(`http://${DEFAULT_PROXY_HOST}:${port}/`, proxyInit)
|
|
415
|
+
: fetch(`http://${DEFAULT_PROXY_HOST}:${port}/`, proxyInit));
|
|
181
416
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
): Promise<Response> {
|
|
190
|
-
const port = await ensureBunProxy();
|
|
191
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
417
|
+
if (response.status === 502) {
|
|
418
|
+
const errorText = await response.text();
|
|
419
|
+
throw new Error(`Bun proxy upstream error: ${errorText}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return response;
|
|
423
|
+
};
|
|
192
424
|
|
|
193
|
-
|
|
425
|
+
if (state.activeChild && state.activePort !== null && !state.activeChild.killed) {
|
|
426
|
+
return fetchFromActiveProxy(true);
|
|
427
|
+
}
|
|
194
428
|
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
}
|
|
197
436
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
};
|
|
449
|
+
|
|
450
|
+
state.pendingFetches.push(pendingFetch);
|
|
451
|
+
|
|
452
|
+
void startProxy(debugOverride).catch(() => {
|
|
453
|
+
state.pendingFetches = state.pendingFetches.filter((candidate) => candidate !== pendingFetch);
|
|
454
|
+
pendingFetch.runNative("proxy-start-error");
|
|
455
|
+
});
|
|
203
456
|
});
|
|
457
|
+
};
|
|
204
458
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
+
}
|
|
210
471
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
stopBunProxy();
|
|
219
|
-
const newPort = await ensureBunProxy();
|
|
220
|
-
if (newPort) {
|
|
221
|
-
healthCheckFails = 0;
|
|
222
|
-
const retryHeaders = new Headers(init.headers);
|
|
223
|
-
retryHeaders.set("x-proxy-url", url);
|
|
224
|
-
return fetch(`http://127.0.0.1:${newPort}/`, {
|
|
225
|
-
method: init.method || "POST",
|
|
226
|
-
headers: retryHeaders,
|
|
227
|
-
body: init.body,
|
|
228
|
-
});
|
|
472
|
+
const defaultBunFetch = (() => {
|
|
473
|
+
let instance: BunFetchInternal | null = null;
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
get(): BunFetchInternal {
|
|
477
|
+
if (!instance) {
|
|
478
|
+
instance = createBunFetch() as BunFetchInternal;
|
|
229
479
|
}
|
|
230
|
-
}
|
|
231
480
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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);
|
|
235
508
|
}
|