betahi-copilot-bridge 0.1.4 → 0.1.6
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 +22 -21
- package/dist/main.js +192 -27
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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.6" 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
|
|
|
8
|
-
> Use GitHub Copilot as a local OpenAI/Anthropic-compatible API, so [Codex CLI](https://
|
|
8
|
+
> Use GitHub Copilot as a local OpenAI/Anthropic-compatible API, so [Codex CLI](https://developers.openai.com/codex/cli), [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) and Continue can talk to Copilot with minimal configuration.
|
|
9
9
|
|
|
10
10
|
> [!WARNING]
|
|
11
11
|
> This is a reverse-engineered bridge for the GitHub Copilot API. It is not
|
|
@@ -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
|
|
|
@@ -153,12 +160,8 @@ Optional slot-specific:
|
|
|
153
160
|
|
|
154
161
|
`MODEL_REASONING_EFFORT` (case-insensitive key lookup) is also read from the
|
|
155
162
|
project-local `.claude/settings.json` and `.claude/settings.local.json` and
|
|
156
|
-
applied to Claude requests only when the model supports reasoning.
|
|
157
|
-
|
|
158
|
-
For backward compatibility, `COPILOT_REASONING_EFFORT` is accepted as an alias.
|
|
159
|
-
|
|
160
|
-
If Claude-side reasoning is not configured, Claude requests fall back to
|
|
161
|
-
`medium` (when the target model supports reasoning).
|
|
163
|
+
applied to Claude requests only when the model supports reasoning. If it is not
|
|
164
|
+
configured, Claude requests do not infer or attach a reasoning effort.
|
|
162
165
|
|
|
163
166
|
## Environment overrides
|
|
164
167
|
|
|
@@ -168,8 +171,7 @@ If Claude-side reasoning is not configured, Claude requests fall back to
|
|
|
168
171
|
| `COPILOT_ACCOUNT_TYPE` | `individual` \| `business` \| `enterprise`. |
|
|
169
172
|
| `COPILOT_BASE_URL` | Override the upstream Copilot base URL. |
|
|
170
173
|
| `COPILOT_VSCODE_VERSION` | Override the VS Code version sent upstream. |
|
|
171
|
-
| `MODEL_REASONING_EFFORT` | Claude-side
|
|
172
|
-
| `COPILOT_REASONING_EFFORT` | Alias of `MODEL_REASONING_EFFORT` (backward compatible). |
|
|
174
|
+
| `MODEL_REASONING_EFFORT` | Claude-side reasoning effort override. |
|
|
173
175
|
|
|
174
176
|
|
|
175
177
|
|
|
@@ -217,13 +219,12 @@ accepts upstream.
|
|
|
217
219
|
|
|
218
220
|
### Reasoning effort
|
|
219
221
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
globally via `MODEL_REASONING_EFFORT` (or
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
`thinking.budget_tokens`) takes precedence.
|
|
222
|
+
For OpenAI-compatible clients, unsupported reasoning values are clamped to the
|
|
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.
|
|
227
228
|
|
|
228
229
|
## Development
|
|
229
230
|
|
package/dist/main.js
CHANGED
|
@@ -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,9 +831,78 @@ 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
|
-
const RESPONSES_ONLY_MODEL_PATTERN = /^(?:gpt-5\.3-codex|gpt-5\.
|
|
905
|
+
const RESPONSES_ONLY_MODEL_PATTERN = /^(?:gpt-5\.5|gpt-5\.4-mini|gpt-5\.3-codex|gpt-5\.2-codex)(?:-|$)/i;
|
|
812
906
|
function shouldUseResponsesApiForModel(model) {
|
|
813
907
|
return RESPONSES_ONLY_MODEL_PATTERN.test(model);
|
|
814
908
|
}
|
|
@@ -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;
|
|
@@ -1070,23 +1163,32 @@ const getEnvValueCaseInsensitive = (env, key) => {
|
|
|
1070
1163
|
};
|
|
1071
1164
|
const getConfiguredReasoningEffort = (client, claudeSettingsEnv) => {
|
|
1072
1165
|
if (client !== "claude") return;
|
|
1073
|
-
return process.env.MODEL_REASONING_EFFORT ??
|
|
1166
|
+
return process.env.MODEL_REASONING_EFFORT ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "MODEL_REASONING_EFFORT");
|
|
1074
1167
|
};
|
|
1075
1168
|
const getRequestedReasoningEffort = (payload, claudeSettingsEnv, client) => {
|
|
1076
1169
|
const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
|
|
1077
|
-
const defaultEffort = client === "claude" ? clampReasoningEffort(payload.model, void 0)?.effort : 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;
|
|
1083
1175
|
const configuredReasoningEffort = getConfiguredReasoningEffort(client, claudeSettingsEnv);
|
|
1084
|
-
return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(configuredReasoningEffort)
|
|
1176
|
+
return payload.output_config?.effort ?? normalizeClaudeOpus47Effort(payload.reasoning_effort) ?? normalizeClaudeOpus47Effort(configuredReasoningEffort);
|
|
1085
1177
|
};
|
|
1086
1178
|
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) {
|
|
@@ -1422,8 +1526,9 @@ function normalizeReasoningEffort(value) {
|
|
|
1422
1526
|
}
|
|
1423
1527
|
function translateReasoningEffort(payload, settings) {
|
|
1424
1528
|
const modelId = translateModelName(payload.model, settings);
|
|
1425
|
-
if (
|
|
1529
|
+
if (isClaudeOpus47Model(modelId)) return;
|
|
1426
1530
|
if (payload.reasoning_effort) return sanitizeReasoningEffortForModel(modelId, normalizeReasoningEffort(payload.reasoning_effort));
|
|
1531
|
+
if (isClaudeModel(modelId)) return;
|
|
1427
1532
|
if (payload.thinking?.type !== "enabled") return;
|
|
1428
1533
|
const budgetTokens = payload.thinking.budget_tokens;
|
|
1429
1534
|
if (budgetTokens === void 0) return sanitizeReasoningEffortForModel(modelId, "medium");
|
|
@@ -1433,8 +1538,10 @@ function translateReasoningEffort(payload, settings) {
|
|
|
1433
1538
|
return sanitizeReasoningEffortForModel(modelId, "xhigh");
|
|
1434
1539
|
}
|
|
1435
1540
|
function translateToOpenAI(payload, settings) {
|
|
1541
|
+
const model = translateModelName(payload.model, settings);
|
|
1542
|
+
const tools = translateAnthropicToolsToOpenAI(payload.tools);
|
|
1436
1543
|
return {
|
|
1437
|
-
model
|
|
1544
|
+
model,
|
|
1438
1545
|
messages: translateAnthropicMessagesToOpenAI(payload.messages, payload.system),
|
|
1439
1546
|
max_tokens: payload.max_tokens,
|
|
1440
1547
|
stop: payload.stop_sequences,
|
|
@@ -1445,8 +1552,8 @@ function translateToOpenAI(payload, settings) {
|
|
|
1445
1552
|
output_config: translateOutputConfig(payload, settings),
|
|
1446
1553
|
reasoning_effort: translateReasoningEffort(payload, settings),
|
|
1447
1554
|
user: payload.metadata?.user_id,
|
|
1448
|
-
tools
|
|
1449
|
-
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice)
|
|
1555
|
+
tools,
|
|
1556
|
+
tool_choice: tools && tools.length > 0 ? translateAnthropicToolChoiceToOpenAI(payload.tool_choice) : void 0
|
|
1450
1557
|
};
|
|
1451
1558
|
}
|
|
1452
1559
|
function translateAnthropicMessagesToOpenAI(anthropicMessages, system) {
|
|
@@ -1538,7 +1645,7 @@ function mapContent(content) {
|
|
|
1538
1645
|
return contentParts;
|
|
1539
1646
|
}
|
|
1540
1647
|
function translateAnthropicToolsToOpenAI(anthropicTools) {
|
|
1541
|
-
if (!anthropicTools) return;
|
|
1648
|
+
if (!anthropicTools || anthropicTools.length === 0) return;
|
|
1542
1649
|
return anthropicTools.map((tool) => ({
|
|
1543
1650
|
type: "function",
|
|
1544
1651
|
function: {
|
|
@@ -2335,6 +2442,11 @@ const codexResponsesRequestSchema = z.object({
|
|
|
2335
2442
|
model: z.string().min(1),
|
|
2336
2443
|
stream: z.boolean().optional()
|
|
2337
2444
|
}).passthrough();
|
|
2445
|
+
const removeReasoningEffort = (reasoning) => {
|
|
2446
|
+
const next = { ...reasoning };
|
|
2447
|
+
delete next.effort;
|
|
2448
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
2449
|
+
};
|
|
2338
2450
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2339
2451
|
const normalizeCodexResponsesRequest = (payload) => {
|
|
2340
2452
|
const parsed = codexResponsesRequestSchema.parse(payload);
|
|
@@ -2349,12 +2461,21 @@ const normalizeCodexResponsesRequest = (payload) => {
|
|
|
2349
2461
|
if ("reasoning" in next) delete next.reasoning;
|
|
2350
2462
|
return next;
|
|
2351
2463
|
}
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2464
|
+
if ("reasoning" in next) if (!isPlainObject(next.reasoning)) delete next.reasoning;
|
|
2465
|
+
else {
|
|
2466
|
+
const incoming = next.reasoning;
|
|
2467
|
+
if (incoming.effort === void 0 || incoming.effort === null) {
|
|
2468
|
+
const reasoning = removeReasoningEffort(incoming);
|
|
2469
|
+
if (reasoning) next.reasoning = reasoning;
|
|
2470
|
+
else delete next.reasoning;
|
|
2471
|
+
} else {
|
|
2472
|
+
const clamped = clampReasoningEffort(canonical, incoming.effort);
|
|
2473
|
+
if (clamped) next.reasoning = {
|
|
2474
|
+
...incoming,
|
|
2475
|
+
effort: clamped.effort
|
|
2476
|
+
};
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2358
2479
|
const text = isPlainObject(next.text) ? next.text : void 0;
|
|
2359
2480
|
if (text && capability.textVerbosity && "verbosity" in text) {
|
|
2360
2481
|
const { supported, default: defaultVerbosity } = capability.textVerbosity;
|
|
@@ -2370,6 +2491,28 @@ const normalizeCodexResponsesRequest = (payload) => {
|
|
|
2370
2491
|
//#endregion
|
|
2371
2492
|
//#region src/routes/responses.ts
|
|
2372
2493
|
const responsesRoutes = new Hono();
|
|
2494
|
+
const summarizeResponsesRequestForDiagnostics = (payload) => ({
|
|
2495
|
+
has_instructions: Boolean(payload.instructions),
|
|
2496
|
+
input_item_count: Array.isArray(payload.input) ? payload.input.length : void 0,
|
|
2497
|
+
input_kind: typeof payload.input === "string" ? "string" : Array.isArray(payload.input) ? "array" : payload.input === void 0 ? void 0 : typeof payload.input,
|
|
2498
|
+
max_output_tokens: payload.max_output_tokens,
|
|
2499
|
+
reasoning_effort: payload.reasoning?.effort,
|
|
2500
|
+
stream: payload.stream ?? void 0,
|
|
2501
|
+
tool_choice: payload.tool_choice,
|
|
2502
|
+
tools: summarizeToolsForDiagnostics(payload.tools)
|
|
2503
|
+
});
|
|
2504
|
+
const logResponsesUpstreamError = async (message, response, context) => {
|
|
2505
|
+
if (!runtimeState.debug) return;
|
|
2506
|
+
const errorBody = await response.clone().text().catch(() => "");
|
|
2507
|
+
consola.error(message, {
|
|
2508
|
+
route: context.route,
|
|
2509
|
+
model: context.model,
|
|
2510
|
+
status: response.status,
|
|
2511
|
+
statusText: response.statusText,
|
|
2512
|
+
body: errorBody || void 0,
|
|
2513
|
+
request: JSON.stringify(summarizeResponsesRequestForDiagnostics(context.request))
|
|
2514
|
+
});
|
|
2515
|
+
};
|
|
2373
2516
|
responsesRoutes.post("/", async (c) => {
|
|
2374
2517
|
try {
|
|
2375
2518
|
await checkRateLimit();
|
|
@@ -2393,6 +2536,11 @@ responsesRoutes.post("/", async (c) => {
|
|
|
2393
2536
|
body: JSON.stringify(chatPayload)
|
|
2394
2537
|
});
|
|
2395
2538
|
if (!upstream$1.ok) {
|
|
2539
|
+
await logResponsesUpstreamError("Failed to create chat completions", upstream$1, {
|
|
2540
|
+
model: payload.model,
|
|
2541
|
+
request: payload,
|
|
2542
|
+
route: "/chat/completions"
|
|
2543
|
+
});
|
|
2396
2544
|
const text = await upstream$1.text();
|
|
2397
2545
|
return new Response(text, {
|
|
2398
2546
|
status: upstream$1.status,
|
|
@@ -2426,6 +2574,11 @@ responsesRoutes.post("/", async (c) => {
|
|
|
2426
2574
|
body: JSON.stringify(payload)
|
|
2427
2575
|
});
|
|
2428
2576
|
const contentType = upstream.headers.get("content-type") ?? "";
|
|
2577
|
+
if (!upstream.ok) await logResponsesUpstreamError("Failed to create responses", upstream, {
|
|
2578
|
+
model: payload.model,
|
|
2579
|
+
request: payload,
|
|
2580
|
+
route: "/responses"
|
|
2581
|
+
});
|
|
2429
2582
|
if (payload.stream && upstream.body && contentType.includes("text/event-stream")) return new Response(normalizeResponsesSseStream(upstream.body), {
|
|
2430
2583
|
status: upstream.status,
|
|
2431
2584
|
headers: upstream.headers
|
|
@@ -2552,6 +2705,11 @@ const start = defineCommand({
|
|
|
2552
2705
|
default: false,
|
|
2553
2706
|
description: "Print GitHub and Copilot tokens during startup."
|
|
2554
2707
|
},
|
|
2708
|
+
debug: {
|
|
2709
|
+
type: "boolean",
|
|
2710
|
+
default: false,
|
|
2711
|
+
description: "Enable upstream request diagnostics in console logs."
|
|
2712
|
+
},
|
|
2555
2713
|
"codex-setup": {
|
|
2556
2714
|
type: "boolean",
|
|
2557
2715
|
default: true,
|
|
@@ -2596,6 +2754,8 @@ const start = defineCommand({
|
|
|
2596
2754
|
host: args.host ? String(args.host) : DEFAULT_HOST,
|
|
2597
2755
|
port: args.port ? Number(args.port) : envPort !== void 0 ? envPort : claudePort !== void 0 ? claudePort : DEFAULT_PORT
|
|
2598
2756
|
});
|
|
2757
|
+
runtimeState.debug = Boolean(args.debug);
|
|
2758
|
+
if (runtimeState.debug) consola.info("Debug mode enabled; upstream errors include request summaries");
|
|
2599
2759
|
const rateLimitRaw = args["rate-limit"];
|
|
2600
2760
|
if (rateLimitRaw !== void 0) {
|
|
2601
2761
|
const parsed = Number.parseInt(String(rateLimitRaw), 10);
|
|
@@ -2630,7 +2790,12 @@ const start = defineCommand({
|
|
|
2630
2790
|
const codexUserConfig = await readCodexUserConfigFromDisk(codexConfigPath);
|
|
2631
2791
|
let chosenModel = codexUserConfig.model;
|
|
2632
2792
|
let chosenEffort = codexUserConfig.modelReasoningEffort;
|
|
2633
|
-
if (chosenModel && !chosenEffort)
|
|
2793
|
+
if (chosenModel && !chosenEffort) {
|
|
2794
|
+
const capability = getModelCapability(chosenModel);
|
|
2795
|
+
if (capability && !capability.reasoning) consola.info(`codex model_reasoning_effort not set; ${chosenModel} does not accept reasoning effort`);
|
|
2796
|
+
else if (capability?.reasoning) consola.info(`codex model_reasoning_effort not set; leaving reasoning effort unset for ${chosenModel}`);
|
|
2797
|
+
else consola.info(`codex model_reasoning_effort not set; leaving reasoning effort unset for ${chosenModel}`);
|
|
2798
|
+
}
|
|
2634
2799
|
if (!isClaudeCodeMode && args["codex-setup"]) try {
|
|
2635
2800
|
const result = await applyCodexConfig({
|
|
2636
2801
|
baseUrl: `${baseUrl}/v1`,
|