@bman654/clodex 1.0.4 → 1.1.0

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 CHANGED
@@ -14,6 +14,8 @@ You can also run clodex as a local OpenAI-compatible endpoint in front of your C
14
14
 
15
15
  > clodex is derived from the original [relay-ai](https://github.com/jacob-bd/relay-ai) project, heavily modified and streamlined for this one use case, with the full commit history preserved.
16
16
 
17
+ Contributions are welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) for how to scope a PR and what the quality bar is.
18
+
17
19
  ## Quick Start (ChatGPT/Codex plan)
18
20
 
19
21
  ```bash
@@ -13,7 +13,11 @@ import { constants as osConstants } from "os";
13
13
 
14
14
  // src/wrapper-env.ts
15
15
  var PROXY_ENV_VARS = ["HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy"];
16
+ var REQUIRE_SERVER_ENV = "CLODEX_REQUIRE_SERVER";
16
17
  var LOCAL_GATEWAY_API_KEY = "clodex-local";
18
+ function wrapperRequiresServer(env) {
19
+ return env[REQUIRE_SERVER_ENV] === "1";
20
+ }
17
21
  function computeWrapperEnv(baseEnv, state) {
18
22
  const env = { ...baseEnv };
19
23
  if (!state) return env;
@@ -55,16 +59,18 @@ function portIsOpen(port, timeoutMs = 100) {
55
59
  }
56
60
  async function main() {
57
61
  const argv = process.argv.slice(2);
58
- let claudePath;
59
- let claudeArgs;
60
- if (argv[0] && isExecutableFile(argv[0])) {
62
+ const checkOnly = argv[0] === "--check";
63
+ let claudePath = null;
64
+ let claudeArgs = [];
65
+ if (checkOnly) {
66
+ } else if (argv[0] && isExecutableFile(argv[0])) {
61
67
  claudePath = argv[0];
62
68
  claudeArgs = argv.slice(1);
63
69
  } else {
64
70
  claudePath = findClaudeBinary();
65
71
  claudeArgs = argv;
66
72
  }
67
- if (!claudePath) {
73
+ if (!checkOnly && !claudePath) {
68
74
  process.stderr.write("clodex-claude: could not find the claude binary (set CLODEX_CLAUDE_PATH)\n");
69
75
  process.exit(127);
70
76
  }
@@ -75,6 +81,11 @@ async function main() {
75
81
  break;
76
82
  }
77
83
  }
84
+ if (checkOnly) process.exit(state ? 0 : 1);
85
+ if (!state && wrapperRequiresServer(process.env)) {
86
+ process.stderr.write("clodex-claude: no live clodex server is available\n");
87
+ process.exit(1);
88
+ }
78
89
  const env = computeWrapperEnv(process.env, state);
79
90
  const child = spawn(claudePath, claudeArgs, {
80
91
  stdio: "inherit",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/claude-wrapper.ts","../src/wrapper-env.ts"],"sourcesContent":["// src/claude-wrapper.ts — the `clodex-claude` bin.\n//\n// A tiny, fast exec-style wrapper around the Claude Code binary that injects\n// bridge env for a running standalone `clodex server` (discovered via\n// ~/.clodex/server-runtime.json). Two invocation shapes:\n//\n// 1. CLAUDE_CODE_PROCESS_WRAPPER contract: Claude Code invokes\n// `clodex-claude <claude-binary-path> <args...>` for every process it\n// spawns (agents view sessions, background agents). First arg is the\n// claude binary to exec.\n// 2. Direct terminal use: `clodex-claude [args...]` — the claude binary is\n// discovered the same way `clodex claude` discovers it\n// (CLODEX_CLAUDE_PATH override, config override, PATH, fallbacks).\n//\n// With a live proxy-mode server: HTTPS_PROXY/HTTP_PROXY + NODE_EXTRA_CA_CERTS\n// point at it and ANTHROPIC_BASE_URL is removed (claude keeps its own\n// Anthropic auth — this is the recommended mode). With a live endpoint-mode\n// server: ANTHROPIC_BASE_URL points at the gateway. With no live server the\n// env is passed through untouched, so claude always launches.\n//\n// This file must stay a thin shell over pure helpers (wrapper-env.ts,\n// server-runtime.ts) with minimal imports — it runs for every spawned agent.\n\nimport { spawn } from 'node:child_process';\nimport { accessSync, constants as fsConstants, statSync } from 'node:fs';\nimport { connect } from 'node:net';\nimport { constants as osConstants } from 'node:os';\nimport {\n orderWrapperServerCandidates,\n readLiveServerRuntimeStates,\n type ServerRuntimeState,\n} from './server-runtime.js';\nimport { computeWrapperEnv } from './wrapper-env.js';\nimport { findClaudeBinary } from './launch.js';\n\nconst isWindows = process.platform === 'win32';\n\nfunction isExecutableFile(path: string): boolean {\n try {\n if (!statSync(path).isFile()) return false;\n if (!isWindows) accessSync(path, fsConstants.X_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Fast TCP probe — the state file can outlive a SIGKILLed listener. Never hangs. */\nfunction portIsOpen(port: number, timeoutMs = 100): Promise<boolean> {\n return new Promise(resolve => {\n const socket = connect({ host: '127.0.0.1', port });\n const done = (result: boolean) => {\n socket.destroy();\n resolve(result);\n };\n socket.setTimeout(timeoutMs, () => done(false));\n socket.once('connect', () => done(true));\n socket.once('error', () => done(false));\n });\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n\n let claudePath: string | null;\n let claudeArgs: string[];\n if (argv[0] && isExecutableFile(argv[0])) {\n // CLAUDE_CODE_PROCESS_WRAPPER shape: first arg is the claude binary path.\n claudePath = argv[0];\n claudeArgs = argv.slice(1);\n } else {\n claudePath = findClaudeBinary();\n claudeArgs = argv;\n }\n\n if (!claudePath) {\n process.stderr.write('clodex-claude: could not find the claude binary (set CLODEX_CLAUDE_PATH)\\n');\n process.exit(127);\n }\n\n // Selection policy (see orderWrapperServerCandidates): proxy-mode servers\n // are preferred over endpoint-mode ones — bridging keeps Claude Code's own\n // Anthropic auth — with newest startedAt breaking ties within a mode. The\n // first candidate whose port answers the TCP probe wins; if only an\n // endpoint server is live it is used; with none, claude launches untouched.\n let state: ServerRuntimeState | null = null;\n for (const candidate of orderWrapperServerCandidates(readLiveServerRuntimeStates())) {\n if (await portIsOpen(candidate.port)) {\n state = candidate;\n break;\n }\n }\n const env = computeWrapperEnv(process.env, state);\n\n const child = spawn(claudePath, claudeArgs, {\n stdio: 'inherit',\n env,\n shell: isWindows,\n });\n\n const forward = (signal: NodeJS.Signals) => child.kill(signal);\n process.once('SIGINT', () => forward('SIGINT'));\n process.once('SIGTERM', () => forward('SIGTERM'));\n\n child.on('error', err => {\n process.stderr.write(`clodex-claude: failed to launch ${claudePath}: ${err.message}\\n`);\n process.exit(127);\n });\n child.on('exit', (code, signal) => {\n if (signal) {\n const signum = osConstants.signals[signal as keyof typeof osConstants.signals];\n process.exit(signum ? 128 + signum : 1);\n }\n process.exit(code ?? 0);\n });\n}\n\nvoid main();\n","// src/wrapper-env.ts\n//\n// Pure env computation for the `clodex-claude` wrapper bin. Given the process\n// env and a live `clodex server` runtime state (or null), returns the env to\n// launch the Claude Code binary with. Kept dependency-free so the wrapper\n// stays tiny and fast — it runs for every Claude-Code-spawned agent process.\n\nimport type { ServerRuntimeState } from './server-runtime.js';\n\nconst PROXY_ENV_VARS = ['HTTPS_PROXY', 'HTTP_PROXY', 'https_proxy', 'http_proxy'] as const;\n\n/**\n * Any non-empty key satisfies the local endpoint gateway (`isAuthorized`\n * accepts everything when no server password is set, i.e. local listen mode).\n */\nexport const LOCAL_GATEWAY_API_KEY = 'clodex-local';\n\nexport function computeWrapperEnv(\n baseEnv: NodeJS.ProcessEnv,\n state: ServerRuntimeState | null,\n): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...baseEnv };\n // No live server: launch claude completely untouched — a down server must\n // never break launching claude.\n if (!state) return env;\n\n if (state.mode === 'proxy') {\n // Selective MITM: claude keeps its own Anthropic credentials; the proxy\n // routes clodex:/alias models to OpenAI and passes everything else through.\n const proxyUrl = `http://127.0.0.1:${state.port}`;\n delete env['ANTHROPIC_BASE_URL'];\n for (const name of PROXY_ENV_VARS) env[name] = proxyUrl;\n if (state.caPath) env['NODE_EXTRA_CA_CERTS'] = state.caPath;\n return env;\n }\n\n // Endpoint gateway: all traffic goes to the local Anthropic-format gateway.\n for (const name of PROXY_ENV_VARS) delete env[name];\n env['ANTHROPIC_BASE_URL'] = `http://127.0.0.1:${state.port}/anthropic`;\n env['ANTHROPIC_API_KEY'] = LOCAL_GATEWAY_API_KEY;\n return env;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/claude-wrapper.ts","../src/wrapper-env.ts"],"sourcesContent":["// src/claude-wrapper.ts — the `clodex-claude` bin.\n//\n// A tiny, fast exec-style wrapper around the Claude Code binary that injects\n// bridge env for a running standalone `clodex server` (discovered via\n// ~/.clodex/server-runtime.json). Two invocation shapes:\n//\n// 1. CLAUDE_CODE_PROCESS_WRAPPER contract: Claude Code invokes\n// `clodex-claude <claude-binary-path> <args...>` for every process it\n// spawns (agents view sessions, background agents). First arg is the\n// claude binary to exec.\n// 2. Direct terminal use: `clodex-claude [args...]` — the claude binary is\n// discovered the same way `clodex claude` discovers it\n// (CLODEX_CLAUDE_PATH override, config override, PATH, fallbacks).\n//\n// With a live proxy-mode server: HTTPS_PROXY/HTTP_PROXY + NODE_EXTRA_CA_CERTS\n// point at it and ANTHROPIC_BASE_URL is removed (claude keeps its own\n// Anthropic auth — this is the recommended mode). With a live endpoint-mode\n// server: ANTHROPIC_BASE_URL points at the gateway. With no live server the\n// env is passed through untouched, so claude always launches.\n//\n// This file must stay a thin shell over pure helpers (wrapper-env.ts,\n// server-runtime.ts) with minimal imports — it runs for every spawned agent.\n\nimport { spawn } from 'node:child_process';\nimport { accessSync, constants as fsConstants, statSync } from 'node:fs';\nimport { connect } from 'node:net';\nimport { constants as osConstants } from 'node:os';\nimport {\n orderWrapperServerCandidates,\n readLiveServerRuntimeStates,\n type ServerRuntimeState,\n} from './server-runtime.js';\nimport { computeWrapperEnv, wrapperRequiresServer } from './wrapper-env.js';\nimport { findClaudeBinary } from './launch.js';\n\nconst isWindows = process.platform === 'win32';\n\nfunction isExecutableFile(path: string): boolean {\n try {\n if (!statSync(path).isFile()) return false;\n if (!isWindows) accessSync(path, fsConstants.X_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Fast TCP probe — the state file can outlive a SIGKILLed listener. Never hangs. */\nfunction portIsOpen(port: number, timeoutMs = 100): Promise<boolean> {\n return new Promise(resolve => {\n const socket = connect({ host: '127.0.0.1', port });\n const done = (result: boolean) => {\n socket.destroy();\n resolve(result);\n };\n socket.setTimeout(timeoutMs, () => done(false));\n socket.once('connect', () => done(true));\n socket.once('error', () => done(false));\n });\n}\n\nasync function main(): Promise<void> {\n const argv = process.argv.slice(2);\n const checkOnly = argv[0] === '--check';\n\n let claudePath: string | null = null;\n let claudeArgs: string[] = [];\n if (checkOnly) {\n // Readiness checks validate discovery and TCP state without launching Claude.\n } else if (argv[0] && isExecutableFile(argv[0])) {\n // CLAUDE_CODE_PROCESS_WRAPPER shape: first arg is the claude binary path.\n claudePath = argv[0];\n claudeArgs = argv.slice(1);\n } else {\n claudePath = findClaudeBinary();\n claudeArgs = argv;\n }\n\n if (!checkOnly && !claudePath) {\n process.stderr.write('clodex-claude: could not find the claude binary (set CLODEX_CLAUDE_PATH)\\n');\n process.exit(127);\n }\n\n // Selection policy (see orderWrapperServerCandidates): proxy-mode servers\n // are preferred over endpoint-mode ones — bridging keeps Claude Code's own\n // Anthropic auth — with newest startedAt breaking ties within a mode. The\n // first candidate whose port answers the TCP probe wins; if only an\n // endpoint server is live it is used; with none, claude launches untouched.\n let state: ServerRuntimeState | null = null;\n for (const candidate of orderWrapperServerCandidates(readLiveServerRuntimeStates())) {\n if (await portIsOpen(candidate.port)) {\n state = candidate;\n break;\n }\n }\n if (checkOnly) process.exit(state ? 0 : 1);\n if (!state && wrapperRequiresServer(process.env)) {\n process.stderr.write('clodex-claude: no live clodex server is available\\n');\n process.exit(1);\n }\n const env = computeWrapperEnv(process.env, state);\n\n const child = spawn(claudePath!, claudeArgs, {\n stdio: 'inherit',\n env,\n shell: isWindows,\n });\n\n const forward = (signal: NodeJS.Signals) => child.kill(signal);\n process.once('SIGINT', () => forward('SIGINT'));\n process.once('SIGTERM', () => forward('SIGTERM'));\n\n child.on('error', err => {\n process.stderr.write(`clodex-claude: failed to launch ${claudePath}: ${err.message}\\n`);\n process.exit(127);\n });\n child.on('exit', (code, signal) => {\n if (signal) {\n const signum = osConstants.signals[signal as keyof typeof osConstants.signals];\n process.exit(signum ? 128 + signum : 1);\n }\n process.exit(code ?? 0);\n });\n}\n\nvoid main();\n","// src/wrapper-env.ts\n//\n// Pure env computation for the `clodex-claude` wrapper bin. Given the process\n// env and a live `clodex server` runtime state (or null), returns the env to\n// launch the Claude Code binary with. Kept dependency-free so the wrapper\n// stays tiny and fast — it runs for every Claude-Code-spawned agent process.\n\nimport type { ServerRuntimeState } from './server-runtime.js';\n\nconst PROXY_ENV_VARS = ['HTTPS_PROXY', 'HTTP_PROXY', 'https_proxy', 'http_proxy'] as const;\nexport const REQUIRE_SERVER_ENV = 'CLODEX_REQUIRE_SERVER';\n\n/**\n * Any non-empty key satisfies the local endpoint gateway (`isAuthorized`\n * accepts everything when no server password is set, i.e. local listen mode).\n */\nexport const LOCAL_GATEWAY_API_KEY = 'clodex-local';\n\nexport function wrapperRequiresServer(env: NodeJS.ProcessEnv): boolean {\n return env[REQUIRE_SERVER_ENV] === '1';\n}\n\nexport function computeWrapperEnv(\n baseEnv: NodeJS.ProcessEnv,\n state: ServerRuntimeState | null,\n): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = { ...baseEnv };\n // No live server: launch claude completely untouched — a down server must\n // never break launching claude.\n if (!state) return env;\n\n if (state.mode === 'proxy') {\n // Selective MITM: claude keeps its own Anthropic credentials; the proxy\n // routes clodex:/alias models to OpenAI and passes everything else through.\n const proxyUrl = `http://127.0.0.1:${state.port}`;\n delete env['ANTHROPIC_BASE_URL'];\n for (const name of PROXY_ENV_VARS) env[name] = proxyUrl;\n if (state.caPath) env['NODE_EXTRA_CA_CERTS'] = state.caPath;\n return env;\n }\n\n // Endpoint gateway: all traffic goes to the local Anthropic-format gateway.\n for (const name of PROXY_ENV_VARS) delete env[name];\n env['ANTHROPIC_BASE_URL'] = `http://127.0.0.1:${state.port}/anthropic`;\n env['ANTHROPIC_API_KEY'] = LOCAL_GATEWAY_API_KEY;\n return env;\n}\n"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -202,7 +202,7 @@ import { join } from "path";
