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 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://github.com/openai/codex), [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) and Continue can talk to Copilot with minimal configuration.
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`, `--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
 
@@ -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 default reasoning effort (preferred key). |
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
- If an unsupported value is sent for a reasoning-capable model, the bridge
221
- falls back to the model's default (`medium` for the GPT-5 / Claude Opus 4.7
222
- families) instead of forwarding an invalid request upstream. Set the default
223
- globally via `MODEL_REASONING_EFFORT` (or `COPILOT_REASONING_EFFORT` alias)
224
- (env, or `env` in
225
- `~/.claude/settings.json`); per-request `reasoning_effort` (or Anthropic
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 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,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\.4-mini)(?:-|$)/i;
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 ?? process.env.COPILOT_REASONING_EFFORT ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "MODEL_REASONING_EFFORT") ?? getEnvValueCaseInsensitive(claudeSettingsEnv, "COPILOT_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) ?? defaultEffort;
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) ?? (client === "claude" ? "medium" : void 0);
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 (isClaudeModel(modelId)) return;
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: translateModelName(payload.model, settings),
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: translateAnthropicToolsToOpenAI(payload.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
- const incoming = isPlainObject(next.reasoning) ? next.reasoning : void 0;
2353
- const clamped = clampReasoningEffort(canonical, incoming?.effort);
2354
- if (clamped) next.reasoning = {
2355
- ...incoming ?? {},
2356
- effort: clamped.effort
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) consola.info(`codex model_reasoning_effort not set; using ${chosenModel}'s default reasoning effort`);
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`,