copilot-relay 0.0.8 → 0.1.0
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 +5 -1
- package/dist/main.js +504 -178
- package/dist/main.js.map +1 -1
- package/package.json +3 -2
package/dist/main.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
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";
|
|
11
|
+
import { Agent, fetch as fetch$1 } from "undici";
|
|
10
12
|
import { events } from "fetch-event-stream";
|
|
11
13
|
import { readFileSync } from "node:fs";
|
|
12
|
-
import {
|
|
14
|
+
import { createAdaptorServer } from "@hono/node-server";
|
|
13
15
|
import { Hono } from "hono";
|
|
14
16
|
import { cors } from "hono/cors";
|
|
15
17
|
import { streamSSE } from "hono/streaming";
|
|
@@ -56,6 +58,7 @@ const copilotTokenPath = path.join(appDir, "copilot_token.json");
|
|
|
56
58
|
const githubTokenPath = path.join(appDir, "github_token");
|
|
57
59
|
const logsDir = path.join(appDir, "logs");
|
|
58
60
|
const logPath = path.join(logsDir, "copilot-relay.log");
|
|
61
|
+
const pidPath = path.join(appDir, "copilot-relay.pid");
|
|
59
62
|
const paths = {
|
|
60
63
|
appDir,
|
|
61
64
|
configPath,
|
|
@@ -68,7 +71,8 @@ const paths = {
|
|
|
68
71
|
],
|
|
69
72
|
legacyCopilotTokenPaths,
|
|
70
73
|
logPath,
|
|
71
|
-
logsDir
|
|
74
|
+
logsDir,
|
|
75
|
+
pidPath
|
|
72
76
|
};
|
|
73
77
|
async function ensurePaths() {
|
|
74
78
|
await fs.mkdir(paths.appDir, { recursive: true });
|
|
@@ -167,7 +171,7 @@ const standardHeaders = () => ({
|
|
|
167
171
|
accept: "application/json",
|
|
168
172
|
"content-type": "application/json"
|
|
169
173
|
});
|
|
170
|
-
const sleep = (ms) => new Promise((resolve$1) => {
|
|
174
|
+
const sleep$1 = (ms) => new Promise((resolve$1) => {
|
|
171
175
|
setTimeout(resolve$1, ms);
|
|
172
176
|
});
|
|
173
177
|
const readGitHubToken = async () => {
|
|
@@ -245,12 +249,12 @@ const pollAccessToken = async (deviceCode) => {
|
|
|
245
249
|
})
|
|
246
250
|
});
|
|
247
251
|
if (!response.ok) {
|
|
248
|
-
await sleep(sleepDuration);
|
|
252
|
+
await sleep$1(sleepDuration);
|
|
249
253
|
continue;
|
|
250
254
|
}
|
|
251
255
|
const json = await response.json();
|
|
252
256
|
if (json.access_token) return json.access_token;
|
|
253
|
-
await sleep(sleepDuration);
|
|
257
|
+
await sleep$1(sleepDuration);
|
|
254
258
|
}
|
|
255
259
|
};
|
|
256
260
|
const getGitHubUser = async (githubToken, vsCodeVersion) => {
|
|
@@ -627,6 +631,179 @@ const auth = defineCommand({
|
|
|
627
631
|
}
|
|
628
632
|
});
|
|
629
633
|
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/lib/lifecycle.ts
|
|
636
|
+
const execFileAsync = promisify(execFile);
|
|
637
|
+
const stopTimeoutMs = 5e3;
|
|
638
|
+
const stopPollMs = 100;
|
|
639
|
+
const isNodeErrno = (error) => typeof error === "object" && error !== null && "code" in error;
|
|
640
|
+
const sleep = (ms) => new Promise((resolve$1) => {
|
|
641
|
+
setTimeout(resolve$1, ms);
|
|
642
|
+
});
|
|
643
|
+
const parsePid = (value) => {
|
|
644
|
+
const pid = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value.trim(), 10) : NaN;
|
|
645
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
646
|
+
};
|
|
647
|
+
const readCommandOutput = async (file, args) => {
|
|
648
|
+
try {
|
|
649
|
+
const { stdout } = await execFileAsync(file, args, {
|
|
650
|
+
maxBuffer: 1024 * 1024,
|
|
651
|
+
windowsHide: true
|
|
652
|
+
});
|
|
653
|
+
return String(stdout);
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (typeof error === "object" && error !== null && "stdout" in error && (typeof error.stdout === "string" || Buffer.isBuffer(error.stdout))) return String(error.stdout);
|
|
656
|
+
return "";
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
const parsePidLines = (output) => output.split(/\r?\n/).map((line) => parsePid(line)).filter((pid) => pid !== void 0);
|
|
660
|
+
const isRelayStartProcess = (command, cwd) => {
|
|
661
|
+
const normalizedCommand = command.replaceAll("\\", "/");
|
|
662
|
+
const normalizedCwd = cwd?.replaceAll("\\", "/");
|
|
663
|
+
if (!/(?:^|\s)start(?:\s|$)/.test(normalizedCommand)) return false;
|
|
664
|
+
if (normalizedCommand.includes("copilot-relay")) return true;
|
|
665
|
+
const usesNode = /(?:^|\s|\/)node(?:\.exe)?(?:\s|$)/i.test(normalizedCommand);
|
|
666
|
+
const usesDistMain = /(?:^|\s|\/)dist\/main\.js(?:\s|$)/.test(normalizedCommand);
|
|
667
|
+
return Boolean(usesNode && usesDistMain && normalizedCwd && normalizedCwd.split("/").some((part) => part.includes("copilot-relay")));
|
|
668
|
+
};
|
|
669
|
+
const isProcessAlive = (pid) => {
|
|
670
|
+
try {
|
|
671
|
+
process.kill(pid, 0);
|
|
672
|
+
return true;
|
|
673
|
+
} catch (error) {
|
|
674
|
+
return isNodeErrno(error) && error.code === "EPERM";
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
const readRelayPidFile = async () => {
|
|
678
|
+
try {
|
|
679
|
+
const trimmed = (await fs.readFile(paths.pidPath, "utf8")).trim();
|
|
680
|
+
if (!trimmed) return;
|
|
681
|
+
if (trimmed.startsWith("{")) return parsePid(JSON.parse(trimmed).pid);
|
|
682
|
+
return parsePid(trimmed);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (isNodeErrno(error) && error.code === "ENOENT") return;
|
|
685
|
+
throw error;
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
const writeRelayPidFile = async (config) => {
|
|
689
|
+
await fs.mkdir(paths.appDir, { recursive: true });
|
|
690
|
+
const payload = {
|
|
691
|
+
host: config.host,
|
|
692
|
+
pid: process.pid,
|
|
693
|
+
port: config.port,
|
|
694
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
695
|
+
};
|
|
696
|
+
await fs.writeFile(paths.pidPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 384 });
|
|
697
|
+
};
|
|
698
|
+
const clearRelayPidFile = async (pid = process.pid, options = {}) => {
|
|
699
|
+
const currentPid = await readRelayPidFile();
|
|
700
|
+
if (!options.force && currentPid !== void 0 && currentPid !== pid) return;
|
|
701
|
+
await fs.unlink(paths.pidPath).catch((error) => {
|
|
702
|
+
if (!isNodeErrno(error) || error.code !== "ENOENT") throw error;
|
|
703
|
+
});
|
|
704
|
+
};
|
|
705
|
+
const getProcessCommand = async (pid) => {
|
|
706
|
+
if (process.platform === "win32") return (await readCommandOutput("powershell.exe", [
|
|
707
|
+
"-NoProfile",
|
|
708
|
+
"-Command",
|
|
709
|
+
`Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" | Select-Object -ExpandProperty CommandLine`
|
|
710
|
+
])).trim() || void 0;
|
|
711
|
+
return (await readCommandOutput("ps", [
|
|
712
|
+
"-p",
|
|
713
|
+
String(pid),
|
|
714
|
+
"-o",
|
|
715
|
+
"command="
|
|
716
|
+
])).trim() || void 0;
|
|
717
|
+
};
|
|
718
|
+
const getProcessCwd = async (pid) => {
|
|
719
|
+
if (process.platform === "win32") return;
|
|
720
|
+
return (await readCommandOutput("lsof", [
|
|
721
|
+
"-a",
|
|
722
|
+
"-p",
|
|
723
|
+
String(pid),
|
|
724
|
+
"-d",
|
|
725
|
+
"cwd",
|
|
726
|
+
"-Fn"
|
|
727
|
+
])).split(/\r?\n/).find((line) => line.startsWith("n"))?.slice(1);
|
|
728
|
+
};
|
|
729
|
+
const getPortListenerPids = async (port) => {
|
|
730
|
+
if (process.platform === "win32") return parsePidLines(await readCommandOutput("powershell.exe", [
|
|
731
|
+
"-NoProfile",
|
|
732
|
+
"-Command",
|
|
733
|
+
`Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique`
|
|
734
|
+
]));
|
|
735
|
+
return parsePidLines(await readCommandOutput("lsof", [
|
|
736
|
+
"-nP",
|
|
737
|
+
`-iTCP:${port}`,
|
|
738
|
+
"-sTCP:LISTEN",
|
|
739
|
+
"-t"
|
|
740
|
+
]));
|
|
741
|
+
};
|
|
742
|
+
const getProcessList = async () => {
|
|
743
|
+
return (process.platform === "win32" ? await readCommandOutput("powershell.exe", [
|
|
744
|
+
"-NoProfile",
|
|
745
|
+
"-Command",
|
|
746
|
+
"Get-CimInstance Win32_Process | ForEach-Object { \"$($_.ProcessId)`t$($_.CommandLine)\" }"
|
|
747
|
+
]) : await readCommandOutput("ps", ["-axo", "pid=,command="])).split(/\r?\n/).flatMap((line) => {
|
|
748
|
+
const match = process.platform === "win32" ? line.match(/^(\d+)\t(.+)$/) : line.match(/^\s*(\d+)\s+(.+)$/);
|
|
749
|
+
const pid = parsePid(match?.[1]);
|
|
750
|
+
const command = match?.[2]?.trim();
|
|
751
|
+
return pid && command ? [{
|
|
752
|
+
command,
|
|
753
|
+
pid
|
|
754
|
+
}] : [];
|
|
755
|
+
});
|
|
756
|
+
};
|
|
757
|
+
const isRelayPid = async (pid) => {
|
|
758
|
+
if (pid === process.pid || !isProcessAlive(pid)) return false;
|
|
759
|
+
const command = await getProcessCommand(pid);
|
|
760
|
+
if (!command) return false;
|
|
761
|
+
return isRelayStartProcess(command, await getProcessCwd(pid));
|
|
762
|
+
};
|
|
763
|
+
const findRelayProcessIds = async (config) => {
|
|
764
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
765
|
+
const storedPid = await readRelayPidFile();
|
|
766
|
+
if (storedPid) candidates.add(storedPid);
|
|
767
|
+
for (const pid of await getPortListenerPids(config.port)) candidates.add(pid);
|
|
768
|
+
for (const { command, pid } of await getProcessList()) {
|
|
769
|
+
if (pid === process.pid) continue;
|
|
770
|
+
if (command.includes("copilot-relay") || command.includes("dist/main.js")) {
|
|
771
|
+
if (isRelayStartProcess(command, await getProcessCwd(pid))) candidates.add(pid);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const relayPids = [];
|
|
775
|
+
for (const pid of candidates) if (await isRelayPid(pid)) relayPids.push(pid);
|
|
776
|
+
return [...new Set(relayPids)].sort((left, right) => left - right);
|
|
777
|
+
};
|
|
778
|
+
const waitForExit = async (pid, timeoutMs) => {
|
|
779
|
+
const deadline = Date.now() + timeoutMs;
|
|
780
|
+
while (Date.now() < deadline) {
|
|
781
|
+
if (!isProcessAlive(pid)) return true;
|
|
782
|
+
await sleep(stopPollMs);
|
|
783
|
+
}
|
|
784
|
+
return !isProcessAlive(pid);
|
|
785
|
+
};
|
|
786
|
+
const stopProcess = async (pid) => {
|
|
787
|
+
if (!isProcessAlive(pid)) return;
|
|
788
|
+
log.info(`Stopping existing copilot-relay process pid=${pid}`);
|
|
789
|
+
process.kill(pid, "SIGTERM");
|
|
790
|
+
if (await waitForExit(pid, stopTimeoutMs)) return;
|
|
791
|
+
log.error(`copilot-relay process pid=${pid} did not stop; forcing termination`);
|
|
792
|
+
process.kill(pid, "SIGKILL");
|
|
793
|
+
if (!await waitForExit(pid, stopTimeoutMs)) throw new Error(`Could not stop copilot-relay process pid=${pid}`);
|
|
794
|
+
};
|
|
795
|
+
const stopExistingRelay = async (config) => {
|
|
796
|
+
const relayPids = await findRelayProcessIds(config);
|
|
797
|
+
if (relayPids.length === 0) {
|
|
798
|
+
log.info("No existing copilot-relay instance found");
|
|
799
|
+
await clearRelayPidFile(0, { force: true });
|
|
800
|
+
return [];
|
|
801
|
+
}
|
|
802
|
+
for (const pid of relayPids) await stopProcess(pid);
|
|
803
|
+
await clearRelayPidFile(0, { force: true });
|
|
804
|
+
return relayPids;
|
|
805
|
+
};
|
|
806
|
+
|
|
630
807
|
//#endregion
|
|
631
808
|
//#region src/lib/claude-settings.ts
|
|
632
809
|
/**
|
|
@@ -692,6 +869,10 @@ const userAgent = `GitHubCopilotChat/${copilotVersion}`;
|
|
|
692
869
|
const apiVersion = "2025-04-01";
|
|
693
870
|
const maxFetchAttempts = 2;
|
|
694
871
|
const copilotRequestTimeoutMs = 18e4;
|
|
872
|
+
const copilotDispatcher = new Agent({
|
|
873
|
+
allowH2: false,
|
|
874
|
+
connect: { allowH2: false }
|
|
875
|
+
});
|
|
695
876
|
const getCopilotProviderContext = (config) => ({
|
|
696
877
|
baseUrl: config.copilotBaseUrl,
|
|
697
878
|
token: config.copilotToken,
|
|
@@ -735,7 +916,7 @@ const readCopilotText = async (response, signal, timeoutMs = copilotRequestTimeo
|
|
|
735
916
|
throw toCopilotAbortHTTPError(error, signal, timeoutMs) ?? error;
|
|
736
917
|
}
|
|
737
918
|
};
|
|
738
|
-
const buildHeaders = (provider, init, options) => {
|
|
919
|
+
const buildHeaders = (provider, init, options, upstreamRequestId) => {
|
|
739
920
|
const headers = new Headers(init.headers);
|
|
740
921
|
headers.set("authorization", `Bearer ${provider.token}`);
|
|
741
922
|
headers.set("copilot-integration-id", "vscode-chat");
|
|
@@ -744,36 +925,57 @@ const buildHeaders = (provider, init, options) => {
|
|
|
744
925
|
headers.set("user-agent", userAgent);
|
|
745
926
|
headers.set("openai-intent", "conversation-panel");
|
|
746
927
|
headers.set("x-github-api-version", apiVersion);
|
|
747
|
-
headers.set("x-request-id",
|
|
928
|
+
headers.set("x-request-id", upstreamRequestId);
|
|
748
929
|
headers.set("x-vscode-user-agent-library-version", "electron-fetch");
|
|
749
930
|
if (options.vision) headers.set("copilot-vision-request", "true");
|
|
750
931
|
if (options.initiator) headers.set("x-initiator", options.initiator);
|
|
751
932
|
if (!headers.has("content-type") && init.body !== void 0) headers.set("content-type", "application/json");
|
|
752
933
|
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
753
|
-
return headers;
|
|
934
|
+
return Object.fromEntries(headers.entries());
|
|
935
|
+
};
|
|
936
|
+
const formatRequestId = (requestId) => requestId ? `request_id=${requestId} ` : "";
|
|
937
|
+
const logUpstreamLifecycle = (requestId, message) => {
|
|
938
|
+
if (requestId) {
|
|
939
|
+
log.info(`${formatRequestId(requestId)}${message}`);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
log.debug(message);
|
|
754
943
|
};
|
|
755
944
|
const fetchCopilot = async (provider, path$1, init, options = {}) => {
|
|
756
945
|
if (!provider.token) throw new ProxyNotImplementedError("Copilot token is not configured for copilot-relay.");
|
|
757
946
|
const timeoutMs = options.timeoutMs ?? copilotRequestTimeoutMs;
|
|
758
947
|
const signal = createCopilotRequestSignal(options.signal, timeoutMs);
|
|
759
948
|
let lastError;
|
|
760
|
-
for (let attempt = 1; attempt <= maxFetchAttempts; attempt++)
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
949
|
+
for (let attempt = 1; attempt <= maxFetchAttempts; attempt++) {
|
|
950
|
+
const upstreamRequestId = randomUUID();
|
|
951
|
+
try {
|
|
952
|
+
const started = performance.now();
|
|
953
|
+
logUpstreamLifecycle(options.requestId, `send upstream method=${init.method ?? "GET"} path=${path$1} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
|
|
954
|
+
const response = await fetch$1(`${provider.baseUrl}${path$1}`, {
|
|
955
|
+
...init,
|
|
956
|
+
headers: buildHeaders(provider, init, options, upstreamRequestId),
|
|
957
|
+
dispatcher: copilotDispatcher,
|
|
958
|
+
signal
|
|
959
|
+
});
|
|
960
|
+
const ms = Math.round(performance.now() - started);
|
|
961
|
+
logUpstreamLifecycle(options.requestId, `return from upstream method=${init.method ?? "GET"} path=${path$1} status=${response.status} ms=${ms} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
|
|
962
|
+
log.debug(`${formatRequestId(options.requestId)}Copilot ${init.method ?? "GET"} ${path$1} -> ${response.status} ${ms}ms (attempt ${attempt}) upstream_request_id=${upstreamRequestId}`);
|
|
963
|
+
if (!shouldRetryResponse(response) || attempt === maxFetchAttempts) return response;
|
|
964
|
+
log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} returned ${response.status}; retrying (${attempt}/${maxFetchAttempts}) upstream_request_id=${upstreamRequestId}`);
|
|
965
|
+
} catch (error) {
|
|
966
|
+
const abortError = toCopilotAbortHTTPError(error, signal, timeoutMs);
|
|
967
|
+
if (abortError) {
|
|
968
|
+
logUpstreamLifecycle(options.requestId, `upstream failed method=${init.method ?? "GET"} path=${path$1} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
|
|
969
|
+
throw abortError;
|
|
970
|
+
}
|
|
971
|
+
logUpstreamLifecycle(options.requestId, `upstream failed method=${init.method ?? "GET"} path=${path$1} attempt=${attempt} upstream_request_id=${upstreamRequestId}`);
|
|
972
|
+
lastError = error;
|
|
973
|
+
if (attempt === maxFetchAttempts) {
|
|
974
|
+
log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} request failed after ${attempt} attempts upstream_request_id=${upstreamRequestId}`, error);
|
|
975
|
+
throw error;
|
|
976
|
+
}
|
|
977
|
+
log.error(`${formatRequestId(options.requestId)}Copilot ${path$1} request failed; retrying (${attempt}/${maxFetchAttempts}) upstream_request_id=${upstreamRequestId}`, error);
|
|
978
|
+
}
|
|
777
979
|
}
|
|
778
980
|
throw lastError;
|
|
779
981
|
};
|
|
@@ -1228,6 +1430,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
1228
1430
|
"Model request",
|
|
1229
1431
|
`client=${client}`,
|
|
1230
1432
|
`requested_model=${requestedModel}`,
|
|
1433
|
+
...options.requestId ? [`request_id=${options.requestId}`] : [],
|
|
1231
1434
|
`upstream_model=${compatiblePayload.model}`,
|
|
1232
1435
|
`requested_think_effort=${requestedThinkEffort}`,
|
|
1233
1436
|
`requested_thinking=${requestedThinking}`,
|
|
@@ -1237,6 +1440,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
1237
1440
|
if (useResponsesApi) return createResponses(provider, compatiblePayload, {
|
|
1238
1441
|
vision: enableVision,
|
|
1239
1442
|
initiator,
|
|
1443
|
+
requestId: options.requestId,
|
|
1240
1444
|
signal,
|
|
1241
1445
|
timeoutMs: options.timeoutMs
|
|
1242
1446
|
});
|
|
@@ -1250,6 +1454,7 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
1250
1454
|
}, {
|
|
1251
1455
|
vision: enableVision,
|
|
1252
1456
|
initiator,
|
|
1457
|
+
requestId: options.requestId,
|
|
1253
1458
|
signal,
|
|
1254
1459
|
timeoutMs: options.timeoutMs
|
|
1255
1460
|
});
|
|
@@ -1257,13 +1462,15 @@ const createChatCompletions = async (config, payload, options = {}) => {
|
|
|
1257
1462
|
if (await shouldRetryWithResponses(response, signal, options.timeoutMs)) return createResponses(provider, compatiblePayload, {
|
|
1258
1463
|
vision: enableVision,
|
|
1259
1464
|
initiator,
|
|
1465
|
+
requestId: options.requestId,
|
|
1260
1466
|
signal,
|
|
1261
1467
|
timeoutMs: options.timeoutMs
|
|
1262
1468
|
});
|
|
1263
1469
|
throw new HTTPError("Failed to create chat completions", response, await logUpstreamError("Failed to create chat completions", response, {
|
|
1264
1470
|
model: payload.model,
|
|
1265
1471
|
request: requestPayload,
|
|
1266
|
-
route: "/chat/completions"
|
|
1472
|
+
route: "/chat/completions",
|
|
1473
|
+
requestId: options.requestId
|
|
1267
1474
|
}, signal, options.timeoutMs));
|
|
1268
1475
|
}
|
|
1269
1476
|
if (compatiblePayload.stream) return events(response);
|
|
@@ -1281,13 +1488,15 @@ async function createResponses(provider, payload, options) {
|
|
|
1281
1488
|
}, {
|
|
1282
1489
|
vision: options.vision,
|
|
1283
1490
|
initiator: options.initiator,
|
|
1491
|
+
requestId: options.requestId,
|
|
1284
1492
|
signal: options.signal,
|
|
1285
1493
|
timeoutMs: options.timeoutMs
|
|
1286
1494
|
});
|
|
1287
1495
|
if (!response.ok) throw new HTTPError("Failed to create responses", response, await logUpstreamError("Failed to create responses", response, {
|
|
1288
1496
|
model: payload.model,
|
|
1289
1497
|
request: requestPayload,
|
|
1290
|
-
route: "/responses"
|
|
1498
|
+
route: "/responses",
|
|
1499
|
+
requestId: options.requestId
|
|
1291
1500
|
}, options.signal, options.timeoutMs));
|
|
1292
1501
|
if (payload.stream) return translateResponsesStreamToChatCompletionStream(events(response));
|
|
1293
1502
|
return translateResponsesToChatCompletion(await readCopilotJson(response, options.signal, options.timeoutMs));
|
|
@@ -1295,10 +1504,11 @@ async function createResponses(provider, payload, options) {
|
|
|
1295
1504
|
async function logUpstreamError(message, response, context, signal, timeoutMs) {
|
|
1296
1505
|
const errorBody = await readCopilotText(response.clone(), signal, timeoutMs).catch(() => "");
|
|
1297
1506
|
const detail = getUpstreamErrorDetail(response, errorBody);
|
|
1298
|
-
log.error(`${message}: route=${context.route} model=${context.model} status=${response.status}`, {
|
|
1507
|
+
log.error(`${message}: route=${context.route} model=${context.model} status=${response.status}${context.requestId ? ` request_id=${context.requestId}` : ""}`, {
|
|
1299
1508
|
message,
|
|
1300
1509
|
route: context.route,
|
|
1301
1510
|
model: context.model,
|
|
1511
|
+
requestId: context.requestId,
|
|
1302
1512
|
request: context.request,
|
|
1303
1513
|
response: {
|
|
1304
1514
|
status: response.status,
|
|
@@ -1993,6 +2203,7 @@ const createClaudeWebSearchExecution = async (config, payload, requestedQuery, o
|
|
|
1993
2203
|
body: JSON.stringify(buildWebSearchRequestPayload(payload, requestedQuery, backendModel))
|
|
1994
2204
|
}, {
|
|
1995
2205
|
initiator: "agent",
|
|
2206
|
+
requestId: options.requestId,
|
|
1996
2207
|
signal,
|
|
1997
2208
|
timeoutMs: options.timeoutMs
|
|
1998
2209
|
});
|
|
@@ -2266,6 +2477,7 @@ const getTokenCount = async (payload, model) => {
|
|
|
2266
2477
|
//#endregion
|
|
2267
2478
|
//#region src/routes/claude.ts
|
|
2268
2479
|
const claudeRoutes = new Hono();
|
|
2480
|
+
const streamKeepAliveIntervalMs = 15e3;
|
|
2269
2481
|
const createTokenCountModel = (modelId) => ({
|
|
2270
2482
|
capabilities: { tokenizer: "o200k_base" },
|
|
2271
2483
|
id: modelId
|
|
@@ -2336,6 +2548,104 @@ const eventsFromClaudeResponse = (response) => {
|
|
|
2336
2548
|
events$1.push({ type: "message_stop" });
|
|
2337
2549
|
return events$1;
|
|
2338
2550
|
};
|
|
2551
|
+
const createQueuedClaudeStreamWriter = (write) => {
|
|
2552
|
+
let pending = Promise.resolve();
|
|
2553
|
+
return (event) => {
|
|
2554
|
+
pending = pending.then(() => write(event));
|
|
2555
|
+
return pending;
|
|
2556
|
+
};
|
|
2557
|
+
};
|
|
2558
|
+
const startClaudeStreamKeepAlive = (writeEvent) => {
|
|
2559
|
+
const writePing = () => {
|
|
2560
|
+
writeEvent({ type: "ping" }).catch(() => void 0);
|
|
2561
|
+
};
|
|
2562
|
+
writePing();
|
|
2563
|
+
const timer = setInterval(writePing, streamKeepAliveIntervalMs);
|
|
2564
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
2565
|
+
return () => {
|
|
2566
|
+
clearInterval(timer);
|
|
2567
|
+
};
|
|
2568
|
+
};
|
|
2569
|
+
const writeClaudeStreamEvents = async (events$1, writeEvent) => {
|
|
2570
|
+
for (const event of events$1) await writeEvent(event);
|
|
2571
|
+
};
|
|
2572
|
+
const handleClaudeMessageRequest = async (config, claudePayload, requestSignal, requestId, writeEvent) => {
|
|
2573
|
+
const upstreamModel = translateModelName(claudePayload.model);
|
|
2574
|
+
const shouldLetModelDecideWebSearch = hasClaudeWebSearch(claudePayload);
|
|
2575
|
+
const decisionPayload = shouldLetModelDecideWebSearch ? prepareClaudeWebSearchDecisionPayload(claudePayload) : claudePayload;
|
|
2576
|
+
const toolNameMapper = createClaudeToolNameMapper(decisionPayload.tools, { ...getToolNameMapperOptionsForModel(upstreamModel) });
|
|
2577
|
+
const openAIPayload = translateToOpenAI(decisionPayload, void 0, toolNameMapper);
|
|
2578
|
+
if (shouldLetModelDecideWebSearch) openAIPayload.stream = false;
|
|
2579
|
+
const response = await createChatCompletions(config, openAIPayload, {
|
|
2580
|
+
client: "claude",
|
|
2581
|
+
requestedModel: claudePayload.model,
|
|
2582
|
+
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2583
|
+
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2584
|
+
requestId,
|
|
2585
|
+
signal: requestSignal,
|
|
2586
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2587
|
+
});
|
|
2588
|
+
if (isNonStreamingResponse(response)) {
|
|
2589
|
+
const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(response, toolNameMapper) : void 0;
|
|
2590
|
+
let claudeResponse;
|
|
2591
|
+
if (webSearchToolCall) {
|
|
2592
|
+
const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
|
|
2593
|
+
requestId,
|
|
2594
|
+
signal: requestSignal,
|
|
2595
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2596
|
+
});
|
|
2597
|
+
const searchResponse = createClaudeWebSearchResponse(search);
|
|
2598
|
+
if (search.results.length === 0) claudeResponse = searchResponse;
|
|
2599
|
+
else {
|
|
2600
|
+
const finalResponse = await createChatCompletions(config, createFinalWebSearchPayload(openAIPayload, search), {
|
|
2601
|
+
client: "claude",
|
|
2602
|
+
requestedModel: claudePayload.model,
|
|
2603
|
+
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2604
|
+
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2605
|
+
requestId,
|
|
2606
|
+
signal: requestSignal,
|
|
2607
|
+
timeoutMs: config.upstreamTimeoutMs
|
|
2608
|
+
});
|
|
2609
|
+
if (!isNonStreamingResponse(finalResponse)) throw new HTTPError("Claude web search final answer request unexpectedly streamed", new Response("Claude web search final answer request unexpectedly streamed", {
|
|
2610
|
+
status: 502,
|
|
2611
|
+
headers: { "content-type": "text/plain" }
|
|
2612
|
+
}));
|
|
2613
|
+
claudeResponse = mergeWebSearchAndFinalResponse(searchResponse, translateToClaude(finalResponse, toolNameMapper));
|
|
2614
|
+
}
|
|
2615
|
+
} else claudeResponse = translateToClaude(response, toolNameMapper);
|
|
2616
|
+
if (writeEvent) {
|
|
2617
|
+
await writeClaudeStreamEvents(eventsFromClaudeResponse(claudeResponse), writeEvent);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
return claudeResponse;
|
|
2621
|
+
}
|
|
2622
|
+
if (shouldLetModelDecideWebSearch) throw new HTTPError("Claude web search model-decision request unexpectedly streamed", new Response("Claude web search model-decision request unexpectedly streamed", {
|
|
2623
|
+
status: 502,
|
|
2624
|
+
headers: { "content-type": "text/plain" }
|
|
2625
|
+
}));
|
|
2626
|
+
if (!writeEvent) throw new HTTPError("Claude non-streaming request unexpectedly streamed", new Response("Claude non-streaming request unexpectedly streamed", {
|
|
2627
|
+
status: 502,
|
|
2628
|
+
headers: { "content-type": "text/plain" }
|
|
2629
|
+
}));
|
|
2630
|
+
const streamState = {
|
|
2631
|
+
messageStartSent: false,
|
|
2632
|
+
contentBlockIndex: 0,
|
|
2633
|
+
contentBlockOpen: false,
|
|
2634
|
+
thinkingBlockOpen: false,
|
|
2635
|
+
toolCalls: {}
|
|
2636
|
+
};
|
|
2637
|
+
for await (const rawEvent of response) {
|
|
2638
|
+
if (rawEvent.data === "[DONE]") break;
|
|
2639
|
+
if (!rawEvent.data) continue;
|
|
2640
|
+
let chunk;
|
|
2641
|
+
try {
|
|
2642
|
+
chunk = JSON.parse(rawEvent.data);
|
|
2643
|
+
} catch {
|
|
2644
|
+
continue;
|
|
2645
|
+
}
|
|
2646
|
+
await writeClaudeStreamEvents(translateChunkToClaudeEvents(chunk, streamState, toolNameMapper), writeEvent);
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2339
2649
|
claudeRoutes.get("/models", (c) => c.json({
|
|
2340
2650
|
object: "list",
|
|
2341
2651
|
data: getExposedModelIds().map((id) => ({
|
|
@@ -2347,94 +2657,29 @@ claudeRoutes.get("/models", (c) => c.json({
|
|
|
2347
2657
|
}));
|
|
2348
2658
|
claudeRoutes.post("/messages", async (c) => {
|
|
2349
2659
|
const config = c.get("config");
|
|
2660
|
+
const requestId = c.get("requestId");
|
|
2350
2661
|
const claudePayload = await c.req.json();
|
|
2351
2662
|
const requestSignal = createCopilotRequestSignal(c.req.raw.signal, config.upstreamTimeoutMs);
|
|
2352
2663
|
log.debug("Full Claude request payload", { payload: claudePayload });
|
|
2353
|
-
|
|
2354
|
-
const
|
|
2355
|
-
const
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
const openAIPayload = translateToOpenAI(decisionPayload, void 0, toolNameMapper);
|
|
2359
|
-
if (shouldLetModelDecideWebSearch) openAIPayload.stream = false;
|
|
2360
|
-
const response = await createChatCompletions(config, openAIPayload, {
|
|
2361
|
-
client: "claude",
|
|
2362
|
-
requestedModel: claudePayload.model,
|
|
2363
|
-
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2364
|
-
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2365
|
-
signal: requestSignal,
|
|
2366
|
-
timeoutMs: config.upstreamTimeoutMs
|
|
2367
|
-
});
|
|
2368
|
-
if (isNonStreamingResponse(response)) {
|
|
2369
|
-
const webSearchToolCall = shouldLetModelDecideWebSearch ? getClaudeWebSearchToolCallFromChatResponse(response, toolNameMapper) : void 0;
|
|
2370
|
-
let claudeResponse;
|
|
2371
|
-
if (webSearchToolCall) {
|
|
2372
|
-
const search = await createClaudeWebSearchExecution(config, claudePayload, webSearchToolCall.query, {
|
|
2373
|
-
signal: requestSignal,
|
|
2374
|
-
timeoutMs: config.upstreamTimeoutMs
|
|
2375
|
-
});
|
|
2376
|
-
const searchResponse = createClaudeWebSearchResponse(search);
|
|
2377
|
-
if (search.results.length === 0) claudeResponse = searchResponse;
|
|
2378
|
-
else {
|
|
2379
|
-
const finalResponse = await createChatCompletions(config, createFinalWebSearchPayload(openAIPayload, search), {
|
|
2380
|
-
client: "claude",
|
|
2381
|
-
requestedModel: claudePayload.model,
|
|
2382
|
-
requestedThinkEffort: getClaudeRequestedThinkEffort(claudePayload),
|
|
2383
|
-
requestedThinking: getClaudeRequestedThinking(claudePayload),
|
|
2384
|
-
signal: requestSignal,
|
|
2385
|
-
timeoutMs: config.upstreamTimeoutMs
|
|
2386
|
-
});
|
|
2387
|
-
if (!isNonStreamingResponse(finalResponse)) throw new HTTPError("Claude web search final answer request unexpectedly streamed", new Response("Claude web search final answer request unexpectedly streamed", {
|
|
2388
|
-
status: 502,
|
|
2389
|
-
headers: { "content-type": "text/plain" }
|
|
2390
|
-
}));
|
|
2391
|
-
claudeResponse = mergeWebSearchAndFinalResponse(searchResponse, translateToClaude(finalResponse, toolNameMapper));
|
|
2392
|
-
}
|
|
2393
|
-
} else claudeResponse = translateToClaude(response, toolNameMapper);
|
|
2394
|
-
if (!claudePayload.stream) return c.json(claudeResponse);
|
|
2395
|
-
return streamSSE(c, async (stream) => {
|
|
2396
|
-
for (const event of eventsFromClaudeResponse(claudeResponse)) await stream.writeSSE({
|
|
2397
|
-
event: event.type,
|
|
2398
|
-
data: JSON.stringify(event)
|
|
2399
|
-
});
|
|
2400
|
-
});
|
|
2401
|
-
}
|
|
2402
|
-
if (shouldLetModelDecideWebSearch) throw new HTTPError("Claude web search model-decision request unexpectedly streamed", new Response("Claude web search model-decision request unexpectedly streamed", {
|
|
2403
|
-
status: 502,
|
|
2404
|
-
headers: { "content-type": "text/plain" }
|
|
2664
|
+
if (claudePayload.stream) return streamSSE(c, async (stream) => {
|
|
2665
|
+
const streamStarted = performance.now();
|
|
2666
|
+
const writeEvent = createQueuedClaudeStreamWriter((event) => stream.writeSSE({
|
|
2667
|
+
event: event.type,
|
|
2668
|
+
data: JSON.stringify(event)
|
|
2405
2669
|
}));
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
try {
|
|
2420
|
-
chunk = JSON.parse(rawEvent.data);
|
|
2421
|
-
} catch {
|
|
2422
|
-
continue;
|
|
2423
|
-
}
|
|
2424
|
-
for (const event of translateChunkToClaudeEvents(chunk, streamState, toolNameMapper)) await stream.writeSSE({
|
|
2425
|
-
event: event.type,
|
|
2426
|
-
data: JSON.stringify(event)
|
|
2427
|
-
});
|
|
2428
|
-
}
|
|
2429
|
-
} catch (error) {
|
|
2430
|
-
log.error("Error during Claude stream translation:", error);
|
|
2431
|
-
const errorEvent = translateErrorToClaudeErrorEvent();
|
|
2432
|
-
await stream.writeSSE({
|
|
2433
|
-
event: errorEvent.type,
|
|
2434
|
-
data: JSON.stringify(errorEvent)
|
|
2435
|
-
});
|
|
2436
|
-
}
|
|
2437
|
-
});
|
|
2670
|
+
const stopKeepAlive = startClaudeStreamKeepAlive(writeEvent);
|
|
2671
|
+
try {
|
|
2672
|
+
await handleClaudeMessageRequest(config, claudePayload, requestSignal, requestId, writeEvent);
|
|
2673
|
+
} catch (error) {
|
|
2674
|
+
log.error("Error during Claude stream request:", error);
|
|
2675
|
+
await writeEvent(translateErrorToClaudeErrorEvent());
|
|
2676
|
+
} finally {
|
|
2677
|
+
stopKeepAlive();
|
|
2678
|
+
log.info(`request_id=${requestId} stream completed ${Math.round(performance.now() - streamStarted)}ms`);
|
|
2679
|
+
}
|
|
2680
|
+
});
|
|
2681
|
+
try {
|
|
2682
|
+
return c.json(await handleClaudeMessageRequest(config, claudePayload, requestSignal, requestId));
|
|
2438
2683
|
} catch (error) {
|
|
2439
2684
|
if (error instanceof ProxyNotImplementedError) {
|
|
2440
2685
|
c.set("requestErrorMessage", error.message);
|
|
@@ -2498,25 +2743,35 @@ const getLoggedHeaders = (request) => Object.fromEntries(loggedRequestHeaders.fl
|
|
|
2498
2743
|
const value = request.headers.get(name);
|
|
2499
2744
|
return value ? [[name, value]] : [];
|
|
2500
2745
|
}));
|
|
2501
|
-
const formatStatusLog = (method, path$1, status, ms, errorMessage) => {
|
|
2502
|
-
const base =
|
|
2746
|
+
const formatStatusLog = (requestId, method, path$1, status, ms, errorMessage) => {
|
|
2747
|
+
const base = `request_id=${requestId} ${method} ${path$1} -> ${status} ${ms}ms`;
|
|
2503
2748
|
return status >= 400 && errorMessage ? `${base} error=${JSON.stringify(errorMessage)}` : base;
|
|
2504
2749
|
};
|
|
2505
2750
|
const createServer = (config) => {
|
|
2506
2751
|
const app = new Hono();
|
|
2507
|
-
app.use("*", cors());
|
|
2508
2752
|
app.use("*", async (c, next) => {
|
|
2753
|
+
const requestId = randomUUID();
|
|
2509
2754
|
c.set("config", config);
|
|
2755
|
+
c.set("requestId", requestId);
|
|
2756
|
+
c.header("x-copilot-relay-request-id", requestId);
|
|
2757
|
+
log.info(`request_id=${requestId} request received method=${c.req.method} path=${c.req.path}`);
|
|
2510
2758
|
const started = performance.now();
|
|
2511
2759
|
try {
|
|
2512
2760
|
await next();
|
|
2513
2761
|
} finally {
|
|
2514
2762
|
const ms = Math.round(performance.now() - started);
|
|
2515
|
-
log.info(formatStatusLog(c.req.method, c.req.path, c.res.status, ms, c.get("requestErrorMessage")));
|
|
2763
|
+
log.info(formatStatusLog(requestId, c.req.method, c.req.path, c.res.status, ms, c.get("requestErrorMessage")));
|
|
2516
2764
|
}
|
|
2517
2765
|
});
|
|
2766
|
+
app.use("*", cors());
|
|
2518
2767
|
app.onError((error, c) => {
|
|
2519
2768
|
c.set("requestErrorMessage", error.message);
|
|
2769
|
+
log.error("Unhandled Claude API request error", {
|
|
2770
|
+
method: c.req.method,
|
|
2771
|
+
path: c.req.path,
|
|
2772
|
+
requestId: c.get("requestId"),
|
|
2773
|
+
error
|
|
2774
|
+
});
|
|
2520
2775
|
return c.json({ error: { message: "Internal server error" } }, 500);
|
|
2521
2776
|
});
|
|
2522
2777
|
app.get("/", (c) => c.json({
|
|
@@ -2531,6 +2786,7 @@ const createServer = (config) => {
|
|
|
2531
2786
|
log.error("Unsupported Claude API request", {
|
|
2532
2787
|
method: c.req.method,
|
|
2533
2788
|
path: c.req.path,
|
|
2789
|
+
requestId: c.get("requestId"),
|
|
2534
2790
|
headers: getLoggedHeaders(c.req.raw),
|
|
2535
2791
|
payload: await readRequestPayloadForLog(c.req.raw)
|
|
2536
2792
|
});
|
|
@@ -2538,79 +2794,147 @@ const createServer = (config) => {
|
|
|
2538
2794
|
});
|
|
2539
2795
|
return app;
|
|
2540
2796
|
};
|
|
2541
|
-
const startServer = (config) =>
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2797
|
+
const startServer = (config) => new Promise((resolve$1, reject) => {
|
|
2798
|
+
const server = createAdaptorServer({
|
|
2799
|
+
fetch: createServer(config).fetch,
|
|
2800
|
+
hostname: config.host,
|
|
2801
|
+
port: config.port
|
|
2802
|
+
});
|
|
2803
|
+
const onError = (error) => {
|
|
2804
|
+
server.off("listening", onListening);
|
|
2805
|
+
reject(error);
|
|
2806
|
+
};
|
|
2807
|
+
const onListening = () => {
|
|
2808
|
+
server.off("error", onError);
|
|
2809
|
+
resolve$1(server);
|
|
2810
|
+
};
|
|
2811
|
+
server.once("error", onError);
|
|
2812
|
+
server.once("listening", onListening);
|
|
2813
|
+
server.listen(config.port, config.host);
|
|
2545
2814
|
});
|
|
2546
2815
|
|
|
2547
2816
|
//#endregion
|
|
2548
2817
|
//#region src/start.ts
|
|
2818
|
+
async function startRelay(appConfig) {
|
|
2819
|
+
appConfig ??= await readAppConfig();
|
|
2820
|
+
setLogLevel(appConfig.logLevel);
|
|
2821
|
+
await cleanupLogs(appConfig.logRetentionDays);
|
|
2822
|
+
const claudeConfigPath$1 = claudeConfigPath;
|
|
2823
|
+
const config = readProxyConfig(appConfig);
|
|
2824
|
+
const applyRuntimeConfig = (nextConfig) => {
|
|
2825
|
+
setLogLevel(nextConfig.logLevel);
|
|
2826
|
+
cleanupLogs(nextConfig.logRetentionDays);
|
|
2827
|
+
runtimeState.debug = nextConfig.logLevel === "debug";
|
|
2828
|
+
runtimeState.thinkEffort = nextConfig.thinkEffort;
|
|
2829
|
+
config.copilotBaseUrl = nextConfig.copilotBaseUrl;
|
|
2830
|
+
config.host = nextConfig.host;
|
|
2831
|
+
config.port = nextConfig.port;
|
|
2832
|
+
config.upstreamTimeoutMs = nextConfig.upstreamTimeoutSeconds * 1e3;
|
|
2833
|
+
config.webSearchBackend = nextConfig.webSearchBackend;
|
|
2834
|
+
runtimeState.modelRouting = {
|
|
2835
|
+
gptModel: nextConfig.gptModel,
|
|
2836
|
+
opusModel: nextConfig.opusModel
|
|
2837
|
+
};
|
|
2838
|
+
};
|
|
2839
|
+
applyRuntimeConfig(appConfig);
|
|
2840
|
+
log.info(`Log level: ${appConfig.logLevel}`);
|
|
2841
|
+
log.info(`Think effort: ${appConfig.thinkEffort}`);
|
|
2842
|
+
log.info(`Upstream timeout: ${appConfig.upstreamTimeoutSeconds}s`);
|
|
2843
|
+
log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
|
|
2844
|
+
const authSession = await setupProxyAuth(config);
|
|
2845
|
+
try {
|
|
2846
|
+
await validateUpstream(config, appConfig.thinkEffort);
|
|
2847
|
+
} catch (error) {
|
|
2848
|
+
log.error("Startup preflight failed:", error);
|
|
2849
|
+
process.exit(1);
|
|
2850
|
+
}
|
|
2851
|
+
const server = await startServer(config);
|
|
2852
|
+
await writeRelayPidFile(config);
|
|
2853
|
+
log.info(`copilot-relay version: ${appVersion}`);
|
|
2854
|
+
if (authSession.githubLogin) log.info(`GitHub user: ${authSession.githubLogin}`);
|
|
2855
|
+
else log.error("GitHub user: unavailable");
|
|
2856
|
+
log.info(`copilot-relay listening on http://${config.host}:${config.port}`);
|
|
2857
|
+
log.info(`copilot base url: ${config.copilotBaseUrl}`);
|
|
2858
|
+
const baseUrl = `http://${config.host}:${config.port}`;
|
|
2859
|
+
if (appConfig.claudeSetup) try {
|
|
2860
|
+
const claudeResult = await applyClaudeConfig({
|
|
2861
|
+
baseUrl,
|
|
2862
|
+
configPath: claudeConfigPath$1
|
|
2863
|
+
});
|
|
2864
|
+
if (claudeResult.changed) {
|
|
2865
|
+
log.info(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);
|
|
2866
|
+
if (claudeResult.previousBaseUrl && claudeResult.previousBaseUrl !== baseUrl) log.info(`ANTHROPIC_BASE_URL: ${claudeResult.previousBaseUrl} → ${baseUrl}`);
|
|
2867
|
+
} else log.info(`claude settings already up to date: ${claudeResult.configPath}`);
|
|
2868
|
+
} catch (error) {
|
|
2869
|
+
log.error(`Could not update claude settings (${claudeConfigPath$1}):`, error);
|
|
2870
|
+
}
|
|
2871
|
+
watchAppConfig((nextConfig) => {
|
|
2872
|
+
applyRuntimeConfig(nextConfig);
|
|
2873
|
+
log.info(`Config reloaded: logLevel=${nextConfig.logLevel} thinkEffort=${nextConfig.thinkEffort} upstreamTimeoutSeconds=${nextConfig.upstreamTimeoutSeconds}`);
|
|
2874
|
+
});
|
|
2875
|
+
const shutdown = (signal) => {
|
|
2876
|
+
log.info(`Received ${signal}; shutting down copilot-relay`);
|
|
2877
|
+
server.close((error) => {
|
|
2878
|
+
if (error) {
|
|
2879
|
+
log.error("Error while shutting down copilot-relay:", error);
|
|
2880
|
+
process.exitCode = 1;
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
};
|
|
2884
|
+
process.once("SIGINT", shutdown);
|
|
2885
|
+
process.once("SIGTERM", shutdown);
|
|
2886
|
+
try {
|
|
2887
|
+
await new Promise((resolve$1, reject) => {
|
|
2888
|
+
server.on("close", resolve$1);
|
|
2889
|
+
server.on("error", reject);
|
|
2890
|
+
});
|
|
2891
|
+
} finally {
|
|
2892
|
+
process.off("SIGINT", shutdown);
|
|
2893
|
+
process.off("SIGTERM", shutdown);
|
|
2894
|
+
await clearRelayPidFile();
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2549
2897
|
const start = defineCommand({
|
|
2550
2898
|
meta: {
|
|
2551
2899
|
name: "start",
|
|
2552
2900
|
description: "Start the copilot-relay HTTP server."
|
|
2553
2901
|
},
|
|
2902
|
+
async run() {
|
|
2903
|
+
await startRelay();
|
|
2904
|
+
}
|
|
2905
|
+
});
|
|
2906
|
+
|
|
2907
|
+
//#endregion
|
|
2908
|
+
//#region src/restart.ts
|
|
2909
|
+
const restart = defineCommand({
|
|
2910
|
+
meta: {
|
|
2911
|
+
name: "restart",
|
|
2912
|
+
description: "Stop any existing copilot-relay instance and start a new one."
|
|
2913
|
+
},
|
|
2554
2914
|
async run() {
|
|
2555
2915
|
const appConfig = await readAppConfig();
|
|
2556
2916
|
setLogLevel(appConfig.logLevel);
|
|
2557
2917
|
await cleanupLogs(appConfig.logRetentionDays);
|
|
2558
|
-
const
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
log.info(`
|
|
2578
|
-
log.info(`Upstream timeout: ${appConfig.upstreamTimeoutSeconds}s`);
|
|
2579
|
-
log.info(`Exposed models: ${getExposedModelIds().join(", ")}`);
|
|
2580
|
-
const authSession = await setupProxyAuth(config);
|
|
2581
|
-
try {
|
|
2582
|
-
await validateUpstream(config, appConfig.thinkEffort);
|
|
2583
|
-
} catch (error) {
|
|
2584
|
-
log.error("Startup preflight failed:", error);
|
|
2585
|
-
process.exit(1);
|
|
2586
|
-
}
|
|
2587
|
-
const server = startServer(config);
|
|
2588
|
-
log.info(`copilot-relay version: ${appVersion}`);
|
|
2589
|
-
if (authSession.githubLogin) log.info(`GitHub user: ${authSession.githubLogin}`);
|
|
2590
|
-
else log.error("GitHub user: unavailable");
|
|
2591
|
-
log.info(`copilot-relay listening on http://${config.host}:${config.port}`);
|
|
2592
|
-
log.info(`copilot base url: ${config.copilotBaseUrl}`);
|
|
2593
|
-
const baseUrl = `http://${config.host}:${config.port}`;
|
|
2594
|
-
if (appConfig.claudeSetup) try {
|
|
2595
|
-
const claudeResult = await applyClaudeConfig({
|
|
2596
|
-
baseUrl,
|
|
2597
|
-
configPath: claudeConfigPath$1
|
|
2598
|
-
});
|
|
2599
|
-
if (claudeResult.changed) {
|
|
2600
|
-
log.info(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);
|
|
2601
|
-
if (claudeResult.previousBaseUrl && claudeResult.previousBaseUrl !== baseUrl) log.info(`ANTHROPIC_BASE_URL: ${claudeResult.previousBaseUrl} → ${baseUrl}`);
|
|
2602
|
-
} else log.info(`claude settings already up to date: ${claudeResult.configPath}`);
|
|
2603
|
-
} catch (error) {
|
|
2604
|
-
log.error(`Could not update claude settings (${claudeConfigPath$1}):`, error);
|
|
2605
|
-
}
|
|
2606
|
-
watchAppConfig((nextConfig) => {
|
|
2607
|
-
applyRuntimeConfig(nextConfig);
|
|
2608
|
-
log.info(`Config reloaded: logLevel=${nextConfig.logLevel} thinkEffort=${nextConfig.thinkEffort} upstreamTimeoutSeconds=${nextConfig.upstreamTimeoutSeconds}`);
|
|
2609
|
-
});
|
|
2610
|
-
await new Promise((resolve$1, reject) => {
|
|
2611
|
-
server.on("close", resolve$1);
|
|
2612
|
-
server.on("error", reject);
|
|
2613
|
-
});
|
|
2918
|
+
const stopped = await stopExistingRelay(readProxyConfig(appConfig));
|
|
2919
|
+
if (stopped.length > 0) log.info(`Stopped copilot-relay pid(s): ${stopped.join(", ")}`);
|
|
2920
|
+
log.info("Starting copilot-relay");
|
|
2921
|
+
await startRelay(appConfig);
|
|
2922
|
+
}
|
|
2923
|
+
});
|
|
2924
|
+
|
|
2925
|
+
//#endregion
|
|
2926
|
+
//#region src/stop.ts
|
|
2927
|
+
const stop = defineCommand({
|
|
2928
|
+
meta: {
|
|
2929
|
+
name: "stop",
|
|
2930
|
+
description: "Stop all detected copilot-relay server instances."
|
|
2931
|
+
},
|
|
2932
|
+
async run() {
|
|
2933
|
+
const appConfig = await readAppConfig();
|
|
2934
|
+
setLogLevel(appConfig.logLevel);
|
|
2935
|
+
await cleanupLogs(appConfig.logRetentionDays);
|
|
2936
|
+
const stopped = await stopExistingRelay(readProxyConfig(appConfig));
|
|
2937
|
+
if (stopped.length > 0) log.info(`Stopped copilot-relay pid(s): ${stopped.join(", ")}`);
|
|
2614
2938
|
}
|
|
2615
2939
|
});
|
|
2616
2940
|
|
|
@@ -2623,7 +2947,9 @@ await runMain(defineCommand({
|
|
|
2623
2947
|
},
|
|
2624
2948
|
subCommands: {
|
|
2625
2949
|
auth,
|
|
2626
|
-
|
|
2950
|
+
restart,
|
|
2951
|
+
start,
|
|
2952
|
+
stop
|
|
2627
2953
|
}
|
|
2628
2954
|
}));
|
|
2629
2955
|
|