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 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`, `--no-codex-setup`,
54
- `--select-model`, `--no-prompt`, `--rate-limit <seconds>`, `--wait`.
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 Codex
99
- uses that model's default reasoning effort (a startup info log also reminds you).
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. Claude-side
217
- reasoning can be set globally via `MODEL_REASONING_EFFORT` (env, or `env` in
218
- `~/.claude/settings.json`); invalid Claude-side values are ignored, and
219
- per-request `reasoning_effort` takes precedence.
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 fetchCopilot = async (provider, path$1, init, options = {}) => {
69
- if (!provider.token) throw new BridgeNotImplementedError("COPILOT_TOKEN is not configured for copilot-bridge.");
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 fetch(`${provider.baseUrl}${path$1}`, {
85
- ...init,
86
- headers
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
- const githubToken = await ensureGitHubToken(config, options);
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
- const refreshIn = await applyCopilotToken();
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) ?? defaultEffort;
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 tools = translateAnthropicToolsToOpenAI(payload.tools);
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: toolCall.function.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: toolCall.function.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 openAIPayload = translateToOpenAI(await c.req.json(), await getClaudeSettings());
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
- const incoming = isPlainObject(next.reasoning) ? next.reasoning : void 0;
2356
- const clamped = clampReasoningEffort(canonical, incoming?.effort);
2357
- if (clamped) next.reasoning = {
2358
- ...incoming ?? {},
2359
- effort: clamped.effort
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) consola.info(`codex model_reasoning_effort not set; using ${chosenModel}'s default reasoning effort`);
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`,