202
202
  // package.json
203
203
  var package_default = {
204
204
  name: "@bman654/clodex",
205
- version: "1.0.4",
205
+ version: "1.1.0",
206
206
  publishConfig: {
207
207
  access: "public"
208
208
  },
@@ -2338,6 +2338,111 @@ async function outboundWsProxyAgent(wsUrl) {
2338
2338
  return new HttpsProxyAgent(proxyUrl);
2339
2339
  }
2340
2340
 
2341
+ // src/upstream-error.ts
2342
+ import { APICallError, RetryError } from "ai";
2343
+ function sdkUpstreamErrorDetails(err) {
2344
+ const retry = RetryError.isInstance(err) ? err : void 0;
2345
+ const inner = retry?.lastError ?? err;
2346
+ if (!APICallError.isInstance(inner)) return void 0;
2347
+ let errorContent = inner.responseBody;
2348
+ if (!errorContent && inner.data !== void 0) {
2349
+ try {
2350
+ errorContent = JSON.stringify(inner.data);
2351
+ } catch {
2352
+ }
2353
+ }
2354
+ return {
2355
+ statusCode: inner.statusCode,
2356
+ errorContent: errorContent || inner.message,
2357
+ isRetryable: inner.isRetryable,
2358
+ attemptCount: retry?.errors.length ?? 1
2359
+ };
2360
+ }
2361
+ function isContextLengthExceededError(err, formattedMessage = "") {
2362
+ const details = sdkUpstreamErrorDetails(err);
2363
+ const rec = err && typeof err === "object" ? err : void 0;
2364
+ const candidates = [
2365
+ formattedMessage,
2366
+ details?.errorContent,
2367
+ rec?.message,
2368
+ rec?.responseBody,
2369
+ rec?.data?.error?.code,
2370
+ rec?.data?.error?.type,
2371
+ rec?.data?.error?.message,
2372
+ rec?.lastError?.message,
2373
+ ...rec?.errors?.map((error) => error.message) ?? []
2374
+ ].filter((value) => typeof value === "string");
2375
+ return candidates.some((value) => /context_length_exceeded/i.test(value) || /context window/i.test(value) || /maximum context length/i.test(value) || /prompt is too long/i.test(value));
2376
+ }
2377
+ function formatUpstreamError(err) {
2378
+ if (!err || typeof err !== "object") return "Upstream model request failed.";
2379
+ const rec = err;
2380
+ if (rec.data?.error?.message) {
2381
+ const short = sanitizeMessage(rec.data.error.message);
2382
+ return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
2383
+ }
2384
+ if (rec.responseBody) {
2385
+ try {
2386
+ const parsed = JSON.parse(rec.responseBody);
2387
+ if (parsed.error?.message) {
2388
+ const short = sanitizeMessage(parsed.error.message);
2389
+ return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
2390
+ }
2391
+ } catch {
2392
+ }
2393
+ }
2394
+ const last = rec.lastError;
2395
+ if (last?.message) {
2396
+ const code = last.statusCode;
2397
+ const short = sanitizeMessage(last.message);
2398
+ return code ? `${short} (HTTP ${code})` : short;
2399
+ }
2400
+ const fromList = rec.errors?.[rec.errors.length - 1];
2401
+ if (fromList?.message) {
2402
+ const short = sanitizeMessage(fromList.message);
2403
+ return fromList.statusCode ? `${short} (HTTP ${fromList.statusCode})` : short;
2404
+ }
2405
+ if (rec.message) {
2406
+ const short = sanitizeMessage(rec.message);
2407
+ if (short && !short.includes("file://") && !short.includes("APICallError") && short.length < 240) {
2408
+ return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
2409
+ }
2410
+ }
2411
+ return "Upstream model request failed.";
2412
+ }
2413
+ function upstreamHttpStatus(err, message) {
2414
+ if (err && typeof err === "object" && "statusCode" in err) {
2415
+ const code = err.statusCode;
2416
+ if (typeof code === "number" && code >= 400 && code <= 599) return code;
2417
+ }
2418
+ if (message.includes("HTTP 429") || message.includes("429")) return 429;
2419
+ if (message.includes("HTTP 400")) return 400;
2420
+ return 500;
2421
+ }
2422
+ function anthropicErrorType(status) {
2423
+ switch (status) {
2424
+ case 400:
2425
+ return "invalid_request_error";
2426
+ case 401:
2427
+ return "authentication_error";
2428
+ case 403:
2429
+ return "permission_error";
2430
+ case 404:
2431
+ return "not_found_error";
2432
+ case 429:
2433
+ return "rate_limit_error";
2434
+ default:
2435
+ return "api_error";
2436
+ }
2437
+ }
2438
+ function sanitizeMessage(message) {
2439
+ const line = message.split("\n")[0]?.trim() ?? message;
2440
+ if (line.startsWith("RetryError") || line.includes("AI_RetryError")) {
2441
+ return "Upstream model request failed after retries.";
2442
+ }
2443
+ return line;
2444
+ }
2445
+
2341
2446
  // src/oauth/responses-websocket.ts
2342
2447
  var RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite";
2343
2448
  var TERMINAL_EVENT_TYPES = /* @__PURE__ */ new Set(["response.completed", "response.failed", "response.incomplete"]);
@@ -2413,6 +2518,10 @@ function hasResponsesLiteHeader(headers) {
2413
2518
  ([key, value]) => key.toLowerCase() === RESPONSES_LITE_HEADER && value.toLowerCase() === "true"
2414
2519
  );
2415
2520
  }
