@bman654/clodex 1.0.3 → 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 +2 -0
- package/dist/claude-wrapper.js +15 -4
- package/dist/claude-wrapper.js.map +1 -1
- package/dist/cli.js +184 -131
- package/dist/cli.js.map +1 -1
- package/docs/background-agents.md +6 -1
- package/package.json +1 -1
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
|
package/dist/claude-wrapper.js
CHANGED
|
@@ -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
|
-
|
|
59
|
-
let
|
|
60
|
-
|
|
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
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
3174
|
+
failContext(entry, ctx, `WebSocket upgrade failed (HTTP ${statusCode})`, {
|
|
3054
3175
|
source: "unexpected_response",
|
|
3055
|
-
httpStatusCode:
|
|
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
|
|
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
|
|
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({
|
|
@@ -7599,6 +7627,8 @@ function anthropicPromptTooLongMessage(body, contextWindow) {
|
|
|
7599
7627
|
}
|
|
7600
7628
|
|
|
7601
7629
|
// src/proxy.ts
|
|
7630
|
+
var STREAM_KEEPALIVE_INTERVAL_MS = 2e4;
|
|
7631
|
+
var STREAM_KEEPALIVE_PING = 'event: ping\ndata: {"type":"ping"}\n\n';
|
|
7602
7632
|
function createTranslationLifecycle(logPath, requestId, modelId, provider) {
|
|
7603
7633
|
if (!logPath || !requestId) return void 0;
|
|
7604
7634
|
const startedAt = Date.now();
|
|
@@ -7942,6 +7972,9 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
7942
7972
|
});
|
|
7943
7973
|
translationLifecycle?.dispatched();
|
|
7944
7974
|
if (clientWantsStream) {
|
|
7975
|
+
const keepAliveMs = Number(process.env.CLODEX_STREAM_KEEPALIVE_INTERVAL_MS) || STREAM_KEEPALIVE_INTERVAL_MS;
|
|
7976
|
+
let lastDownstreamWriteAt = Date.now();
|
|
7977
|
+
let lastUpstreamPartAt = Date.now();
|
|
7945
7978
|
const writeStreamChunk = (chunk) => {
|
|
7946
7979
|
translationLifecycle?.onOutput(chunk);
|
|
7947
7980
|
if (!res.headersSent) {
|
|
@@ -7951,23 +7984,43 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
|
|
|
7951
7984
|
"Connection": "keep-alive"
|
|
7952
7985
|
});
|
|
7953
7986
|
}
|
|
7987
|
+
lastDownstreamWriteAt = Date.now();
|
|
7954
7988
|
res.write(chunk);
|
|
7955
7989
|
};
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
{
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7990
|
+
const keepAlive = setInterval(() => {
|
|
7991
|
+
if (res.writableEnded || !res.headersSent) return;
|
|
7992
|
+
const now = Date.now();
|
|
7993
|
+
const outputIdleMs = now - lastDownstreamWriteAt;
|
|
7994
|
+
const upstreamIdleMs = now - lastUpstreamPartAt;
|
|
7995
|
+
if (outputIdleMs >= keepAliveMs && upstreamIdleMs < keepAliveMs) {
|
|
7996
|
+
lastDownstreamWriteAt = now;
|
|
7997
|
+
res.write(STREAM_KEEPALIVE_PING);
|
|
7998
|
+
plog(() => `stream keepalive ping: output idle ${outputIdleMs}ms, upstream active (${upstreamIdleMs}ms since last part)`);
|
|
7999
|
+
}
|
|
8000
|
+
}, keepAliveMs);
|
|
8001
|
+
keepAlive.unref();
|
|
8002
|
+
try {
|
|
8003
|
+
await withResponsesWebSocketDiagnosticContext(
|
|
8004
|
+
{ requestId: relayRequestId, claudeSessionId },
|
|
8005
|
+
() => streamAnthropicResponse(
|
|
8006
|
+
model,
|
|
8007
|
+
params,
|
|
8008
|
+
originalModel,
|
|
8009
|
+
writeStreamChunk,
|
|
8010
|
+
plog,
|
|
8011
|
+
{
|
|
8012
|
+
onPart: (partType) => {
|
|
8013
|
+
lastUpstreamPartAt = Date.now();
|
|
8014
|
+
translationLifecycle?.onPart(partType);
|
|
8015
|
+
},
|
|
8016
|
+
initialInputTokens: estimateAnthropicInputTokens(anthropicBody),
|
|
8017
|
+
abortSignal: clientAbort.signal
|
|
8018
|
+
}
|
|
8019
|
+
)
|
|
8020
|
+
);
|
|
8021
|
+
} finally {
|
|
8022
|
+
clearInterval(keepAlive);
|
|
8023
|
+
}
|
|
7971
8024
|
translationLifecycle?.complete();
|
|
7972
8025
|
if (!res.headersSent) writeStreamChunk("");
|
|
7973
8026
|
res.end();
|