betahi-copilot-bridge 0.1.5 → 0.1.7
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 +17 -9
- package/dist/main.js +288 -42
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<h1 align="center">copilot-bridge</h1>
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<a href="https://www.npmjs.com/package/betahi-copilot-bridge"><img src="https://img.shields.io/npm/v/betahi-copilot-bridge.svg" alt="npm version"></a>
|
|
4
|
+
<a href="https://www.npmjs.com/package/betahi-copilot-bridge"><img src="https://img.shields.io/npm/v/betahi-copilot-bridge.svg?v=0.1.7" alt="npm version"></a>
|
|
5
5
|
<a href="https://github.com/betahi/copilot-bridge/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/betahi-copilot-bridge.svg" alt="license"></a>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
@@ -50,8 +50,9 @@ npx betahi-copilot-bridge@latest auth
|
|
|
50
50
|
npx betahi-copilot-bridge@latest start
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
`start` flags: `--host`, `--port`, `--show-token`, `--
|
|
54
|
-
`--select-model`, `--no-prompt`,
|
|
53
|
+
`start` flags: `--host`, `--port`, `--show-token`, `--debug`,
|
|
54
|
+
`--no-codex-setup`, `--select-model`, `--no-prompt`,
|
|
55
|
+
`--rate-limit <seconds>`, `--wait`.
|
|
55
56
|
|
|
56
57
|
After startup the banner prints a **Usage Viewer** link of the form
|
|
57
58
|
`https://betahi.github.io/copilot-bridge?endpoint=http://127.0.0.1:4142/usage`,
|
|
@@ -68,6 +69,12 @@ globally for browser-based clients.
|
|
|
68
69
|
requests (anti–abuse-detection throttle). Add `--wait` to block instead
|
|
69
70
|
of returning HTTP 429 when the window has not elapsed.
|
|
70
71
|
|
|
72
|
+
`--debug` enables extra upstream error diagnostics in console logs.
|
|
73
|
+
- Includes: token limits, stream mode, tool count, invalid tool names, and suspicious tool-schema paths.
|
|
74
|
+
- Does not include: request messages, prompt text, bearer tokens, tool descriptions, or the full request body.
|
|
75
|
+
|
|
76
|
+
**Review or redact debug logs before sharing them publicly.**
|
|
77
|
+
|
|
71
78
|
## Configure Codex CLI
|
|
72
79
|
|
|
73
80
|
`start` writes a managed block into **`~/.codex/config.toml`**. You don't edit
|
|
@@ -95,8 +102,8 @@ model = "gpt-5.3-codex"
|
|
|
95
102
|
model_reasoning_effort = "high"
|
|
96
103
|
```
|
|
97
104
|
|
|
98
|
-
If `model_reasoning_effort` is omitted, the bridge leaves it unset and
|
|
99
|
-
|
|
105
|
+
If `model_reasoning_effort` is omitted, the bridge leaves it unset and does not
|
|
106
|
+
send a reasoning effort for Codex requests (a startup info log also reminds you).
|
|
100
107
|
|
|
101
108
|
`model_reasoning_effort` only affects Codex requests; it does not affect Claude.
|
|
102
109
|
|
|
@@ -213,10 +220,11 @@ accepts upstream.
|
|
|
213
220
|
### Reasoning effort
|
|
214
221
|
|
|
215
222
|
For OpenAI-compatible clients, unsupported reasoning values are clamped to the
|
|
216
|
-
model capability table instead of being forwarded upstream.
|
|
217
|
-
reasoning
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
model capability table instead of being forwarded upstream. If a request omits
|
|
224
|
+
reasoning effort, the bridge leaves it omitted rather than inferring a default.
|
|
225
|
+
Claude-side reasoning can be set globally via `MODEL_REASONING_EFFORT` (env, or
|
|
226
|
+
`env` in `~/.claude/settings.json`); invalid Claude-side values are ignored,
|
|
227
|
+
and per-request `reasoning_effort` takes precedence.
|
|
220
228
|
|
|
221
229
|
## Development
|
|
222
230
|
|
package/dist/main.js
CHANGED
|
@@ -4,7 +4,7 @@ import consola from "consola";
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { serve } from "@hono/node-server";
|
|
10
10
|
import { Hono } from "hono";
|
|
@@ -60,13 +60,14 @@ const COPILOT_VERSION$2 = "0.26.7";
|
|
|
60
60
|
const EDITOR_PLUGIN_VERSION$2 = `copilot-chat/${COPILOT_VERSION$2}`;
|
|
61
61
|
const USER_AGENT$2 = `GitHubCopilotChat/${COPILOT_VERSION$2}`;
|
|
62
62
|
const API_VERSION = "2025-04-01";
|
|
63
|
+
const MAX_FETCH_ATTEMPTS = 2;
|
|
63
64
|
const getCopilotProviderContext = (config) => ({
|
|
64
65
|
baseUrl: config.copilotBaseUrl,
|
|
65
66
|
token: config.copilotToken,
|
|
66
67
|
vsCodeVersion: config.vsCodeVersion
|
|
67
68
|
});
|
|
68
|
-
const
|
|
69
|
-
|
|
69
|
+
const shouldRetryResponse = (response) => response.status >= 500 && response.status <= 599;
|
|
70
|
+
const buildHeaders = (provider, init, options) => {
|
|
70
71
|
const headers = new Headers(init.headers);
|
|
71
72
|
headers.set("authorization", `Bearer ${provider.token}`);
|
|
72
73
|
headers.set("copilot-integration-id", "vscode-chat");
|
|
@@ -81,10 +82,22 @@ const fetchCopilot = async (provider, path$1, init, options = {}) => {
|
|
|
81
82
|
if (options.initiator) headers.set("x-initiator", options.initiator);
|
|
82
83
|
if (!headers.has("content-type") && init.body !== void 0) headers.set("content-type", "application/json");
|
|
83
84
|
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
return headers;
|
|
86
|
+
};
|
|
87
|
+
const fetchCopilot = async (provider, path$1, init, options = {}) => {
|
|
88
|
+
if (!provider.token) throw new BridgeNotImplementedError("COPILOT_TOKEN is not configured for copilot-bridge.");
|
|
89
|
+
let lastError;
|
|
90
|
+
for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++) try {
|
|
91
|
+
const response = await fetch(`${provider.baseUrl}${path$1}`, {
|
|
92
|
+
...init,
|
|
93
|
+
headers: buildHeaders(provider, init, options)
|
|
94
|
+
});
|
|
95
|
+
if (!shouldRetryResponse(response) || attempt === MAX_FETCH_ATTEMPTS) return response;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
lastError = error;
|
|
98
|
+
if (attempt === MAX_FETCH_ATTEMPTS) throw error;
|
|
99
|
+
}
|
|
100
|
+
throw lastError;
|
|
88
101
|
};
|
|
89
102
|
|
|
90
103
|
//#endregion
|
|
@@ -171,6 +184,7 @@ const getCopilotToken = async (githubToken, vsCodeVersion) => {
|
|
|
171
184
|
if (!response.ok) throw new HTTPError("Failed to get Copilot token", response);
|
|
172
185
|
return await response.json();
|
|
173
186
|
};
|
|
187
|
+
const isCopilotTokenError = (error) => error instanceof HTTPError && error.message === "Failed to get Copilot token";
|
|
174
188
|
const ensureGitHubToken = async (config, options = {}) => {
|
|
175
189
|
await ensurePaths();
|
|
176
190
|
const existingToken = options.force ? "" : await readGitHubToken$1();
|
|
@@ -190,14 +204,25 @@ const setupBridgeAuth = async (config, options = {}) => {
|
|
|
190
204
|
await loadModels(config);
|
|
191
205
|
return;
|
|
192
206
|
}
|
|
193
|
-
|
|
207
|
+
let githubToken = await ensureGitHubToken(config, options);
|
|
194
208
|
const applyCopilotToken = async () => {
|
|
195
209
|
const { refresh_in, token } = await getCopilotToken(githubToken, config.vsCodeVersion);
|
|
196
210
|
config.copilotToken = token;
|
|
197
211
|
if (options.showToken) consola.info("Copilot token:", token);
|
|
198
212
|
return refresh_in;
|
|
199
213
|
};
|
|
200
|
-
|
|
214
|
+
let refreshIn;
|
|
215
|
+
try {
|
|
216
|
+
refreshIn = await applyCopilotToken();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (options.force || !isCopilotTokenError(error)) throw error;
|
|
219
|
+
consola.warn("Cached GitHub auth could not get a Copilot token; running device auth again.");
|
|
220
|
+
githubToken = await ensureGitHubToken(config, {
|
|
221
|
+
...options,
|
|
222
|
+
force: true
|
|
223
|
+
});
|
|
224
|
+
refreshIn = await applyCopilotToken();
|
|
225
|
+
}
|
|
201
226
|
const refreshInterval = Math.max(refreshIn - 60, 60) * 1e3;
|
|
202
227
|
setInterval(async () => {
|
|
203
228
|
try {
|
|
@@ -806,6 +831,75 @@ var RateLimitError = class extends Error {
|
|
|
806
831
|
}
|
|
807
832
|
};
|
|
808
833
|
|
|
834
|
+
//#endregion
|
|
835
|
+
//#region src/lib/upstream-diagnostics.ts
|
|
836
|
+
const OPENAI_TOOL_NAME_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
837
|
+
const TOOL_SCHEMA_SUSPECT_KEYS = new Set([
|
|
838
|
+
"$defs",
|
|
839
|
+
"allOf",
|
|
840
|
+
"definitions",
|
|
841
|
+
"dependentRequired",
|
|
842
|
+
"dependentSchemas",
|
|
843
|
+
"else",
|
|
844
|
+
"if",
|
|
845
|
+
"not",
|
|
846
|
+
"oneOf",
|
|
847
|
+
"patternProperties",
|
|
848
|
+
"then"
|
|
849
|
+
]);
|
|
850
|
+
const MAX_DIAGNOSTIC_ITEMS = 8;
|
|
851
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
852
|
+
const getToolName = (tool) => {
|
|
853
|
+
if (!isRecord(tool)) return "";
|
|
854
|
+
const functionValue = tool.function;
|
|
855
|
+
if (isRecord(functionValue) && typeof functionValue.name === "string") return functionValue.name;
|
|
856
|
+
return typeof tool.name === "string" ? tool.name : "";
|
|
857
|
+
};
|
|
858
|
+
const getToolParameters = (tool) => {
|
|
859
|
+
if (!isRecord(tool)) return;
|
|
860
|
+
const functionValue = tool.function;
|
|
861
|
+
if (isRecord(functionValue) && "parameters" in functionValue) return functionValue.parameters;
|
|
862
|
+
return tool.parameters;
|
|
863
|
+
};
|
|
864
|
+
const collectSuspiciousSchemaKeys = (value, path$1 = "$", keys = []) => {
|
|
865
|
+
if (keys.length >= MAX_DIAGNOSTIC_ITEMS) return keys;
|
|
866
|
+
if (Array.isArray(value)) {
|
|
867
|
+
for (const [index, item] of value.entries()) {
|
|
868
|
+
collectSuspiciousSchemaKeys(item, `${path$1}[${index}]`, keys);
|
|
869
|
+
if (keys.length >= MAX_DIAGNOSTIC_ITEMS) break;
|
|
870
|
+
}
|
|
871
|
+
return keys;
|
|
872
|
+
}
|
|
873
|
+
if (!isRecord(value)) return keys;
|
|
874
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
875
|
+
const nextPath = `${path$1}.${key}`;
|
|
876
|
+
if (TOOL_SCHEMA_SUSPECT_KEYS.has(key)) {
|
|
877
|
+
if (!keys.includes(nextPath)) keys.push(nextPath);
|
|
878
|
+
if (keys.length >= MAX_DIAGNOSTIC_ITEMS) break;
|
|
879
|
+
}
|
|
880
|
+
collectSuspiciousSchemaKeys(nestedValue, nextPath, keys);
|
|
881
|
+
if (keys.length >= MAX_DIAGNOSTIC_ITEMS) break;
|
|
882
|
+
}
|
|
883
|
+
return keys;
|
|
884
|
+
};
|
|
885
|
+
const summarizeToolsForDiagnostics = (tools) => {
|
|
886
|
+
if (!Array.isArray(tools) || tools.length === 0) return;
|
|
887
|
+
const invalidNames = tools.map((tool) => getToolName(tool)).filter((name) => !OPENAI_TOOL_NAME_PATTERN.test(name)).slice(0, MAX_DIAGNOSTIC_ITEMS);
|
|
888
|
+
const suspiciousSchemas = tools.flatMap((tool) => {
|
|
889
|
+
const name = getToolName(tool) || "<missing>";
|
|
890
|
+
const keys = collectSuspiciousSchemaKeys(getToolParameters(tool));
|
|
891
|
+
return keys.length > 0 ? [{
|
|
892
|
+
keys,
|
|
893
|
+
name
|
|
894
|
+
}] : [];
|
|
895
|
+
}).slice(0, MAX_DIAGNOSTIC_ITEMS);
|
|
896
|
+
return {
|
|
897
|
+
count: tools.length,
|
|
898
|
+
...invalidNames.length > 0 ? { invalidNames } : {},
|
|
899
|
+
...suspiciousSchemas.length > 0 ? { suspiciousSchemas } : {}
|
|
900
|
+
};
|
|
901
|
+
};
|
|
902
|
+
|
|
809
903
|
//#endregion
|
|
810
904
|
//#region src/services/copilot/responses.ts
|
|
811
905
|
const RESPONSES_ONLY_MODEL_PATTERN = /^(?:gpt-5\.5|gpt-5\.4-mini|gpt-5\.3-codex|gpt-5\.2-codex)(?:-|$)/i;
|
|
@@ -1036,7 +1130,6 @@ function getFinishReason(response, hasFunctionCalls) {
|
|
|
1036
1130
|
const usesMaxCompletionTokens = (modelId) => modelId.startsWith("gpt-5");
|
|
1037
1131
|
const isClaudeOpus47Model$1 = (modelId) => modelId.startsWith("claude-opus-4.7");
|
|
1038
1132
|
const MAX_USER_LENGTH = 64;
|
|
1039
|
-
const defaultReasoningEffort = (modelId) => usesMaxCompletionTokens(modelId) ? "medium" : void 0;
|
|
1040
1133
|
const sanitizeReasoningEffortForModel = (modelId, reasoningEffort) => {
|
|
1041
1134
|
if (!reasoningEffort) return;
|
|
1042
1135
|
return clampReasoningEffort(modelId, reasoningEffort)?.effort;
|
|
@@ -1074,9 +1167,8 @@ const getConfiguredReasoningEffort = (client, claudeSettingsEnv) => {
|
|
|
1074
1167
|
};
|
|
1075
1168
|
const getRequestedReasoningEffort = (payload, claudeSettingsEnv, client) => {
|
|
1076
1169
|
const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
|
|
1077
|
-
const defaultEffort = client === "claude" ? void 0 : defaultReasoningEffort(payload.model);
|
|
1078
1170
|
const requestedReasoningEffort = payload.reasoning_effort ?? normalizeReasoningEffort$1(configuredReasoningEffort);
|
|
1079
|
-
return sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort)
|
|
1171
|
+
return sanitizeReasoningEffortForModel(payload.model, requestedReasoningEffort);
|
|
1080
1172
|
};
|
|
1081
1173
|
const getRequestedClaudeOpus47Effort = (payload, claudeSettingsEnv, client) => {
|
|
1082
1174
|
if (!isClaudeOpus47Model$1(payload.model)) return;
|
|
@@ -1087,6 +1179,16 @@ const sanitizeUserIdentifier = (user) => {
|
|
|
1087
1179
|
if (!user) return;
|
|
1088
1180
|
return user.slice(0, MAX_USER_LENGTH);
|
|
1089
1181
|
};
|
|
1182
|
+
const summarizeRequestForDiagnostics = (payload) => ({
|
|
1183
|
+
max_completion_tokens: payload.max_completion_tokens,
|
|
1184
|
+
max_tokens: payload.max_tokens,
|
|
1185
|
+
message_count: payload.messages.length,
|
|
1186
|
+
output_config_effort: payload.output_config?.effort,
|
|
1187
|
+
reasoning_effort: payload.reasoning_effort,
|
|
1188
|
+
stream: payload.stream ?? void 0,
|
|
1189
|
+
tool_choice: payload.tool_choice,
|
|
1190
|
+
tools: summarizeToolsForDiagnostics(payload.tools)
|
|
1191
|
+
});
|
|
1090
1192
|
const buildRequestPayload = (payload, claudeSettingsEnv, client) => {
|
|
1091
1193
|
const requestedReasoningEffort = getRequestedReasoningEffort(payload, claudeSettingsEnv, client);
|
|
1092
1194
|
const requestedClaudeOpus47Effort = getRequestedClaudeOpus47Effort(payload, claudeSettingsEnv, client);
|
|
@@ -1142,6 +1244,7 @@ const createChatCompletions = async (config, payload, options) => {
|
|
|
1142
1244
|
});
|
|
1143
1245
|
await logUpstreamError("Failed to create chat completions", response, {
|
|
1144
1246
|
model: payload.model,
|
|
1247
|
+
request: requestPayload,
|
|
1145
1248
|
route: "/chat/completions"
|
|
1146
1249
|
});
|
|
1147
1250
|
throw new HTTPError("Failed to create chat completions", response);
|
|
@@ -1179,7 +1282,8 @@ async function logUpstreamError(message, response, context) {
|
|
|
1179
1282
|
model: context.model,
|
|
1180
1283
|
status: response.status,
|
|
1181
1284
|
statusText: response.statusText,
|
|
1182
|
-
body: errorBody || void 0
|
|
1285
|
+
body: errorBody || void 0,
|
|
1286
|
+
request: runtimeState.debug && context.request ? JSON.stringify(summarizeRequestForDiagnostics(context.request)) : void 0
|
|
1183
1287
|
});
|
|
1184
1288
|
}
|
|
1185
1289
|
async function shouldRetryWithResponses(response) {
|
|
@@ -1338,6 +1442,84 @@ function mapOpenAIStopReasonToAnthropic(finishReason) {
|
|
|
1338
1442
|
}[finishReason];
|
|
1339
1443
|
}
|
|
1340
1444
|
|
|
1445
|
+
//#endregion
|
|
1446
|
+
//#region src/bridges/claude/tool-names.ts
|
|
1447
|
+
const STRICT_TOOL_NAME_CHARS_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
1448
|
+
const DOTTED_TOOL_NAME_CHARS_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
1449
|
+
const DEFAULT_TOOL_NAME_MAX_LENGTH = 64;
|
|
1450
|
+
const EXTENDED_TOOL_NAME_MAX_LENGTH = 128;
|
|
1451
|
+
const HASH_LENGTH = 10;
|
|
1452
|
+
const getToolNameMapperOptionsForModel = (modelId) => {
|
|
1453
|
+
const normalized = modelId.trim().toLowerCase().replace(/\[1m\]$/, "-1m").replace(/[._]/g, "-");
|
|
1454
|
+
if (/^claude-opus-4-(?:6|7)(?:$|-)/.test(normalized)) return {
|
|
1455
|
+
allowDots: false,
|
|
1456
|
+
maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH
|
|
1457
|
+
};
|
|
1458
|
+
if (/^claude-sonnet-4(?:$|-\d{8}$)/.test(normalized)) return {
|
|
1459
|
+
allowDots: false,
|
|
1460
|
+
maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH
|
|
1461
|
+
};
|
|
1462
|
+
if (normalized.startsWith("gemini-")) return {
|
|
1463
|
+
allowDots: true,
|
|
1464
|
+
maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH
|
|
1465
|
+
};
|
|
1466
|
+
if (/^gpt-5-(?:2|4)(?:$|-)/.test(normalized) || /^gpt-5-(?:2|3)-codex(?:$|-)/.test(normalized) || /^gpt-5-4-mini(?:$|-)/.test(normalized) || /^gpt-5-5(?:$|-)/.test(normalized)) return {
|
|
1467
|
+
allowDots: false,
|
|
1468
|
+
maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH
|
|
1469
|
+
};
|
|
1470
|
+
if (normalized.startsWith("gpt-")) return {
|
|
1471
|
+
allowDots: true,
|
|
1472
|
+
maxNameLength: EXTENDED_TOOL_NAME_MAX_LENGTH
|
|
1473
|
+
};
|
|
1474
|
+
return {
|
|
1475
|
+
allowDots: false,
|
|
1476
|
+
maxNameLength: DEFAULT_TOOL_NAME_MAX_LENGTH
|
|
1477
|
+
};
|
|
1478
|
+
};
|
|
1479
|
+
const makeHash = (value) => createHash("sha1").update(value).digest("hex").slice(0, HASH_LENGTH);
|
|
1480
|
+
const getAllowedNamePattern = (allowDots) => allowDots ? DOTTED_TOOL_NAME_CHARS_PATTERN : STRICT_TOOL_NAME_CHARS_PATTERN;
|
|
1481
|
+
const cleanToolName = (name, allowDots) => {
|
|
1482
|
+
const invalidCharsPattern = allowDots ? /[^A-Za-z0-9_.-]/g : /[^A-Za-z0-9_-]/g;
|
|
1483
|
+
return name.replace(invalidCharsPattern, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "") || "tool";
|
|
1484
|
+
};
|
|
1485
|
+
const isValidToolName = (name, maxNameLength, allowDots) => name.length > 0 && name.length <= maxNameLength && getAllowedNamePattern(allowDots).test(name);
|
|
1486
|
+
const makeValidToolName = (name, maxNameLength, allowDots) => {
|
|
1487
|
+
if (isValidToolName(name, maxNameLength, allowDots)) return name;
|
|
1488
|
+
const cleaned = cleanToolName(name, allowDots);
|
|
1489
|
+
if (cleaned.length <= maxNameLength) return cleaned;
|
|
1490
|
+
const hash = makeHash(name);
|
|
1491
|
+
const prefixLength = maxNameLength - hash.length - 1;
|
|
1492
|
+
return `${cleaned.slice(0, prefixLength)}_${hash}`;
|
|
1493
|
+
};
|
|
1494
|
+
const makeUniqueToolName = (name, used, maxNameLength, allowDots) => {
|
|
1495
|
+
const candidate = makeValidToolName(name, maxNameLength, allowDots);
|
|
1496
|
+
if (!used.has(candidate)) return candidate;
|
|
1497
|
+
for (let index = 2;; index++) {
|
|
1498
|
+
const suffix = `_${makeHash(`${name}:${index}`)}`;
|
|
1499
|
+
const prefixLength = maxNameLength - suffix.length;
|
|
1500
|
+
const next = `${cleanToolName(name, allowDots).slice(0, prefixLength)}${suffix}`;
|
|
1501
|
+
if (!used.has(next)) return next;
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
const createAnthropicToolNameMapper = (tools, options = {}) => {
|
|
1505
|
+
const maxNameLength = options.maxNameLength ?? DEFAULT_TOOL_NAME_MAX_LENGTH;
|
|
1506
|
+
const allowDots = options.allowDots ?? false;
|
|
1507
|
+
const anthropicToOpenAI = /* @__PURE__ */ new Map();
|
|
1508
|
+
const openAIToAnthropic = /* @__PURE__ */ new Map();
|
|
1509
|
+
const used = /* @__PURE__ */ new Set();
|
|
1510
|
+
for (const tool of tools ?? []) {
|
|
1511
|
+
if (anthropicToOpenAI.has(tool.name)) continue;
|
|
1512
|
+
const openAIName = makeUniqueToolName(tool.name, used, maxNameLength, allowDots);
|
|
1513
|
+
used.add(openAIName);
|
|
1514
|
+
anthropicToOpenAI.set(tool.name, openAIName);
|
|
1515
|
+
openAIToAnthropic.set(openAIName, tool.name);
|
|
1516
|
+
}
|
|
1517
|
+
return {
|
|
1518
|
+
toAnthropic: (name) => openAIToAnthropic.get(name) ?? name,
|
|
1519
|
+
toOpenAI: (name) => anthropicToOpenAI.get(name) ?? makeValidToolName(name, maxNameLength, allowDots)
|
|
1520
|
+
};
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1341
1523
|
//#endregion
|
|
1342
1524
|
//#region src/bridges/claude/non-stream-translation.ts
|
|
1343
1525
|
const normalizeClaudeModelAlias = (model) => {
|
|
@@ -1433,12 +1615,13 @@ function translateReasoningEffort(payload, settings) {
|
|
|
1433
1615
|
if (budgetTokens <= 24576) return sanitizeReasoningEffortForModel(modelId, "high");
|
|
1434
1616
|
return sanitizeReasoningEffortForModel(modelId, "xhigh");
|
|
1435
1617
|
}
|
|
1436
|
-
function translateToOpenAI(payload, settings) {
|
|
1618
|
+
function translateToOpenAI(payload, settings, toolNameMapper) {
|
|
1437
1619
|
const model = translateModelName(payload.model, settings);
|
|
1438
|
-
const
|
|
1620
|
+
const mapper = toolNameMapper ?? createAnthropicToolNameMapper(payload.tools, { ...getToolNameMapperOptionsForModel(model) });
|
|
1621
|
+
const tools = translateAnthropicToolsToOpenAI(payload.tools, mapper);
|
|
1439
1622
|
return {
|
|
1440
1623
|
model,
|
|
1441
|
-
messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system),
|
|
1624
|
+
messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system, mapper),
|
|
1442
1625
|
max_tokens: payload.max_tokens,
|
|
1443
1626
|
stop: payload.stop_sequences,
|
|
1444
1627
|
stream: payload.stream,
|
|
@@ -1449,12 +1632,12 @@ function translateToOpenAI(payload, settings) {
|
|
|
1449
1632
|
reasoning_effort: translateReasoningEffort(payload, settings),
|
|
1450
1633
|
user: payload.metadata?.user_id,
|
|
1451
1634
|
tools,
|
|
1452
|
-
tool_choice: tools && tools.length > 0 ? translateAnthropicToolChoiceToOpenAI(payload.tool_choice) : void 0
|
|
1635
|
+
tool_choice: tools && tools.length > 0 ? translateAnthropicToolChoiceToOpenAI(payload.tool_choice, mapper) : void 0
|
|
1453
1636
|
};
|
|
1454
1637
|
}
|
|
1455
|
-
function translateAnthropicMessagesToOpenAI(anthropicMessages, system) {
|
|
1638
|
+
function translateAnthropicMessagesToOpenAI(anthropicMessages, system, toolNameMapper) {
|
|
1456
1639
|
const systemMessages = handleSystemPrompt(system);
|
|
1457
|
-
const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message));
|
|
1640
|
+
const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, toolNameMapper));
|
|
1458
1641
|
return [...systemMessages, ...otherMessages];
|
|
1459
1642
|
}
|
|
1460
1643
|
function handleSystemPrompt(system) {
|
|
@@ -1488,7 +1671,7 @@ function handleUserMessage(message) {
|
|
|
1488
1671
|
});
|
|
1489
1672
|
return newMessages;
|
|
1490
1673
|
}
|
|
1491
|
-
function handleAssistantMessage(message) {
|
|
1674
|
+
function handleAssistantMessage(message, toolNameMapper) {
|
|
1492
1675
|
if (!Array.isArray(message.content)) return [{
|
|
1493
1676
|
role: "assistant",
|
|
1494
1677
|
content: mapContent(message.content)
|
|
@@ -1504,7 +1687,7 @@ function handleAssistantMessage(message) {
|
|
|
1504
1687
|
id: toolUse.id,
|
|
1505
1688
|
type: "function",
|
|
1506
1689
|
function: {
|
|
1507
|
-
name: toolUse.name,
|
|
1690
|
+
name: toolNameMapper.toOpenAI(toolUse.name),
|
|
1508
1691
|
arguments: JSON.stringify(toolUse.input)
|
|
1509
1692
|
}
|
|
1510
1693
|
}))
|
|
@@ -1540,18 +1723,18 @@ function mapContent(content) {
|
|
|
1540
1723
|
}
|
|
1541
1724
|
return contentParts;
|
|
1542
1725
|
}
|
|
1543
|
-
function translateAnthropicToolsToOpenAI(anthropicTools) {
|
|
1726
|
+
function translateAnthropicToolsToOpenAI(anthropicTools, toolNameMapper) {
|
|
1544
1727
|
if (!anthropicTools || anthropicTools.length === 0) return;
|
|
1545
1728
|
return anthropicTools.map((tool) => ({
|
|
1546
1729
|
type: "function",
|
|
1547
1730
|
function: {
|
|
1548
|
-
name: tool.name,
|
|
1731
|
+
name: toolNameMapper.toOpenAI(tool.name),
|
|
1549
1732
|
description: tool.description,
|
|
1550
1733
|
parameters: tool.input_schema
|
|
1551
1734
|
}
|
|
1552
1735
|
}));
|
|
1553
1736
|
}
|
|
1554
|
-
function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice) {
|
|
1737
|
+
function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice, toolNameMapper) {
|
|
1555
1738
|
if (!anthropicToolChoice) return;
|
|
1556
1739
|
switch (anthropicToolChoice.type) {
|
|
1557
1740
|
case "auto": return "auto";
|
|
@@ -1559,21 +1742,21 @@ function translateAnthropicToolChoiceToOpenAI(anthropicToolChoice) {
|
|
|
1559
1742
|
case "tool":
|
|
1560
1743
|
if (anthropicToolChoice.name) return {
|
|
1561
1744
|
type: "function",
|
|
1562
|
-
function: { name: anthropicToolChoice.name }
|
|
1745
|
+
function: { name: toolNameMapper.toOpenAI(anthropicToolChoice.name) }
|
|
1563
1746
|
};
|
|
1564
1747
|
return;
|
|
1565
1748
|
case "none": return "none";
|
|
1566
1749
|
default: return;
|
|
1567
1750
|
}
|
|
1568
1751
|
}
|
|
1569
|
-
function translateToAnthropic(response) {
|
|
1752
|
+
function translateToAnthropic(response, toolNameMapper = createAnthropicToolNameMapper(void 0)) {
|
|
1570
1753
|
const allTextBlocks = [];
|
|
1571
1754
|
const allToolUseBlocks = [];
|
|
1572
1755
|
let stopReason = null;
|
|
1573
1756
|
stopReason = response.choices[0]?.finish_reason ?? stopReason;
|
|
1574
1757
|
for (const choice of response.choices) {
|
|
1575
1758
|
allTextBlocks.push(...getAnthropicTextBlocks(choice.message.content));
|
|
1576
|
-
allToolUseBlocks.push(...getAnthropicToolUseBlocks(choice.message.tool_calls));
|
|
1759
|
+
allToolUseBlocks.push(...getAnthropicToolUseBlocks(choice.message.tool_calls, toolNameMapper));
|
|
1577
1760
|
if (choice.finish_reason === "tool_calls" || stopReason === "stop") stopReason = choice.finish_reason;
|
|
1578
1761
|
}
|
|
1579
1762
|
return {
|
|
@@ -1602,12 +1785,12 @@ function getAnthropicTextBlocks(messageContent) {
|
|
|
1602
1785
|
}));
|
|
1603
1786
|
return [];
|
|
1604
1787
|
}
|
|
1605
|
-
function getAnthropicToolUseBlocks(toolCalls) {
|
|
1788
|
+
function getAnthropicToolUseBlocks(toolCalls, toolNameMapper) {
|
|
1606
1789
|
if (!toolCalls) return [];
|
|
1607
1790
|
return toolCalls.map((toolCall) => ({
|
|
1608
1791
|
type: "tool_use",
|
|
1609
1792
|
id: toolCall.id,
|
|
1610
|
-
name: toolCall.function.name,
|
|
1793
|
+
name: toolNameMapper.toAnthropic(toolCall.function.name),
|
|
1611
1794
|
input: safeJsonParse(toolCall.function.arguments)
|
|
1612
1795
|
}));
|
|
1613
1796
|
}
|
|
@@ -1625,7 +1808,7 @@ function isToolBlockOpen(state) {
|
|
|
1625
1808
|
if (!state.contentBlockOpen) return false;
|
|
1626
1809
|
return Object.values(state.toolCalls).some((tc) => tc.anthropicBlockIndex === state.contentBlockIndex);
|
|
1627
1810
|
}
|
|
1628
|
-
function translateChunkToAnthropicEvents(chunk, state) {
|
|
1811
|
+
function translateChunkToAnthropicEvents(chunk, state, toolNameMapper = createAnthropicToolNameMapper(void 0)) {
|
|
1629
1812
|
const events$1 = [];
|
|
1630
1813
|
if (chunk.choices.length === 0) return events$1;
|
|
1631
1814
|
const choice = chunk.choices[0];
|
|
@@ -1689,10 +1872,11 @@ function translateChunkToAnthropicEvents(chunk, state) {
|
|
|
1689
1872
|
state.contentBlockIndex++;
|
|
1690
1873
|
state.contentBlockOpen = false;
|
|
1691
1874
|
}
|
|
1875
|
+
const toolName = toolNameMapper.toAnthropic(toolCall.function.name);
|
|
1692
1876
|
const anthropicBlockIndex = state.contentBlockIndex;
|
|
1693
1877
|
state.toolCalls[toolCall.index] = {
|
|
1694
1878
|
id: toolCall.id,
|
|
1695
|
-
name:
|
|
1879
|
+
name: toolName,
|
|
1696
1880
|
anthropicBlockIndex
|
|
1697
1881
|
};
|
|
1698
1882
|
events$1.push({
|
|
@@ -1701,7 +1885,7 @@ function translateChunkToAnthropicEvents(chunk, state) {
|
|
|
1701
1885
|
content_block: {
|
|
1702
1886
|
type: "tool_use",
|
|
1703
1887
|
id: toolCall.id,
|
|
1704
|
-
name:
|
|
1888
|
+
name: toolName,
|
|
1705
1889
|
input: {}
|
|
1706
1890
|
}
|
|
1707
1891
|
});
|
|
@@ -1919,10 +2103,14 @@ messageRoutes.post("/", async (c) => {
|
|
|
1919
2103
|
if (error instanceof RateLimitError) return c.json({ error: { message: error.message } }, 429);
|
|
1920
2104
|
throw error;
|
|
1921
2105
|
}
|
|
1922
|
-
const
|
|
2106
|
+
const anthropicPayload = await c.req.json();
|
|
2107
|
+
const claudeSettings = await getClaudeSettings();
|
|
2108
|
+
const upstreamModel = translateModelName(anthropicPayload.model, claudeSettings);
|
|
2109
|
+
const toolNameMapper = createAnthropicToolNameMapper(anthropicPayload.tools, { ...getToolNameMapperOptionsForModel(upstreamModel) });
|
|
2110
|
+
const openAIPayload = translateToOpenAI(anthropicPayload, claudeSettings, toolNameMapper);
|
|
1923
2111
|
try {
|
|
1924
2112
|
const response = await createChatCompletions(config, openAIPayload, { client: "claude" });
|
|
1925
|
-
if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response));
|
|
2113
|
+
if (isNonStreamingResponse(response)) return c.json(translateToAnthropic(response, toolNameMapper));
|
|
1926
2114
|
return streamSSE(c, async (stream) => {
|
|
1927
2115
|
const streamState = {
|
|
1928
2116
|
messageStartSent: false,
|
|
@@ -1940,7 +2128,7 @@ messageRoutes.post("/", async (c) => {
|
|
|
1940
2128
|
} catch {
|
|
1941
2129
|
continue;
|
|
1942
2130
|
}
|
|
1943
|
-
const events$1 = translateChunkToAnthropicEvents(chunk, streamState);
|
|
2131
|
+
const events$1 = translateChunkToAnthropicEvents(chunk, streamState, toolNameMapper);
|
|
1944
2132
|
for (const event of events$1) await stream.writeSSE({
|
|
1945
2133
|
event: event.type,
|
|
1946
2134
|
data: JSON.stringify(event)
|
|
@@ -2338,6 +2526,11 @@ const codexResponsesRequestSchema = z.object({
|
|
|
2338
2526
|
model: z.string().min(1),
|
|
2339
2527
|
stream: z.boolean().optional()
|
|
2340
2528
|
}).passthrough();
|
|
2529
|
+
const removeReasoningEffort = (reasoning) => {
|
|
2530
|
+
const next = { ...reasoning };
|
|
2531
|
+
delete next.effort;
|
|
2532
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
2533
|
+
};
|
|
2341
2534
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2342
2535
|
const normalizeCodexResponsesRequest = (payload) => {
|
|
2343
2536
|
const parsed = codexResponsesRequestSchema.parse(payload);
|
|
@@ -2352,12 +2545,21 @@ const normalizeCodexResponsesRequest = (payload) => {
|
|
|
2352
2545
|
if ("reasoning" in next) delete next.reasoning;
|
|
2353
2546
|
return next;
|
|
2354
2547
|
}
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2548
|
+
if ("reasoning" in next) if (!isPlainObject(next.reasoning)) delete next.reasoning;
|
|
2549
|
+
else {
|
|
2550
|
+
const incoming = next.reasoning;
|
|
2551
|
+
if (incoming.effort === void 0 || incoming.effort === null) {
|
|
2552
|
+
const reasoning = removeReasoningEffort(incoming);
|
|
2553
|
+
if (reasoning) next.reasoning = reasoning;
|
|
2554
|
+
else delete next.reasoning;
|
|
2555
|
+
} else {
|
|
2556
|
+
const clamped = clampReasoningEffort(canonical, incoming.effort);
|
|
2557
|
+
if (clamped) next.reasoning = {
|
|
2558
|
+
...incoming,
|
|
2559
|
+
effort: clamped.effort
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2361
2563
|
const text = isPlainObject(next.text) ? next.text : void 0;
|
|
2362
2564
|
if (text && capability.textVerbosity && "verbosity" in text) {
|
|
2363
2565
|
const { supported, default: defaultVerbosity } = capability.textVerbosity;
|
|
@@ -2373,6 +2575,28 @@ const normalizeCodexResponsesRequest = (payload) => {
|
|
|
2373
2575
|
//#endregion
|
|
2374
2576
|
//#region src/routes/responses.ts
|
|
2375
2577
|
const responsesRoutes = new Hono();
|
|
2578
|
+
const summarizeResponsesRequestForDiagnostics = (payload) => ({
|
|
2579
|
+
has_instructions: Boolean(payload.instructions),
|
|
2580
|
+
input_item_count: Array.isArray(payload.input) ? payload.input.length : void 0,
|
|
2581
|
+
input_kind: typeof payload.input === "string" ? "string" : Array.isArray(payload.input) ? "array" : payload.input === void 0 ? void 0 : typeof payload.input,
|
|
2582
|
+
max_output_tokens: payload.max_output_tokens,
|
|
2583
|
+
reasoning_effort: payload.reasoning?.effort,
|
|
2584
|
+
stream: payload.stream ?? void 0,
|
|
2585
|
+
tool_choice: payload.tool_choice,
|
|
2586
|
+
tools: summarizeToolsForDiagnostics(payload.tools)
|
|
2587
|
+
});
|
|
2588
|
+
const logResponsesUpstreamError = async (message, response, context) => {
|
|
2589
|
+
if (!runtimeState.debug) return;
|
|
2590
|
+
const errorBody = await response.clone().text().catch(() => "");
|
|
2591
|
+
consola.error(message, {
|
|
2592
|
+
route: context.route,
|
|
2593
|
+
model: context.model,
|
|
2594
|
+
status: response.status,
|
|
2595
|
+
statusText: response.statusText,
|
|
2596
|
+
body: errorBody || void 0,
|
|
2597
|
+
request: JSON.stringify(summarizeResponsesRequestForDiagnostics(context.request))
|
|
2598
|
+
});
|
|
2599
|
+
};
|
|
2376
2600
|
responsesRoutes.post("/", async (c) => {
|
|
2377
2601
|
try {
|
|
2378
2602
|
await checkRateLimit();
|
|
@@ -2396,6 +2620,11 @@ responsesRoutes.post("/", async (c) => {
|
|
|
2396
2620
|
body: JSON.stringify(chatPayload)
|
|
2397
2621
|
});
|
|
2398
2622
|
if (!upstream$1.ok) {
|
|
2623
|
+
await logResponsesUpstreamError("Failed to create chat completions", upstream$1, {
|
|
2624
|
+
model: payload.model,
|
|
2625
|
+
request: payload,
|
|
2626
|
+
route: "/chat/completions"
|
|
2627
|
+
});
|
|
2399
2628
|
const text = await upstream$1.text();
|
|
2400
2629
|
return new Response(text, {
|
|
2401
2630
|
status: upstream$1.status,
|
|
@@ -2429,6 +2658,11 @@ responsesRoutes.post("/", async (c) => {
|
|
|
2429
2658
|
body: JSON.stringify(payload)
|
|
2430
2659
|
});
|
|
2431
2660
|
const contentType = upstream.headers.get("content-type") ?? "";
|
|
2661
|
+
if (!upstream.ok) await logResponsesUpstreamError("Failed to create responses", upstream, {
|
|
2662
|
+
model: payload.model,
|
|
2663
|
+
request: payload,
|
|
2664
|
+
route: "/responses"
|
|
2665
|
+
});
|
|
2432
2666
|
if (payload.stream && upstream.body && contentType.includes("text/event-stream")) return new Response(normalizeResponsesSseStream(upstream.body), {
|
|
2433
2667
|
status: upstream.status,
|
|
2434
2668
|
headers: upstream.headers
|
|
@@ -2555,6 +2789,11 @@ const start = defineCommand({
|
|
|
2555
2789
|
default: false,
|
|
2556
2790
|
description: "Print GitHub and Copilot tokens during startup."
|
|
2557
2791
|
},
|
|
2792
|
+
debug: {
|
|
2793
|
+
type: "boolean",
|
|
2794
|
+
default: false,
|
|
2795
|
+
description: "Enable upstream request diagnostics in console logs."
|
|
2796
|
+
},
|
|
2558
2797
|
"codex-setup": {
|
|
2559
2798
|
type: "boolean",
|
|
2560
2799
|
default: true,
|
|
@@ -2599,6 +2838,8 @@ const start = defineCommand({
|
|
|
2599
2838
|
host: args.host ? String(args.host) : DEFAULT_HOST,
|
|
2600
2839
|
port: args.port ? Number(args.port) : envPort !== void 0 ? envPort : claudePort !== void 0 ? claudePort : DEFAULT_PORT
|
|
2601
2840
|
});
|
|
2841
|
+
runtimeState.debug = Boolean(args.debug);
|
|
2842
|
+
if (runtimeState.debug) consola.info("Debug mode enabled; upstream errors include request summaries");
|
|
2602
2843
|
const rateLimitRaw = args["rate-limit"];
|
|
2603
2844
|
if (rateLimitRaw !== void 0) {
|
|
2604
2845
|
const parsed = Number.parseInt(String(rateLimitRaw), 10);
|
|
@@ -2633,7 +2874,12 @@ const start = defineCommand({
|
|
|
2633
2874
|
const codexUserConfig = await readCodexUserConfigFromDisk(codexConfigPath);
|
|
2634
2875
|
let chosenModel = codexUserConfig.model;
|
|
2635
2876
|
let chosenEffort = codexUserConfig.modelReasoningEffort;
|
|
2636
|
-
if (chosenModel && !chosenEffort)
|
|
2877
|
+
if (chosenModel && !chosenEffort) {
|
|
2878
|
+
const capability = getModelCapability(chosenModel);
|
|
2879
|
+
if (capability && !capability.reasoning) consola.info(`codex model_reasoning_effort not set; ${chosenModel} does not accept reasoning effort`);
|
|
2880
|
+
else if (capability?.reasoning) consola.info(`codex model_reasoning_effort not set; leaving reasoning effort unset for ${chosenModel}`);
|
|
2881
|
+
else consola.info(`codex model_reasoning_effort not set; leaving reasoning effort unset for ${chosenModel}`);
|
|
2882
|
+
}
|
|
2637
2883
|
if (!isClaudeCodeMode && args["codex-setup"]) try {
|
|
2638
2884
|
const result = await applyCodexConfig({
|
|
2639
2885
|
baseUrl: `${baseUrl}/v1`,
|