copilot-relay 0.0.7 → 0.0.9

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/dist/main.js CHANGED
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain } from "citty";
3
3
  import fs from "node:fs/promises";
4
- import { inspect } from "node:util";
4
+ import { inspect, promisify } from "node:util";
5
5
  import consola from "consola";
6
6
  import os from "node:os";
7
7
  import path, { dirname, resolve } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
+ import { execFile } from "node:child_process";
9
10
  import { createHash, randomUUID } from "node:crypto";
10
11
  import { events } from "fetch-event-stream";
11
12
  import { readFileSync } from "node:fs";
12
- import { serve } from "@hono/node-server";
13
+ import { createAdaptorServer } from "@hono/node-server";
13
14
  import { Hono } from "hono";
14
15
  import { cors } from "hono/cors";
15
16
  import { streamSSE } from "hono/streaming";
@@ -56,6 +57,7 @@ const copilotTokenPath = path.join(appDir, "copilot_token.json");
56
57
  const githubTokenPath = path.join(appDir, "github_token");
57
58
  const logsDir = path.join(appDir, "logs");
58
59
  const logPath = path.join(logsDir, "copilot-relay.log");
60
+ const pidPath = path.join(appDir, "copilot-relay.pid");
59
61
  const paths = {
60
62
  appDir,
61
63
  configPath,
@@ -68,7 +70,8 @@ const paths = {
68
70
  ],
69
71
  legacyCopilotTokenPaths,
70
72
  logPath,
71
- logsDir
73
+ logsDir,
74
+ pidPath
72
75
  };
73
76
  async function ensurePaths() {
74
77
  await fs.mkdir(paths.appDir, { recursive: true });
@@ -167,7 +170,7 @@ const standardHeaders = () => ({
167
170
  accept: "application/json",
168
171
  "content-type": "application/json"
169
172
  });
170
- const sleep = (ms) => new Promise((resolve$1) => {
173
+ const sleep$1 = (ms) => new Promise((resolve$1) => {
171
174
  setTimeout(resolve$1, ms);
172
175
  });
173
176
  const readGitHubToken = async () => {
@@ -245,12 +248,12 @@ const pollAccessToken = async (deviceCode) => {
245
248
  })
246
249
  });
247
250
  if (!response.ok) {
248
- await sleep(sleepDuration);
251
+ await sleep$1(sleepDuration);
249
252
  continue;
250
253
  }
251
254
  const json = await response.json();
252
255
  if (json.access_token) return json.access_token;
253
- await sleep(sleepDuration);
256
+ await sleep$1(sleepDuration);
254
257
  }
255
258
  };
