copilot-relay 0.4.8 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/config.default.yaml +9 -1
- package/dist/main.js +1600 -85
- package/dist/main.js.map +1 -1
- package/package.json +5 -3
- package/vendor/codex/LICENSE +201 -0
- package/vendor/codex/NOTICE +6 -0
- package/vendor/codex/README.md +15 -0
- package/vendor/codex/models.json +1602 -0
- package/vendor/codex/prompt.md +275 -0
package/dist/main.js
CHANGED
|
@@ -17,6 +17,7 @@ import { Hono } from "hono";
|
|
|
17
17
|
import { streamSSE } from "hono/streaming";
|
|
18
18
|
import { events } from "fetch-event-stream";
|
|
19
19
|
import { execFile } from "node:child_process";
|
|
20
|
+
import { TomlError, parse } from "smol-toml";
|
|
20
21
|
|
|
21
22
|
//#region src/lib/error.ts
|
|
22
23
|
var ProxyNotImplementedError = class extends Error {
|
|
@@ -93,6 +94,7 @@ const logsDir = path.join(appDir, "logs");
|
|
|
93
94
|
const logFileBaseName = "copilot-relay";
|
|
94
95
|
const legacyLogPath = path.join(logsDir, `${logFileBaseName}.log`);
|
|
95
96
|
const pidPath = path.join(appDir, "copilot-relay.pid");
|
|
97
|
+
const codexCatalogPath = path.join(appDir, "codex-models.json");
|
|
96
98
|
/**
|
|
97
99
|
* Local-date stamp used in log file names.
|
|
98
100
|
*
|
|
@@ -112,6 +114,7 @@ const formatLogDate = (date) => [
|
|
|
112
114
|
const getLogPath = (date = /* @__PURE__ */ new Date()) => path.join(logsDir, `${logFileBaseName}.${formatLogDate(date)}.log`);
|
|
113
115
|
const paths = {
|
|
114
116
|
appDir,
|
|
117
|
+
codexCatalogPath,
|
|
115
118
|
configPath,
|
|
116
119
|
copilotTokenPath,
|
|
117
120
|
githubTokenPath,
|
|
@@ -1236,7 +1239,7 @@ const assertUnchanged = async (expected) => {
|
|
|
1236
1239
|
if (current.resolvedPath !== expected.resolvedPath || current.raw !== expected.raw || current.mode !== expected.mode || !sameIdentity(current.identity, expected.identity)) throw new FileConflictError();
|
|
1237
1240
|
};
|
|
1238
1241
|
const pendingWrites = /* @__PURE__ */ new Map();
|
|
1239
|
-
const publish = async (snapshot, content, options) => {
|
|
1242
|
+
const publish$1 = async (snapshot, content, options) => {
|
|
1240
1243
|
await assertUnchanged(snapshot);
|
|
1241
1244
|
const directory = path.dirname(snapshot.resolvedPath);
|
|
1242
1245
|
await fs.mkdir(directory, {
|
|
@@ -1267,7 +1270,7 @@ const publish = async (snapshot, content, options) => {
|
|
|
1267
1270
|
}
|
|
1268
1271
|
};
|
|
1269
1272
|
const writeFileSnapshot = async (snapshot, content, options = {}) => {
|
|
1270
|
-
const current = (pendingWrites.get(snapshot.resolvedPath) ?? Promise.resolve()).catch(() => void 0).then(() => publish(snapshot, content, options));
|
|
1273
|
+
const current = (pendingWrites.get(snapshot.resolvedPath) ?? Promise.resolve()).catch(() => void 0).then(() => publish$1(snapshot, content, options));
|
|
1271
1274
|
pendingWrites.set(snapshot.resolvedPath, current);
|
|
1272
1275
|
try {
|
|
1273
1276
|
await current;
|
|
@@ -1317,6 +1320,7 @@ const logLevels = [
|
|
|
1317
1320
|
const defaultConfig = {
|
|
1318
1321
|
apiKey: "",
|
|
1319
1322
|
claudeSetup: true,
|
|
1323
|
+
codexSetup: true,
|
|
1320
1324
|
copilotBaseUrl: "https://api.githubcopilot.com",
|
|
1321
1325
|
gptModel: "gpt-6-astra",
|
|
1322
1326
|
host: "127.0.0.1",
|
|
@@ -1343,14 +1347,14 @@ const normalizeClaudeUpstreamApi = (value) => {
|
|
|
1343
1347
|
if (value === "auto" || value === "messages" || value === "chat-completions") return value;
|
|
1344
1348
|
throw new Error("Invalid claudeUpstreamApi: expected auto, messages, or chat-completions");
|
|
1345
1349
|
};
|
|
1346
|
-
const normalizeBoolean = (value) => {
|
|
1350
|
+
const normalizeBoolean = (value, name) => {
|
|
1347
1351
|
if (value === void 0) return;
|
|
1348
1352
|
if (typeof value === "boolean") return value;
|
|
1349
1353
|
if (typeof value === "string") {
|
|
1350
1354
|
if (value.toLowerCase() === "true") return true;
|
|
1351
1355
|
if (value.toLowerCase() === "false") return false;
|
|
1352
1356
|
}
|
|
1353
|
-
throw new Error(
|
|
1357
|
+
throw new Error(`Invalid ${name}: expected true or false`);
|
|
1354
1358
|
};
|
|
1355
1359
|
const normalizeInteger = (value, key, minimum, maximum = Number.MAX_SAFE_INTEGER) => {
|
|
1356
1360
|
if (value === void 0) return;
|
|
@@ -1547,6 +1551,7 @@ const readDefaultConfigTemplate = async () => {
|
|
|
1547
1551
|
const configAliases = {
|
|
1548
1552
|
api_key: "apiKey",
|
|
1549
1553
|
claude_setup: "claudeSetup",
|
|
1554
|
+
codex_setup: "codexSetup",
|
|
1550
1555
|
copilot_base_url: "copilotBaseUrl",
|
|
1551
1556
|
gpt_model: "gptModel",
|
|
1552
1557
|
log_level: "logLevel",
|
|
@@ -1583,11 +1588,11 @@ const parseConfigYaml = (content) => {
|
|
|
1583
1588
|
const match = /^([A-Za-z][A-Za-z0-9_-]*)[ \t]*:[ \t]*(.*)$/.exec(line);
|
|
1584
1589
|
if (!match) throw new Error(`Invalid config syntax on line ${index + 1}: expected a flat scalar key`);
|
|
1585
1590
|
const [, key, value] = match;
|
|
1586
|
-
const canonical = Object.hasOwn(configAliases, key) ? configAliases[key] : key;
|
|
1587
|
-
if (seen.has(canonical)) throw new Error(`Duplicate config key on line ${index + 1}`);
|
|
1588
|
-
seen.add(canonical);
|
|
1591
|
+
const canonical$1 = Object.hasOwn(configAliases, key) ? configAliases[key] : key;
|
|
1592
|
+
if (seen.has(canonical$1)) throw new Error(`Duplicate config key on line ${index + 1}`);
|
|
1593
|
+
seen.add(canonical$1);
|
|
1589
1594
|
const scalar = parseYamlScalar(value);
|
|
1590
|
-
if (Object.hasOwn(defaultConfig, canonical)) config[canonical] = scalar;
|
|
1595
|
+
if (Object.hasOwn(defaultConfig, canonical$1)) config[canonical$1] = scalar;
|
|
1591
1596
|
}
|
|
1592
1597
|
return config;
|
|
1593
1598
|
};
|
|
@@ -1605,7 +1610,7 @@ const serializeConfig = (config) => [
|
|
|
1605
1610
|
"# copilot-relay configuration",
|
|
1606
1611
|
"#",
|
|
1607
1612
|
"# Valid complete edits hot-reload without rewriting this file.",
|
|
1608
|
-
"# host, port, claudeSetup, and upstreamProxy require restart.",
|
|
1613
|
+
"# host, port, claudeSetup, codexSetup, and upstreamProxy require restart.",
|
|
1609
1614
|
"",
|
|
1610
1615
|
"# Local host for the Claude Code-compatible HTTP server.",
|
|
1611
1616
|
`host: ${config.host}`,
|
|
@@ -1627,6 +1632,9 @@ const serializeConfig = (config) => [
|
|
|
1627
1632
|
"# Update ~/.claude/settings.json on start.",
|
|
1628
1633
|
`claudeSetup: ${config.claudeSetup}`,
|
|
1629
1634
|
"",
|
|
1635
|
+
"# Update Codex's config.toml and model list on start, when Codex is installed.",
|
|
1636
|
+
`codexSetup: ${config.codexSetup}`,
|
|
1637
|
+
"",
|
|
1630
1638
|
"# Log verbosity:",
|
|
1631
1639
|
"# error - startup/preflight/request failures only",
|
|
1632
1640
|
"# info - errors plus status, model/effort and semantic completion summaries",
|
|
@@ -1659,7 +1667,8 @@ const serializeConfig = (config) => [
|
|
|
1659
1667
|
].join("\n");
|
|
1660
1668
|
const resolveConfig = (raw) => ({
|
|
1661
1669
|
apiKey: normalizeApiKey(raw.apiKey) ?? defaultConfig.apiKey,
|
|
1662
|
-
claudeSetup: normalizeBoolean(raw.claudeSetup) ?? defaultConfig.claudeSetup,
|
|
1670
|
+
claudeSetup: normalizeBoolean(raw.claudeSetup, "claudeSetup") ?? defaultConfig.claudeSetup,
|
|
1671
|
+
codexSetup: normalizeBoolean(raw.codexSetup, "codexSetup") ?? defaultConfig.codexSetup,
|
|
1663
1672
|
copilotBaseUrl: normalizeCopilotBaseUrl(normalizeRequiredString(raw.copilotBaseUrl, "copilotBaseUrl")) ?? defaultConfig.copilotBaseUrl,
|
|
1664
1673
|
gptModel: normalizeRequiredString(raw.gptModel, "gptModel") ?? defaultConfig.gptModel,
|
|
1665
1674
|
host: normalizeRequiredString(raw.host, "host") ?? defaultConfig.host,
|
|
@@ -3164,8 +3173,8 @@ const parseJsonBody = (body) => {
|
|
|
3164
3173
|
};
|
|
3165
3174
|
const isJsonObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3166
3175
|
const parseTokenCount = (digits) => {
|
|
3167
|
-
const count = Number(digits);
|
|
3168
|
-
return Number.isSafeInteger(count) ? count : void 0;
|
|
3176
|
+
const count$1 = Number(digits);
|
|
3177
|
+
return Number.isSafeInteger(count$1) ? count$1 : void 0;
|
|
3169
3178
|
};
|
|
3170
3179
|
const toPromptTooLongError = (status$1, body, detail) => {
|
|
3171
3180
|
if (status$1 !== 400) return;
|
|
@@ -3343,10 +3352,10 @@ const pinCopilotModelCatalog = (config) => {
|
|
|
3343
3352
|
pinnedCatalogs.set(config, config.modelCatalog);
|
|
3344
3353
|
};
|
|
3345
3354
|
const pendingCatalogs = /* @__PURE__ */ new WeakMap();
|
|
3346
|
-
const isRecord$
|
|
3355
|
+
const isRecord$9 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3347
3356
|
const isPositiveInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
3348
3357
|
function parseModelTokenLimits(value) {
|
|
3349
|
-
if (!isRecord$
|
|
3358
|
+
if (!isRecord$9(value)) return;
|
|
3350
3359
|
const context = value.max_context_window_tokens;
|
|
3351
3360
|
const prompt = value.max_prompt_tokens;
|
|
3352
3361
|
const output = value.max_output_tokens;
|
|
@@ -3402,13 +3411,13 @@ async function loadCopilotModelCatalog(config) {
|
|
|
3402
3411
|
});
|
|
3403
3412
|
if (!response.ok) throw new HTTPError("Failed to validate upstream models", response);
|
|
3404
3413
|
const payload = await readCopilotJson(response, signal, config.upstreamTimeoutMs);
|
|
3405
|
-
if (!isRecord$
|
|
3414
|
+
if (!isRecord$9(payload) || !Array.isArray(payload.data)) throw new Error("Copilot /models returned an invalid model catalog.");
|
|
3406
3415
|
const models$1 = /* @__PURE__ */ new Map();
|
|
3407
3416
|
for (const model of payload.data) {
|
|
3408
|
-
if (!isRecord$
|
|
3409
|
-
const capabilities = isRecord$
|
|
3417
|
+
if (!isRecord$9(model) || typeof model.id !== "string" || !model.id) continue;
|
|
3418
|
+
const capabilities = isRecord$9(model.capabilities) ? model.capabilities : void 0;
|
|
3410
3419
|
const limits = parseModelTokenLimits(capabilities?.limits);
|
|
3411
|
-
const supports = isRecord$
|
|
3420
|
+
const supports = isRecord$9(capabilities?.supports) ? capabilities.supports : void 0;
|
|
3412
3421
|
const supportedEndpoints = parseStringArray(model.supported_endpoints);
|
|
3413
3422
|
const reasoningEfforts = parseReasoningEfforts(supports);
|
|
3414
3423
|
models$1.set(model.id, {
|
|
@@ -3602,8 +3611,8 @@ const renderProbeSummary = (rows) => {
|
|
|
3602
3611
|
NOT_TESTED: "not tested"
|
|
3603
3612
|
};
|
|
3604
3613
|
return "Summary: " + Object.keys(labels).flatMap((status$1) => {
|
|
3605
|
-
const count = rows.filter((row) => row.status === status$1).length;
|
|
3606
|
-
return count ? [`${count} ${labels[status$1]}`] : [];
|
|
3614
|
+
const count$1 = rows.filter((row) => row.status === status$1).length;
|
|
3615
|
+
return count$1 ? [`${count$1} ${labels[status$1]}`] : [];
|
|
3607
3616
|
}).join(" · ");
|
|
3608
3617
|
};
|
|
3609
3618
|
const renderProbeDetails = (row, diagnostic) => {
|
|
@@ -3753,7 +3762,7 @@ const claudeCodeContext = [
|
|
|
3753
3762
|
const claudeCodeContextPatterns = [/^\S+ hook additional context:/];
|
|
3754
3763
|
const statusProbePrompt = "Reply with the single word: ok";
|
|
3755
3764
|
const statusProbeMaxTokens = 16;
|
|
3756
|
-
const isRecord$
|
|
3765
|
+
const isRecord$8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3757
3766
|
const innermostReminder = /<system-reminder>(?:(?!<system-reminder>)[\s\S])*?<\/system-reminder>/g;
|
|
3758
3767
|
const withoutReminders = (text$1) => {
|
|
3759
3768
|
let previous = text$1;
|
|
@@ -3766,22 +3775,22 @@ const withoutReminders = (text$1) => {
|
|
|
3766
3775
|
};
|
|
3767
3776
|
const startsWithAny = (text$1, prefixes) => prefixes.some((prefix) => text$1.startsWith(prefix));
|
|
3768
3777
|
const matchesAny = (text$1, prefixes, patterns) => startsWithAny(text$1, prefixes) || patterns.some((pattern) => pattern.test(text$1));
|
|
3769
|
-
const contentBlocks = (content) => {
|
|
3778
|
+
const contentBlocks$1 = (content) => {
|
|
3770
3779
|
if (typeof content === "string") return [{
|
|
3771
3780
|
type: "text",
|
|
3772
3781
|
text: content
|
|
3773
3782
|
}];
|
|
3774
|
-
return Array.isArray(content) ? content.filter(isRecord$
|
|
3783
|
+
return Array.isArray(content) ? content.filter(isRecord$8) : [];
|
|
3775
3784
|
};
|
|
3776
3785
|
const originatingTurn = (messages) => {
|
|
3777
3786
|
const blocks = [];
|
|
3778
3787
|
let sawUser = false;
|
|
3779
3788
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
3780
3789
|
const message = messages[index];
|
|
3781
|
-
if (!isRecord$
|
|
3790
|
+
if (!isRecord$8(message) || message.role === "system") continue;
|
|
3782
3791
|
if (message.role !== "user") break;
|
|
3783
3792
|
sawUser = true;
|
|
3784
|
-
blocks.unshift(...contentBlocks(message.content));
|
|
3793
|
+
blocks.unshift(...contentBlocks$1(message.content));
|
|
3785
3794
|
}
|
|
3786
3795
|
return sawUser ? blocks : void 0;
|
|
3787
3796
|
};
|
|
@@ -3798,11 +3807,11 @@ const blockOrigin = (block) => {
|
|
|
3798
3807
|
const isStatusProbe = (body) => {
|
|
3799
3808
|
if (Object.keys(body).sort().join(",") !== "max_tokens,messages,model" || body.max_tokens !== statusProbeMaxTokens) return false;
|
|
3800
3809
|
const messages = body.messages;
|
|
3801
|
-
return Array.isArray(messages) && messages.length === 1 && isRecord$
|
|
3810
|
+
return Array.isArray(messages) && messages.length === 1 && isRecord$8(messages[0]) && messages[0].role === "user" && messages[0].content === statusProbePrompt;
|
|
3802
3811
|
};
|
|
3803
|
-
const systemTexts = (system) => contentBlocks(system).flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []);
|
|
3812
|
+
const systemTexts = (system) => contentBlocks$1(system).flatMap((block) => block.type === "text" && typeof block.text === "string" ? [block.text] : []);
|
|
3804
3813
|
const getClaudeRequestInitiator = (body) => {
|
|
3805
|
-
if (!isRecord$
|
|
3814
|
+
if (!isRecord$8(body) || !Array.isArray(body.messages) || isStatusProbe(body)) return "agent";
|
|
3806
3815
|
const payload = body;
|
|
3807
3816
|
if (readBillingLineField(payload, "cc_is_subagent") === "true" || readBillingLineField(payload, "cc_workload") === "cron") return "agent";
|
|
3808
3817
|
if (systemTexts(removeBillingLine(payload).system).some((text$1) => startsWithAny(text$1.trimStart(), ownRequestSystemPrompts))) return "agent";
|
|
@@ -4406,7 +4415,7 @@ const webSearchInputSchema = {
|
|
|
4406
4415
|
required: ["query"],
|
|
4407
4416
|
additionalProperties: false
|
|
4408
4417
|
};
|
|
4409
|
-
const isRecord$
|
|
4418
|
+
const isRecord$7 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4410
4419
|
const isAnthropicNativeWebSearchTool = (tool) => tool.name === "web_search" && typeof tool.type === "string" && anthropicWebSearchToolPattern.test(tool.type);
|
|
4411
4420
|
const isClaudeCodeWebSearchTool = (tool) => tool.name === claudeCodeWebSearchToolName;
|
|
4412
4421
|
const isClaudeWebSearchTool = (tool) => isAnthropicNativeWebSearchTool(tool) || isClaudeCodeWebSearchTool(tool);
|
|
@@ -4446,7 +4455,7 @@ const getQueryFromToolArguments = (value) => {
|
|
|
4446
4455
|
if (!trimmed) return;
|
|
4447
4456
|
try {
|
|
4448
4457
|
const parsed = JSON.parse(trimmed);
|
|
4449
|
-
if (isRecord$
|
|
4458
|
+
if (isRecord$7(parsed) && typeof parsed.query === "string" && parsed.query.trim()) return parsed.query.trim();
|
|
4450
4459
|
} catch {
|
|
4451
4460
|
return trimmed;
|
|
4452
4461
|
}
|
|
@@ -4503,7 +4512,7 @@ const getReportedQuery = (action, queries) => {
|
|
|
4503
4512
|
const getSearchQuery = (response, requestedQuery) => {
|
|
4504
4513
|
for (const item of response.output ?? []) {
|
|
4505
4514
|
if (item.type !== "web_search_call") continue;
|
|
4506
|
-
const action = isRecord$
|
|
4515
|
+
const action = isRecord$7(item.action) ? item.action : void 0;
|
|
4507
4516
|
const query = getReportedQuery(action, Array.isArray(action?.queries) ? action.queries : []);
|
|
4508
4517
|
if (query) return query;
|
|
4509
4518
|
}
|
|
@@ -4511,7 +4520,7 @@ const getSearchQuery = (response, requestedQuery) => {
|
|
|
4511
4520
|
};
|
|
4512
4521
|
const getResponseText = (response) => (response.output ?? []).flatMap((item) => {
|
|
4513
4522
|
if (item.type !== "message") return [];
|
|
4514
|
-
return (Array.isArray(item.content) ? item.content : []).flatMap((part) => isRecord$
|
|
4523
|
+
return (Array.isArray(item.content) ? item.content : []).flatMap((part) => isRecord$7(part) && part.type === "output_text" && typeof part.text === "string" ? [part.text] : []);
|
|
4515
4524
|
}).join("\n").trim();
|
|
4516
4525
|
const cleanTitle = (line, url) => {
|
|
4517
4526
|
return line.slice(0, line.indexOf(url)).replace(/^\s*(?:[-*]|\d+[.)])\s*/, "").replace(/\s*(?:[-:|])\s*$/, "").trim() || new URL(url).hostname;
|
|
@@ -4535,15 +4544,15 @@ const parseSearchResults = (text$1) => {
|
|
|
4535
4544
|
const getStructuredSearchResults = (response) => {
|
|
4536
4545
|
const sources = [];
|
|
4537
4546
|
for (const item of response.output ?? []) {
|
|
4538
|
-
if (item.type === "web_search_call" && isRecord$
|
|
4547
|
+
if (item.type === "web_search_call" && isRecord$7(item.action) && Array.isArray(item.action.sources)) sources.push(...item.action.sources);
|
|
4539
4548
|
if (item.type === "message" && Array.isArray(item.content)) {
|
|
4540
|
-
for (const part of item.content) if (isRecord$
|
|
4549
|
+
for (const part of item.content) if (isRecord$7(part) && Array.isArray(part.annotations)) sources.push(...part.annotations.filter((annotation) => isRecord$7(annotation) && annotation.type === "url_citation"));
|
|
4541
4550
|
}
|
|
4542
4551
|
}
|
|
4543
4552
|
const results = [];
|
|
4544
4553
|
const seen = /* @__PURE__ */ new Set();
|
|
4545
4554
|
for (const source of sources) {
|
|
4546
|
-
if (!isRecord$
|
|
4555
|
+
if (!isRecord$7(source) || typeof source.url !== "string") continue;
|
|
4547
4556
|
try {
|
|
4548
4557
|
const url = new URL(source.url);
|
|
4549
4558
|
if (!/^https?:$/.test(url.protocol) || url.username || url.password || seen.has(url.href)) continue;
|
|
@@ -4568,7 +4577,7 @@ const responseStates = [
|
|
|
4568
4577
|
"in_progress"
|
|
4569
4578
|
];
|
|
4570
4579
|
const searchStates = [...responseStates, "searching"];
|
|
4571
|
-
const incompleteReasons = ["max_output_tokens", "content_filter"];
|
|
4580
|
+
const incompleteReasons$1 = ["max_output_tokens", "content_filter"];
|
|
4572
4581
|
const outputTypes = [
|
|
4573
4582
|
"message",
|
|
4574
4583
|
"reasoning",
|
|
@@ -4584,7 +4593,7 @@ const safeResponseId = (value, token) => typeof value === "string" && /^(?:resp|
|
|
|
4584
4593
|
const summarizeCounts = (values) => {
|
|
4585
4594
|
const counts = /* @__PURE__ */ new Map();
|
|
4586
4595
|
for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
4587
|
-
return [...counts].sort(([a], [b]) => a.localeCompare(b)).map(([key, count]) => `${key}:${count}`).join(",") || "none";
|
|
4596
|
+
return [...counts].sort(([a], [b]) => a.localeCompare(b)).map(([key, count$1]) => `${key}:${count$1}`).join(",") || "none";
|
|
4588
4597
|
};
|
|
4589
4598
|
const getSearchProvenance = (calls, callStates, structured) => {
|
|
4590
4599
|
if (calls.length) return callStates.every((state) => state === "completed") ? "completed_call" : "call_unreported";
|
|
@@ -4595,21 +4604,21 @@ const getResultSource = (structured, results) => {
|
|
|
4595
4604
|
return results.length ? "text" : "none";
|
|
4596
4605
|
};
|
|
4597
4606
|
const interpretSearchResponse = (raw, config, requestedQuery, request, requestedEffort, requestId) => {
|
|
4598
|
-
const malformed = !isRecord$
|
|
4599
|
-
const upstream = isRecord$
|
|
4607
|
+
const malformed = !isRecord$7(raw) || raw.output !== void 0 && (!Array.isArray(raw.output) || !raw.output.every(isRecord$7));
|
|
4608
|
+
const upstream = isRecord$7(raw) ? {
|
|
4600
4609
|
...raw,
|
|
4601
|
-
output: Array.isArray(raw.output) ? raw.output.filter(isRecord$
|
|
4610
|
+
output: Array.isArray(raw.output) ? raw.output.filter(isRecord$7) : []
|
|
4602
4611
|
} : {};
|
|
4603
4612
|
const status$1 = recognizedValue(upstream.status, responseStates);
|
|
4604
|
-
const reason = recognizedValue(isRecord$
|
|
4613
|
+
const reason = recognizedValue(isRecord$7(upstream.incomplete_details) ? upstream.incomplete_details.reason : void 0, incompleteReasons$1);
|
|
4605
4614
|
const items = upstream.output ?? [];
|
|
4606
4615
|
const calls = items.filter((item) => item.type === "web_search_call");
|
|
4607
4616
|
const callStates = calls.map((item) => recognizedValue(item.status, searchStates));
|
|
4608
|
-
const usage$1 = isRecord$
|
|
4617
|
+
const usage$1 = isRecord$7(upstream.usage) ? upstream.usage : {};
|
|
4609
4618
|
const inputTokens = reportedTokens(usage$1.input_tokens);
|
|
4610
|
-
const cachedInputTokens = reportedTokens(isRecord$
|
|
4619
|
+
const cachedInputTokens = reportedTokens(isRecord$7(usage$1.input_tokens_details) ? usage$1.input_tokens_details.cached_tokens : void 0);
|
|
4611
4620
|
const outputTokens = reportedTokens(usage$1.output_tokens);
|
|
4612
|
-
const reasoningTokens = reportedTokens(isRecord$
|
|
4621
|
+
const reasoningTokens = reportedTokens(isRecord$7(usage$1.output_tokens_details) ? usage$1.output_tokens_details.reasoning_tokens : void 0);
|
|
4613
4622
|
const upstreamResponseId = safeResponseId(upstream.id, config.copilotToken);
|
|
4614
4623
|
const safeRequestId = typeof requestId === "string" && /^[a-f0-9-]{36}$/i.test(requestId) ? requestId : "unreported";
|
|
4615
4624
|
const text$1 = getResponseText(upstream);
|
|
@@ -4697,7 +4706,7 @@ const getSearchFailureDetail = (body, token) => {
|
|
|
4697
4706
|
let detail = body.trim();
|
|
4698
4707
|
try {
|
|
4699
4708
|
const parsed = JSON.parse(detail);
|
|
4700
|
-
detail = getErrorMessageOrCode(isRecord$
|
|
4709
|
+
detail = getErrorMessageOrCode(isRecord$7(parsed) && isRecord$7(parsed.error) ? parsed.error : void 0);
|
|
4701
4710
|
} catch {
|
|
4702
4711
|
if (/^[{[<]/.test(detail)) return "";
|
|
4703
4712
|
}
|
|
@@ -4829,11 +4838,11 @@ const removeWebSearchTool = (tools, toolNameMapper) => {
|
|
|
4829
4838
|
const remaining = tools.filter((tool) => !isWebSearchTool(tool, toolNameMapper));
|
|
4830
4839
|
return remaining.length > 0 ? remaining : void 0;
|
|
4831
4840
|
};
|
|
4832
|
-
const normalizeFinalToolChoice = (toolChoice, tools, toolNameMapper) => {
|
|
4841
|
+
const normalizeFinalToolChoice = (toolChoice$1, tools, toolNameMapper) => {
|
|
4833
4842
|
if (!tools || tools.length === 0) return;
|
|
4834
|
-
if (toolChoice === "required") return "auto";
|
|
4835
|
-
if (toolChoice && typeof toolChoice === "object" && isClaudeWebSearchToolName(toolNameMapper.toClaude(toolChoice.function.name))) return "auto";
|
|
4836
|
-
return toolChoice;
|
|
4843
|
+
if (toolChoice$1 === "required") return "auto";
|
|
4844
|
+
if (toolChoice$1 && typeof toolChoice$1 === "object" && isClaudeWebSearchToolName(toolNameMapper.toClaude(toolChoice$1.function.name))) return "auto";
|
|
4845
|
+
return toolChoice$1;
|
|
4837
4846
|
};
|
|
4838
4847
|
const createFinalWebSearchPayload = (payload, search, toolNameMapper) => {
|
|
4839
4848
|
const tools = removeWebSearchTool(payload.tools, toolNameMapper);
|
|
@@ -5269,7 +5278,7 @@ const lookaroundPrefixes = [
|
|
|
5269
5278
|
"(?<=",
|
|
5270
5279
|
"(?<!"
|
|
5271
5280
|
];
|
|
5272
|
-
const isRecord$
|
|
5281
|
+
const isRecord$6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5273
5282
|
function hasUnsupportedPatternFeatures(pattern) {
|
|
5274
5283
|
let inCharacterClass = false;
|
|
5275
5284
|
for (let index = 0; index < pattern.length; index++) {
|
|
@@ -5315,12 +5324,12 @@ function mapArray(values) {
|
|
|
5315
5324
|
}
|
|
5316
5325
|
function normalizeSchemaValue(value) {
|
|
5317
5326
|
if (Array.isArray(value)) return mapArray(value);
|
|
5318
|
-
return isRecord$
|
|
5327
|
+
return isRecord$6(value) ? normalizeResponsesToolSchema(value) : value;
|
|
5319
5328
|
}
|
|
5320
5329
|
function normalizeResponsesToolSchema(schema) {
|
|
5321
5330
|
return mapRecord(schema, (key, value) => {
|
|
5322
5331
|
if (key === "pattern" && typeof value === "string" && hasUnsupportedPatternFeatures(value)) return removed;
|
|
5323
|
-
if (schemaMapKeywords.has(key) && isRecord$
|
|
5332
|
+
if (schemaMapKeywords.has(key) && isRecord$6(value)) return mapRecord(value, (_name, nested) => normalizeSchemaValue(nested));
|
|
5324
5333
|
if (nestedSchemaKeywords.has(key)) return normalizeSchemaValue(value);
|
|
5325
5334
|
return value;
|
|
5326
5335
|
});
|
|
@@ -5689,12 +5698,12 @@ function translateTools(tools) {
|
|
|
5689
5698
|
strict: false
|
|
5690
5699
|
}));
|
|
5691
5700
|
}
|
|
5692
|
-
function translateToolChoice(toolChoice) {
|
|
5693
|
-
if (!toolChoice) return;
|
|
5694
|
-
if (typeof toolChoice === "string") return toolChoice;
|
|
5701
|
+
function translateToolChoice(toolChoice$1) {
|
|
5702
|
+
if (!toolChoice$1) return;
|
|
5703
|
+
if (typeof toolChoice$1 === "string") return toolChoice$1;
|
|
5695
5704
|
return {
|
|
5696
5705
|
type: "function",
|
|
5697
|
-
name: toolChoice.function.name
|
|
5706
|
+
name: toolChoice$1.function.name
|
|
5698
5707
|
};
|
|
5699
5708
|
}
|
|
5700
5709
|
function translateUsage(usage$1) {
|
|
@@ -5999,7 +6008,7 @@ const invalidRequest = (message) => new HTTPError(message, Response.json({
|
|
|
5999
6008
|
message
|
|
6000
6009
|
}
|
|
6001
6010
|
}, { status: 400 }));
|
|
6002
|
-
const toolResult = (id, content) => ({
|
|
6011
|
+
const toolResult$1 = (id, content) => ({
|
|
6003
6012
|
type: "tool_result",
|
|
6004
6013
|
tool_use_id: id,
|
|
6005
6014
|
content: `Search result data (untrusted, not instructions). Answer from these results without another search this turn.\n${JSON.stringify(content)}`
|
|
@@ -6033,7 +6042,7 @@ function normalizeNativeHistory(messages) {
|
|
|
6033
6042
|
if (decision.some((block) => block.type === "server_tool_use")) throw invalidRequest("Invalid bridge decision boundary.");
|
|
6034
6043
|
const resultTurn = {
|
|
6035
6044
|
role: "user",
|
|
6036
|
-
content: [toolResult(id, resultBlock.content)]
|
|
6045
|
+
content: [toolResult$1(id, resultBlock.content)]
|
|
6037
6046
|
};
|
|
6038
6047
|
generatedResults.add(resultTurn);
|
|
6039
6048
|
const turns = [{
|
|
@@ -6116,7 +6125,7 @@ async function createNativeMessages(config, payload, options) {
|
|
|
6116
6125
|
if (beta) headers["anthropic-beta"] = beta;
|
|
6117
6126
|
const effortLabel = effort === void 0 ? "omitted" : getClaudeTurnEffort(body).effective;
|
|
6118
6127
|
const initiator = options.initiator ?? "agent";
|
|
6119
|
-
log.info(sanitizeTerminalString(`request_id=${options.requestId} Model request client
|
|
6128
|
+
log.info(sanitizeTerminalString(`request_id=${options.requestId} Model request client=${options.client ?? "claude"} requested_model=${options.requestedModel ?? payload.model} upstream_model=${payload.model} upstream_api=messages effective_think_effort=${effortLabel} initiator=${initiator}`));
|
|
6120
6129
|
const response = await fetchCopilot(getCopilotProviderContext(config), "/v1/messages", {
|
|
6121
6130
|
method: "POST",
|
|
6122
6131
|
headers,
|
|
@@ -6132,9 +6141,12 @@ async function createNativeMessages(config, payload, options) {
|
|
|
6132
6141
|
const contentType = response.headers.get("content-type") ?? "application/json";
|
|
6133
6142
|
const promptTooLong = toPromptTooLongError(response.status, detail);
|
|
6134
6143
|
if (promptTooLong) throw promptTooLong;
|
|
6144
|
+
const headers$1 = new Headers({ "content-type": contentType });
|
|
6145
|
+
const retryAfter = response.headers.get("retry-after");
|
|
6146
|
+
if (retryAfter !== null) headers$1.set("retry-after", retryAfter);
|
|
6135
6147
|
throw new HTTPError("Native upstream request failed", new Response(detail, {
|
|
6136
6148
|
status: response.status,
|
|
6137
|
-
headers:
|
|
6149
|
+
headers: headers$1
|
|
6138
6150
|
}));
|
|
6139
6151
|
}
|
|
6140
6152
|
return response;
|
|
@@ -6359,7 +6371,7 @@ async function handleNativeMessages(config, payload, options, write) {
|
|
|
6359
6371
|
type: "web_search_tool_result",
|
|
6360
6372
|
tool_use_id: marker
|
|
6361
6373
|
};
|
|
6362
|
-
const upstreamToolResult = toolResult(searchCall.id, resultBlock.content);
|
|
6374
|
+
const upstreamToolResult = toolResult$1(searchCall.id, resultBlock.content);
|
|
6363
6375
|
const hasSiblingTools = message.content.some((block) => block.type === "tool_use" && block !== searchCall);
|
|
6364
6376
|
let usage$1 = mergeUsage(message.usage, searchMessage.usage);
|
|
6365
6377
|
const combined = [...displayDecision, resultBlock];
|
|
@@ -6897,6 +6909,1121 @@ claudeRoutes.post("/messages/count_tokens", async (c) => {
|
|
|
6897
6909
|
}
|
|
6898
6910
|
});
|
|
6899
6911
|
|
|
6912
|
+
//#endregion
|
|
6913
|
+
//#region src/codex/thinking.ts
|
|
6914
|
+
const thinkingPrefix = "copilot-relay.claude-thinking.v1:";
|
|
6915
|
+
const isRecord$5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6916
|
+
const encodeThinking = (block) => thinkingPrefix + Buffer.from(JSON.stringify(block), "utf8").toString("base64url");
|
|
6917
|
+
/** Whether a reasoning item's encrypted_content is one the relay wrote for Claude. */
|
|
6918
|
+
const isRelayThinking = (encrypted) => typeof encrypted === "string" && encrypted.startsWith(thinkingPrefix);
|
|
6919
|
+
/** The Claude thinking block a reasoning item carries, or undefined when it is not the relay's. */
|
|
6920
|
+
const decodeThinking = (encrypted) => {
|
|
6921
|
+
if (typeof encrypted !== "string" || !isRelayThinking(encrypted)) return;
|
|
6922
|
+
let block;
|
|
6923
|
+
try {
|
|
6924
|
+
block = JSON.parse(Buffer.from(encrypted.slice(33), "base64url").toString("utf8"));
|
|
6925
|
+
} catch {
|
|
6926
|
+
return;
|
|
6927
|
+
}
|
|
6928
|
+
if (!isRecord$5(block)) return;
|
|
6929
|
+
if (block.type === "thinking" && typeof block.thinking === "string" && typeof block.signature === "string") return {
|
|
6930
|
+
type: "thinking",
|
|
6931
|
+
thinking: block.thinking,
|
|
6932
|
+
signature: block.signature
|
|
6933
|
+
};
|
|
6934
|
+
if (block.type === "redacted_thinking" && typeof block.data === "string") return {
|
|
6935
|
+
type: "redacted_thinking",
|
|
6936
|
+
data: block.data
|
|
6937
|
+
};
|
|
6938
|
+
};
|
|
6939
|
+
|
|
6940
|
+
//#endregion
|
|
6941
|
+
//#region src/codex/request.ts
|
|
6942
|
+
const copilotReasoningEfforts = [
|
|
6943
|
+
"none",
|
|
6944
|
+
"minimal",
|
|
6945
|
+
"low",
|
|
6946
|
+
"medium",
|
|
6947
|
+
"high",
|
|
6948
|
+
"xhigh",
|
|
6949
|
+
"max"
|
|
6950
|
+
];
|
|
6951
|
+
const effortOrder = [
|
|
6952
|
+
"none",
|
|
6953
|
+
"minimal",
|
|
6954
|
+
"low",
|
|
6955
|
+
"medium",
|
|
6956
|
+
"high",
|
|
6957
|
+
"xhigh",
|
|
6958
|
+
"max",
|
|
6959
|
+
"ultra"
|
|
6960
|
+
];
|
|
6961
|
+
const isRecord$4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6962
|
+
const codexRequestError = (message, code, status$1 = 400) => new HTTPError(message, Response.json({ error: {
|
|
6963
|
+
message,
|
|
6964
|
+
type: "invalid_request_error",
|
|
6965
|
+
code
|
|
6966
|
+
} }, { status: status$1 }), message);
|
|
6967
|
+
const parseCodexPayload = (text$1) => {
|
|
6968
|
+
let body;
|
|
6969
|
+
try {
|
|
6970
|
+
body = JSON.parse(text$1);
|
|
6971
|
+
} catch {
|
|
6972
|
+
throw codexRequestError("The request body is not valid JSON.", "invalid_json");
|
|
6973
|
+
}
|
|
6974
|
+
if (!isRecord$4(body) || typeof body.model !== "string" || body.model.trim() === "") throw codexRequestError("The request needs a model.", "missing_model");
|
|
6975
|
+
if (typeof body.input !== "string" && !Array.isArray(body.input)) throw codexRequestError("The request needs an input string or array.", "missing_input");
|
|
6976
|
+
return body;
|
|
6977
|
+
};
|
|
6978
|
+
const catalogDescribesUpstream = (config) => config.modelCatalog?.baseUrl === config.copilotBaseUrl;
|
|
6979
|
+
/**
|
|
6980
|
+
* The Copilot model a Codex request is sent to, and the API that serves it.
|
|
6981
|
+
*
|
|
6982
|
+
* A model Copilot's catalog lists is used as named, so Codex's own model IDs reach the same
|
|
6983
|
+
* Copilot model. A GPT model listed with /responses is forwarded there; a Claude model listed with
|
|
6984
|
+
* /v1/messages is translated to Messages, because Copilot serves Claude only there and on Chat
|
|
6985
|
+
* Completions. A name the catalog does not list, such as Codex's internal codex-auto-review, goes
|
|
6986
|
+
* to the configured gptModel, as Claude Code's aliases do. Without a catalog a non-Claude name is
|
|
6987
|
+
* forwarded unchanged.
|
|
6988
|
+
*/
|
|
6989
|
+
const resolveCodexModel = async (config, requested) => {
|
|
6990
|
+
const normalized = normalizeCopilotModelId(requested);
|
|
6991
|
+
await ensureCopilotModelCatalog(config, normalized);
|
|
6992
|
+
pinCopilotModelCatalog(config);
|
|
6993
|
+
if (isClaudeModelId(normalized)) {
|
|
6994
|
+
const capabilities$1 = getCachedCopilotModel(config, normalized);
|
|
6995
|
+
if (capabilities$1 === void 0) throw codexRequestError(`${requested} is not in Copilot's model list. Check copilot-relay models for the exact Claude model ID.`, "relay_unsupported_model");
|
|
6996
|
+
if (capabilities$1.supportedEndpoints !== void 0 && !capabilities$1.supportedEndpoints.includes("/v1/messages")) throw codexRequestError(`${normalized} does not serve Claude Messages on Copilot, which Codex needs for Claude models.`, "relay_unsupported_endpoint");
|
|
6997
|
+
return {
|
|
6998
|
+
model: normalized,
|
|
6999
|
+
api: "messages"
|
|
7000
|
+
};
|
|
7001
|
+
}
|
|
7002
|
+
const model = !(getCachedCopilotModel(config, normalized) !== void 0) && catalogDescribesUpstream(config) ? getModelRouting().gptModel : normalized;
|
|
7003
|
+
const capabilities = getCachedCopilotModel(config, model);
|
|
7004
|
+
const notChat = capabilities?.type !== void 0 && capabilities.type !== "chat";
|
|
7005
|
+
const noResponses = capabilities?.supportedEndpoints !== void 0 && !capabilities.supportedEndpoints.includes("/responses");
|
|
7006
|
+
if (notChat || noResponses) throw codexRequestError(`${model} does not serve the Responses API on Copilot. Choose a model that lists /responses in copilot-relay models.`, "relay_unsupported_endpoint");
|
|
7007
|
+
return {
|
|
7008
|
+
model,
|
|
7009
|
+
api: "responses"
|
|
7010
|
+
};
|
|
7011
|
+
};
|
|
7012
|
+
const effortRank = (effort) => effortOrder.indexOf(effort);
|
|
7013
|
+
/**
|
|
7014
|
+
* The reasoning effort to send for a requested one.
|
|
7015
|
+
*
|
|
7016
|
+
* A level the model advertises, or one this relay does not know, is sent as requested. Any other
|
|
7017
|
+
* level becomes the highest advertised level below it, or the lowest advertised level when none is
|
|
7018
|
+
* below it. A model that advertises no levels gets none, since Copilot rejects the field for it.
|
|
7019
|
+
*/
|
|
7020
|
+
const clampReasoningEffort = (requested, advertised) => {
|
|
7021
|
+
if (advertised?.length === 0) return;
|
|
7022
|
+
const accepted = advertised ?? copilotReasoningEfforts;
|
|
7023
|
+
if (accepted.includes(requested) || effortRank(requested) === -1) return requested;
|
|
7024
|
+
const ranked = accepted.filter((effort) => effortRank(effort) !== -1).sort((left, right) => effortRank(left) - effortRank(right));
|
|
7025
|
+
return ranked.filter((effort) => effortRank(effort) <= effortRank(requested)).at(-1) ?? ranked[0];
|
|
7026
|
+
};
|
|
7027
|
+
/**
|
|
7028
|
+
* The body sent to Copilot for a Codex request.
|
|
7029
|
+
*
|
|
7030
|
+
* Copilot answers HTTP 400 to `service_tier`, to an `image_generation` tool, and to an effort it
|
|
7031
|
+
* does not accept, so those change. Reasoning items holding the relay's Claude thinking are removed.
|
|
7032
|
+
* Every other field, including Codex's prompt_cache_key and GPT's encrypted reasoning, is forwarded
|
|
7033
|
+
* unchanged.
|
|
7034
|
+
*/
|
|
7035
|
+
const prepareCodexPayload = (payload, model, capabilities) => {
|
|
7036
|
+
const prepared = {
|
|
7037
|
+
...payload,
|
|
7038
|
+
model
|
|
7039
|
+
};
|
|
7040
|
+
const removed$1 = [];
|
|
7041
|
+
if (Object.hasOwn(prepared, "service_tier")) {
|
|
7042
|
+
delete prepared.service_tier;
|
|
7043
|
+
removed$1.push("service_tier");
|
|
7044
|
+
}
|
|
7045
|
+
if (Array.isArray(prepared.input)) {
|
|
7046
|
+
const input = prepared.input.filter((item) => !(isRecord$4(item) && item.type === "reasoning" && isRelayThinking(item.encrypted_content)));
|
|
7047
|
+
if (input.length !== prepared.input.length) {
|
|
7048
|
+
prepared.input = input;
|
|
7049
|
+
removed$1.push("claude_reasoning");
|
|
7050
|
+
}
|
|
7051
|
+
}
|
|
7052
|
+
if (Array.isArray(prepared.tools)) {
|
|
7053
|
+
const tools = prepared.tools.filter((tool) => !(isRecord$4(tool) && tool.type === "image_generation"));
|
|
7054
|
+
if (tools.length !== prepared.tools.length) {
|
|
7055
|
+
prepared.tools = tools;
|
|
7056
|
+
removed$1.push("image_generation");
|
|
7057
|
+
}
|
|
7058
|
+
}
|
|
7059
|
+
const reasoning = isRecord$4(prepared.reasoning) ? prepared.reasoning : void 0;
|
|
7060
|
+
const requestedEffort = typeof reasoning?.effort === "string" ? reasoning.effort : void 0;
|
|
7061
|
+
let effectiveEffort = requestedEffort;
|
|
7062
|
+
if (reasoning !== void 0 && requestedEffort !== void 0) {
|
|
7063
|
+
effectiveEffort = clampReasoningEffort(requestedEffort, capabilities?.reasoningEfforts);
|
|
7064
|
+
if (effectiveEffort === void 0) {
|
|
7065
|
+
const withoutEffort = { ...reasoning };
|
|
7066
|
+
delete withoutEffort.effort;
|
|
7067
|
+
prepared.reasoning = withoutEffort;
|
|
7068
|
+
} else if (effectiveEffort !== requestedEffort) prepared.reasoning = {
|
|
7069
|
+
...reasoning,
|
|
7070
|
+
effort: effectiveEffort
|
|
7071
|
+
};
|
|
7072
|
+
}
|
|
7073
|
+
return {
|
|
7074
|
+
payload: prepared,
|
|
7075
|
+
removed: removed$1,
|
|
7076
|
+
requestedEffort,
|
|
7077
|
+
effectiveEffort
|
|
7078
|
+
};
|
|
7079
|
+
};
|
|
7080
|
+
/**
|
|
7081
|
+
* Who started a Codex request, for Copilot's x-initiator header.
|
|
7082
|
+
*
|
|
7083
|
+
* A request continues the agent's own work unless its last input item is a user message: after a
|
|
7084
|
+
* tool call the last item is the tool's output. Codex marks its subagents (review, compaction,
|
|
7085
|
+
* memory consolidation, spawned agents) with x-openai-subagent, and those are always the agent.
|
|
7086
|
+
*/
|
|
7087
|
+
const getCodexInitiator = (input, subagent) => {
|
|
7088
|
+
if (subagent !== void 0) return "agent";
|
|
7089
|
+
if (typeof input === "string") return "user";
|
|
7090
|
+
const last = Array.isArray(input) ? input.at(-1) : void 0;
|
|
7091
|
+
return isRecord$4(last) && last.role === "user" && (last.type === void 0 || last.type === "message") ? "user" : "agent";
|
|
7092
|
+
};
|
|
7093
|
+
const includesImage = (value) => {
|
|
7094
|
+
if (Array.isArray(value)) return value.some(includesImage);
|
|
7095
|
+
if (!isRecord$4(value)) return false;
|
|
7096
|
+
return value.type === "input_image" || Object.values(value).some(includesImage);
|
|
7097
|
+
};
|
|
7098
|
+
/**
|
|
7099
|
+
* A stream holding only `response.failed` with code `context_length_exceeded`.
|
|
7100
|
+
*
|
|
7101
|
+
* Copilot reports a prompt over the model's input limit as HTTP 400, which Codex treats as a fatal
|
|
7102
|
+
* error. Codex recognizes a context-window overflow only from this event, and answers it by
|
|
7103
|
+
* compacting the conversation.
|
|
7104
|
+
*/
|
|
7105
|
+
const contextOverflowStream = (requestId, message) => {
|
|
7106
|
+
const event = {
|
|
7107
|
+
type: "response.failed",
|
|
7108
|
+
response: {
|
|
7109
|
+
id: `resp_relay_${requestId}`,
|
|
7110
|
+
object: "response",
|
|
7111
|
+
status: "failed",
|
|
7112
|
+
error: {
|
|
7113
|
+
code: "context_length_exceeded",
|
|
7114
|
+
message
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
};
|
|
7118
|
+
return `event: response.failed\ndata: ${JSON.stringify(event)}\n\n`;
|
|
7119
|
+
};
|
|
7120
|
+
|
|
7121
|
+
//#endregion
|
|
7122
|
+
//#region src/codex/messages.ts
|
|
7123
|
+
const claudeReasoningEfforts = [
|
|
7124
|
+
"low",
|
|
7125
|
+
"medium",
|
|
7126
|
+
"high",
|
|
7127
|
+
"xhigh",
|
|
7128
|
+
"max"
|
|
7129
|
+
];
|
|
7130
|
+
const defaultMaxTokens = 32e3;
|
|
7131
|
+
const isRecord$3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7132
|
+
const nameLimit = 64;
|
|
7133
|
+
/**
|
|
7134
|
+
* Claude tool names for Codex tools, and the way back.
|
|
7135
|
+
*
|
|
7136
|
+
* Claude accepts letters, digits, underscores and hyphens, at most 64 characters. A Codex name
|
|
7137
|
+
* outside that, or a tool inside a namespace, gets a sanitized name, made unique with a numbered
|
|
7138
|
+
* suffix. Names are assigned in the order tools are listed, so the same request gets the same names.
|
|
7139
|
+
*/
|
|
7140
|
+
var CodexToolNames = class {
|
|
7141
|
+
byKey = /* @__PURE__ */ new Map();
|
|
7142
|
+
byClaudeName = /* @__PURE__ */ new Map();
|
|
7143
|
+
claudeName(target) {
|
|
7144
|
+
const key = `${target.kind}\u0000${target.namespace ?? ""}\u0000${target.name}`;
|
|
7145
|
+
const known = this.byKey.get(key);
|
|
7146
|
+
if (known !== void 0) return known;
|
|
7147
|
+
const base = (target.namespace === void 0 ? target.name : `${target.namespace}__${target.name}`).replaceAll(/[^A-Za-z0-9_-]/g, "_").slice(0, nameLimit) || "tool";
|
|
7148
|
+
let name = base;
|
|
7149
|
+
let suffix = 2;
|
|
7150
|
+
while (this.byClaudeName.has(name)) {
|
|
7151
|
+
const tail = `_${suffix}`;
|
|
7152
|
+
name = base.slice(0, nameLimit - tail.length) + tail;
|
|
7153
|
+
suffix++;
|
|
7154
|
+
}
|
|
7155
|
+
this.byKey.set(key, name);
|
|
7156
|
+
this.byClaudeName.set(name, target);
|
|
7157
|
+
return name;
|
|
7158
|
+
}
|
|
7159
|
+
target(claudeName) {
|
|
7160
|
+
return this.byClaudeName.get(claudeName) ?? {
|
|
7161
|
+
kind: "function",
|
|
7162
|
+
name: claudeName
|
|
7163
|
+
};
|
|
7164
|
+
}
|
|
7165
|
+
};
|
|
7166
|
+
const customInputSchema = {
|
|
7167
|
+
type: "object",
|
|
7168
|
+
properties: { input: {
|
|
7169
|
+
type: "string",
|
|
7170
|
+
description: "The raw tool input."
|
|
7171
|
+
} },
|
|
7172
|
+
required: ["input"]
|
|
7173
|
+
};
|
|
7174
|
+
const customDescription = (tool) => {
|
|
7175
|
+
const description = typeof tool.description === "string" ? tool.description : "";
|
|
7176
|
+
const format = isRecord$3(tool.format) ? tool.format : void 0;
|
|
7177
|
+
if (format?.type !== "grammar" || typeof format.definition !== "string") return description;
|
|
7178
|
+
return `${description}\n\nPut the raw input in the input field. It must match this ${String(format.syntax ?? "")} grammar:\n${format.definition}`.trim();
|
|
7179
|
+
};
|
|
7180
|
+
const functionTool = (tool, name) => ({
|
|
7181
|
+
name,
|
|
7182
|
+
...typeof tool.description === "string" && tool.description && { description: tool.description },
|
|
7183
|
+
input_schema: isRecord$3(tool.parameters) ? tool.parameters : {
|
|
7184
|
+
type: "object",
|
|
7185
|
+
properties: {}
|
|
7186
|
+
}
|
|
7187
|
+
});
|
|
7188
|
+
const convertTool = (tool, names, namespace) => {
|
|
7189
|
+
if (typeof tool.name !== "string") return;
|
|
7190
|
+
const scope = namespace === void 0 ? {} : { namespace };
|
|
7191
|
+
if (tool.type === "function") return functionTool(tool, names.claudeName({
|
|
7192
|
+
kind: "function",
|
|
7193
|
+
name: tool.name,
|
|
7194
|
+
...scope
|
|
7195
|
+
}));
|
|
7196
|
+
if (tool.type === "custom") return {
|
|
7197
|
+
name: names.claudeName({
|
|
7198
|
+
kind: "custom",
|
|
7199
|
+
name: tool.name,
|
|
7200
|
+
...scope
|
|
7201
|
+
}),
|
|
7202
|
+
description: customDescription(tool),
|
|
7203
|
+
input_schema: customInputSchema
|
|
7204
|
+
};
|
|
7205
|
+
};
|
|
7206
|
+
const convertTools = (tools, names, dropped) => {
|
|
7207
|
+
if (!Array.isArray(tools)) return [];
|
|
7208
|
+
const converted = [];
|
|
7209
|
+
for (const tool of tools) {
|
|
7210
|
+
if (!isRecord$3(tool) || typeof tool.type !== "string") continue;
|
|
7211
|
+
if (tool.type === "namespace" && typeof tool.name === "string" && Array.isArray(tool.tools)) {
|
|
7212
|
+
for (const inner of tool.tools) {
|
|
7213
|
+
if (!isRecord$3(inner)) continue;
|
|
7214
|
+
const claudeTool$1 = convertTool(inner, names, tool.name);
|
|
7215
|
+
if (claudeTool$1) converted.push(claudeTool$1);
|
|
7216
|
+
else dropped.add(`tool:${tool.name}.${String(inner.type)}`);
|
|
7217
|
+
}
|
|
7218
|
+
continue;
|
|
7219
|
+
}
|
|
7220
|
+
const claudeTool = convertTool(tool, names, void 0);
|
|
7221
|
+
if (claudeTool) converted.push(claudeTool);
|
|
7222
|
+
else dropped.add(`tool:${tool.type}`);
|
|
7223
|
+
}
|
|
7224
|
+
return converted;
|
|
7225
|
+
};
|
|
7226
|
+
const imageBlock = (url) => {
|
|
7227
|
+
const data = /^data:(image\/(?:png|jpeg|gif|webp));base64,(.+)$/s.exec(url);
|
|
7228
|
+
if (data) return {
|
|
7229
|
+
type: "image",
|
|
7230
|
+
source: {
|
|
7231
|
+
type: "base64",
|
|
7232
|
+
media_type: data[1],
|
|
7233
|
+
data: data[2]
|
|
7234
|
+
}
|
|
7235
|
+
};
|
|
7236
|
+
if (/^https?:\/\//i.test(url)) return {
|
|
7237
|
+
type: "image",
|
|
7238
|
+
source: {
|
|
7239
|
+
type: "url",
|
|
7240
|
+
url
|
|
7241
|
+
}
|
|
7242
|
+
};
|
|
7243
|
+
};
|
|
7244
|
+
const contentBlocks = (content, allowImages) => {
|
|
7245
|
+
if (typeof content === "string") return content ? [{
|
|
7246
|
+
type: "text",
|
|
7247
|
+
text: content
|
|
7248
|
+
}] : [];
|
|
7249
|
+
if (!Array.isArray(content)) return [];
|
|
7250
|
+
const blocks = [];
|
|
7251
|
+
for (const part of content) {
|
|
7252
|
+
if (!isRecord$3(part)) continue;
|
|
7253
|
+
const text$1 = part.type === "refusal" ? part.refusal : part.text;
|
|
7254
|
+
if ((part.type === "input_text" || part.type === "output_text" || part.type === "refusal") && typeof text$1 === "string" && text$1) blocks.push({
|
|
7255
|
+
type: "text",
|
|
7256
|
+
text: text$1
|
|
7257
|
+
});
|
|
7258
|
+
else if (part.type === "input_image" && allowImages && typeof part.image_url === "string") {
|
|
7259
|
+
const image = imageBlock(part.image_url);
|
|
7260
|
+
if (image) blocks.push(image);
|
|
7261
|
+
}
|
|
7262
|
+
}
|
|
7263
|
+
return blocks;
|
|
7264
|
+
};
|
|
7265
|
+
const toolResult = (callId, output) => {
|
|
7266
|
+
const blocks = contentBlocks(output, true);
|
|
7267
|
+
return {
|
|
7268
|
+
type: "tool_result",
|
|
7269
|
+
tool_use_id: callId,
|
|
7270
|
+
content: blocks.length > 0 ? blocks : [{
|
|
7271
|
+
type: "text",
|
|
7272
|
+
text: "(no output)"
|
|
7273
|
+
}]
|
|
7274
|
+
};
|
|
7275
|
+
};
|
|
7276
|
+
const pushBlocks = (turns, role, blocks) => {
|
|
7277
|
+
if (blocks.length === 0) return;
|
|
7278
|
+
const last = turns.at(-1);
|
|
7279
|
+
if (last?.role === role) last.content.push(...blocks);
|
|
7280
|
+
else turns.push({
|
|
7281
|
+
role,
|
|
7282
|
+
content: [...blocks]
|
|
7283
|
+
});
|
|
7284
|
+
};
|
|
7285
|
+
const toolId = (id) => typeof id === "string" && id ? id.replaceAll(/[^A-Za-z0-9_-]/g, "_") : "call_missing";
|
|
7286
|
+
const parseArguments = (value) => {
|
|
7287
|
+
if (typeof value !== "string") return {};
|
|
7288
|
+
try {
|
|
7289
|
+
const parsed = JSON.parse(value);
|
|
7290
|
+
return isRecord$3(parsed) ? parsed : {};
|
|
7291
|
+
} catch {
|
|
7292
|
+
return {};
|
|
7293
|
+
}
|
|
7294
|
+
};
|
|
7295
|
+
const isCacheable = (block) => block.type !== "thinking" && block.type !== "redacted_thinking";
|
|
7296
|
+
const markCache = (block) => {
|
|
7297
|
+
if (block) block.cache_control = { type: "ephemeral" };
|
|
7298
|
+
};
|
|
7299
|
+
const lastCacheable = (turn) => turn?.content.filter(isCacheable).at(-1);
|
|
7300
|
+
/**
|
|
7301
|
+
* Prompt-cache breakpoints as Claude Code places them: the end of the system prompt, the last tool,
|
|
7302
|
+
* the end of the conversation, and the end of the user turn before it, so the next request in a
|
|
7303
|
+
* tool loop still finds this one's prefix. Four marks, Claude's limit.
|
|
7304
|
+
*/
|
|
7305
|
+
const markCacheBreakpoints = (system, tools, turns) => {
|
|
7306
|
+
markCache(system.at(-1));
|
|
7307
|
+
markCache(tools.at(-1));
|
|
7308
|
+
markCache(lastCacheable(turns.at(-1)));
|
|
7309
|
+
markCache(lastCacheable(turns.slice(0, -1).filter((turn) => turn.role === "user").at(-1)));
|
|
7310
|
+
};
|
|
7311
|
+
const toolChoice = (choice, parallel, names) => {
|
|
7312
|
+
let selected;
|
|
7313
|
+
if (choice === "required") selected = { type: "any" };
|
|
7314
|
+
else if (choice === "none") selected = { type: "none" };
|
|
7315
|
+
else if (isRecord$3(choice) && choice.type === "function" && typeof choice.name === "string") selected = {
|
|
7316
|
+
type: "tool",
|
|
7317
|
+
name: names.claudeName({
|
|
7318
|
+
kind: "function",
|
|
7319
|
+
name: choice.name
|
|
7320
|
+
})
|
|
7321
|
+
};
|
|
7322
|
+
else if (choice === "auto" || parallel === false) selected = { type: "auto" };
|
|
7323
|
+
if (selected && parallel === false && selected.type !== "none") selected.disable_parallel_tool_use = true;
|
|
7324
|
+
return selected;
|
|
7325
|
+
};
|
|
7326
|
+
const jsonSchemaFormat = (text$1) => {
|
|
7327
|
+
const format = isRecord$3(text$1) && isRecord$3(text$1.format) ? text$1.format : void 0;
|
|
7328
|
+
if (format?.type !== "json_schema" || !isRecord$3(format.schema)) return;
|
|
7329
|
+
return `Your final answer must be a single JSON value that matches this JSON Schema, with no other text and no code fence:\n${JSON.stringify(format.schema)}`;
|
|
7330
|
+
};
|
|
7331
|
+
/**
|
|
7332
|
+
* The Claude Messages request for a Codex Responses request.
|
|
7333
|
+
*
|
|
7334
|
+
* Instructions and leading developer or system messages become the system prompt; a later developer
|
|
7335
|
+
* message becomes user text in place, so the history keeps its order. Function, custom and namespace
|
|
7336
|
+
* tools become Claude tools; hosted tools such as web_search are dropped. Calls and their outputs
|
|
7337
|
+
* become tool_use and tool_result blocks, and the relay's reasoning items become Claude thinking.
|
|
7338
|
+
*/
|
|
7339
|
+
const toClaudeRequest = (payload, model, capabilities) => {
|
|
7340
|
+
const names = new CodexToolNames();
|
|
7341
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
7342
|
+
const tools = convertTools(payload.tools, names, dropped);
|
|
7343
|
+
const system = [];
|
|
7344
|
+
const turns = [];
|
|
7345
|
+
if (typeof payload.instructions === "string" && payload.instructions) system.push({
|
|
7346
|
+
type: "text",
|
|
7347
|
+
text: payload.instructions
|
|
7348
|
+
});
|
|
7349
|
+
const outputSchema = jsonSchemaFormat(payload.text);
|
|
7350
|
+
if (outputSchema !== void 0) system.push({
|
|
7351
|
+
type: "text",
|
|
7352
|
+
text: outputSchema
|
|
7353
|
+
});
|
|
7354
|
+
const items = typeof payload.input === "string" ? [{
|
|
7355
|
+
type: "message",
|
|
7356
|
+
role: "user",
|
|
7357
|
+
content: payload.input
|
|
7358
|
+
}] : payload.input;
|
|
7359
|
+
for (const item of items) {
|
|
7360
|
+
if (!isRecord$3(item)) continue;
|
|
7361
|
+
const type = typeof item.type === "string" ? item.type : "message";
|
|
7362
|
+
if (type === "message" && (item.role === "developer" || item.role === "system")) {
|
|
7363
|
+
const blocks = contentBlocks(item.content, false);
|
|
7364
|
+
if (turns.length === 0) system.push(...blocks);
|
|
7365
|
+
else pushBlocks(turns, "user", blocks);
|
|
7366
|
+
} else if (type === "message") {
|
|
7367
|
+
const role = item.role === "assistant" ? "assistant" : "user";
|
|
7368
|
+
pushBlocks(turns, role, contentBlocks(item.content, role === "user"));
|
|
7369
|
+
} else if (type === "reasoning") {
|
|
7370
|
+
const block = decodeThinking(item.encrypted_content);
|
|
7371
|
+
if (block) pushBlocks(turns, "assistant", [block]);
|
|
7372
|
+
else dropped.add("reasoning");
|
|
7373
|
+
} else if ((type === "function_call" || type === "custom_tool_call") && typeof item.name === "string") {
|
|
7374
|
+
const custom = type === "custom_tool_call";
|
|
7375
|
+
const target = {
|
|
7376
|
+
kind: custom ? "custom" : "function",
|
|
7377
|
+
name: item.name,
|
|
7378
|
+
...typeof item.namespace === "string" && { namespace: item.namespace }
|
|
7379
|
+
};
|
|
7380
|
+
const input = custom ? { input: typeof item.input === "string" ? item.input : "" } : parseArguments(item.arguments);
|
|
7381
|
+
pushBlocks(turns, "assistant", [{
|
|
7382
|
+
type: "tool_use",
|
|
7383
|
+
id: toolId(item.call_id),
|
|
7384
|
+
name: names.claudeName(target),
|
|
7385
|
+
input
|
|
7386
|
+
}]);
|
|
7387
|
+
} else if (type === "function_call_output" || type === "custom_tool_call_output") pushBlocks(turns, "user", [toolResult(toolId(item.call_id), item.output)]);
|
|
7388
|
+
else dropped.add(type);
|
|
7389
|
+
}
|
|
7390
|
+
for (const turn of turns) if (turn.role === "user") turn.content.sort((left, right) => Number(right.type === "tool_result") - Number(left.type === "tool_result"));
|
|
7391
|
+
markCacheBreakpoints(system, tools, turns);
|
|
7392
|
+
const reasoning = isRecord$3(payload.reasoning) ? payload.reasoning : void 0;
|
|
7393
|
+
const requestedEffort = typeof reasoning?.effort === "string" ? reasoning.effort : void 0;
|
|
7394
|
+
const advertised = capabilities?.reasoningEfforts?.filter((effort) => claudeReasoningEfforts.includes(effort));
|
|
7395
|
+
const effectiveEffort = requestedEffort === void 0 ? void 0 : clampReasoningEffort(requestedEffort, advertised ?? claudeReasoningEfforts);
|
|
7396
|
+
const maxTokens = typeof payload.max_output_tokens === "number" && payload.max_output_tokens > 0 ? payload.max_output_tokens : capabilities?.limits?.max_output_tokens ?? defaultMaxTokens;
|
|
7397
|
+
const choice = tools.length > 0 ? toolChoice(payload.tool_choice, payload.parallel_tool_calls, names) : void 0;
|
|
7398
|
+
return {
|
|
7399
|
+
payload: {
|
|
7400
|
+
model,
|
|
7401
|
+
max_tokens: maxTokens,
|
|
7402
|
+
messages: turns,
|
|
7403
|
+
stream: true,
|
|
7404
|
+
...system.length > 0 && { system },
|
|
7405
|
+
...tools.length > 0 && { tools },
|
|
7406
|
+
...choice && { tool_choice: choice },
|
|
7407
|
+
...effectiveEffort !== void 0 && {
|
|
7408
|
+
thinking: { type: "adaptive" },
|
|
7409
|
+
output_config: { effort: effectiveEffort }
|
|
7410
|
+
}
|
|
7411
|
+
},
|
|
7412
|
+
tools: names,
|
|
7413
|
+
dropped: [...dropped],
|
|
7414
|
+
requestedEffort,
|
|
7415
|
+
effectiveEffort
|
|
7416
|
+
};
|
|
7417
|
+
};
|
|
7418
|
+
const callItem = (block, status$1) => {
|
|
7419
|
+
if (block.target.kind === "custom") {
|
|
7420
|
+
let input = block.json;
|
|
7421
|
+
try {
|
|
7422
|
+
const parsed = JSON.parse(block.json);
|
|
7423
|
+
if (isRecord$3(parsed) && typeof parsed.input === "string") input = parsed.input;
|
|
7424
|
+
} catch {}
|
|
7425
|
+
return {
|
|
7426
|
+
type: "custom_tool_call",
|
|
7427
|
+
id: block.id,
|
|
7428
|
+
call_id: block.callId,
|
|
7429
|
+
name: block.target.name,
|
|
7430
|
+
...block.target.namespace !== void 0 && { namespace: block.target.namespace },
|
|
7431
|
+
input,
|
|
7432
|
+
status: status$1
|
|
7433
|
+
};
|
|
7434
|
+
}
|
|
7435
|
+
return {
|
|
7436
|
+
type: "function_call",
|
|
7437
|
+
id: block.id,
|
|
7438
|
+
call_id: block.callId,
|
|
7439
|
+
name: block.target.name,
|
|
7440
|
+
...block.target.namespace !== void 0 && { namespace: block.target.namespace },
|
|
7441
|
+
arguments: status$1 === "completed" ? block.json || "{}" : "",
|
|
7442
|
+
status: status$1
|
|
7443
|
+
};
|
|
7444
|
+
};
|
|
7445
|
+
const newBlock = (start$1, names, fallbackCallId) => {
|
|
7446
|
+
if (start$1.type === "text") return {
|
|
7447
|
+
kind: "message",
|
|
7448
|
+
text: typeof start$1.text === "string" ? start$1.text : ""
|
|
7449
|
+
};
|
|
7450
|
+
if (start$1.type === "thinking") return {
|
|
7451
|
+
kind: "reasoning",
|
|
7452
|
+
thinking: typeof start$1.thinking === "string" ? start$1.thinking : "",
|
|
7453
|
+
signature: ""
|
|
7454
|
+
};
|
|
7455
|
+
if (start$1.type === "redacted_thinking") return {
|
|
7456
|
+
kind: "redacted",
|
|
7457
|
+
data: typeof start$1.data === "string" ? start$1.data : ""
|
|
7458
|
+
};
|
|
7459
|
+
if (start$1.type === "tool_use" && typeof start$1.name === "string") return {
|
|
7460
|
+
kind: "call",
|
|
7461
|
+
callId: typeof start$1.id === "string" ? start$1.id : fallbackCallId,
|
|
7462
|
+
target: names.target(start$1.name),
|
|
7463
|
+
json: ""
|
|
7464
|
+
};
|
|
7465
|
+
};
|
|
7466
|
+
const applyDelta = (block, delta) => {
|
|
7467
|
+
if (block.kind === "message" && delta.type === "text_delta" && typeof delta.text === "string") {
|
|
7468
|
+
block.text += delta.text;
|
|
7469
|
+
return delta.text;
|
|
7470
|
+
}
|
|
7471
|
+
if (block.kind === "reasoning" && delta.type === "thinking_delta" && typeof delta.thinking === "string") {
|
|
7472
|
+
block.thinking += delta.thinking;
|
|
7473
|
+
return delta.thinking;
|
|
7474
|
+
}
|
|
7475
|
+
if (block.kind === "reasoning" && delta.type === "signature_delta" && typeof delta.signature === "string") {
|
|
7476
|
+
block.signature += delta.signature;
|
|
7477
|
+
return "";
|
|
7478
|
+
}
|
|
7479
|
+
if (block.kind === "call" && delta.type === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
7480
|
+
block.json += delta.partial_json;
|
|
7481
|
+
return delta.partial_json;
|
|
7482
|
+
}
|
|
7483
|
+
return "";
|
|
7484
|
+
};
|
|
7485
|
+
const streamed = (block) => {
|
|
7486
|
+
if (block.kind === "message") return block.text;
|
|
7487
|
+
if (block.kind === "reasoning") return block.thinking;
|
|
7488
|
+
if (block.kind === "call") return block.json;
|
|
7489
|
+
return "";
|
|
7490
|
+
};
|
|
7491
|
+
const itemFor = (block) => {
|
|
7492
|
+
if (block.kind === "message") return {
|
|
7493
|
+
type: "message",
|
|
7494
|
+
id: block.id,
|
|
7495
|
+
role: "assistant",
|
|
7496
|
+
status: "completed",
|
|
7497
|
+
content: [{
|
|
7498
|
+
type: "output_text",
|
|
7499
|
+
text: block.text,
|
|
7500
|
+
annotations: []
|
|
7501
|
+
}]
|
|
7502
|
+
};
|
|
7503
|
+
if (block.kind === "reasoning") return {
|
|
7504
|
+
type: "reasoning",
|
|
7505
|
+
id: block.id,
|
|
7506
|
+
summary: block.thinking ? [{
|
|
7507
|
+
type: "summary_text",
|
|
7508
|
+
text: block.thinking
|
|
7509
|
+
}] : [],
|
|
7510
|
+
encrypted_content: encodeThinking({
|
|
7511
|
+
type: "thinking",
|
|
7512
|
+
thinking: block.thinking,
|
|
7513
|
+
signature: block.signature
|
|
7514
|
+
})
|
|
7515
|
+
};
|
|
7516
|
+
if (block.kind === "redacted") return {
|
|
7517
|
+
type: "reasoning",
|
|
7518
|
+
id: block.id,
|
|
7519
|
+
summary: [],
|
|
7520
|
+
encrypted_content: encodeThinking({
|
|
7521
|
+
type: "redacted_thinking",
|
|
7522
|
+
data: block.data
|
|
7523
|
+
})
|
|
7524
|
+
};
|
|
7525
|
+
return callItem(block, "completed");
|
|
7526
|
+
};
|
|
7527
|
+
const startEvents = (block) => {
|
|
7528
|
+
if (block.kind === "message") return [{
|
|
7529
|
+
type: "response.output_item.added",
|
|
7530
|
+
output_index: block.index,
|
|
7531
|
+
item: {
|
|
7532
|
+
type: "message",
|
|
7533
|
+
id: block.id,
|
|
7534
|
+
role: "assistant",
|
|
7535
|
+
status: "in_progress",
|
|
7536
|
+
content: []
|
|
7537
|
+
}
|
|
7538
|
+
}, {
|
|
7539
|
+
type: "response.content_part.added",
|
|
7540
|
+
item_id: block.id,
|
|
7541
|
+
output_index: block.index,
|
|
7542
|
+
content_index: 0,
|
|
7543
|
+
part: {
|
|
7544
|
+
type: "output_text",
|
|
7545
|
+
text: "",
|
|
7546
|
+
annotations: []
|
|
7547
|
+
}
|
|
7548
|
+
}];
|
|
7549
|
+
if (block.kind === "reasoning") return [{
|
|
7550
|
+
type: "response.output_item.added",
|
|
7551
|
+
output_index: block.index,
|
|
7552
|
+
item: {
|
|
7553
|
+
type: "reasoning",
|
|
7554
|
+
id: block.id,
|
|
7555
|
+
summary: []
|
|
7556
|
+
}
|
|
7557
|
+
}, {
|
|
7558
|
+
type: "response.reasoning_summary_part.added",
|
|
7559
|
+
item_id: block.id,
|
|
7560
|
+
output_index: block.index,
|
|
7561
|
+
summary_index: 0,
|
|
7562
|
+
part: {
|
|
7563
|
+
type: "summary_text",
|
|
7564
|
+
text: ""
|
|
7565
|
+
}
|
|
7566
|
+
}];
|
|
7567
|
+
if (block.kind === "redacted") return [{
|
|
7568
|
+
type: "response.output_item.added",
|
|
7569
|
+
output_index: block.index,
|
|
7570
|
+
item: {
|
|
7571
|
+
type: "reasoning",
|
|
7572
|
+
id: block.id,
|
|
7573
|
+
summary: []
|
|
7574
|
+
}
|
|
7575
|
+
}];
|
|
7576
|
+
return [{
|
|
7577
|
+
type: "response.output_item.added",
|
|
7578
|
+
output_index: block.index,
|
|
7579
|
+
item: callItem(block, "in_progress")
|
|
7580
|
+
}];
|
|
7581
|
+
};
|
|
7582
|
+
const deltaEvents = (block, delta) => {
|
|
7583
|
+
if (!delta) return [];
|
|
7584
|
+
if (block.kind === "message") return [{
|
|
7585
|
+
type: "response.output_text.delta",
|
|
7586
|
+
item_id: block.id,
|
|
7587
|
+
output_index: block.index,
|
|
7588
|
+
content_index: 0,
|
|
7589
|
+
delta
|
|
7590
|
+
}];
|
|
7591
|
+
if (block.kind === "reasoning") return [{
|
|
7592
|
+
type: "response.reasoning_summary_text.delta",
|
|
7593
|
+
item_id: block.id,
|
|
7594
|
+
output_index: block.index,
|
|
7595
|
+
summary_index: 0,
|
|
7596
|
+
delta
|
|
7597
|
+
}];
|
|
7598
|
+
if (block.kind === "call" && block.target.kind === "function") return [{
|
|
7599
|
+
type: "response.function_call_arguments.delta",
|
|
7600
|
+
item_id: block.id,
|
|
7601
|
+
output_index: block.index,
|
|
7602
|
+
delta
|
|
7603
|
+
}];
|
|
7604
|
+
return [];
|
|
7605
|
+
};
|
|
7606
|
+
const doneEvents = (block, item) => {
|
|
7607
|
+
const itemDone = {
|
|
7608
|
+
type: "response.output_item.done",
|
|
7609
|
+
output_index: block.index,
|
|
7610
|
+
item
|
|
7611
|
+
};
|
|
7612
|
+
if (block.kind === "message") {
|
|
7613
|
+
const part = {
|
|
7614
|
+
type: "output_text",
|
|
7615
|
+
text: block.text,
|
|
7616
|
+
annotations: []
|
|
7617
|
+
};
|
|
7618
|
+
return [
|
|
7619
|
+
{
|
|
7620
|
+
type: "response.output_text.done",
|
|
7621
|
+
item_id: block.id,
|
|
7622
|
+
output_index: block.index,
|
|
7623
|
+
content_index: 0,
|
|
7624
|
+
text: block.text
|
|
7625
|
+
},
|
|
7626
|
+
{
|
|
7627
|
+
type: "response.content_part.done",
|
|
7628
|
+
item_id: block.id,
|
|
7629
|
+
output_index: block.index,
|
|
7630
|
+
content_index: 0,
|
|
7631
|
+
part
|
|
7632
|
+
},
|
|
7633
|
+
itemDone
|
|
7634
|
+
];
|
|
7635
|
+
}
|
|
7636
|
+
if (block.kind === "reasoning") return [
|
|
7637
|
+
{
|
|
7638
|
+
type: "response.reasoning_summary_text.done",
|
|
7639
|
+
item_id: block.id,
|
|
7640
|
+
output_index: block.index,
|
|
7641
|
+
summary_index: 0,
|
|
7642
|
+
text: block.thinking
|
|
7643
|
+
},
|
|
7644
|
+
{
|
|
7645
|
+
type: "response.reasoning_summary_part.done",
|
|
7646
|
+
item_id: block.id,
|
|
7647
|
+
output_index: block.index,
|
|
7648
|
+
summary_index: 0,
|
|
7649
|
+
part: {
|
|
7650
|
+
type: "summary_text",
|
|
7651
|
+
text: block.thinking
|
|
7652
|
+
}
|
|
7653
|
+
},
|
|
7654
|
+
itemDone
|
|
7655
|
+
];
|
|
7656
|
+
if (block.kind === "call" && block.target.kind === "function") return [{
|
|
7657
|
+
type: "response.function_call_arguments.done",
|
|
7658
|
+
item_id: block.id,
|
|
7659
|
+
output_index: block.index,
|
|
7660
|
+
arguments: item.arguments
|
|
7661
|
+
}, itemDone];
|
|
7662
|
+
return [itemDone];
|
|
7663
|
+
};
|
|
7664
|
+
const incompleteReasons = {
|
|
7665
|
+
max_tokens: "max_output_tokens",
|
|
7666
|
+
refusal: "content_filter"
|
|
7667
|
+
};
|
|
7668
|
+
const heldProgressMs = 3e4;
|
|
7669
|
+
const count = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
7670
|
+
const responsesUsage = (usage$1) => {
|
|
7671
|
+
const cacheRead = count(usage$1.cache_read_input_tokens);
|
|
7672
|
+
const inputTokens = count(usage$1.input_tokens) + cacheRead + count(usage$1.cache_creation_input_tokens);
|
|
7673
|
+
const outputTokens = count(usage$1.output_tokens);
|
|
7674
|
+
return {
|
|
7675
|
+
input_tokens: inputTokens,
|
|
7676
|
+
input_tokens_details: { cached_tokens: cacheRead },
|
|
7677
|
+
output_tokens: outputTokens,
|
|
7678
|
+
output_tokens_details: { reasoning_tokens: 0 },
|
|
7679
|
+
total_tokens: inputTokens + outputTokens
|
|
7680
|
+
};
|
|
7681
|
+
};
|
|
7682
|
+
/**
|
|
7683
|
+
* The Responses events for a Claude Messages stream.
|
|
7684
|
+
*
|
|
7685
|
+
* Text becomes a message item, thinking a reasoning item whose summary is the thinking text and
|
|
7686
|
+
* whose encrypted_content carries the signed block back on the next turn, and tool_use a
|
|
7687
|
+
* function_call or custom_tool_call. Codex runs a call as soon as its item is done, so from the first
|
|
7688
|
+
* tool_use on, blocks are held in order until Claude stops: released with their calls when it stops
|
|
7689
|
+
* with tool_use, released without them when it stops any other way, as when max_tokens cuts a call
|
|
7690
|
+
* short. The stream ends with response.completed, or response.incomplete when Claude stopped at
|
|
7691
|
+
* max_tokens or refused. An upstream failure, or a stream that ends with a block still open, ends it
|
|
7692
|
+
* with response.failed, which Codex retries.
|
|
7693
|
+
*/
|
|
7694
|
+
async function* toCodexEvents(events$1, model, names) {
|
|
7695
|
+
let responseId = "resp_relay";
|
|
7696
|
+
const createdAt = Math.floor(Date.now() / 1e3);
|
|
7697
|
+
const output = [];
|
|
7698
|
+
const open = /* @__PURE__ */ new Map();
|
|
7699
|
+
const held = [];
|
|
7700
|
+
let holding = false;
|
|
7701
|
+
let lastProgress = 0;
|
|
7702
|
+
let usage$1 = {};
|
|
7703
|
+
let stopReason;
|
|
7704
|
+
let nextIndex = 0;
|
|
7705
|
+
let stopped = false;
|
|
7706
|
+
const response = (status$1, extra = {}) => ({
|
|
7707
|
+
id: responseId,
|
|
7708
|
+
object: "response",
|
|
7709
|
+
created_at: createdAt,
|
|
7710
|
+
model,
|
|
7711
|
+
status: status$1,
|
|
7712
|
+
output: output.filter(Boolean),
|
|
7713
|
+
...extra
|
|
7714
|
+
});
|
|
7715
|
+
const place = (block) => {
|
|
7716
|
+
const index = nextIndex;
|
|
7717
|
+
nextIndex++;
|
|
7718
|
+
return {
|
|
7719
|
+
...block,
|
|
7720
|
+
id: `${responseId}_${index}`,
|
|
7721
|
+
index
|
|
7722
|
+
};
|
|
7723
|
+
};
|
|
7724
|
+
try {
|
|
7725
|
+
for await (const event of events$1) {
|
|
7726
|
+
if (holding && Date.now() - lastProgress >= heldProgressMs) {
|
|
7727
|
+
lastProgress = Date.now();
|
|
7728
|
+
yield {
|
|
7729
|
+
type: "response.in_progress",
|
|
7730
|
+
response: response("in_progress")
|
|
7731
|
+
};
|
|
7732
|
+
}
|
|
7733
|
+
if (event.type === "message_start") {
|
|
7734
|
+
const message = isRecord$3(event.message) ? event.message : {};
|
|
7735
|
+
if (typeof message.id === "string") responseId = `resp_${message.id}`;
|
|
7736
|
+
usage$1 = {
|
|
7737
|
+
...usage$1,
|
|
7738
|
+
...isRecord$3(message.usage) ? message.usage : {}
|
|
7739
|
+
};
|
|
7740
|
+
yield {
|
|
7741
|
+
type: "response.created",
|
|
7742
|
+
response: response("in_progress")
|
|
7743
|
+
};
|
|
7744
|
+
yield {
|
|
7745
|
+
type: "response.in_progress",
|
|
7746
|
+
response: response("in_progress")
|
|
7747
|
+
};
|
|
7748
|
+
} else if (event.type === "content_block_start" && typeof event.index === "number" && isRecord$3(event.content_block)) {
|
|
7749
|
+
const block = newBlock(event.content_block, names, `${responseId}_call_${event.index}`);
|
|
7750
|
+
if (block?.kind === "call" && !holding) {
|
|
7751
|
+
holding = true;
|
|
7752
|
+
lastProgress = Date.now();
|
|
7753
|
+
}
|
|
7754
|
+
if (block !== void 0 && holding) {
|
|
7755
|
+
held.push(block);
|
|
7756
|
+
open.set(event.index, { block });
|
|
7757
|
+
} else if (block !== void 0) {
|
|
7758
|
+
const placed = place(block);
|
|
7759
|
+
open.set(event.index, {
|
|
7760
|
+
block: placed,
|
|
7761
|
+
placed
|
|
7762
|
+
});
|
|
7763
|
+
yield* startEvents(placed);
|
|
7764
|
+
yield* deltaEvents(placed, streamed(placed));
|
|
7765
|
+
}
|
|
7766
|
+
} else if (event.type === "content_block_delta" && typeof event.index === "number" && isRecord$3(event.delta)) {
|
|
7767
|
+
const entry = open.get(event.index);
|
|
7768
|
+
if (entry !== void 0) {
|
|
7769
|
+
const piece = applyDelta(entry.block, event.delta);
|
|
7770
|
+
if (entry.placed !== void 0) yield* deltaEvents(entry.placed, piece);
|
|
7771
|
+
}
|
|
7772
|
+
} else if (event.type === "content_block_stop" && typeof event.index === "number") {
|
|
7773
|
+
const entry = open.get(event.index);
|
|
7774
|
+
open.delete(event.index);
|
|
7775
|
+
if (entry?.placed !== void 0) {
|
|
7776
|
+
const item = itemFor(entry.placed);
|
|
7777
|
+
output[entry.placed.index] = item;
|
|
7778
|
+
yield* doneEvents(entry.placed, item);
|
|
7779
|
+
}
|
|
7780
|
+
} else if (event.type === "message_delta") {
|
|
7781
|
+
if (isRecord$3(event.delta) && typeof event.delta.stop_reason === "string") stopReason = event.delta.stop_reason;
|
|
7782
|
+
usage$1 = {
|
|
7783
|
+
...usage$1,
|
|
7784
|
+
...isRecord$3(event.usage) ? event.usage : {}
|
|
7785
|
+
};
|
|
7786
|
+
} else if (event.type === "message_stop") {
|
|
7787
|
+
stopped = true;
|
|
7788
|
+
if (open.size > 0) throw new Error("Claude stream stopped with a content block still open.");
|
|
7789
|
+
for (const block of held) {
|
|
7790
|
+
if (block.kind === "call" && stopReason !== "tool_use") continue;
|
|
7791
|
+
const placed = place(block);
|
|
7792
|
+
const item = itemFor(placed);
|
|
7793
|
+
output[placed.index] = item;
|
|
7794
|
+
yield* startEvents(placed);
|
|
7795
|
+
yield* deltaEvents(placed, streamed(placed));
|
|
7796
|
+
yield* doneEvents(placed, item);
|
|
7797
|
+
}
|
|
7798
|
+
const incompleteReason = incompleteReasons[String(stopReason)];
|
|
7799
|
+
if (incompleteReason !== void 0) yield {
|
|
7800
|
+
type: "response.incomplete",
|
|
7801
|
+
response: response("incomplete", {
|
|
7802
|
+
usage: responsesUsage(usage$1),
|
|
7803
|
+
incomplete_details: { reason: incompleteReason }
|
|
7804
|
+
})
|
|
7805
|
+
};
|
|
7806
|
+
else yield {
|
|
7807
|
+
type: "response.completed",
|
|
7808
|
+
response: response("completed", { usage: responsesUsage(usage$1) })
|
|
7809
|
+
};
|
|
7810
|
+
}
|
|
7811
|
+
}
|
|
7812
|
+
if (!stopped) throw new Error("Claude stream ended without message_stop.");
|
|
7813
|
+
} catch (error) {
|
|
7814
|
+
yield {
|
|
7815
|
+
type: "response.failed",
|
|
7816
|
+
response: response("failed", { error: {
|
|
7817
|
+
code: "server_error",
|
|
7818
|
+
message: error instanceof Error ? error.message : "Claude stream failed."
|
|
7819
|
+
} })
|
|
7820
|
+
};
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7823
|
+
/** The final response of a translated stream, for a request that did not ask to stream. */
|
|
7824
|
+
const collectCodexResponse = async (events$1) => {
|
|
7825
|
+
let final = {
|
|
7826
|
+
status: "failed",
|
|
7827
|
+
error: {
|
|
7828
|
+
code: "server_error",
|
|
7829
|
+
message: "Claude stream produced no response."
|
|
7830
|
+
}
|
|
7831
|
+
};
|
|
7832
|
+
for await (const event of events$1) if ((event.type === "response.completed" || event.type === "response.incomplete" || event.type === "response.failed") && isRecord$3(event.response)) final = event.response;
|
|
7833
|
+
return final;
|
|
7834
|
+
};
|
|
7835
|
+
/** Responses events as SSE bytes. */
|
|
7836
|
+
async function* encodeSse(events$1) {
|
|
7837
|
+
const encoder = new TextEncoder();
|
|
7838
|
+
for await (const event of events$1) yield encoder.encode(`event: ${String(event.type)}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
7839
|
+
}
|
|
7840
|
+
|
|
7841
|
+
//#endregion
|
|
7842
|
+
//#region src/routes/responses.ts
|
|
7843
|
+
const responsesRoutes = new Hono();
|
|
7844
|
+
const forwardedHeaders = [
|
|
7845
|
+
"content-type",
|
|
7846
|
+
"retry-after",
|
|
7847
|
+
"openai-model",
|
|
7848
|
+
"x-request-id",
|
|
7849
|
+
"x-github-request-id",
|
|
7850
|
+
"x-copilot-service-request-id"
|
|
7851
|
+
];
|
|
7852
|
+
const copyForwardedHeaders = (source) => {
|
|
7853
|
+
const headers = new Headers();
|
|
7854
|
+
for (const name of forwardedHeaders) {
|
|
7855
|
+
const value = source.get(name);
|
|
7856
|
+
if (value !== null) headers.set(name, value);
|
|
7857
|
+
}
|
|
7858
|
+
return headers;
|
|
7859
|
+
};
|
|
7860
|
+
const readSubagent = (value) => {
|
|
7861
|
+
if (value === void 0) return;
|
|
7862
|
+
return /^[\w.-]{1,64}$/.test(value) ? value : "other";
|
|
7863
|
+
};
|
|
7864
|
+
const sourceOf = (chunks) => ({
|
|
7865
|
+
read: async () => await chunks.next(),
|
|
7866
|
+
cancel: async () => await chunks.return(void 0)
|
|
7867
|
+
});
|
|
7868
|
+
const sseHeaders = {
|
|
7869
|
+
"content-type": "text/event-stream",
|
|
7870
|
+
"cache-control": "no-cache"
|
|
7871
|
+
};
|
|
7872
|
+
/**
|
|
7873
|
+
* Passes the upstream body to the client and settles the request trace when it ends.
|
|
7874
|
+
*
|
|
7875
|
+
* The trace cancels every upstream body still open once the handler settles, so a streamed answer
|
|
7876
|
+
* must settle the handler only after its last byte, an error, or the client's cancellation.
|
|
7877
|
+
*/
|
|
7878
|
+
const settleWhenDone = (source, trace) => {
|
|
7879
|
+
let settled = false;
|
|
7880
|
+
const settle = (error) => {
|
|
7881
|
+
if (settled) return;
|
|
7882
|
+
settled = true;
|
|
7883
|
+
trace?.handlerSettled(error);
|
|
7884
|
+
};
|
|
7885
|
+
return new ReadableStream({
|
|
7886
|
+
pull: async (controller) => {
|
|
7887
|
+
try {
|
|
7888
|
+
const next = await source.read();
|
|
7889
|
+
if (settled) return;
|
|
7890
|
+
if (next.done || next.value === void 0) {
|
|
7891
|
+
controller.close();
|
|
7892
|
+
settle();
|
|
7893
|
+
return;
|
|
7894
|
+
}
|
|
7895
|
+
controller.enqueue(next.value);
|
|
7896
|
+
} catch (error) {
|
|
7897
|
+
if (!settled) controller.error(error);
|
|
7898
|
+
settle(error);
|
|
7899
|
+
}
|
|
7900
|
+
},
|
|
7901
|
+
cancel: async (reason) => {
|
|
7902
|
+
settle();
|
|
7903
|
+
await source.cancel(reason).catch(() => {});
|
|
7904
|
+
}
|
|
7905
|
+
}, { highWaterMark: 0 });
|
|
7906
|
+
};
|
|
7907
|
+
responsesRoutes.post("/responses", async (c) => {
|
|
7908
|
+
const config = c.get("config");
|
|
7909
|
+
const requestId = c.get("requestId");
|
|
7910
|
+
const trace = c.get("requestTrace");
|
|
7911
|
+
const signal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
|
|
7912
|
+
let response;
|
|
7913
|
+
let stream = false;
|
|
7914
|
+
try {
|
|
7915
|
+
const payload = parseCodexPayload(await c.req.text());
|
|
7916
|
+
stream = payload.stream === true;
|
|
7917
|
+
const route = await resolveCodexModel(config, payload.model);
|
|
7918
|
+
const capabilities = getCachedCopilotModel(config, route.model);
|
|
7919
|
+
const subagent = readSubagent(c.req.header("x-openai-subagent"));
|
|
7920
|
+
const initiator = getCodexInitiator(payload.input, subagent);
|
|
7921
|
+
if (route.api === "messages") {
|
|
7922
|
+
const request = toClaudeRequest(payload, route.model, capabilities);
|
|
7923
|
+
if (request.dropped.length > 0) log.info(sanitizeTerminalString(`request_id=${requestId} Codex items Claude has no equivalent for, dropped: ${request.dropped.join(",")}`));
|
|
7924
|
+
const events$1 = toCodexEvents(nativeEvents(await createNativeMessages(config, request.payload, {
|
|
7925
|
+
requestId,
|
|
7926
|
+
initiator,
|
|
7927
|
+
signal,
|
|
7928
|
+
client: "codex",
|
|
7929
|
+
requestedModel: payload.model
|
|
7930
|
+
})), payload.model, request.tools);
|
|
7931
|
+
if (!stream) {
|
|
7932
|
+
const final = await collectCodexResponse(events$1);
|
|
7933
|
+
return c.json(final, final.status === "failed" ? 502 : 200);
|
|
7934
|
+
}
|
|
7935
|
+
trace?.deferHandler();
|
|
7936
|
+
return new Response(settleWhenDone(sourceOf(encodeSse(events$1)), trace), {
|
|
7937
|
+
status: 200,
|
|
7938
|
+
headers: sseHeaders
|
|
7939
|
+
});
|
|
7940
|
+
}
|
|
7941
|
+
const prepared = prepareCodexPayload(payload, route.model, capabilities);
|
|
7942
|
+
log.info(sanitizeTerminalString([
|
|
7943
|
+
`request_id=${requestId}`,
|
|
7944
|
+
"Model request client=codex",
|
|
7945
|
+
`requested_model=${payload.model}`,
|
|
7946
|
+
`upstream_model=${route.model}`,
|
|
7947
|
+
"upstream_api=responses",
|
|
7948
|
+
`requested_think_effort=${prepared.requestedEffort ?? "unset"}`,
|
|
7949
|
+
`effective_think_effort=${prepared.effectiveEffort ?? "omitted"}`,
|
|
7950
|
+
`initiator=${initiator}`,
|
|
7951
|
+
...subagent === void 0 ? [] : [`subagent=${subagent}`],
|
|
7952
|
+
...prepared.removed.length === 0 ? [] : [`removed=${prepared.removed.join(",")}`]
|
|
7953
|
+
].join(" ")));
|
|
7954
|
+
response = await fetchCopilot(getCopilotProviderContext(config), "/responses", {
|
|
7955
|
+
method: "POST",
|
|
7956
|
+
headers: {
|
|
7957
|
+
accept: stream ? "text/event-stream" : "application/json",
|
|
7958
|
+
"content-type": "application/json"
|
|
7959
|
+
},
|
|
7960
|
+
body: JSON.stringify(prepared.payload)
|
|
7961
|
+
}, {
|
|
7962
|
+
initiator,
|
|
7963
|
+
requestId,
|
|
7964
|
+
signal,
|
|
7965
|
+
timeoutMs: config.upstreamTimeoutMs,
|
|
7966
|
+
vision: includesImage(prepared.payload.input)
|
|
7967
|
+
});
|
|
7968
|
+
if (!response.ok) {
|
|
7969
|
+
const detail = await readCopilotText(response, signal, config.upstreamTimeoutMs);
|
|
7970
|
+
const overflow = toPromptTooLongError(response.status, detail);
|
|
7971
|
+
if (overflow && stream) {
|
|
7972
|
+
c.set("requestErrorMessage", overflow.message);
|
|
7973
|
+
log.error(`request_id=${requestId} ${overflow.message}; sent to Codex as context_length_exceeded`);
|
|
7974
|
+
return new Response(contextOverflowStream(requestId, overflow.message), {
|
|
7975
|
+
status: 200,
|
|
7976
|
+
headers: sseHeaders
|
|
7977
|
+
});
|
|
7978
|
+
}
|
|
7979
|
+
const message = `Copilot /responses returned ${response.status}`;
|
|
7980
|
+
c.set("requestErrorMessage", message);
|
|
7981
|
+
log.error(`request_id=${requestId} ${message}`);
|
|
7982
|
+
return new Response(detail, {
|
|
7983
|
+
status: response.status,
|
|
7984
|
+
headers: copyForwardedHeaders(response.headers)
|
|
7985
|
+
});
|
|
7986
|
+
}
|
|
7987
|
+
} catch (error) {
|
|
7988
|
+
if (error instanceof ProxyNotImplementedError) {
|
|
7989
|
+
c.set("requestErrorMessage", error.message);
|
|
7990
|
+
return c.json({ error: {
|
|
7991
|
+
message: error.message,
|
|
7992
|
+
type: "server_error",
|
|
7993
|
+
code: "relay_not_configured"
|
|
7994
|
+
} }, 501);
|
|
7995
|
+
}
|
|
7996
|
+
if (!(error instanceof HTTPError)) throw error;
|
|
7997
|
+
if (error instanceof PromptTooLongError && stream) {
|
|
7998
|
+
c.set("requestErrorMessage", error.message);
|
|
7999
|
+
log.error(`request_id=${requestId} ${error.message}; sent to Codex as context_length_exceeded`);
|
|
8000
|
+
return new Response(contextOverflowStream(requestId, error.message), {
|
|
8001
|
+
status: 200,
|
|
8002
|
+
headers: sseHeaders
|
|
8003
|
+
});
|
|
8004
|
+
}
|
|
8005
|
+
const message = sanitizeTerminalString(error.message);
|
|
8006
|
+
c.set("requestErrorMessage", message);
|
|
8007
|
+
log.error(`request_id=${requestId} ${message}`);
|
|
8008
|
+
if (error.response.status === 400 && trace?.manifest.exchanges.length === 0) trace.recordFailure("local-validation");
|
|
8009
|
+
return new Response(await error.response.arrayBuffer(), {
|
|
8010
|
+
status: error.response.status,
|
|
8011
|
+
headers: copyForwardedHeaders(error.response.headers)
|
|
8012
|
+
});
|
|
8013
|
+
}
|
|
8014
|
+
const headers = copyForwardedHeaders(response.headers);
|
|
8015
|
+
if (stream) headers.set("cache-control", "no-cache");
|
|
8016
|
+
if (!response.body) return new Response(null, {
|
|
8017
|
+
status: response.status,
|
|
8018
|
+
headers
|
|
8019
|
+
});
|
|
8020
|
+
trace?.deferHandler();
|
|
8021
|
+
return new Response(settleWhenDone(response.body.getReader(), trace), {
|
|
8022
|
+
status: response.status,
|
|
8023
|
+
headers
|
|
8024
|
+
});
|
|
8025
|
+
});
|
|
8026
|
+
|
|
6900
8027
|
//#endregion
|
|
6901
8028
|
//#region src/server.ts
|
|
6902
8029
|
const loggedRequestHeaders = [
|
|
@@ -6951,6 +8078,11 @@ const presentsApiKey = (headers, apiKey) => {
|
|
|
6951
8078
|
for (const presented of [headers.get("x-api-key"), bearer]) if (typeof presented === "string" && timingSafeEqual(sha256(presented), expected)) matched = true;
|
|
6952
8079
|
return matched;
|
|
6953
8080
|
};
|
|
8081
|
+
const jsonBodyPaths = new Set([
|
|
8082
|
+
"/v1/messages",
|
|
8083
|
+
"/v1/messages/count_tokens",
|
|
8084
|
+
"/v1/responses"
|
|
8085
|
+
]);
|
|
6954
8086
|
const isOpenProbe = (method, path$1) => method === "GET" && path$1 === "/healthz" || (method === "GET" || method === "HEAD") && path$1 === "/api/hello";
|
|
6955
8087
|
const createServer = (config) => {
|
|
6956
8088
|
const app = new Hono();
|
|
@@ -7007,7 +8139,7 @@ const createServer = (config) => {
|
|
|
7007
8139
|
}
|
|
7008
8140
|
}, 401, { "www-authenticate": "Bearer" });
|
|
7009
8141
|
}
|
|
7010
|
-
if (c.req.method === "POST" && (c.req.path
|
|
8142
|
+
if (c.req.method === "POST" && jsonBodyPaths.has(c.req.path) && (incoming ? Boolean(incoming.headers["transfer-encoding"]) || Number(incoming.headers["content-length"] ?? 0) > 0 : c.req.raw.body !== null) && c.req.header("content-type")?.split(";", 1)[0].trim().toLowerCase() !== "application/json") {
|
|
7011
8143
|
const message = "Content-Type must be application/json";
|
|
7012
8144
|
c.set("requestErrorMessage", message);
|
|
7013
8145
|
return c.json({ error: { message } }, 415);
|
|
@@ -7051,6 +8183,7 @@ const createServer = (config) => {
|
|
|
7051
8183
|
}));
|
|
7052
8184
|
app.on(["GET", "HEAD"], "/api/hello", (c) => c.body(null, 200));
|
|
7053
8185
|
app.route("/v1", claudeRoutes);
|
|
8186
|
+
app.route("/v1", responsesRoutes);
|
|
7054
8187
|
app.notFound(async (c) => {
|
|
7055
8188
|
const message = "Unsupported Claude API route";
|
|
7056
8189
|
c.set("requestErrorMessage", message);
|
|
@@ -7428,14 +8561,14 @@ const renderConfigGuide = (configPath$1, configured, chosen) => {
|
|
|
7428
8561
|
//#region src/models.ts
|
|
7429
8562
|
var ModelsUsageError = class extends Error {};
|
|
7430
8563
|
const printable = (text$1) => scrubLogSecrets(terminalText(text$1));
|
|
7431
|
-
const displayName = (name) => {
|
|
8564
|
+
const displayName$1 = (name) => {
|
|
7432
8565
|
const text$1 = printable(name ?? "").trim();
|
|
7433
8566
|
if (text$1 === "") return;
|
|
7434
8567
|
return text$1.length > 80 ? `${text$1.slice(0, 77)}...` : text$1;
|
|
7435
8568
|
};
|
|
7436
8569
|
const listedModels = (config, catalog) => [...catalog.models.keys()].sort().map((id) => {
|
|
7437
8570
|
const selection = selectCopilotEndpoint(config, id);
|
|
7438
|
-
const name = displayName(catalog.models.get(id)?.name);
|
|
8571
|
+
const name = displayName$1(catalog.models.get(id)?.name);
|
|
7439
8572
|
return {
|
|
7440
8573
|
id: printable(id),
|
|
7441
8574
|
...name !== void 0 && { name },
|
|
@@ -8230,9 +9363,9 @@ const packagedEntryCandidate = (command, cwd) => {
|
|
|
8230
9363
|
const packageEntryProof = async (entry) => {
|
|
8231
9364
|
try {
|
|
8232
9365
|
if (!(await fs.stat(entry)).isFile()) return "nonrelay";
|
|
8233
|
-
const canonical = (await fs.realpath(entry)).replaceAll("\\", "/");
|
|
8234
|
-
if (!/\/(?:dist\/main\.js|src\/main\.ts)$/.test(canonical)) return "nonrelay";
|
|
8235
|
-
const manifest = JSON.parse(await fs.readFile(posix.join(posix.dirname(posix.dirname(canonical)), "package.json"), "utf8"));
|
|
9366
|
+
const canonical$1 = (await fs.realpath(entry)).replaceAll("\\", "/");
|
|
9367
|
+
if (!/\/(?:dist\/main\.js|src\/main\.ts)$/.test(canonical$1)) return "nonrelay";
|
|
9368
|
+
const manifest = JSON.parse(await fs.readFile(posix.join(posix.dirname(posix.dirname(canonical$1)), "package.json"), "utf8"));
|
|
8236
9369
|
return typeof manifest === "object" && manifest !== null && "name" in manifest && manifest.name === "copilot-relay" ? "relay" : "nonrelay";
|
|
8237
9370
|
} catch {
|
|
8238
9371
|
return "nonrelay";
|
|
@@ -8300,9 +9433,9 @@ const verifyCommand = async (command, cwd) => {
|
|
|
8300
9433
|
}
|
|
8301
9434
|
const program = resolve$1(flat.program);
|
|
8302
9435
|
if (!(await fs.stat(program)).isFile()) return "unknown";
|
|
8303
|
-
const canonical = (await fs.realpath(program)).replaceAll("\\", "/");
|
|
8304
|
-
if (!/(?:^|\/)copilot-relay(?:-[A-Za-z0-9._-]+)?\/(?:dist\/main\.js|src\/main\.ts)$/.test(canonical)) return "unknown";
|
|
8305
|
-
const packagePath = posix.join(posix.dirname(posix.dirname(canonical)), "package.json");
|
|
9436
|
+
const canonical$1 = (await fs.realpath(program)).replaceAll("\\", "/");
|
|
9437
|
+
if (!/(?:^|\/)copilot-relay(?:-[A-Za-z0-9._-]+)?\/(?:dist\/main\.js|src\/main\.ts)$/.test(canonical$1)) return "unknown";
|
|
9438
|
+
const packagePath = posix.join(posix.dirname(posix.dirname(canonical$1)), "package.json");
|
|
8306
9439
|
if (!(await fs.stat(packagePath)).isFile()) return "unknown";
|
|
8307
9440
|
const manifest = JSON.parse(await fs.readFile(packagePath, "utf8"));
|
|
8308
9441
|
if (typeof manifest !== "object" || manifest === null || !("name" in manifest) || manifest.name !== "copilot-relay") return "unknown";
|
|
@@ -8601,11 +9734,11 @@ const knownModelEnvKeys = [
|
|
|
8601
9734
|
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
8602
9735
|
"CLAUDE_CODE_SUBAGENT_MODEL"
|
|
8603
9736
|
];
|
|
8604
|
-
const isRecord$
|
|
9737
|
+
const isRecord$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8605
9738
|
const hasMalformedModelOverride = (parsed) => {
|
|
8606
9739
|
if (parsed.model !== void 0 && typeof parsed.model !== "string") return true;
|
|
8607
9740
|
const env = parsed.env;
|
|
8608
|
-
if (!isRecord$
|
|
9741
|
+
if (!isRecord$2(env)) return false;
|
|
8609
9742
|
return knownModelEnvKeys.some((key) => env[key] !== void 0 && typeof env[key] !== "string");
|
|
8610
9743
|
};
|
|
8611
9744
|
const hasPrimaryModelOverride = (parsed, env) => typeof parsed.model === "string" || typeof env.ANTHROPIC_MODEL === "string";
|
|
@@ -8629,7 +9762,7 @@ async function applyClaudeConfig(input) {
|
|
|
8629
9762
|
};
|
|
8630
9763
|
if (raw.trim().length > 0) try {
|
|
8631
9764
|
const value = JSON.parse(raw);
|
|
8632
|
-
if (!isRecord$
|
|
9765
|
+
if (!isRecord$2(value) || value.env !== void 0 && !isRecord$2(value.env) || hasMalformedModelOverride(value)) return {
|
|
8633
9766
|
configPath: configPath$1,
|
|
8634
9767
|
changed: false,
|
|
8635
9768
|
created: false
|
|
@@ -8642,7 +9775,7 @@ async function applyClaudeConfig(input) {
|
|
|
8642
9775
|
created: false
|
|
8643
9776
|
};
|
|
8644
9777
|
}
|
|
8645
|
-
const env = isRecord$
|
|
9778
|
+
const env = isRecord$2(parsed.env) ? { ...parsed.env } : {};
|
|
8646
9779
|
const previousBaseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
8647
9780
|
const hadPrimaryModelOverride = hasPrimaryModelOverride(parsed, env);
|
|
8648
9781
|
const contextWindow = input.gptLimits?.max_context_window_tokens;
|
|
@@ -8680,9 +9813,370 @@ async function applyClaudeConfig(input) {
|
|
|
8680
9813
|
};
|
|
8681
9814
|
}
|
|
8682
9815
|
|
|
9816
|
+
//#endregion
|
|
9817
|
+
//#region src/codex/catalog.ts
|
|
9818
|
+
const effortDescriptions = {
|
|
9819
|
+
none: "No extra reasoning",
|
|
9820
|
+
minimal: "Minimal reasoning",
|
|
9821
|
+
low: "Fast responses with lighter reasoning",
|
|
9822
|
+
medium: "Balances speed and reasoning depth for everyday tasks",
|
|
9823
|
+
high: "Greater reasoning depth for complex problems",
|
|
9824
|
+
xhigh: "Extra high reasoning depth for complex problems",
|
|
9825
|
+
max: "Maximum reasoning depth for the hardest problems"
|
|
9826
|
+
};
|
|
9827
|
+
const codexContextWindow = 272e3;
|
|
9828
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9829
|
+
const readVendorFile = async (name) => {
|
|
9830
|
+
let currentDir = dirname(fileURLToPath(import.meta.url));
|
|
9831
|
+
while (true) try {
|
|
9832
|
+
return await fs.readFile(resolve(currentDir, "vendor", "codex", name), "utf8");
|
|
9833
|
+
} catch (error) {
|
|
9834
|
+
if (error.code !== "ENOENT") throw error;
|
|
9835
|
+
const parentDir = dirname(currentDir);
|
|
9836
|
+
if (parentDir === currentDir) throw new Error(`vendor/codex/${name} is missing from the package.`);
|
|
9837
|
+
currentDir = parentDir;
|
|
9838
|
+
}
|
|
9839
|
+
};
|
|
9840
|
+
const readCodexVendorFiles = async () => {
|
|
9841
|
+
const models$1 = JSON.parse(await readVendorFile("models.json"));
|
|
9842
|
+
const entries = isRecord$1(models$1) && Array.isArray(models$1.models) ? models$1.models : [];
|
|
9843
|
+
const bundled = /* @__PURE__ */ new Map();
|
|
9844
|
+
for (const entry of entries) if (isRecord$1(entry) && typeof entry.slug === "string") bundled.set(entry.slug, entry);
|
|
9845
|
+
return {
|
|
9846
|
+
bundled,
|
|
9847
|
+
prompt: await readVendorFile("prompt.md")
|
|
9848
|
+
};
|
|
9849
|
+
};
|
|
9850
|
+
const displayName = (model) => model.capabilities?.name || model.id;
|
|
9851
|
+
/**
|
|
9852
|
+
* A catalog entry for a model Codex does not ship.
|
|
9853
|
+
*
|
|
9854
|
+
* It enables Codex's freeform apply_patch tool and its shell tool, lists the reasoning levels Copilot
|
|
9855
|
+
* advertises, and uses the instructions Codex gives any model it does not know.
|
|
9856
|
+
*/
|
|
9857
|
+
const generatedEntry = (model, priority, prompt) => {
|
|
9858
|
+
const levels = (model.capabilities?.reasoningEfforts ?? []).filter((effort) => Object.hasOwn(effortDescriptions, effort)).map((effort) => ({
|
|
9859
|
+
effort,
|
|
9860
|
+
description: effortDescriptions[effort]
|
|
9861
|
+
}));
|
|
9862
|
+
const defaultLevel = levels.find((level) => level.effort === "medium") ?? levels[0];
|
|
9863
|
+
const maxPrompt = model.capabilities?.limits?.max_prompt_tokens;
|
|
9864
|
+
return {
|
|
9865
|
+
slug: model.id,
|
|
9866
|
+
display_name: displayName(model),
|
|
9867
|
+
description: `${displayName(model)} through copilot-relay`,
|
|
9868
|
+
...defaultLevel && { default_reasoning_level: defaultLevel.effort },
|
|
9869
|
+
supported_reasoning_levels: levels,
|
|
9870
|
+
shell_type: "shell_command",
|
|
9871
|
+
visibility: "list",
|
|
9872
|
+
supported_in_api: true,
|
|
9873
|
+
priority,
|
|
9874
|
+
support_verbosity: false,
|
|
9875
|
+
truncation_policy: {
|
|
9876
|
+
mode: "tokens",
|
|
9877
|
+
limit: 1e4
|
|
9878
|
+
},
|
|
9879
|
+
experimental_supported_tools: [],
|
|
9880
|
+
apply_patch_tool_type: "freeform",
|
|
9881
|
+
supports_parallel_tool_calls: true,
|
|
9882
|
+
input_modalities: ["text", "image"],
|
|
9883
|
+
...maxPrompt !== void 0 && {
|
|
9884
|
+
context_window: Math.min(codexContextWindow, maxPrompt),
|
|
9885
|
+
max_context_window: maxPrompt
|
|
9886
|
+
},
|
|
9887
|
+
model_messages: { instructions_template: prompt }
|
|
9888
|
+
};
|
|
9889
|
+
};
|
|
9890
|
+
/**
|
|
9891
|
+
* The catalog for these models, in order: the first is Codex's default.
|
|
9892
|
+
*
|
|
9893
|
+
* A model Codex ships keeps Codex's own entry, so its tuned instructions and settings are unchanged;
|
|
9894
|
+
* only its position and visibility change. Any other model, such as a Claude model, gets a generated
|
|
9895
|
+
* entry.
|
|
9896
|
+
*/
|
|
9897
|
+
const buildCodexCatalog = (models$1, vendor) => {
|
|
9898
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9899
|
+
const entries = [];
|
|
9900
|
+
for (const model of models$1) {
|
|
9901
|
+
if (seen.has(model.id)) continue;
|
|
9902
|
+
seen.add(model.id);
|
|
9903
|
+
const priority = entries.length + 1;
|
|
9904
|
+
const bundled = vendor.bundled.get(model.id);
|
|
9905
|
+
entries.push(bundled ? {
|
|
9906
|
+
...bundled,
|
|
9907
|
+
visibility: "list",
|
|
9908
|
+
priority
|
|
9909
|
+
} : generatedEntry(model, priority, vendor.prompt));
|
|
9910
|
+
}
|
|
9911
|
+
return { models: entries };
|
|
9912
|
+
};
|
|
9913
|
+
|
|
9914
|
+
//#endregion
|
|
9915
|
+
//#region src/codex/setup.ts
|
|
9916
|
+
const codexProviderId = "copilot-relay";
|
|
9917
|
+
/** config.toml holds the relay's provider in a form the relay will not rewrite safely. */
|
|
9918
|
+
var CodexConfigError = class extends Error {
|
|
9919
|
+
constructor(message) {
|
|
9920
|
+
super(message);
|
|
9921
|
+
this.name = "CodexConfigError";
|
|
9922
|
+
}
|
|
9923
|
+
};
|
|
9924
|
+
const isTable = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
9925
|
+
const tomlString = (value) => JSON.stringify(value);
|
|
9926
|
+
const parseOptions = { integersAsBigInt: "asNeeded" };
|
|
9927
|
+
const providerPath = ["model_providers", codexProviderId];
|
|
9928
|
+
const providerBlock = (input) => [
|
|
9929
|
+
`[model_providers.${codexProviderId}]`,
|
|
9930
|
+
"# Written by copilot-relay start (codexSetup); edits in this table are replaced on the next start.",
|
|
9931
|
+
`name = ${tomlString(codexProviderId)}`,
|
|
9932
|
+
`base_url = ${tomlString(`${input.baseUrl}/v1`)}`,
|
|
9933
|
+
"wire_api = \"responses\"",
|
|
9934
|
+
"requires_openai_auth = false",
|
|
9935
|
+
"supports_websockets = false",
|
|
9936
|
+
...input.apiKey ? [`http_headers = { "x-api-key" = ${tomlString(input.apiKey)} }`] : []
|
|
9937
|
+
];
|
|
9938
|
+
const providerValue = (input) => ({
|
|
9939
|
+
name: codexProviderId,
|
|
9940
|
+
base_url: `${input.baseUrl}/v1`,
|
|
9941
|
+
wire_api: "responses",
|
|
9942
|
+
requires_openai_auth: false,
|
|
9943
|
+
supports_websockets: false,
|
|
9944
|
+
...input.apiKey ? { http_headers: { "x-api-key": input.apiKey } } : {}
|
|
9945
|
+
});
|
|
9946
|
+
const isProviderPath = (parts) => providerPath.every((part, index) => parts[index] === part);
|
|
9947
|
+
const findClose = (line, from, quote) => {
|
|
9948
|
+
let at = from;
|
|
9949
|
+
while (at < line.length) if (quote === "\"" && line[at] === "\\") at += 2;
|
|
9950
|
+
else if (line[at] === quote) return at;
|
|
9951
|
+
else at++;
|
|
9952
|
+
return -1;
|
|
9953
|
+
};
|
|
9954
|
+
const unescapeBasic = (raw) => {
|
|
9955
|
+
try {
|
|
9956
|
+
return JSON.parse(`"${raw}"`);
|
|
9957
|
+
} catch {
|
|
9958
|
+
return raw;
|
|
9959
|
+
}
|
|
9960
|
+
};
|
|
9961
|
+
const skipSpace = (line, from) => {
|
|
9962
|
+
let at = from;
|
|
9963
|
+
while (line[at] === " " || line[at] === " ") at++;
|
|
9964
|
+
return at;
|
|
9965
|
+
};
|
|
9966
|
+
const bareKey = /[A-Za-z0-9_-]+/y;
|
|
9967
|
+
const readKey = (line, start$1) => {
|
|
9968
|
+
const parts = [];
|
|
9969
|
+
let at = skipSpace(line, start$1);
|
|
9970
|
+
while (at < line.length) {
|
|
9971
|
+
const quote = line[at];
|
|
9972
|
+
if (quote === "\"" || quote === "'") {
|
|
9973
|
+
const close = findClose(line, at + 1, quote);
|
|
9974
|
+
if (close === -1) return;
|
|
9975
|
+
const raw = line.slice(at + 1, close);
|
|
9976
|
+
parts.push(quote === "\"" ? unescapeBasic(raw) : raw);
|
|
9977
|
+
at = close + 1;
|
|
9978
|
+
} else {
|
|
9979
|
+
bareKey.lastIndex = at;
|
|
9980
|
+
const match = bareKey.exec(line);
|
|
9981
|
+
if (!match) return;
|
|
9982
|
+
parts.push(match[0]);
|
|
9983
|
+
at = bareKey.lastIndex;
|
|
9984
|
+
}
|
|
9985
|
+
at = skipSpace(line, at);
|
|
9986
|
+
if (line[at] !== ".") return {
|
|
9987
|
+
parts,
|
|
9988
|
+
end: at
|
|
9989
|
+
};
|
|
9990
|
+
at = skipSpace(line, at + 1);
|
|
9991
|
+
}
|
|
9992
|
+
};
|
|
9993
|
+
const readHeader = (line) => {
|
|
9994
|
+
const open = /^\s*\[\[?/.exec(line);
|
|
9995
|
+
if (!open) return;
|
|
9996
|
+
const key = readKey(line, open[0].length);
|
|
9997
|
+
if (!key) return;
|
|
9998
|
+
const close = open[0].trimStart() === "[[" ? "]]" : "]";
|
|
9999
|
+
const rest = line.slice(key.end);
|
|
10000
|
+
if (!rest.startsWith(close) || !/^\s*(?:#.*)?$/.test(rest.slice(close.length))) return;
|
|
10001
|
+
return key.parts;
|
|
10002
|
+
};
|
|
10003
|
+
const readAssignedKey = (line) => {
|
|
10004
|
+
const key = readKey(line, 0);
|
|
10005
|
+
if (!key || line[key.end] !== "=") return;
|
|
10006
|
+
return key.parts;
|
|
10007
|
+
};
|
|
10008
|
+
const stringStateAfter = (line, state) => {
|
|
10009
|
+
let open = state;
|
|
10010
|
+
let at = 0;
|
|
10011
|
+
while (at < line.length) {
|
|
10012
|
+
const char = line[at];
|
|
10013
|
+
if (open === "\"\"\"" && char === "\\") at += 2;
|
|
10014
|
+
else if (open !== "" && line.startsWith(open, at)) {
|
|
10015
|
+
at += 3;
|
|
10016
|
+
while (line[at] === open[0]) at++;
|
|
10017
|
+
open = "";
|
|
10018
|
+
} else if (open !== "") at++;
|
|
10019
|
+
else if (char === "#") return "";
|
|
10020
|
+
else if (line.startsWith("\"\"\"", at) || line.startsWith("'''", at)) {
|
|
10021
|
+
open = line.slice(at, at + 3);
|
|
10022
|
+
at += 3;
|
|
10023
|
+
} else if (char === "\"" || char === "'") {
|
|
10024
|
+
const close = findClose(line, at + 1, char);
|
|
10025
|
+
at = close === -1 ? line.length : close + 1;
|
|
10026
|
+
} else at++;
|
|
10027
|
+
}
|
|
10028
|
+
return open;
|
|
10029
|
+
};
|
|
10030
|
+
const parseLines = (lines) => {
|
|
10031
|
+
const parsed = [];
|
|
10032
|
+
let table = [];
|
|
10033
|
+
let state = "";
|
|
10034
|
+
for (const text$1 of lines) {
|
|
10035
|
+
if (state !== "") parsed.push({
|
|
10036
|
+
text: text$1,
|
|
10037
|
+
table,
|
|
10038
|
+
header: false
|
|
10039
|
+
});
|
|
10040
|
+
else {
|
|
10041
|
+
const header = readHeader(text$1);
|
|
10042
|
+
if (header) table = header;
|
|
10043
|
+
const key = header ? void 0 : readAssignedKey(text$1);
|
|
10044
|
+
parsed.push({
|
|
10045
|
+
text: text$1,
|
|
10046
|
+
table,
|
|
10047
|
+
header: header !== void 0,
|
|
10048
|
+
...key && { key }
|
|
10049
|
+
});
|
|
10050
|
+
}
|
|
10051
|
+
state = stringStateAfter(text$1, state);
|
|
10052
|
+
}
|
|
10053
|
+
return parsed;
|
|
10054
|
+
};
|
|
10055
|
+
const parseToml = (text$1) => {
|
|
10056
|
+
try {
|
|
10057
|
+
return parse(text$1, parseOptions);
|
|
10058
|
+
} catch (error) {
|
|
10059
|
+
if (error instanceof TomlError) throw new CodexConfigError(`config.toml is not valid TOML (line ${error.line}); fix it, or set codexSetup: false.`);
|
|
10060
|
+
throw error;
|
|
10061
|
+
}
|
|
10062
|
+
};
|
|
10063
|
+
const canonical = (value) => {
|
|
10064
|
+
if (value instanceof Date) return ["date", value.toISOString()];
|
|
10065
|
+
if (Array.isArray(value)) return ["array", value.map(canonical)];
|
|
10066
|
+
if (isTable(value)) return ["table", Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]))];
|
|
10067
|
+
if (typeof value === "number" && Object.is(value, -0)) return ["number", "-0"];
|
|
10068
|
+
return [typeof value, String(value)];
|
|
10069
|
+
};
|
|
10070
|
+
const sameToml = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right));
|
|
10071
|
+
const parsesTo = (text$1) => {
|
|
10072
|
+
try {
|
|
10073
|
+
return parse(text$1, parseOptions);
|
|
10074
|
+
} catch {
|
|
10075
|
+
return;
|
|
10076
|
+
}
|
|
10077
|
+
};
|
|
10078
|
+
/**
|
|
10079
|
+
* config.toml with the relay's provider selected.
|
|
10080
|
+
*
|
|
10081
|
+
* Sets the top-level model_provider and model_catalog_json, and model when it does not already name
|
|
10082
|
+
* one of the catalog's models. Replaces the [model_providers.copilot-relay] table, or appends it.
|
|
10083
|
+
* The edit works line by line so comments and layout stay, and is then checked by parsing: unless
|
|
10084
|
+
* the result reads as the original with only those values changed, it throws instead.
|
|
10085
|
+
*/
|
|
10086
|
+
const editCodexConfig = (text$1, input) => {
|
|
10087
|
+
const original = parseToml(text$1);
|
|
10088
|
+
const eol = text$1.includes("\r\n") ? "\r\n" : "\n";
|
|
10089
|
+
const parsed = parseLines(text$1.length === 0 ? [] : text$1.replace(/\r?\n$/, "").split(/\r?\n/));
|
|
10090
|
+
for (const [index, line] of parsed.entries()) {
|
|
10091
|
+
if (line.key === void 0 || isProviderPath(line.table)) continue;
|
|
10092
|
+
const assigned = [...line.table, ...line.key];
|
|
10093
|
+
if (isProviderPath(assigned)) throw new CodexConfigError(`config.toml line ${index + 1} defines model_providers.${codexProviderId} outside its own table; move it into [model_providers.${codexProviderId}] or remove it.`);
|
|
10094
|
+
if (assigned.length === 1 && assigned[0] === "model_providers") throw new CodexConfigError(`config.toml line ${index + 1} defines model_providers as an inline table, which cannot take a [model_providers.${codexProviderId}] table; write it as [model_providers.<name>] tables.`);
|
|
10095
|
+
}
|
|
10096
|
+
const kept = parsed.filter((line) => !isProviderPath(line.table));
|
|
10097
|
+
const lines = kept.map((line) => line.text);
|
|
10098
|
+
let firstHeader = kept.findIndex((line) => line.header);
|
|
10099
|
+
if (firstHeader === -1) firstHeader = lines.length;
|
|
10100
|
+
const currentModel = typeof original.model === "string" ? original.model : void 0;
|
|
10101
|
+
const model = currentModel !== void 0 && input.models.includes(currentModel) ? currentModel : input.models[0];
|
|
10102
|
+
const settings = [
|
|
10103
|
+
["model_provider", codexProviderId],
|
|
10104
|
+
["model_catalog_json", input.catalogPath],
|
|
10105
|
+
["model", model]
|
|
10106
|
+
];
|
|
10107
|
+
const inserted = [];
|
|
10108
|
+
for (const [key, value] of settings) {
|
|
10109
|
+
const existing = kept.findIndex((line, index) => index < firstHeader && line.key?.length === 1 && line.key[0] === key);
|
|
10110
|
+
if (existing === -1) inserted.push(`${key} = ${tomlString(value)}`);
|
|
10111
|
+
else lines[existing] = `${key} = ${tomlString(value)}`;
|
|
10112
|
+
}
|
|
10113
|
+
if (inserted.length > 0) {
|
|
10114
|
+
let insertAt = firstHeader;
|
|
10115
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") insertAt--;
|
|
10116
|
+
const needsGap = insertAt === firstHeader && firstHeader < lines.length;
|
|
10117
|
+
lines.splice(insertAt, 0, ...inserted, ...needsGap ? [""] : []);
|
|
10118
|
+
}
|
|
10119
|
+
while (lines.length > 0 && lines.at(-1).trim() === "") lines.pop();
|
|
10120
|
+
const block = providerBlock(input);
|
|
10121
|
+
const result = (lines.length > 0 ? [
|
|
10122
|
+
...lines,
|
|
10123
|
+
"",
|
|
10124
|
+
...block
|
|
10125
|
+
] : block).join(eol) + eol;
|
|
10126
|
+
const otherProviders = isTable(original.model_providers) ? original.model_providers : {};
|
|
10127
|
+
const expected = {
|
|
10128
|
+
...original,
|
|
10129
|
+
model_provider: codexProviderId,
|
|
10130
|
+
model_catalog_json: input.catalogPath,
|
|
10131
|
+
model,
|
|
10132
|
+
model_providers: {
|
|
10133
|
+
...otherProviders,
|
|
10134
|
+
[codexProviderId]: providerValue(input)
|
|
10135
|
+
}
|
|
10136
|
+
};
|
|
10137
|
+
const actual = parsesTo(result);
|
|
10138
|
+
if (actual === void 0 || !sameToml(actual, expected)) throw new CodexConfigError(`config.toml could not be edited without changing other settings; add the ${codexProviderId} provider by hand, or set codexSetup: false.`);
|
|
10139
|
+
return result;
|
|
10140
|
+
};
|
|
10141
|
+
const publish = async (snapshot, content, holdsKey) => {
|
|
10142
|
+
const keyExposed = holdsKey && process.platform !== "win32" && snapshot.raw !== null && (snapshot.mode & 63) !== 0;
|
|
10143
|
+
if (snapshot.raw === content && !keyExposed) return false;
|
|
10144
|
+
await writeFileSnapshot(snapshot, content, { ownerOnly: holdsKey });
|
|
10145
|
+
return true;
|
|
10146
|
+
};
|
|
10147
|
+
/**
|
|
10148
|
+
* Sets up Codex for this relay when Codex is installed, that is when $CODEX_HOME exists.
|
|
10149
|
+
*
|
|
10150
|
+
* Writes the two-model catalog, then config.toml. Neither file is rewritten when it is already
|
|
10151
|
+
* current. config.toml is edited from the snapshot it is published against, so an edit saved in
|
|
10152
|
+
* between is refused as a conflict rather than lost.
|
|
10153
|
+
*/
|
|
10154
|
+
const applyCodexSetup = async (input) => {
|
|
10155
|
+
const configPath$1 = path.join(input.codexHome, "config.toml");
|
|
10156
|
+
if (!(await fs.stat(input.codexHome).catch(() => void 0))?.isDirectory()) return {
|
|
10157
|
+
configPath: configPath$1,
|
|
10158
|
+
skipped: true,
|
|
10159
|
+
changed: false
|
|
10160
|
+
};
|
|
10161
|
+
const catalog = buildCodexCatalog(input.models, await readCodexVendorFiles());
|
|
10162
|
+
await fs.mkdir(path.dirname(input.catalogPath), { recursive: true });
|
|
10163
|
+
const catalogChanged = await publish(await readFileSnapshot(input.catalogPath), `${JSON.stringify(catalog, null, 2)}\n`, false);
|
|
10164
|
+
const snapshot = await readFileSnapshot(configPath$1);
|
|
10165
|
+
const configChanged = await publish(snapshot, editCodexConfig(snapshot.raw ?? "", {
|
|
10166
|
+
...input,
|
|
10167
|
+
models: input.models.map((model) => model.id)
|
|
10168
|
+
}), Boolean(input.apiKey));
|
|
10169
|
+
return {
|
|
10170
|
+
configPath: configPath$1,
|
|
10171
|
+
skipped: false,
|
|
10172
|
+
changed: catalogChanged || configChanged
|
|
10173
|
+
};
|
|
10174
|
+
};
|
|
10175
|
+
|
|
8683
10176
|
//#endregion
|
|
8684
10177
|
//#region src/lib/defaults.ts
|
|
8685
10178
|
const claudeConfigPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
10179
|
+
const getCodexHome = () => process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
8686
10180
|
|
|
8687
10181
|
//#endregion
|
|
8688
10182
|
//#region src/lib/preflight.ts
|
|
@@ -8853,6 +10347,26 @@ async function startRelay(appConfig) {
|
|
|
8853
10347
|
} catch (error) {
|
|
8854
10348
|
log.error(`Could not update claude settings (${claudeConfigPath$1}):`, error);
|
|
8855
10349
|
}
|
|
10350
|
+
if (appConfig.codexSetup) {
|
|
10351
|
+
const codexHome = getCodexHome();
|
|
10352
|
+
try {
|
|
10353
|
+
const codexResult = await applyCodexSetup({
|
|
10354
|
+
apiKey: appConfig.apiKey,
|
|
10355
|
+
baseUrl,
|
|
10356
|
+
catalogPath: paths.codexCatalogPath,
|
|
10357
|
+
codexHome,
|
|
10358
|
+
models: [appConfig.gptModel, appConfig.opusModel].map((id) => ({
|
|
10359
|
+
id,
|
|
10360
|
+
capabilities: getCachedCopilotModel(config, id)
|
|
10361
|
+
}))
|
|
10362
|
+
});
|
|
10363
|
+
if (codexResult.skipped) log.info(`codex setup skipped: ${codexHome} does not exist`);
|
|
10364
|
+
else if (codexResult.changed) log.info(`codex settings updated: ${codexResult.configPath}`);
|
|
10365
|
+
else log.info(`codex settings already up to date: ${codexResult.configPath}`);
|
|
10366
|
+
} catch (error) {
|
|
10367
|
+
log.error(`Could not update codex settings (${codexHome}):`, error);
|
|
10368
|
+
}
|
|
10369
|
+
}
|
|
8856
10370
|
const configWatcher = watchAppConfig((nextConfig) => {
|
|
8857
10371
|
const previousApiKey = config.apiKey;
|
|
8858
10372
|
applyRuntimeConfig(nextConfig);
|
|
@@ -9001,14 +10515,15 @@ const configRowOrder = {
|
|
|
9001
10515
|
copilotBaseUrl: 3,
|
|
9002
10516
|
upstreamProxy: 4,
|
|
9003
10517
|
claudeSetup: 5,
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
10518
|
+
codexSetup: 6,
|
|
10519
|
+
logLevel: 7,
|
|
10520
|
+
logRetentionDays: 8,
|
|
10521
|
+
thinkEffort: 9,
|
|
10522
|
+
upstreamTimeoutSeconds: 10,
|
|
10523
|
+
webSearchBackend: 11,
|
|
10524
|
+
gptModel: 12,
|
|
10525
|
+
opusModel: 13,
|
|
10526
|
+
claudeUpstreamApi: 14
|
|
9012
10527
|
};
|
|
9013
10528
|
const configRowKeys = Object.keys(configRowOrder).sort((a, b) => configRowOrder[a] - configRowOrder[b]);
|
|
9014
10529
|
const unsetConfigText = {
|
|
@@ -9021,7 +10536,7 @@ const unsetConfigText = {
|
|
|
9021
10536
|
*
|
|
9022
10537
|
* These are the values on disk. That is not the same question as "what is the
|
|
9023
10538
|
* running daemon honouring", and the footnote says so rather than leaving it
|
|
9024
|
-
* implied: applyRuntimeConfig() in start.ts never reads claudeSetup, and
|
|
10539
|
+
* implied: applyRuntimeConfig() in start.ts never reads claudeSetup or codexSetup, and
|
|
9025
10540
|
* deliberately neither rebinds the listening socket when host or port changes
|
|
9026
10541
|
* nor rebuilds the upstream dispatcher when upstreamProxy changes; it
|
|
9027
10542
|
* hot-reloads the rest. Printing every key without that line would imply a
|
|
@@ -9036,7 +10551,7 @@ const renderConfig = (config, running) => {
|
|
|
9036
10551
|
const rendered = (value === null || value === "" ? unsetConfigText[key] : void 0) ?? String(value);
|
|
9037
10552
|
return ` ${key.padEnd(24)}${sanitizeTerminalString(rendered)}`;
|
|
9038
10553
|
});
|
|
9039
|
-
if (running) lines.push(" host, port, claudeSetup and upstreamProxy take effect on restart; the rest hot-reload.");
|
|
10554
|
+
if (running) lines.push(" host, port, claudeSetup, codexSetup and upstreamProxy take effect on restart; the rest hot-reload.");
|
|
9040
10555
|
return lines;
|
|
9041
10556
|
};
|
|
9042
10557
|
/**
|
|
@@ -9514,7 +11029,7 @@ const usage = defineCommand({
|
|
|
9514
11029
|
await runMain(defineCommand({
|
|
9515
11030
|
meta: {
|
|
9516
11031
|
name: "copilot-relay",
|
|
9517
|
-
description: "Yet, just another relay for Claude Code to use a GitHub Copilot subscription."
|
|
11032
|
+
description: "Yet, just another relay for Claude Code and Codex to use a GitHub Copilot subscription."
|
|
9518
11033
|
},
|
|
9519
11034
|
subCommands: {
|
|
9520
11035
|
auth,
|