2521
+ function authorizationHeaderFingerprint(headers) {
2522
+ const authorization = Object.entries(headers).find(([key]) => key.toLowerCase() === "authorization")?.[1];
2523
+ return authorization ? createHash("sha256").update(authorization).digest("hex") : "";
2524
+ }
2416
2525
  function bodyToString(body) {
2417
2526
  if (body == null) return "";
2418
2527
  if (typeof body === "string") return body;
@@ -2471,7 +2580,7 @@ function instructionChangeSummary(previous, current) {
2471
2580
  const firstDiffLine = previous.slice(0, prefix).split("\n").length;
2472
2581
  return `instructions changed: previous_chars=${previous.length} current_chars=${current.length} common_prefix_chars=${prefix} common_suffix_chars=${suffix} first_diff_line=${firstDiffLine}`;
2473
2582
  }
2474
- function responsesWebSocketPartitionKey(wsUrl, payload, options = {}) {
2583
+ function responsesWebSocketPartitionKey(wsUrl, payload, options = {}, authorizationFingerprint = "") {
2475
2584
  const promptCacheKey = payload.prompt_cache_key;
2476
2585
  const model = payload.model;
2477
2586
  if (typeof promptCacheKey !== "string" || !promptCacheKey || typeof model !== "string" || !model) return void 0;
@@ -2483,7 +2592,8 @@ function responsesWebSocketPartitionKey(wsUrl, payload, options = {}) {
2483
2592
  options.accountId ?? "",
2484
2593
  model,
2485
2594
  effort,
2486
- promptCacheKey
2595
+ promptCacheKey,
2596
+ authorizationFingerprint
2487
2597
  ].join("");
2488
2598
  return createHash("sha256").update(material).digest("hex");
2489
2599
  }
@@ -2852,7 +2962,7 @@ function deleteEntry(entry, closeSocket = true) {
2852
2962
  }
2853
2963
  }
2854
2964
  }
