copilot-relay 0.0.3 → 0.0.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 D0n9X1n
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -52,12 +52,11 @@ thinkEffort: xhigh
52
52
 
53
53
  | Level | Logs |
54
54
  | --- | --- |
55
- | `silent` | Nothing |
56
55
  | `error` | Startup, preflight, and request failures |
57
- | `warn` | Errors plus recoverable warnings |
58
- | `info` | Warnings plus startup status, preflight status, HTTP requests, and model routing summaries |
59
- | `debug` | Info plus Copilot upstream timings, token refresh scheduling, and upstream error request summaries |
60
- | `trace` | Debug plus full Claude request payloads and full upstream Copilot request payloads without redaction |
56
+ | `info` | Errors plus startup status, preflight status, and local HTTP status codes |
57
+ | `debug` | Info plus model routing summaries, Copilot upstream timings, and request payloads |
58
+
59
+ Any other `logLevel` value is invalid and stops startup.
61
60
 
62
61
  Valid `thinkEffort`: `none`, `low`, `medium`, `high`, `xhigh`.
63
62
 
@@ -72,17 +71,27 @@ copilot-relay start
72
71
 
73
72
  ## Logging
74
73
 
75
- At `info`, every model request logs the requested model, upstream model, requested think effort, requested thinking, and effective think effort.
74
+ At `debug`, every model request logs the requested model, upstream model, requested think effort, requested thinking, and effective think effort.
76
75
 
77
- At `trace`, copilot-relay logs the full Claude request payload and full upstream request payload without redaction.
76
+ Upstream failures are logged at `error` with full request and response context in the same log file.
78
77
 
79
78
  Logs are written to `~/.copilot-relay/logs/copilot-relay.log`; old `.log` files are cleaned according to `logRetentionDays`.
80
79
 
80
+ Quick inspection:
81
+
82
+ ```sh
83
+ tail -f ~/.copilot-relay/logs/copilot-relay.log
84
+ grep -n "Failed to create\\|Startup preflight failed" ~/.copilot-relay/logs/copilot-relay.log
85
+ ```
86
+
87
+ See [`docs/troubleshooting.md`](docs/troubleshooting.md) for common debugging workflows.
88
+
81
89
  ## Development
82
90
 
83
91
  Developer notes live in [`docs/development.md`](docs/development.md).
84
92
  Architecture notes live in [`docs/architecture.md`](docs/architecture.md).
85
93
  Logging notes live in [`docs/logging.md`](docs/logging.md).
94
+ Troubleshooting notes live in [`docs/troubleshooting.md`](docs/troubleshooting.md).
86
95
 
