@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9

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