2855
- function failContext(entry, ctx, message, diagnosticDetails) {
2965
+ function failContext(entry, ctx, message, diagnosticDetails, statusCode) {
2856
2966
  if (ctx.closed || entry.current !== ctx) return;
2857
2967
  entry.debug(`fail: ${message}`);
2858
2968
  emitResponseErrorDiagnostic(entry, ctx, {
@@ -2860,7 +2970,16 @@ function failContext(entry, ctx, message, diagnosticDetails) {
2860
2970
  ...diagnosticTextFingerprint("errorMessage", message)
2861
2971
  });
2862
2972
  flushPending(ctx);
2863
- encodeSse(ctx, { type: "error", error: { message } });
2973
+ encodeSse(ctx, {
2974
+ type: "error",
2975
+ sequence_number: ctx.frameCount,
2976
+ error: {
2977
+ type: statusCode === void 0 ? "transport_error" : anthropicErrorType(statusCode),
2978
+ code: statusCode === void 0 ? "websocket_transport_error" : String(statusCode),
2979
+ message,
2980
+ param: null
2981
+ }
2982
+ });
2864
2983
  deleteEntry(entry);
2865
2984
  closeContext(ctx);
2866
2985
  }
@@ -3047,13 +3166,17 @@ function createConnection(WebSocket, wsUrl, headers, persistent, key, options, d
3047
3166
  if (ctx && !ctx.closed) sendContext(entry, ctx);
3048
3167
  });
3049
3168
  socket.on("unexpected-response", (_request, response) => {
3050
- debug(`unexpected-response status=${response.statusCode}`);
3169
+ const statusCode = response.statusCode ?? 502;
3170
+ debug(`unexpected-response status=${statusCode}`);
3171
+ response.resume();
3051
3172
  const ctx = entry.current;
3052
3173
  if (ctx && !ctx.closed) {
3053
- emitResponseErrorDiagnostic(entry, ctx, {
3174
+ failContext(entry, ctx, `WebSocket upgrade failed (HTTP ${statusCode})`, {
3054
3175
  source: "unexpected_response",
3055
- httpStatusCode: response.statusCode
3056
- });
3176
+ httpStatusCode: statusCode
3177
+ }, statusCode);
3178
+ } else {
3179
+ deleteEntry(entry);
3057
3180
  }
3058
3181
  });
3059
3182
  socket.on("message", (data) => handleSocketMessage(entry, data));
@@ -3111,7 +3234,13 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
3111
3234
  payload = {};
3112
3235
  }