87
96
  ```sh
88
97
  npm install
@@ -2,6 +2,8 @@
2
2
  #
3
3
  # This file is copied to ~/.copilot-relay/config.yaml on first start.
4
4
  # Edit the user config file, not this template, for local changes.
5
+ # Most values hot-reload while the service is running. host and port still
6
+ # require restart because the listening socket is already bound.
5
7
 
6
8
  # Local host for the Claude Code-compatible HTTP server.
7
9
  host: 127.0.0.1
@@ -18,15 +20,12 @@ copilotBaseUrl: https://api.githubcopilot.com
18
20
  claudeSetup: true
19
21
 
20
22
  # Log verbosity:
21
- # silent - no logs
22
- # error - startup/preflight/request failures only
23
- # warn - error logs plus recoverable warnings
24
- # info - warn logs plus startup status, preflight status, HTTP requests,
25
- # and model routing summaries
26
- # debug - info logs plus Copilot upstream timings, token refresh scheduling,
27
- # and upstream error request summaries
28
- # trace - debug logs plus full Claude request payloads and full upstream
29
- # Copilot request payloads without redaction
23
+ # error - startup/preflight/request failures only
24
+ # info - error logs plus startup status, preflight status, and local HTTP
25
+ # status codes
26
+ # debug - info logs plus model routing summaries, Copilot upstream timings,
27
+ # and request payloads
28
+ # Any other value is invalid and stops startup.
30
29
  logLevel: info
31
30
 
32
31
  # Number of days to keep files in ~/.copilot-relay/logs.
package/dist/main.js CHANGED
@@ -22,10 +22,12 @@ var ProxyNotImplementedError = class extends Error {
22
22
  }
23
23
  };
24
24
  var HTTPError = class extends Error {
25
+ detail;
25
26
  response;
26
- constructor(message, response) {
27
+ constructor(message, response, detail) {
27
28
  super(message);
28
29
  this.name = "HTTPError";
30
+ this.detail = detail;
29
31
  this.response = response;
30
32
  }
31
33
  };
@@ -100,14 +102,18 @@ const logCleanupCheckIntervalMs = 3600 * 1e3;
100
102
  let logRetentionDays = 3;
101
103
  let nextLogCleanupCheckAt = 0;
102
104
  const consolaLevelByName = {
103
- silent: -Infinity,
104
105
  error: 0,
105
- warn: 1,
106
106
  info: 3,
107
- debug: 4,
108
- trace: 5
107
+ debug: 4
109
108
  };
109
+ const fileLevelByMethod = {
110
+ error: consolaLevelByName.error,
111
+ info: consolaLevelByName.info,
112
+ debug: consolaLevelByName.debug
113
+ };
114
+ let currentLogLevel = consolaLevelByName.info;
110
115
  const setLogLevel = (level) => {
116
+ currentLogLevel = consolaLevelByName[level];
111
117
  consola.level = consolaLevelByName[level];
112
118
  };
113
119
  const formatLogValue = (value) => typeof value === "string" ? value : inspect(value, { depth: null });
@@ -128,15 +134,12 @@ const cleanupLogsIfDue = async () => {
128
134
  await cleanupLogs(logRetentionDays);
129
135
  };
130
136
  const wrapFileLog = (level, fn) => ((...args) => {
131
- writeLogFile(level, args).catch(() => void 0);
137
+ if ((fileLevelByMethod[level] ?? consolaLevelByName.info) <= currentLogLevel) writeLogFile(level, args).catch(() => void 0);
132
138
  return fn(...args);
133
139
  });
134
140
  consola.error = wrapFileLog("error", consola.error.bind(consola));
135
- consola.warn = wrapFileLog("warn", consola.warn.bind(consola));
136
141
  consola.info = wrapFileLog("info", consola.info.bind(consola));
137
- consola.success = wrapFileLog("success", consola.success.bind(consola));
138
142
  consola.debug = wrapFileLog("debug", consola.debug.bind(consola));
139
- consola.trace = wrapFileLog("trace", consola.trace.bind(consola));
140
143
  const cleanupLogs = async (retentionDays) => {
141
144
  logRetentionDays = retentionDays;
142
145
  await fs.mkdir(paths.logsDir, { recursive: true });
@@ -264,21 +267,21 @@ const isCopilotTokenError = (error) => error instanceof HTTPError && error.messa
264
267
  const ensureGitHubToken = async (options = {}) => {
265
268
  const existingToken = options.force ? "" : await readGitHubToken();
266
269
  if (existingToken) {
267
- log.debug(`Using cached GitHub token at ${paths.githubTokenPath}`);
270
+ log.info(`Using cached GitHub token at ${paths.githubTokenPath}`);
268
271
  return existingToken;
269
272
  }
270
273
  const deviceCode = await getDeviceCode();
271
274
  log.info(`Open ${deviceCode.verification_uri} and enter code ${deviceCode.user_code}`);
272
275
  const githubToken = await pollAccessToken(deviceCode);
273
276
  await writeGitHubToken(githubToken);
274
- log.success(`GitHub token synced to ${paths.githubTokenPath}`);
277
+ log.info(`GitHub token synced to ${paths.githubTokenPath}`);
275
278
  return githubToken;
276
279
  };
277
280
  const loadGitHubLogin = async (githubToken, vsCodeVersion) => {
278
281
  try {
279
282
  return (await getGitHubUser(githubToken, vsCodeVersion)).login;
280
283
  } catch (error) {
281
- log.warn("Could not load GitHub user:", error);
284
+ log.error("Could not load GitHub user:", error);
282
285
  return;
283
286
  }
284
287
  };
@@ -293,11 +296,11 @@ const setupProxyAuth = async (config, options = {}) => {
293
296
  const scheduleCopilotTokenRefresh = (refreshIn$1) => {
294
297
  if (copilotTokenRefreshTimer) clearTimeout(copilotTokenRefreshTimer);
295
298
  const refreshMs = Math.max(refreshIn$1 - 60, 60) * 1e3;
296
- log.debug(`Next Copilot token refresh in ${Math.round(refreshMs / 1e3)}s`);
299
+ log.info(`Next Copilot token refresh in ${Math.round(refreshMs / 1e3)}s`);
297
300
  copilotTokenRefreshTimer = setTimeout(async () => {
298
301
  try {
299
302
  const nextRefreshIn = await applyCopilotToken();
300
- log.debug("Refreshed Copilot token");
303
+ log.info("Refreshed Copilot token");
301
304
  scheduleCopilotTokenRefresh(nextRefreshIn);
302
305
  } catch (error) {
303
306
  log.error("Failed to refresh Copilot token:", error);
@@ -311,12 +314,12 @@ const setupProxyAuth = async (config, options = {}) => {
311
314
  if (storedToken && getStoredTokenRemainingSeconds(storedToken) > 60) {
312
315
  config.copilotToken = storedToken.token;
313
316
  refreshIn = getStoredTokenRemainingSeconds(storedToken);
314
- log.debug(`Using cached Copilot token at ${paths.copilotTokenPath}`);
317
+ log.info(`Using cached Copilot token at ${paths.copilotTokenPath}`);
315
318
  } else try {
316
319
  refreshIn = await applyCopilotToken();
317
320
  } catch (error) {
318
321
  if (options.force || !isCopilotTokenError(error)) throw error;
319
- log.warn("Cached GitHub auth could not get a Copilot token; running device auth again.");
322
+ log.error("Cached GitHub auth could not get a Copilot token; running device auth again.");
320
323
  githubToken = await ensureGitHubToken({
321
324
  ...options,
322
325
  force: true
@@ -355,12 +358,9 @@ const routeModelId = (model) => {
355
358
  //#endregion
356
359
  //#region src/lib/app-config.ts
357
360
  const logLevels = [
358
- "silent",
359
361
  "error",
360
- "warn",
361
362
  "info",
362
- "debug",
363
- "trace"
363
+ "debug"
364
364
  ];
365
365
  const defaultConfig = {
366
366
  claudeSetup: true,
@@ -374,7 +374,13 @@ const defaultConfig = {
374
374
  thinkEffort: defaultReasoningEffort
375
375
  };
376
376
  const isLogLevelName = (value) => typeof value === "string" && logLevels.includes(value.toLowerCase());
377
- const normalizeLogLevel = (value) => isLogLevelName(value) ? value.toLowerCase() : void 0;
377
+ const normalizeLogLevel = (value) => {
378
+ if (value === void 0) return;
379
+ if (typeof value !== "string") throw new Error(`Invalid logLevel: expected one of ${logLevels.join(", ")}`);
380
+ const normalized = value.toLowerCase();
381
+ if (!isLogLevelName(normalized)) throw new Error(`Invalid logLevel "${value}": expected one of ${logLevels.join(", ")}`);
382
+ return normalized;
383
+ };
378
384
  const normalizeBoolean = (value) => {
379
385
  if (typeof value === "boolean") return value;
380
386
  if (typeof value !== "string") return;
@@ -498,15 +504,11 @@ const serializeConfig = (config) => [
498
504
  `claudeSetup: ${config.claudeSetup}`,
499
505
  "",
500
506
  "# Log verbosity:",
501
- "# silent - no logs",
502
- "# error - startup/preflight/request failures only",
503
- "# warn - error logs plus recoverable warnings",
504
- "# info - warn logs plus startup status, preflight status, HTTP requests,",
505
- "# and model routing summaries",
506
- "# debug - info logs plus Copilot upstream timings, token refresh scheduling,",
507
- "# and upstream error request summaries",
508
- "# trace - debug logs plus full Claude request payloads and full upstream",
509
- "# Copilot request payloads without redaction",
507
+ "# error - startup/preflight/request failures only",
508
+ "# info - error logs plus startup status, preflight status, and local HTTP",
509
+ "# status codes",
510
+ "# debug - info logs plus model routing summaries, Copilot upstream timings,",
511
+ "# and request payloads",
510
512
  `logLevel: ${config.logLevel}`,
511
513
  "",
512
514
  "# Number of days to keep files in ~/.copilot-relay/logs.",
@@ -594,7 +596,7 @@ const auth = defineCommand({
594
596
  config.copilotToken = void 0;
595
597
  const authSession = await setupProxyAuth(config, { force: true });
596
598
  if (authSession.githubLogin) log.info(`Logged in as ${authSession.githubLogin}`);
597
- log.success("copilot-relay auth completed");
599
+ log.info("copilot-relay auth completed");
598
600
  }
599
601
  });
600
602
 
@@ -697,84 +699,15 @@ const fetchCopilot = async (provider, path$1, init, options = {}) => {
697
699
  const ms = Math.round(performance.now() - started);
698
700
  log.debug(`Copilot ${init.method ?? "GET"} ${path$1} -> ${response.status} ${ms}ms (attempt ${attempt})`);
699
701
  if (!shouldRetryResponse(response) || attempt === maxFetchAttempts) return response;
700
- log.warn(`Copilot ${path$1} returned ${response.status}; retrying (${attempt}/${maxFetchAttempts})`);
702
+ log.error(`Copilot ${path$1} returned ${response.status}; retrying (${attempt}/${maxFetchAttempts})`);
701
703
  } catch (error) {
702
704
  lastError = error;
703
705
  if (attempt === maxFetchAttempts) throw error;
704
- log.warn(`Copilot ${path$1} request failed; retrying (${attempt}/${maxFetchAttempts})`, error);
706
+ log.error(`Copilot ${path$1} request failed; retrying (${attempt}/${maxFetchAttempts})`, error);
705
707
  }
706
708
  throw lastError;
707
709
  };
708
710
 
709
- //#endregion
710
- //#region src/lib/upstream-diagnostics.ts
711
- const openaiToolNamePattern = /^[A-Za-z0-9_-]{1,64}$/;
712
- const toolSchemaSuspectKeys = new Set([
713
- "$defs",
714
- "allOf",
715
- "definitions",
716
- "dependentRequired",
717
- "dependentSchemas",
718
- "else",
719
- "if",
720
- "not",
721
- "oneOf",
722
- "patternProperties",
723
- "then"
724
- ]);
725
- const maxDiagnosticItems = 8;
726
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
727
- const getToolName = (tool) => {
728
- if (!isRecord(tool)) return "";
729
- const functionValue = tool.function;
730
- if (isRecord(functionValue) && typeof functionValue.name === "string") return functionValue.name;
731
- return typeof tool.name === "string" ? tool.name : "";
732
- };
733
- const getToolParameters = (tool) => {
734
- if (!isRecord(tool)) return;
735
- const functionValue = tool.function;
736
- if (isRecord(functionValue) && "parameters" in functionValue) return functionValue.parameters;
737
- return tool.parameters;
738
- };
739
- const collectSuspiciousSchemaKeys = (value, path$1 = "$", keys = []) => {
740
- if (keys.length >= maxDiagnosticItems) return keys;
741
- if (Array.isArray(value)) {
742
- for (const [index, item] of value.entries()) {
743
- collectSuspiciousSchemaKeys(item, `${path$1}[${index}]`, keys);
744
- if (keys.length >= maxDiagnosticItems) break;
745
- }
746
- return keys;
747
- }
748
- if (!isRecord(value)) return keys;
749
- for (const [key, nestedValue] of Object.entries(value)) {
750
- const nextPath = `${path$1}.${key}`;
751
- if (toolSchemaSuspectKeys.has(key)) {
752
- if (!keys.includes(nextPath)) keys.push(nextPath);
753
- if (keys.length >= maxDiagnosticItems) break;
754
- }
755
- collectSuspiciousSchemaKeys(nestedValue, nextPath, keys);
756
- if (keys.length >= maxDiagnosticItems) break;
757
- }
758
- return keys;
759
- };
760
- const summarizeToolsForDiagnostics = (tools) => {
761
- if (!Array.isArray(tools) || tools.length === 0) return;
762
- const invalidNames = tools.map((tool) => getToolName(tool)).filter((name) => !openaiToolNamePattern.test(name)).slice(0, maxDiagnosticItems);
763
- const suspiciousSchemas = tools.flatMap((tool) => {
764
- const name = getToolName(tool) || "<missing>";
765
- const keys = collectSuspiciousSchemaKeys(getToolParameters(tool));
766
- return keys.length > 0 ? [{
767
- keys,
768
- name
769
- }] : [];
770
- }).slice(0, maxDiagnosticItems);
771
- return {
772
- count: tools.length,
773
- ...invalidNames.length > 0 ? { invalidNames } : {},
774
- ...suspiciousSchemas.length > 0 ? { suspiciousSchemas } : {}
775
- };
776
- };
777
-
778
711
  //#endregion
779
712
  //#region src/copilot/responses.ts
780
713
  const responsesOnlyModelPattern = /^(?:gpt-5\.5)(?:-|$)/i;
@@ -1145,16 +1078,6 @@ const sanitizeUserIdentifier = (user) => {
1145
1078
  if (!user) return;
1146
1079
  return user.slice(0, maxUserLength);
1147
1080
  };
1148
- const summarizeRequestForDiagnostics = (payload) => ({
1149
- max_completion_tokens: payload.max_completion_tokens,
1150
- max_tokens: payload.max_tokens,
1151
- message_count: payload.messages.length,
1152
- output_config_effort: payload.output_config?.effort,
1153
- reasoning_effort: payload.reasoning_effort,
1154
- stream: payload.stream ?? void 0,
1155
- tool_choice: payload.tool_choice,
1156
- tools: summarizeToolsForDiagnostics(payload.tools)
1157
- });
1158
1081
  const buildRequestPayload = (payload) => {
1159
1082
  const requestedReasoningEffort = getRequestedReasoningEffort(payload);
1160
1083
  if (!usesMaxCompletionTokens(payload.model) || payload.max_tokens === null || payload.max_tokens === void 0) return {
@@ -1186,7 +1109,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
1186
1109
  const enableVision = messagesIncludeImage(upstreamPayload.messages);
1187
1110
  const initiator = isAgentInitiator(upstreamPayload.messages);
1188
1111
  const requestPayload = buildRequestPayload(upstreamPayload);
1189
- log.info([
1112
+ log.debug([
1190
1113
  "Model request",
1191
1114
  `client=${client}`,
1192
1115
  `requested_model=${requestedModel}`,
@@ -1195,7 +1118,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
1195
1118
  `requested_thinking=${requestedThinking}`,
1196
1119
  `effective_think_effort=${requestPayload.reasoning_effort ?? "none"}`
1197
1120
  ].join(" "));
1198
- log.trace("Full request payload", { payload: requestPayload });
1121
+ log.debug("Full request payload", { payload: requestPayload });
1199
1122
  if (shouldUseResponsesApiForModel(upstreamPayload.model)) return createResponses(provider, upstreamPayload, {
1200
1123
  vision: enableVision,
1201
1124
  initiator
@@ -1216,49 +1139,62 @@ const createChatCompletions = async (config, payload, options = {}) => {
1216
1139
  vision: enableVision,
1217
1140
  initiator
1218
1141
  });
1219
- await logUpstreamError("Failed to create chat completions", response, {
1142
+ throw new HTTPError("Failed to create chat completions", response, await logUpstreamError("Failed to create chat completions", response, {
1220
1143
  model: payload.model,
1221
1144
  request: requestPayload,
1222
1145
  route: "/chat/completions"
1223
- });
1224
- throw new HTTPError("Failed to create chat completions", response);
1146
+ }));
1225
1147
  }
1226
1148
  if (upstreamPayload.stream) return events(response);
1227
1149
  return await response.json();
1228
1150
  };
1229
1151
  async function createResponses(provider, payload, options) {
1230
- const reasoningEffort = getRequestedReasoningEffort(payload);
1152
+ const requestPayload = buildResponsesRequestPayload(payload, getRequestedReasoningEffort(payload));
1231
1153
  const response = await fetchCopilot(provider, "/responses", {
1232
1154
  method: "POST",
1233
1155
  headers: {
1234
1156
  accept: payload.stream ? "text/event-stream" : "application/json",
1235
1157
  "content-type": "application/json"
1236
1158
  },
1237
- body: JSON.stringify(buildResponsesRequestPayload(payload, reasoningEffort))
1159
+ body: JSON.stringify(requestPayload)
1238
1160
  }, {
1239
1161
  vision: options.vision,
1240
1162
  initiator: options.initiator
1241
1163
  });
1242
- if (!response.ok) {
1243
- await logUpstreamError("Failed to create responses", response, {
1244
- model: payload.model,
1245
- route: "/responses"
1246
- });
1247
- throw new HTTPError("Failed to create responses", response);
1248
- }
1164
+ if (!response.ok) throw new HTTPError("Failed to create responses", response, await logUpstreamError("Failed to create responses", response, {
1165
+ model: payload.model,
1166
+ request: requestPayload,
1167
+ route: "/responses"
1168
+ }));
1249
1169
  if (payload.stream) return translateResponsesStreamToChatCompletionStream(events(response));
1250
1170
  return translateResponsesToChatCompletion(await response.json());
1251
1171
  }
1252
1172
  async function logUpstreamError(message, response, context) {
1253
1173
  const errorBody = await response.clone().text().catch(() => "");
1254
- log.error(message, {
1174
+ const detail = getUpstreamErrorDetail(response, errorBody);
1175
+ log.error(`${message}: route=${context.route} model=${context.model} status=${response.status}`, {
1176
+ message,
1255
1177
  route: context.route,
1256
1178
  model: context.model,
1257
- status: response.status,
1258
- statusText: response.statusText,
1259
- body: errorBody || void 0,
1260
- request: runtimeState.debug && context.request ? JSON.stringify(summarizeRequestForDiagnostics(context.request)) : void 0
1179
+ request: context.request,
1180
+ response: {
1181
+ status: response.status,
1182
+ statusText: response.statusText || void 0,
1183
+ url: response.url || void 0,
1184
+ headers: Object.fromEntries(response.headers.entries()),
1185
+ body: errorBody || void 0
1186
+ }
1261
1187
  });
1188
+ return detail;
1189
+ }
1190
+ function getUpstreamErrorDetail(response, body) {
1191
+ if (!body) return response.statusText || void 0;
1192
+ try {
1193
+ const payload = JSON.parse(body);
1194
+ return payload.error?.message ?? payload.error?.code ?? body.slice(0, 240);
1195
+ } catch {
1196
+ return body.slice(0, 240);
1197
+ }
1262
1198
  }
1263
1199
  async function shouldRetryWithResponses(response) {
1264
1200
  try {
@@ -1419,7 +1355,7 @@ function translateToOpenAI(payload, _settings, toolNameMapper) {
1419
1355
  const tools = translateClaudeToolsToOpenAI(payload.tools, mapper);
1420
1356
  return {
1421
1357
  model,
1422
- messages: translateClaudeMessagesToOpenAI(payload.messages, payload.system, mapper),
1358
+ messages: normalizeFinalAssistantPrefill(translateClaudeMessagesToOpenAI(payload.messages, payload.system, mapper)),
1423
1359
  max_tokens: payload.max_tokens,
1424
1360
  stop: payload.stop_sequences,
1425
1361
  stream: payload.stream,
@@ -1492,6 +1428,31 @@ function handleAssistantMessage(message, toolNameMapper) {
1492
1428
  content: mapContent(message.content)
1493
1429
  }];
1494
1430
  }
1431
+ const continuePrefillPrompt = "Continue the assistant response from the previous assistant message.";
1432
+ const normalizeFinalAssistantPrefill = (messages) => {
1433
+ let lastAssistantIndex = -1;
1434
+ for (let index = messages.length - 1; index >= 0; index--) if (messages[index]?.role === "assistant") {
1435
+ lastAssistantIndex = index;
1436
+ break;
1437
+ }
1438
+ if (lastAssistantIndex < 0) return messages;
1439
+ if (lastAssistantIndex !== messages.length - 1) return messages;
1440
+ const lastAssistant = messages[lastAssistantIndex];
1441
+ if (typeof lastAssistant.content !== "string") return messages.slice(0, -1);
1442
+ const trimmedContent = lastAssistant.content.trimEnd();
1443
+ const nextMessages = [...messages];
1444
+ if (trimmedContent) {
1445
+ nextMessages[lastAssistantIndex] = {
1446
+ ...lastAssistant,
1447
+ content: trimmedContent
1448
+ };
1449
+ nextMessages.push({
1450
+ role: "user",
1451
+ content: continuePrefillPrompt
1452
+ });
1453
+ } else nextMessages.pop();
1454
+ return nextMessages;
1455
+ };
1495
1456
  function mapContent(content) {
1496
1457
  if (typeof content === "string") return content;
1497
1458
  if (!Array.isArray(content)) return null;
@@ -2003,7 +1964,7 @@ claudeRoutes.get("/models", (c) => c.json({
2003
1964
  claudeRoutes.post("/messages", async (c) => {
2004
1965
  const config = c.get("config");
2005
1966
  const claudePayload = await c.req.json();
2006
- log.trace("Full Claude request payload", { payload: claudePayload });
1967
+ log.debug("Full Claude request payload", { payload: claudePayload });
2007
1968
  try {
2008
1969
  const upstreamModel = translateModelName(claudePayload.model);
2009
1970
  const toolNameMapper = createClaudeToolNameMapper(claudePayload.tools, { ...getToolNameMapperOptionsForModel(upstreamModel) });
@@ -2056,12 +2017,16 @@ claudeRoutes.post("/messages", async (c) => {
2056
2017
  }
2057
2018
  });
2058
2019
  } catch (error) {
2059
- if (error instanceof ProxyNotImplementedError) return c.json({ error: {
2060
- type: error.name,
2061
- message: error.message
2062
- } }, 501);
2020
+ if (error instanceof ProxyNotImplementedError) {
2021
+ c.set("requestErrorMessage", error.message);
2022
+ return c.json({ error: {
2023
+ type: error.name,
2024
+ message: error.message
2025
+ } }, 501);
2026
+ }
2063
2027
  if (error instanceof HTTPError) {
2064
2028
  const text = await error.response.text().catch(() => "");
2029
+ c.set("requestErrorMessage", error.detail ?? text.slice(0, 240));
2065
2030
  return new Response(text, {
2066
2031
  status: error.response.status,
2067
2032
  headers: { "content-type": error.response.headers.get("content-type") ?? "application/json" }
@@ -2095,6 +2060,10 @@ claudeRoutes.post("/messages/count_tokens", async (c) => {
2095
2060
 
2096
2061
  //#endregion
2097
2062
  //#region src/server.ts
2063
+ const formatStatusLog = (method, path$1, status, ms, errorMessage) => {
2064
+ const base = `${method} ${path$1} -> ${status} ${ms}ms`;
2065
+ return status >= 400 && errorMessage ? `${base} error=${JSON.stringify(errorMessage)}` : base;
2066
+ };
2098
2067
  const createServer = (config) => {
2099
2068
  const app = new Hono();
2100
2069
  app.use("*", cors());
@@ -2105,11 +2074,11 @@ const createServer = (config) => {
2105
2074
  await next();
2106
2075
  } finally {
2107
2076
  const ms = Math.round(performance.now() - started);
2108
- log.info(`${c.req.method} ${c.req.path} -> ${c.res.status} ${ms}ms`);
2077
+ log.info(formatStatusLog(c.req.method, c.req.path, c.res.status, ms, c.get("requestErrorMessage")));
2109
2078
  }
2110
2079
  });
2111
2080
  app.onError((error, c) => {
2112
- log.error(`${c.req.method} ${c.req.path} failed`, error);
2081
+ c.set("requestErrorMessage", error.message);
2113
2082
  return c.json({ error: { message: "Internal server error" } }, 500);
2114
2083
  });
2115
2084
  app.get("/", (c) => c.json({
@@ -2142,7 +2111,7 @@ const start = defineCommand({
2142
2111
  const applyRuntimeConfig = (nextConfig) => {
2143
2112
  setLogLevel(nextConfig.logLevel);
2144
2113
  cleanupLogs(nextConfig.logRetentionDays);
2145
- runtimeState.debug = nextConfig.logLevel === "debug" || nextConfig.logLevel === "trace";
2114
+ runtimeState.debug = nextConfig.logLevel === "debug";
2146
2115
  runtimeState.thinkEffort = nextConfig.thinkEffort;
2147
2116
  config.copilotBaseUrl = nextConfig.copilotBaseUrl;
2148
2117
  config.host = nextConfig.host;
@@ -2153,7 +2122,6 @@ const start = defineCommand({
2153
2122
  };
2154
2123
  };
2155
2124
  applyRuntimeConfig(appConfig);
2156
- if (runtimeState.debug) log.info("Debug diagnostics enabled; upstream errors include request summaries");
2157
2125
  log.info(`Log level: ${appConfig.logLevel}`);
2158
2126
  log.info(`Think effort: ${appConfig.thinkEffort}`);
2159
2127
  log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
@@ -2167,9 +2135,9 @@ const start = defineCommand({
2167
2135
  const server = startServer(config);
2168
2136
  log.info(`copilot-relay version: ${appVersion}`);
2169
2137
  if (authSession.githubLogin) log.info(`GitHub user: ${authSession.githubLogin}`);
2170
- else log.warn("GitHub user: unavailable");
2171
- log.success(`copilot-relay listening on http://${config.host}:${config.port}`);
2172
- log.debug(`copilot base url: ${config.copilotBaseUrl}`);
2138
+ else log.error("GitHub user: unavailable");
2139
+ log.info(`copilot-relay listening on http://${config.host}:${config.port}`);
2140
+ log.info(`copilot base url: ${config.copilotBaseUrl}`);
2173
2141
  const baseUrl = `http://${config.host}:${config.port}`;
2174
2142
  if (appConfig.claudeSetup) try {
2175
2143
  const claudeResult = await applyClaudeConfig({
@@ -2177,11 +2145,11 @@ const start = defineCommand({
2177
2145
  configPath: claudeConfigPath$1
2178
2146
  });
2179
2147
  if (claudeResult.changed) {
2180
- log.success(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);
2148
+ log.info(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);
2181
2149
  if (claudeResult.previousBaseUrl && claudeResult.previousBaseUrl !== baseUrl) log.info(`ANTHROPIC_BASE_URL: ${claudeResult.previousBaseUrl} → ${baseUrl}`);
2182
2150
  } else log.info(`claude settings already up to date: ${claudeResult.configPath}`);
2183
2151
  } catch (error) {
2184
- log.warn(`Could not update claude settings (${claudeConfigPath$1}):`, error);
2152
+ log.error(`Could not update claude settings (${claudeConfigPath$1}):`, error);
2185
2153
  }
2186
2154
  watchAppConfig((nextConfig) => {
2187
2155
  applyRuntimeConfig(nextConfig);