256
259
  const getGitHubUser = async (githubToken, vsCodeVersion) => {
@@ -372,6 +375,7 @@ const defaultConfig = {
372
375
  opusModel: "claude-opus-4.8",
373
376
  port: 4142,
374
377
  thinkEffort: defaultReasoningEffort,
378
+ upstreamTimeoutSeconds: 180,
375
379
  webSearchBackend: void 0
376
380
  };
377
381
  const isLogLevelName = (value) => typeof value === "string" && logLevels.includes(value.toLowerCase());
@@ -403,6 +407,12 @@ const normalizeThinkEffort = (value) => {
403
407
  const normalized = value.toLowerCase() === "minimal" ? "low" : value.toLowerCase();
404
408
  return isReasoningEffort(normalized) ? normalized : void 0;
405
409
  };
410
+ const normalizeUpstreamTimeoutSeconds = (value) => {
411
+ if (value === void 0) return;
412
+ const timeout = normalizePositiveInteger(value);
413
+ if (timeout === void 0) throw new Error("Invalid upstreamTimeoutSeconds: expected a positive integer");
414
+ return timeout;
415
+ };
406
416
  const readRawConfig = async () => {
407
417
  try {
408
418
  return parseConfigYaml(await fs.readFile(paths.configPath, "utf8"));
@@ -476,6 +486,10 @@ const parseConfigYaml = (content) => {
476
486
  case "think_effort":
477
487
  config.thinkEffort = unquoteYamlScalar(value);
478
488
  break;
489
+ case "upstreamTimeoutSeconds":
490
+ case "upstream_timeout_seconds":
491
+ config.upstreamTimeoutSeconds = unquoteYamlScalar(value);
492
+ break;
479
493
  case "webSearchBackend":
480
494
  case "web_search_backend":
481
495
  config.webSearchBackend = unquoteYamlScalar(value);
@@ -522,6 +536,9 @@ const serializeConfig = (config) => [
522
536
  "# Default upstream thinking/reasoning effort: none, low, medium, high, xhigh.",
523
537
  `thinkEffort: ${config.thinkEffort}`,
524
538
  "",
539
+ "# Max seconds to wait for a single Claude request's upstream Copilot calls.",
540
+ `upstreamTimeoutSeconds: ${config.upstreamTimeoutSeconds}`,
541
+ "",
525
542
  "# Copilot model used for bridge-managed Claude WebSearch. Empty uses gptModel.",
526
543
  `webSearchBackend: ${config.webSearchBackend ?? ""}`,
527
544
  "",
@@ -542,6 +559,7 @@ async function readAppConfig() {
542
559
  const logRetentionDays$1 = normalizePositiveInteger(raw.logRetentionDays);
543
560
  const port = normalizePort(raw.port);
544
561
  const thinkEffort = normalizeThinkEffort(raw.thinkEffort);
562
+ const upstreamTimeoutSeconds = normalizeUpstreamTimeoutSeconds(raw.upstreamTimeoutSeconds);
545
563
  const config = {
546
564
  claudeSetup: claudeSetup ?? defaultConfig.claudeSetup,
547
565
  copilotBaseUrl: normalizeString(raw.copilotBaseUrl) ?? defaultConfig.copilotBaseUrl,
@@ -552,6 +570,7 @@ async function readAppConfig() {
552
570
  opusModel: normalizeString(raw.opusModel) ?? defaultConfig.opusModel,
553
571
  port: port ?? defaultConfig.port,
554
572
  thinkEffort: thinkEffort ?? defaultConfig.thinkEffort,
573
+ upstreamTimeoutSeconds: upstreamTimeoutSeconds ?? defaultConfig.upstreamTimeoutSeconds,
555
574
  webSearchBackend: normalizeString(raw.webSearchBackend)
556
575
  };
557
576
  await writeConfig(config);
@@ -590,6 +609,7 @@ const readProxyConfig = (config) => ({
590
609
  copilotToken: void 0,
591
610
  host: config.host,
592
611
  port: config.port,
612
+ upstreamTimeoutMs: config.upstreamTimeoutSeconds * 1e3,
593
613
  vsCodeVersion: vscodeVersion,
594
614
  webSearchBackend: config.webSearchBackend
595
615
  });
@@ -610,6 +630,179 @@ const auth = defineCommand({
610
630
  }
611
631
  });
612
632
 
633
+ //#endregion
634
+ //#region src/lib/lifecycle.ts
635
+ const execFileAsync = promisify(execFile);
636
+ const stopTimeoutMs = 5e3;
637
+ const stopPollMs = 100;
638
+ const isNodeErrno = (error) => typeof error === "object" && error !== null && "code" in error;
639
+ const sleep = (ms) => new Promise((resolve$1) => {
640
+ setTimeout(resolve$1, ms);
641
+ });
642
+ const parsePid = (value) => {
643
+ const pid = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value.trim(), 10) : NaN;
644
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
645
+ };
646
+ const readCommandOutput = async (file, args) => {
647
+ try {
648
+ const { stdout } = await execFileAsync(file, args, {
649
+ maxBuffer: 1024 * 1024,
650
+ windowsHide: true
651
+ });
652
+ return String(stdout);
653
+ } catch (error) {
654
+ if (typeof error === "object" && error !== null && "stdout" in error && (typeof error.stdout === "string" || Buffer.isBuffer(error.stdout))) return String(error.stdout);
655
+ return "";
656
+ }
657
+ };
658
+ const parsePidLines = (output) => output.split(/\r?\n/).map((line) => parsePid(line)).filter((pid) => pid !== void 0);
659
+ const isRelayStartProcess = (command, cwd) => {
660
+ const normalizedCommand = command.replaceAll("\\", "/");
661
+ const normalizedCwd = cwd?.replaceAll("\\", "/");
662
+ if (!/(?:^|\s)start(?:\s|$)/.test(normalizedCommand)) return false;
663
+ if (normalizedCommand.includes("copilot-relay")) return true;
664
+ const usesNode = /(?:^|\s|\/)node(?:\.exe)?(?:\s|$)/i.test(normalizedCommand);
665
+ const usesDistMain = /(?:^|\s|\/)dist\/main\.js(?:\s|$)/.test(normalizedCommand);
666
+ return Boolean(usesNode && usesDistMain && normalizedCwd && normalizedCwd.split("/").some((part) => part.includes("copilot-relay")));
667
+ };
668
+ const isProcessAlive = (pid) => {
669
+ try {
670
+ process.kill(pid, 0);
671
+ return true;
672
+ } catch (error) {
673
+ return isNodeErrno(error) && error.code === "EPERM";
674
+ }
675
+ };
676
+ const readRelayPidFile = async () => {
677
+ try {
678
+ const trimmed = (await fs.readFile(paths.pidPath, "utf8")).trim();
679
+ if (!trimmed) return;
680
+ if (trimmed.startsWith("{")) return parsePid(JSON.parse(trimmed).pid);
681
+ return parsePid(trimmed);
682
+ } catch (error) {
683
+ if (isNodeErrno(error) && error.code === "ENOENT") return;
684
+ throw error;
685
+ }
686
+ };
687
+ const writeRelayPidFile = async (config) => {
688
+ await fs.mkdir(paths.appDir, { recursive: true });
689
+ const payload = {
690
+ host: config.host,
691
+ pid: process.pid,
692
+ port: config.port,
693
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
694
+ };
695
+ await fs.writeFile(paths.pidPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 384 });
696
+ };
697
+ const clearRelayPidFile = async (pid = process.pid, options = {}) => {
698
+ const currentPid = await readRelayPidFile();
699
+ if (!options.force && currentPid !== void 0 && currentPid !== pid) return;
700
+ await fs.unlink(paths.pidPath).catch((error) => {
701
+ if (!isNodeErrno(error) || error.code !== "ENOENT") throw error;
702
+ });
703
+ };
704
+ const getProcessCommand = async (pid) => {
705
+ if (process.platform === "win32") return (await readCommandOutput("powershell.exe", [
706
+ "-NoProfile",
707
+ "-Command",
708
+ `Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" | Select-Object -ExpandProperty CommandLine`
709
+ ])).trim() || void 0;
710
+ return (await readCommandOutput("ps", [
711
+ "-p",
712
+ String(pid),
713
+ "-o",
714
+ "command="
715
+ ])).trim() || void 0;
716
+ };
717
+ const getProcessCwd = async (pid) => {
718
+ if (process.platform === "win32") return;
719
+ return (await readCommandOutput("lsof", [
720
+ "-a",
721
+ "-p",
722
+ String(pid),
723
+ "-d",
724
+ "cwd",
725
+ "-Fn"
726
+ ])).split(/\r?\n/).find((line) => line.startsWith("n"))?.slice(1);
727
+ };
728
+ const getPortListenerPids = async (port) => {
729
+ if (process.platform === "win32") return parsePidLines(await readCommandOutput("powershell.exe", [
730
+ "-NoProfile",
731
+ "-Command",
732
+ `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique`
733
+ ]));
734
+ return parsePidLines(await readCommandOutput("lsof", [
735
+ "-nP",
736
+ `-iTCP:${port}`,
737
+ "-sTCP:LISTEN",
738
+ "-t"
739
+ ]));
740
+ };
741
+ const getProcessList = async () => {
742
+ return (process.platform === "win32" ? await readCommandOutput("powershell.exe", [
743
+ "-NoProfile",
744
+ "-Command",
745
+ "Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId)`t$($_.CommandLine)\" }"
746
+ ]) : await readCommandOutput("ps", ["-axo", "pid=,command="])).split(/\r?\n/).flatMap((line) => {
747
+ const match = process.platform === "win32" ? line.match(/^(\d+)\t(.+)$/) : line.match(/^\s*(\d+)\s+(.+)$/);
748
+ const pid = parsePid(match?.[1]);
749
+ const command = match?.[2]?.trim();
750
+ return pid && command ? [{
751
+ command,
752
+ pid
753
+ }] : [];
754
+ });
755
+ };
756
+ const isRelayPid = async (pid) => {
757
+ if (pid === process.pid || !isProcessAlive(pid)) return false;
758
+ const command = await getProcessCommand(pid);
759
+ if (!command) return false;
760
+ return isRelayStartProcess(command, await getProcessCwd(pid));
761
+ };
762
+ const findRelayProcessIds = async (config) => {
763
+ const candidates = /* @__PURE__ */ new Set();
764
+ const storedPid = await readRelayPidFile();
765
+ if (storedPid) candidates.add(storedPid);
766
+ for (const pid of await getPortListenerPids(config.port)) candidates.add(pid);
767
+ for (const { command, pid } of await getProcessList()) {
768
+ if (pid === process.pid) continue;
769
+ if (command.includes("copilot-relay") || command.includes("dist/main.js")) {
770
+ if (isRelayStartProcess(command, await getProcessCwd(pid))) candidates.add(pid);
771
+ }
772
+ }
773
+ const relayPids = [];
774
+ for (const pid of candidates) if (await isRelayPid(pid)) relayPids.push(pid);
775
+ return [...new Set(relayPids)].sort((left, right) => left - right);
776
+ };
777
+ const waitForExit = async (pid, timeoutMs) => {
778
+ const deadline = Date.now() + timeoutMs;
779
+ while (Date.now() < deadline) {
780
+ if (!isProcessAlive(pid)) return true;
781
+ await sleep(stopPollMs);
782
+ }
783
+ return !isProcessAlive(pid);
784
+ };
785
+ const stopProcess = async (pid) => {
786
+ if (!isProcessAlive(pid)) return;
787
+ log.info(`Stopping existing copilot-relay process pid=${pid}`);
788
+ process.kill(pid, "SIGTERM");
789
+ if (await waitForExit(pid, stopTimeoutMs)) return;
790
+ log.error(`copilot-relay process pid=${pid} did not stop; forcing termination`);
791
+ process.kill(pid, "SIGKILL");
792
+ if (!await waitForExit(pid, stopTimeoutMs)) throw new Error(`Could not stop copilot-relay process pid=${pid}`);
793
+ };
794
+ const stopExistingRelay = async (config) => {
795
+ const relayPids = await findRelayProcessIds(config);
796
+ if (relayPids.length === 0) {
797
+ log.info("No existing copilot-relay instance found");
798
+ await clearRelayPidFile(0, { force: true });
799
+ return [];
800
+ }
801
+ for (const pid of relayPids) await stopProcess(pid);
802
+ await clearRelayPidFile(0, { force: true });
803
+ return relayPids;
804
+ };
805
+
613
806
  //#endregion
614
807
  //#region src/lib/claude-settings.ts
615
808
  /**
@@ -674,13 +867,51 @@ const editorPluginVersion = `copilot-chat/${copilotVersion}`;
674
867
  const userAgent = `GitHubCopilotChat/${copilotVersion}`;
675
868
  const apiVersion = "2025-04-01";
676
869
  const maxFetchAttempts = 2;
870
+ const copilotRequestTimeoutMs = 18e4;
677
871
  const getCopilotProviderContext = (config) => ({
678
872
  baseUrl: config.copilotBaseUrl,
679
873
  token: config.copilotToken,
680
874
  vsCodeVersion: config.vsCodeVersion
681
875
  });
682
876
  const shouldRetryResponse = (response) => response.status >= 500 && response.status <= 599;
683
- const buildHeaders = (provider, init, options) => {
877
+ const createCopilotRequestSignal = (signal, timeoutMs = copilotRequestTimeoutMs) => {
878
+ const signals = [signal, timeoutMs > 0 ? AbortSignal.timeout(timeoutMs) : void 0].filter((value) => value !== void 0);
879
+ if (signals.length === 0) return;
880
+ return signals.length === 1 ? signals[0] : AbortSignal.any(signals);
881
+ };
882
+ const getAbortName = (value) => typeof value === "object" && value !== null && "name" in value && typeof value.name === "string" ? value.name : void 0;
883
+ const isAbortLikeError = (error) => {
884
+ const name = getAbortName(error);
885
+ return name === "AbortError" || name === "TimeoutError";
886
+ };
887
+ const toCopilotAbortHTTPError = (error, signal, timeoutMs = copilotRequestTimeoutMs) => {
888
+ if (!signal?.aborted && !isAbortLikeError(error)) return;
889
+ const timedOut = getAbortName(signal?.reason) === "TimeoutError" || getAbortName(error) === "TimeoutError";
890
+ const message = timedOut ? `Copilot upstream request timed out after ${Math.round(timeoutMs / 1e3)}s.` : "Client request cancelled before Copilot upstream completed.";
891
+ const code = timedOut ? "upstream_timeout" : "request_cancelled";
892
+ return new HTTPError(message, new Response(JSON.stringify({ error: {
893
+ message,
894
+ code
895
+ } }), {
896
+ status: timedOut ? 504 : 499,
897
+ headers: { "content-type": "application/json" }
898
+ }), message);
899
+ };
900
+ const readCopilotJson = async (response, signal, timeoutMs = copilotRequestTimeoutMs) => {
901
+ try {
902
+ return await response.json();
903
+ } catch (error) {
904
+ throw toCopilotAbortHTTPError(error, signal, timeoutMs) ?? error;
905
+ }
906
+ };
907
+ const readCopilotText = async (response, signal, timeoutMs = copilotRequestTimeoutMs) => {
908
+ try {
909
+ return await response.text();
910
+ } catch (error) {
911
+ throw toCopilotAbortHTTPError(error, signal, timeoutMs) ?? error;
912
+ }
913
+ };
914
+ const buildHeaders = (provider, init, options, upstreamRequestId) => {
684
915
  const headers = new Headers(init.headers);
685
916
  headers.set("authorization", `Bearer ${provider.token}`);
686
917
  headers.set("copilot-integration-id", "vscode-chat");
@@ -689,7 +920,7 @@ const buildHeaders = (provider, init, options) => {
689
920
  headers.set("user-agent", userAgent);
690
921
  headers.set("openai-intent", "conversation-panel");
691
922
  headers.set("x-github-api-version", apiVersion);
692
- headers.set("x-request-id", randomUUID());
923
+ headers.set("x-request-id", upstreamRequestId);
693
924
  headers.set("x-vscode-user-agent-library-version", "electron-fetch");
694
925
  if (options.vision) headers.set("copilot-vision-request", "true");
695
926
  if (options.initiator) headers.set("x-initiator", options.initiator);
@@ -697,23 +928,48 @@ const buildHeaders = (provider, init, options) => {
697
928
  if (!headers.has("accept")) headers.set("accept", "application/json");
698
929
  return headers;
699
930
  };
931
+ const formatRequestId = (requestId) => requestId ? `request_id=${requestId} ` : "";
932
+ const logUpstreamLifecycle = (requestId, message) => {
933
+ if (requestId) {
934
+ log.info(`${formatRequestId(requestId)}${message}`);
935
+ return;
936
+ }
937
+ log.debug(message);
938
+ };
700
939
  const fetchCopilot = async (provider, path$1, init, options = {}) => {
701
940
  if (!provider.token) throw new ProxyNotImplementedError("Copilot token is not configured for copilot-relay.");
941
+ const timeoutMs = options.timeoutMs ?? copilotRequestTimeoutMs;
942
+ const signal = createCopilotRequestSignal(options.signal, timeoutMs);
702
943
  let lastError;
703
- for (let attempt = 1; attempt <= maxFetchAttempts; attempt++) try {
704
- const started = performance.now();
705
- const response = await fetch(`${provider.baseUrl}${path$1}`, {
706
- ...init,
707
- headers: buildHeaders(provider, init, options)
708
- });
709
- const ms = Math.round(performance.now() - started);
710
- log.debug(`Copilot ${init.method ?? "GET"} ${path$1} -> ${response.status} ${ms}ms (attempt ${attempt})`);
711
- if (!shouldRetryResponse(response) || attempt === maxFetchAttempts) return response;
712
- log.error(`Copilot ${path$1} returned ${response.status}; retrying (${attempt}/${maxFetchAttempts})`);
713
- } catch (error) {
714
- lastError = error;
715
- if (attempt === maxFetchAttempts) throw error;
716
- log.error(`Copilot ${path$1} request failed; retrying (${attempt}/${maxFetchAttempts})`, error);
944
+ for (let attempt = 1; attempt <= maxFetchAttempts; attempt++) {
945
+ const upstreamRequestId = randomUUID();
946
+ try {
947
+ const started = performance.now();
948
+ logUpstreamLifecycle(options.requestId, `send upstream method=${init.method ?? "GET"} path=${path$1} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
949
+ const response = await fetch(`${provider.baseUrl}${path$1}`, {
950
+ ...init,
951
+ headers: buildHeaders(provider, init, options, upstreamRequestId),
952
+ signal
953
+ });
954
+ const ms = Math.round(performance.now() - started);
955
+ logUpstreamLifecycle(options.requestId, `return from upstream method=${init.method ?? "GET"} path=${path$1} status=${response.status} ms=${ms} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
956
+ log.debug(`${formatRequestId(options.requestId)}Copilot ${init.method ?? "GET"} ${path$1} -> ${response.status} ${ms}ms (attempt ${attempt}) upstream_request_id=${upstreamRequestId}`);
957
+ if (!shouldRetryResponse(response) || attempt === maxFetchAttempts) return response;
958
+ log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} returned ${response.status}; retrying (${attempt}/${maxFetchAttempts}) upstream_request_id=${upstreamRequestId}`);
959
+ } catch (error) {
960
+ const abortError = toCopilotAbortHTTPError(error, signal, timeoutMs);
961
+ if (abortError) {
962
+ logUpstreamLifecycle(options.requestId, `upstream failed method=${init.method ?? "GET"} path=${path$1} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
963
+ throw abortError;
964
+ }
965
+ logUpstreamLifecycle(options.requestId, `upstream failed method=${init.method ?? "GET"} path=${path$1} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
966
+ lastError = error;
967
+ if (attempt === maxFetchAttempts) {
968
+ log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} request failed after ${attempt} attempts upstream_request_id=${upstreamRequestId}`, error);
969
+ throw error;
970
+ }
971
+ log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} request failed; retrying (${attempt}/${maxFetchAttempts}) upstream_request_id=${upstreamRequestId}`, error);
972
+ }
717
973
  }
718
974
  throw lastError;
719
975
  };
@@ -1086,6 +1342,7 @@ function getFinishReason(response, hasFunctionCalls) {
1086
1342
  //#endregion
1087
1343
  //#region src/copilot/chat.ts
1088
1344
  const usesMaxCompletionTokens = (modelId) => modelId.startsWith("gpt-5");
1345
+ const continuePrefillPrompt$1 = "Continue the assistant response from the previous assistant message.";
1089
1346
  const maxUserLength = 64;
1090
1347
  const sanitizeReasoningEffortForModel = (_modelId, reasoningEffort) => {
1091
1348
  return reasoningEffort ?? void 0;
@@ -1112,6 +1369,38 @@ const buildRequestPayload = (payload) => {
1112
1369
  user: sanitizeUserIdentifier(payload.user)
1113
1370
  };
1114
1371
  };
1372
+ const normalizeFinalAssistantPrefill$1 = (payload) => {
1373
+ const lastMessage = payload.messages.at(-1);
1374
+ if (lastMessage?.role !== "assistant") return payload;
1375
+ const messages = [...payload.messages];
1376
+ if (typeof lastMessage.content !== "string") {
1377
+ messages.pop();
1378
+ return {
1379
+ ...payload,
1380
+ messages
1381
+ };
1382
+ }
1383
+ const content = lastMessage.content.trimEnd();
1384
+ if (!content) {
1385
+ messages.pop();
1386
+ return {
1387
+ ...payload,
1388
+ messages
1389
+ };
1390
+ }
1391
+ messages[messages.length - 1] = {
1392
+ ...lastMessage,
1393
+ content
1394
+ };
1395
+ messages.push({
1396
+ role: "user",
1397
+ content: continuePrefillPrompt$1
1398
+ });
1399
+ return {
1400
+ ...payload,
1401
+ messages
1402
+ };
1403
+ };
1115
1404
  const isAgentInitiator = (messages) => messages.some((msg) => msg.role === "assistant" || msg.role === "tool") ? "agent" : "user";
1116
1405
  const messagesIncludeImage = (messages) => messages.some((msg) => typeof msg.content !== "string" && msg.content?.some((part) => part.type === "image_url"));
1117
1406
  const createChatCompletions = async (config, payload, options = {}) => {
@@ -1119,53 +1408,67 @@ const createChatCompletions = async (config, payload, options = {}) => {
1119
1408
  const requestedModel = options.requestedModel ?? payload.model;
1120
1409
  const requestedThinkEffort = options.requestedThinkEffort ?? payload.reasoning_effort ?? "none";
1121
1410
  const requestedThinking = options.requestedThinking ?? "none";
1411
+ const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
1122
1412
  const upstreamModelId = routeModelId(payload.model);
1123
1413
  const upstreamPayload = upstreamModelId === payload.model ? payload : {
1124
1414
  ...payload,
1125
1415
  model: upstreamModelId
1126
1416
  };
1417
+ const useResponsesApi = shouldUseResponsesApiForModel(upstreamPayload.model);
1418
+ const compatiblePayload = useResponsesApi ? upstreamPayload : normalizeFinalAssistantPrefill$1(upstreamPayload);
1127
1419
  const provider = getCopilotProviderContext(config);
1128
- const enableVision = messagesIncludeImage(upstreamPayload.messages);
1129
- const initiator = isAgentInitiator(upstreamPayload.messages);
1130
- const requestPayload = buildRequestPayload(upstreamPayload);
1420
+ const enableVision = messagesIncludeImage(compatiblePayload.messages);
1421
+ const initiator = isAgentInitiator(compatiblePayload.messages);
1422
+ const requestPayload = buildRequestPayload(compatiblePayload);
1131
1423
  log.debug([
1132
1424
  "Model request",
1133
1425
  `client=${client}`,
1134
1426
  `requested_model=${requestedModel}`,
1135
- `upstream_model=${upstreamPayload.model}`,
1427
+ ...options.requestId ? [`request_id=${options.requestId}`] : [],
1428
+ `upstream_model=${compatiblePayload.model}`,
1136
1429
  `requested_think_effort=${requestedThinkEffort}`,
1137
1430
  `requested_thinking=${requestedThinking}`,
1138
1431
  `effective_think_effort=${requestPayload.reasoning_effort ?? "none"}`
1139
1432
  ].join(" "));
1140
1433
  log.debug("Full request payload", { payload: requestPayload });
1141
- if (shouldUseResponsesApiForModel(upstreamPayload.model)) return createResponses(provider, upstreamPayload, {
1434
+ if (useResponsesApi) return createResponses(provider, compatiblePayload, {
1142
1435
  vision: enableVision,
1143
- initiator
1436
+ initiator,
1437
+ requestId: options.requestId,
1438
+ signal,
1439
+ timeoutMs: options.timeoutMs
1144
1440
  });
1145
1441
  const response = await fetchCopilot(provider, "/chat/completions", {
1146
1442
  method: "POST",
1147
1443
  headers: {
1148
- accept: upstreamPayload.stream ? "text/event-stream" : "application/json",
1444
+ accept: compatiblePayload.stream ? "text/event-stream" : "application/json",
1149
1445
  "content-type": "application/json"
1150
1446
  },
1151
1447
  body: JSON.stringify(requestPayload)
1152
1448
  }, {
1153
1449
  vision: enableVision,
1154
- initiator
1450
+ initiator,
1451
+ requestId: options.requestId,
1452
+ signal,
1453
+ timeoutMs: options.timeoutMs
1155
1454
  });
1156
1455
  if (!response.ok) {
1157
- if (await shouldRetryWithResponses(response)) return createResponses(provider, upstreamPayload, {
1456
+ if (await shouldRetryWithResponses(response, signal, options.timeoutMs)) return createResponses(provider, compatiblePayload, {
1158
1457
  vision: enableVision,
1159
- initiator
1458
+ initiator,
1459
+ requestId: options.requestId,
1460
+ signal,
1461
+ timeoutMs: options.timeoutMs
1160
1462
  });
1161
1463
  throw new HTTPError("Failed to create chat completions", response, await logUpstreamError("Failed to create chat completions", response, {
1162
1464
  model: payload.model,
1163
1465
  request: requestPayload,
1164
- route: "/chat/completions"
1165
- }));
1466
+ route: "/chat/completions",
1467
+ requestId: options.requestId
1468
+ }, signal, options.timeoutMs));
1166
1469
  }
1167
- if (upstreamPayload.stream) return events(response);
1168
- return await response.json();
1470
+ if (compatiblePayload.stream) return events(response);
1471
+ return readCopilotJson(response, signal, options.timeoutMs);
1169
1472
  };
1170
1473
  async function createResponses(provider, payload, options) {
1171
1474
  const requestPayload = buildResponsesRequestPayload(payload, getRequestedReasoningEffort(payload));
@@ -1178,23 +1481,28 @@ async function createResponses(provider, payload, options) {
1178
1481
  body: JSON.stringify(requestPayload)
1179
1482
  }, {
1180
1483
  vision: options.vision,
1181
- initiator: options.initiator
1484
+ initiator: options.initiator,
1485
+ requestId: options.requestId,
1486
+ signal: options.signal,
1487
+ timeoutMs: options.timeoutMs
1182
1488
  });
1183
1489
  if (!response.ok) throw new HTTPError("Failed to create responses", response, await logUpstreamError("Failed to create responses", response, {
1184
1490
  model: payload.model,
1185
1491
  request: requestPayload,
1186
- route: "/responses"
1187
- }));
1492
+ route: "/responses",
1493
+ requestId: options.requestId
1494
+ }, options.signal, options.timeoutMs));
1188
1495
  if (payload.stream) return translateResponsesStreamToChatCompletionStream(events(response));
1189
- return translateResponsesToChatCompletion(await response.json());
1496
+ return translateResponsesToChatCompletion(await readCopilotJson(response, options.signal, options.timeoutMs));
1190
1497
  }
1191
- async function logUpstreamError(message, response, context) {
1192
- const errorBody = await response.clone().text().catch(() => "");
1498
+ async function logUpstreamError(message, response, context, signal, timeoutMs) {
1499
+ const errorBody = await readCopilotText(response.clone(), signal, timeoutMs).catch(() => "");
1193
1500
  const detail = getUpstreamErrorDetail(response, errorBody);
1194
- log.error(`${message}: route=${context.route} model=${context.model} status=${response.status}`, {
1501
+ log.error(`${message}: route=${context.route} model=${context.model} status=${response.status}${context.requestId ? ` request_id=${context.requestId}` : ""}`, {
1195
1502
  message,
1196
1503
  route: context.route,
1197
1504
  model: context.model,
1505
+ requestId: context.requestId,
1198
1506
  request: context.request,
1199
1507
  response: {
1200
1508
  status: response.status,
@@ -1215,9 +1523,9 @@ function getUpstreamErrorDetail(response, body) {
1215
1523
  return body.slice(0, 240);
1216
1524
  }
1217
1525
  }
1218
- async function shouldRetryWithResponses(response) {
1526
+ async function shouldRetryWithResponses(response, signal, timeoutMs) {
1219
1527
  try {
1220
- return (await response.clone().json()).error?.code === "unsupported_api_for_model";
1528
+ return (await readCopilotJson(response.clone(), signal, timeoutMs)).error?.code === "unsupported_api_for_model";
1221
1529
  } catch {
1222
1530
  return false;
1223
1531
  }
@@ -1226,12 +1534,17 @@ async function shouldRetryWithResponses(response) {
1226
1534
  //#endregion
1227
1535
  //#region src/lib/preflight.ts
1228
1536
  const getUpstreamModelIds = async (config) => {
1229
- const response = await fetchCopilot(getCopilotProviderContext(config), "/models", {
1537
+ const provider = getCopilotProviderContext(config);
1538
+ const signal = createCopilotRequestSignal(void 0, config.upstreamTimeoutMs);
1539
+ const response = await fetchCopilot(provider, "/models", {
1230
1540
  method: "GET",
1231
1541
  headers: { accept: "application/json" }
1542
+ }, {
1543
+ signal,
1544
+ timeoutMs: config.upstreamTimeoutMs
1232
1545
  });
1233
1546
  if (!response.ok) throw new HTTPError("Failed to validate upstream models", response);
1234
- const payload = await response.json();
1547
+ const payload = await readCopilotJson(response, signal, config.upstreamTimeoutMs);
1235
1548
  return new Set((payload.data ?? []).map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0));
1236
1549
  };
1237
1550
  const ensureRequiredModels = async (config) => {
@@ -1254,7 +1567,8 @@ const validateModelRequest = async (config, model, thinkEffort) => {
1254
1567
  try {
1255
1568
  const response = await createChatCompletions(config, createProbePayload(model), {
1256
1569
  client: "generic",
1257
- requestedModel: model
1570
+ requestedModel: model,
1571
+ timeoutMs: config.upstreamTimeoutMs
1258
1572
  });
1259
1573
  if (typeof response !== "object" || response === null || !("choices" in response)) throw new Error(`Preflight request for ${model} unexpectedly streamed`);
1260
1574
  log.info(`Preflight OK: model=${model} think_effort=${thinkEffort}`);
@@ -1871,8 +2185,9 @@ const createFailedSearchExecution = (payload, requestedQuery, model, message) =>
1871
2185
  results: [],
1872
2186
  text: message
1873
2187
  });
1874
- const createClaudeWebSearchExecution = async (config, payload, requestedQuery) => {
2188
+ const createClaudeWebSearchExecution = async (config, payload, requestedQuery, options = {}) => {
1875
2189
  const backendModel = getWebSearchBackendModel(config);
2190
+ const signal = createCopilotRequestSignal(options.signal, options.timeoutMs);
1876
2191
  const response = await fetchCopilot(getCopilotProviderContext(config), "/responses", {
1877
2192
  method: "POST",
1878
2193
  headers: {
@@ -1880,12 +2195,17 @@ const createClaudeWebSearchExecution = async (config, payload, requestedQuery) =
1880
2195
  "content-type": "application/json"
1881
2196
  },
1882
2197
  body: JSON.stringify(buildWebSearchRequestPayload(payload, requestedQuery, backendModel))
1883
- }, { initiator: "agent" });
2198
+ }, {
2199
+ initiator: "agent",
2200
+ requestId: options.requestId,
2201
+ signal,
2202
+ timeoutMs: options.timeoutMs
2203
+ });
1884
2204
  if (!response.ok) {
1885
- const detail = await response.text().catch(() => "");
2205
+ const detail = await readCopilotText(response, signal, options.timeoutMs).catch(() => "");
1886
2206
  return createFailedSearchExecution(payload, requestedQuery, backendModel, [`Copilot web search is not available for model ${backendModel}.`, detail ? `Upstream response: ${detail}` : ""].filter(Boolean).join("\n"));
1887
2207
  }
1888
- const upstream = await response.json();
2208
+ const upstream = await readCopilotJson(response, signal, options.timeoutMs);
1889
2209
  const text = getResponseText(upstream);
1890
2210
  const query = getSearchQuery(upstream, requestedQuery);
1891
2211
  const results = parseSearchResults(text);
@@ -2232,7 +2552,9 @@ claudeRoutes.get("/models", (c) => c.json({
2232
2552
  }));
2233
2553
  claudeRoutes.post("/messages", async (c) => {
2234
2554
  const config = c.get("config");
2555
+ const requestId = c.get("requestId");
2235
2556
  const claudePayload = await c.req.json();
2557
+ const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
2236
2558
  log.debug("Full Claude request payload", { payload: claudePayload });
2237
2559
  try {
2238
2560
  const upstreamModel = translateModelName(claudePayload.model);
@@ -2245,13 +2567,20 @@ claudeRoutes.post("/messages", async (c) => {
2245
2567
  client: "claude",
2246
2568
  requestedModel: claudePayload.model,
2247
2569
  requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
2248
- requestedThinking: getClaudeRequestedThinking(claudePayload)
2570
+ requestedThinking: getClaudeRequestedThinking(claudePayload),
2571
+ requestId,
2572
+ signal: requestSignal,
2573
+ timeoutMs: config.upstreamTimeoutMs
2249
2574
  });
2250
2575
  if (isNonStreamingResponse(response)) {
2251
2576
  const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(response, toolNameMapper) : void 0;
2252
2577
  let claudeResponse;
2253
2578
  if (webSearchToolCall) {
2254
- const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query);
2579
+ const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
2580
+ requestId,
2581
+ signal: requestSignal,
2582
+ timeoutMs: config.upstreamTimeoutMs
2583
+ });
2255
2584
  const searchResponse = createClaudeWebSearchResponse(search);
2256
2585
  if (search.results.length === 0) claudeResponse = searchResponse;
2257
2586
  else {
@@ -2259,7 +2588,10 @@ claudeRoutes.post("/messages", async (c) => {
2259
2588
  client: "claude",
2260
2589
  requestedModel: claudePayload.model,
2261
2590
  requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
2262
- requestedThinking: getClaudeRequestedThinking(claudePayload)
2591
+ requestedThinking: getClaudeRequestedThinking(claudePayload),
2592
+ requestId,
2593
+ signal: requestSignal,
2594
+ timeoutMs: config.upstreamTimeoutMs
2263
2595
  });
2264
2596
  if (!isNonStreamingResponse(finalResponse)) throw new HTTPError("Claude web search final answer request unexpectedly streamed", new Response("Claude web search final answer request unexpectedly streamed", {
2265
2597
  status: 502,
@@ -2375,25 +2707,35 @@ const getLoggedHeaders = (request) => Object.fromEntries(loggedRequestHeaders.fl
2375
2707
  const value = request.headers.get(name);
2376
2708
  return value ? [[name, value]] : [];
2377
2709
  }));
2378
- const formatStatusLog = (method, path$1, status, ms, errorMessage) => {
2379
- const base = `${method} ${path$1} -> ${status} ${ms}ms`;
2710
+ const formatStatusLog = (requestId, method, path$1, status, ms, errorMessage) => {
2711
+ const base = `request_id=${requestId} ${method} ${path$1} -> ${status} ${ms}ms`;
2380
2712
  return status >= 400 && errorMessage ? `${base} error=${JSON.stringify(errorMessage)}` : base;
2381
2713
  };
2382
2714
  const createServer = (config) => {
2383
2715
  const app = new Hono();
2384
- app.use("*", cors());
2385
2716
  app.use("*", async (c, next) => {
2717
+ const requestId = randomUUID();
2386
2718
  c.set("config", config);
2719
+ c.set("requestId", requestId);
2720
+ c.header("x-copilot-relay-request-id", requestId);
2721
+ log.info(`request_id=${requestId} request received method=${c.req.method} path=${c.req.path}`);
2387
2722
  const started = performance.now();
2388
2723
  try {
2389
2724
  await next();
2390
2725
  } finally {
2391
2726
  const ms = Math.round(performance.now() - started);
2392
- log.info(formatStatusLog(c.req.method, c.req.path, c.res.status, ms, c.get("requestErrorMessage")));
2727
+ log.info(formatStatusLog(requestId, c.req.method, c.req.path, c.res.status, ms, c.get("requestErrorMessage")));
2393
2728
  }
2394
2729
  });
2730
+ app.use("*", cors());
2395
2731
  app.onError((error, c) => {
2396
2732
  c.set("requestErrorMessage", error.message);
2733
+ log.error("Unhandled Claude API request error", {
2734
+ method: c.req.method,
2735
+ path: c.req.path,
2736
+ requestId: c.get("requestId"),
2737
+ error
2738
+ });
2397
2739
  return c.json({ error: { message: "Internal server error" } }, 500);
2398
2740
  });
2399
2741
  app.get("/", (c) => c.json({
@@ -2408,6 +2750,7 @@ const createServer = (config) => {
2408
2750
  log.error("Unsupported Claude API request", {
2409
2751
  method: c.req.method,
2410
2752
  path: c.req.path,
2753
+ requestId: c.get("requestId"),
2411
2754
  headers: getLoggedHeaders(c.req.raw),
2412
2755
  payload: await readRequestPayloadForLog(c.req.raw)
2413
2756
  });
@@ -2415,76 +2758,131 @@ const createServer = (config) => {
2415
2758
  });
2416
2759
  return app;
2417
2760
  };
2418
- const startServer = (config) => serve({
2419
- fetch: createServer(config).fetch,
2420
- hostname: config.host,
2421
- port: config.port
2761
+ const startServer = (config) => new Promise((resolve$1, reject) => {
2762
+ const server = createAdaptorServer({
2763
+ fetch: createServer(config).fetch,
2764
+ hostname: config.host,
2765
+ port: config.port
2766
+ });
2767
+ const onError = (error) => {
2768
+ server.off("listening", onListening);
2769
+ reject(error);
2770
+ };
2771
+ const onListening = () => {
2772
+ server.off("error", onError);
2773
+ resolve$1(server);
2774
+ };
2775
+ server.once("error", onError);
2776
+ server.once("listening", onListening);
2777
+ server.listen(config.port, config.host);
2422
2778
  });
2423
2779
 
2424
2780
  //#endregion
2425
2781
  //#region src/start.ts
2782
+ async function startRelay(appConfig) {
2783
+ appConfig ??= await readAppConfig();
2784
+ setLogLevel(appConfig.logLevel);
2785
+ await cleanupLogs(appConfig.logRetentionDays);
2786
+ const claudeConfigPath$1 = claudeConfigPath;
2787
+ const config = readProxyConfig(appConfig);
2788
+ const applyRuntimeConfig = (nextConfig) => {
2789
+ setLogLevel(nextConfig.logLevel);
2790
+ cleanupLogs(nextConfig.logRetentionDays);
2791
+ runtimeState.debug = nextConfig.logLevel === "debug";
2792
+ runtimeState.thinkEffort = nextConfig.thinkEffort;
2793
+ config.copilotBaseUrl = nextConfig.copilotBaseUrl;
2794
+ config.host = nextConfig.host;
2795
+ config.port = nextConfig.port;
2796
+ config.upstreamTimeoutMs = nextConfig.upstreamTimeoutSeconds * 1e3;
2797
+ config.webSearchBackend = nextConfig.webSearchBackend;
2798
+ runtimeState.modelRouting = {
2799
+ gptModel: nextConfig.gptModel,
2800
+ opusModel: nextConfig.opusModel
2801
+ };
2802
+ };
2803
+ applyRuntimeConfig(appConfig);
2804
+ log.info(`Log level: ${appConfig.logLevel}`);
2805
+ log.info(`Think effort: ${appConfig.thinkEffort}`);
2806
+ log.info(`Upstream timeout: ${appConfig.upstreamTimeoutSeconds}s`);
2807
+ log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
2808
+ const authSession = await setupProxyAuth(config);
2809
+ try {
2810
+ await validateUpstream(config, appConfig.thinkEffort);
2811
+ } catch (error) {
2812
+ log.error("Startup preflight failed:", error);
2813
+ process.exit(1);
2814
+ }
2815
+ const server = await startServer(config);
2816
+ await writeRelayPidFile(config);
2817
+ log.info(`copilot-relay version: ${appVersion}`);
2818
+ if (authSession.githubLogin) log.info(`GitHub user: ${authSession.githubLogin}`);
2819
+ else log.error("GitHub user: unavailable");
2820
+ log.info(`copilot-relay listening on http://${config.host}:${config.port}`);
2821
+ log.info(`copilot base url: ${config.copilotBaseUrl}`);
2822
+ const baseUrl = `http://${config.host}:${config.port}`;
2823
+ if (appConfig.claudeSetup) try {
2824
+ const claudeResult = await applyClaudeConfig({
2825
+ baseUrl,
2826
+ configPath: claudeConfigPath$1
2827
+ });
2828
+ if (claudeResult.changed) {
2829
+ log.info(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);
2830
+ if (claudeResult.previousBaseUrl && claudeResult.previousBaseUrl !== baseUrl) log.info(`ANTHROPIC_BASE_URL: ${claudeResult.previousBaseUrl} → ${baseUrl}`);
2831
+ } else log.info(`claude settings already up to date: ${claudeResult.configPath}`);
2832
+ } catch (error) {
2833
+ log.error(`Could not update claude settings (${claudeConfigPath$1}):`, error);
2834
+ }
2835
+ watchAppConfig((nextConfig) => {
2836
+ applyRuntimeConfig(nextConfig);
2837
+ log.info(`Config reloaded: logLevel=${nextConfig.logLevel} thinkEffort=${nextConfig.thinkEffort} upstreamTimeoutSeconds=${nextConfig.upstreamTimeoutSeconds}`);
2838
+ });
2839
+ const shutdown = (signal) => {
2840
+ log.info(`Received ${signal}; shutting down copilot-relay`);
2841
+ server.close((error) => {
2842
+ if (error) {
2843
+ log.error("Error while shutting down copilot-relay:", error);
2844
+ process.exitCode = 1;
2845
+ }
2846
+ });
2847
+ };
2848
+ process.once("SIGINT", shutdown);
2849
+ process.once("SIGTERM", shutdown);
2850
+ try {
2851
+ await new Promise((resolve$1, reject) => {
2852
+ server.on("close", resolve$1);
2853
+ server.on("error", reject);
2854
+ });
2855
+ } finally {
2856
+ process.off("SIGINT", shutdown);
2857
+ process.off("SIGTERM", shutdown);
2858
+ await clearRelayPidFile();
2859
+ }
2860
+ }
2426
2861
  const start = defineCommand({
2427
2862
  meta: {
2428
2863
  name: "start",
2429
2864
  description: "Start the copilot-relay HTTP server."
2430
2865
  },
2866
+ async run() {
2867
+ await startRelay();
2868
+ }
2869
+ });
2870
+
2871
+ //#endregion
2872
+ //#region src/restart.ts
2873
+ const restart = defineCommand({
2874
+ meta: {
2875
+ name: "restart",
2876
+ description: "Stop any existing copilot-relay instance and start a new one."
2877
+ },
2431
2878
  async run() {
2432
2879
  const appConfig = await readAppConfig();
2433
2880
  setLogLevel(appConfig.logLevel);
2434
2881
  await cleanupLogs(appConfig.logRetentionDays);
2435
- const claudeConfigPath$1 = claudeConfigPath;
2436
- const config = readProxyConfig(appConfig);
2437
- const applyRuntimeConfig = (nextConfig) => {
2438
- setLogLevel(nextConfig.logLevel);
2439
- cleanupLogs(nextConfig.logRetentionDays);
2440
- runtimeState.debug = nextConfig.logLevel === "debug";
2441
- runtimeState.thinkEffort = nextConfig.thinkEffort;
2442
- config.copilotBaseUrl = nextConfig.copilotBaseUrl;
2443
- config.host = nextConfig.host;
2444
- config.port = nextConfig.port;
2445
- runtimeState.modelRouting = {
2446
- gptModel: nextConfig.gptModel,
2447
- opusModel: nextConfig.opusModel
2448
- };
2449
- };
2450
- applyRuntimeConfig(appConfig);
2451
- log.info(`Log level: ${appConfig.logLevel}`);
2452
- log.info(`Think effort: ${appConfig.thinkEffort}`);
2453
- log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
2454
- const authSession = await setupProxyAuth(config);
2455
- try {
2456
- await validateUpstream(config, appConfig.thinkEffort);
2457
- } catch (error) {
2458
- log.error("Startup preflight failed:", error);
2459
- process.exit(1);
2460
- }
2461
- const server = startServer(config);
2462
- log.info(`copilot-relay version: ${appVersion}`);
2463
- if (authSession.githubLogin) log.info(`GitHub user: ${authSession.githubLogin}`);
2464
- else log.error("GitHub user: unavailable");
2465
- log.info(`copilot-relay listening on http://${config.host}:${config.port}`);
2466
- log.info(`copilot base url: ${config.copilotBaseUrl}`);
2467
- const baseUrl = `http://${config.host}:${config.port}`;
2468
- if (appConfig.claudeSetup) try {
2469
- const claudeResult = await applyClaudeConfig({
2470
- baseUrl,
2471
- configPath: claudeConfigPath$1
2472
- });
2473
- if (claudeResult.changed) {
2474
- log.info(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);
2475
- if (claudeResult.previousBaseUrl && claudeResult.previousBaseUrl !== baseUrl) log.info(`ANTHROPIC_BASE_URL: ${claudeResult.previousBaseUrl} → ${baseUrl}`);
2476
- } else log.info(`claude settings already up to date: ${claudeResult.configPath}`);
2477
- } catch (error) {
2478
- log.error(`Could not update claude settings (${claudeConfigPath$1}):`, error);
2479
- }
2480
- watchAppConfig((nextConfig) => {
2481
- applyRuntimeConfig(nextConfig);
2482
- log.info(`Config reloaded: logLevel=${nextConfig.logLevel} thinkEffort=${nextConfig.thinkEffort}`);
2483
- });
2484
- await new Promise((resolve$1, reject) => {
2485
- server.on("close", resolve$1);
2486
- server.on("error", reject);
2487
- });
2882
+ const stopped = await stopExistingRelay(readProxyConfig(appConfig));
2883
+ if (stopped.length > 0) log.info(`Stopped copilot-relay pid(s): ${stopped.join(", ")}`);
2884
+ log.info("Starting copilot-relay");
2885
+ await startRelay(appConfig);
2488
2886
  }
2489
2887
  });
2490
2888
 
@@ -2497,6 +2895,7 @@ await runMain(defineCommand({
2497
2895
  },
2498
2896
  subCommands: {
2499
2897
  auth,
2898
+ restart,
2500
2899
  start
2501
2900
  }
2502
2901
  }));