3113
3236
  if (hasResponsesLiteHeader(headers)) payload = applyResponsesLiteShape(payload);
3114
- const partitionKey = responsesWebSocketPartitionKey(wsUrl, payload, options);
3237
+ const authorizationFingerprint = authorizationHeaderFingerprint(headers);
3238
+ const partitionKey = responsesWebSocketPartitionKey(
3239
+ wsUrl,
3240
+ payload,
3241
+ options,
3242
+ authorizationFingerprint
3243
+ );
3115
3244
  const promptFingerprint = responsesWebSocketPromptFingerprint(payload);
3116
3245
  const promptFieldHashes = responsesWebSocketPromptFieldHashes(payload);
3117
3246
  const instructionsSnapshot = instructionsFromPayload(payload);
@@ -3480,7 +3609,8 @@ async function createLanguageModel(spec) {
3480
3609
  if (npm === "@ai-sdk/openai") {
3481
3610
  const { createOpenAI } = await import("@ai-sdk/openai");
3482
3611
  const useResponsesEndpoint = shouldUseOpenAiResponsesEndpoint(modelId);
3483
- const accountId = spec.authType === "oauth" ? spec.oauthAccountId ?? extractOpenAiAccountId({ access_token: apiKey }) : void 0;
3612
+ const tokenAccountId = spec.authType === "oauth" ? extractOpenAiAccountId({ access_token: apiKey })?.trim() : void 0;
3613
+ const accountId = spec.authType === "oauth" ? tokenAccountId || spec.oauthAccountId : void 0;
3484
3614
  const oauthOptions = spec.authType === "oauth" ? {
3485
3615
  apiKey,
3486
3616
  baseURL: "https://chatgpt.com/backend-api/codex",
@@ -6783,111 +6913,6 @@ function resolveUpstreamTools(tools, messages) {
6783
6913
  return upstream;
6784
6914
  }
6785
6915
 
6786
- // src/upstream-error.ts
6787
- import { APICallError, RetryError } from "ai";
6788
- function sdkUpstreamErrorDetails(err) {
6789
- const retry = RetryError.isInstance(err) ? err : void 0;
6790
- const inner = retry?.lastError ?? err;
6791
- if (!APICallError.isInstance(inner)) return void 0;
6792
- let errorContent = inner.responseBody;
6793
- if (!errorContent && inner.data !== void 0) {
6794
- try {
6795
- errorContent = JSON.stringify(inner.data);
6796
- } catch {
6797
- }
6798
- }
6799
- return {
6800
- statusCode: inner.statusCode,
6801
- errorContent: errorContent || inner.message,
6802
- isRetryable: inner.isRetryable,
6803
- attemptCount: retry?.errors.length ?? 1
6804
- };
6805
- }
6806
- function isContextLengthExceededError(err, formattedMessage = "") {
6807
- const details = sdkUpstreamErrorDetails(err);
6808
- const rec = err && typeof err === "object" ? err : void 0;
6809
- const candidates = [
6810
- formattedMessage,
6811
- details?.errorContent,
6812
- rec?.message,
6813
- rec?.responseBody,
6814
- rec?.data?.error?.code,
6815
- rec?.data?.error?.type,
6816
- rec?.data?.error?.message,
6817
- rec?.lastError?.message,
6818
- ...rec?.errors?.map((error) => error.message) ?? []
6819
- ].filter((value) => typeof value === "string");
6820
- return candidates.some((value) => /context_length_exceeded/i.test(value) || /context window/i.test(value) || /maximum context length/i.test(value) || /prompt is too long/i.test(value));
6821
- }
6822
- function formatUpstreamError(err) {
6823
- if (!err || typeof err !== "object") return "Upstream model request failed.";
6824
- const rec = err;
6825
- if (rec.data?.error?.message) {
6826
- const short = sanitizeMessage(rec.data.error.message);
6827
- return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
6828
- }
6829
- if (rec.responseBody) {
6830
- try {
6831
- const parsed = JSON.parse(rec.responseBody);
6832
- if (parsed.error?.message) {
6833
- const short = sanitizeMessage(parsed.error.message);
6834
- return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
6835
- }
6836
- } catch {
6837
- }
6838
- }
6839
- const last = rec.lastError;
6840
- if (last?.message) {
6841
- const code = last.statusCode;
6842
- const short = sanitizeMessage(last.message);
6843
- return code ? `${short} (HTTP ${code})` : short;
6844
- }
6845
- const fromList = rec.errors?.[rec.errors.length - 1];
6846
- if (fromList?.message) {
6847
- const short = sanitizeMessage(fromList.message);
6848
- return fromList.statusCode ? `${short} (HTTP ${fromList.statusCode})` : short;
6849
- }
6850
- if (rec.message) {
6851
- const short = sanitizeMessage(rec.message);
6852
- if (short && !short.includes("file://") && !short.includes("APICallError") && short.length < 240) {
6853
- return rec.statusCode ? `${short} (HTTP ${rec.statusCode})` : short;
6854
- }
6855
- }
6856
- return "Upstream model request failed.";
6857
- }
6858
- function upstreamHttpStatus(err, message) {
6859
- if (err && typeof err === "object" && "statusCode" in err) {
6860
- const code = err.statusCode;
6861
- if (typeof code === "number" && code >= 400 && code <= 599) return code;
6862
- }
6863
- if (message.includes("HTTP 429") || message.includes("429")) return 429;
6864
- if (message.includes("HTTP 400")) return 400;
6865
- return 500;
6866
- }
6867
- function anthropicErrorType(status) {
6868
- switch (status) {
6869
- case 400:
6870
- return "invalid_request_error";
6871
- case 401:
6872
- return "authentication_error";
6873
- case 403:
6874
- return "permission_error";
6875
- case 404:
6876
- return "not_found_error";
6877
- case 429:
6878
- return "rate_limit_error";
6879
- default:
6880
- return "api_error";
6881
- }
6882
- }
6883
- function sanitizeMessage(message) {
6884
- const line = message.split("\n")[0]?.trim() ?? message;
6885
- if (line.startsWith("RetryError") || line.includes("AI_RetryError")) {
6886
- return "Upstream model request failed after retries.";
6887
- }
6888
- return line;
6889
- }
6890
-
6891
6916
  // src/sdk-adapter.ts
6892
6917
  function sdkTranslationErrorSignature(error) {
6893
6918
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : void 0;
@@ -7495,6 +7520,9 @@ async function generateAnthropicResponse(model, params, modelId, options) {
7495
7520
  if (abortSignal.aborted || part.type === "abort") {
7496
7521
  throw streamAbortError(abortSignal);
7497
7522
  }
7523
+ if (part.type === "error") {
7524
+ throw part.error instanceof Error || part.error && typeof part.error === "object" ? part.error : new Error(typeof part.error === "string" ? part.error : "Upstream stream failed");
7525
+ }
7498
7526
  if (part.type === "text-delta") streamedText.push(part.text ?? "");
7499
7527
  else if (part.type === "tool-call") {
7500
7528
  streamedToolCalls.push({