copilot-relay 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/main.js +51 -12
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,8 +135,10 @@ check that proves the relay can actually serve Claude Code — a relay whose
|
|
|
135
135
|
Copilot token expired an hour ago still answers `/healthz` and `/v1/models`,
|
|
136
136
|
because neither contacts upstream. It is opt-in because it spends a few tokens.
|
|
137
137
|
|
|
138
|
-
`--json` emits machine-readable output. Exit codes: `0` running
|
|
139
|
-
running, `2` running but the
|
|
138
|
+
`--json` emits machine-readable output. Exit codes: `0` running and reachable,
|
|
139
|
+
`1` not running, `2` running but not usable — the health probe failed, or
|
|
140
|
+
`--deep` was requested and failed. `0` requires a live process *and* a passing
|
|
141
|
+
health probe, so a relay that cannot answer `/healthz` never reports success.
|
|
140
142
|
|
|
141
143
|
## Logging
|
|
142
144
|
|
package/dist/main.js
CHANGED
|
@@ -7,11 +7,11 @@ import os from "node:os";
|
|
|
7
7
|
import path, { dirname, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
|
+
import { createAdaptorServer } from "@hono/node-server";
|
|
10
11
|
import { createHash, randomUUID } from "node:crypto";
|
|
11
12
|
import { Agent, fetch as fetch$1 } from "undici";
|
|
12
13
|
import { events } from "fetch-event-stream";
|
|
13
14
|
import { readFileSync } from "node:fs";
|
|
14
|
-
import { createAdaptorServer } from "@hono/node-server";
|
|
15
15
|
import { Hono } from "hono";
|
|
16
16
|
import { cors } from "hono/cors";
|
|
17
17
|
import { streamSSE } from "hono/streaming";
|
|
@@ -864,6 +864,27 @@ const isRelayPid = async (pid) => {
|
|
|
864
864
|
if (!command) return false;
|
|
865
865
|
return isRelayStartProcess(command, await getProcessCwd(pid));
|
|
866
866
|
};
|
|
867
|
+
/**
|
|
868
|
+
* The relay serving `config.port`, or undefined if nothing is.
|
|
869
|
+
*
|
|
870
|
+
* Deliberately narrower than findRelayProcessIds: it never consults the global
|
|
871
|
+
* process list. `stop` wants "any relay anywhere" so it can clean up strays;
|
|
872
|
+
* `status` is asked about one configured relay, and answering with a different
|
|
873
|
+
* one is worse than answering "not running" because it looks authoritative.
|
|
874
|
+
*
|
|
875
|
+
* Returns pid and address as a single coherent record, so a pid discovered one
|
|
876
|
+
* way can never be printed next to an address taken from another.
|
|
877
|
+
*/
|
|
878
|
+
const findRelayOnPort = async (config) => {
|
|
879
|
+
const entry = await readRelayPidFileEntry();
|
|
880
|
+
if (entry && entry.port === config.port && isProcessAlive(entry.pid) && await isRelayPid(entry.pid)) return entry;
|
|
881
|
+
for (const pid of await getPortListenerPids(config.port)) if (await isRelayPid(pid)) return {
|
|
882
|
+
host: config.host,
|
|
883
|
+
pid,
|
|
884
|
+
port: config.port,
|
|
885
|
+
startedAt: ""
|
|
886
|
+
};
|
|
887
|
+
};
|
|
867
888
|
const findRelayProcessIds = async (config) => {
|
|
868
889
|
const candidates = /* @__PURE__ */ new Set();
|
|
869
890
|
const storedPid = await readRelayPidFile();
|
|
@@ -2939,6 +2960,7 @@ const startServer = (config) => new Promise((resolve$1, reject) => {
|
|
|
2939
2960
|
|
|
2940
2961
|
//#endregion
|
|
2941
2962
|
//#region src/start.ts
|
|
2963
|
+
const canCloseConnections = (server) => typeof server.closeIdleConnections === "function" && typeof server.closeAllConnections === "function";
|
|
2942
2964
|
async function startRelay(appConfig) {
|
|
2943
2965
|
appConfig ??= await readAppConfig();
|
|
2944
2966
|
setLogLevel(appConfig.logLevel);
|
|
@@ -2997,6 +3019,7 @@ async function startRelay(appConfig) {
|
|
|
2997
3019
|
applyRuntimeConfig(nextConfig);
|
|
2998
3020
|
log.info(`Config reloaded: logLevel=${nextConfig.logLevel} thinkEffort=${nextConfig.thinkEffort} upstreamTimeoutSeconds=${nextConfig.upstreamTimeoutSeconds}`);
|
|
2999
3021
|
});
|
|
3022
|
+
const shutdownGraceMs = 2e3;
|
|
3000
3023
|
const shutdown = (signal) => {
|
|
3001
3024
|
log.info(`Received ${signal}; shutting down copilot-relay`);
|
|
3002
3025
|
server.close((error) => {
|
|
@@ -3005,6 +3028,13 @@ async function startRelay(appConfig) {
|
|
|
3005
3028
|
process.exitCode = 1;
|
|
3006
3029
|
}
|
|
3007
3030
|
});
|
|
3031
|
+
if (canCloseConnections(server)) {
|
|
3032
|
+
server.closeIdleConnections();
|
|
3033
|
+
setTimeout(() => {
|
|
3034
|
+
log.info("Grace period elapsed; closing remaining connections");
|
|
3035
|
+
server.closeAllConnections();
|
|
3036
|
+
}, shutdownGraceMs).unref();
|
|
3037
|
+
}
|
|
3008
3038
|
};
|
|
3009
3039
|
process.once("SIGINT", shutdown);
|
|
3010
3040
|
process.once("SIGTERM", shutdown);
|
|
@@ -3054,14 +3084,26 @@ const restart = defineCommand({
|
|
|
3054
3084
|
*
|
|
3055
3085
|
* 2 is distinct from 1 on purpose: "running but broken" and "not running" call
|
|
3056
3086
|
* for different responses - restart the service versus re-authenticate.
|
|
3087
|
+
*
|
|
3088
|
+
* A failed health probe is a 2, not a 0. Printing FAILED while exiting 0 would
|
|
3089
|
+
* make every scripted caller treat an unreachable relay as fine. See #34.
|
|
3057
3090
|
*/
|
|
3058
3091
|
const exitCodes = {
|
|
3059
|
-
deepCheckFailed: 2,
|
|
3060
3092
|
notRunning: 1,
|
|
3093
|
+
notUsable: 2,
|
|
3061
3094
|
running: 0
|
|
3062
3095
|
};
|
|
3063
3096
|
const localProbeTimeoutMs = 5e3;
|
|
3064
3097
|
const deepProbeTimeoutMs = 6e4;
|
|
3098
|
+
/**
|
|
3099
|
+
* Status to exit code. Pure, because this mapping is the contract every
|
|
3100
|
+
* scripted caller depends on and deserves direct coverage.
|
|
3101
|
+
*
|
|
3102
|
+
* 0 requires a live process *and* a passing health probe. Printing FAILED while
|
|
3103
|
+
* exiting 0 would make every scripted caller treat an unreachable relay as
|
|
3104
|
+
* fine. See #34.
|
|
3105
|
+
*/
|
|
3106
|
+
const resolveExitCode = (status$1) => !status$1.running ? exitCodes.notRunning : !status$1.health?.ok ? exitCodes.notUsable : status$1.deep && !status$1.deep.ok ? exitCodes.notUsable : exitCodes.running;
|
|
3065
3107
|
const formatUptime = (startedAt) => {
|
|
3066
3108
|
if (!startedAt) return "unknown";
|
|
3067
3109
|
const started = Date.parse(startedAt);
|
|
@@ -3189,15 +3231,12 @@ const collectStatus = async (options) => {
|
|
|
3189
3231
|
thinkEffort: appConfig.thinkEffort,
|
|
3190
3232
|
version: appVersion
|
|
3191
3233
|
};
|
|
3192
|
-
const
|
|
3193
|
-
if (
|
|
3234
|
+
const relay = await findRelayOnPort(readProxyConfig(appConfig));
|
|
3235
|
+
if (!relay) return {
|
|
3194
3236
|
...base,
|
|
3195
3237
|
running: false
|
|
3196
3238
|
};
|
|
3197
|
-
const
|
|
3198
|
-
const host = entry?.host ?? appConfig.host;
|
|
3199
|
-
const port = entry?.port ?? appConfig.port;
|
|
3200
|
-
const baseUrl = `http://${host}:${port}`;
|
|
3239
|
+
const baseUrl = `http://${relay.host}:${relay.port}`;
|
|
3201
3240
|
const health = await checkHealth(baseUrl);
|
|
3202
3241
|
const models = health.ok ? await readModels(baseUrl) : [];
|
|
3203
3242
|
const deep = options.deep && health.ok ? await checkDeep(baseUrl, models[0] ?? appConfig.gptModel) : void 0;
|
|
@@ -3207,10 +3246,10 @@ const collectStatus = async (options) => {
|
|
|
3207
3246
|
...deep ? { deep } : {},
|
|
3208
3247
|
health,
|
|
3209
3248
|
models,
|
|
3210
|
-
pid:
|
|
3211
|
-
port,
|
|
3249
|
+
pid: relay.pid,
|
|
3250
|
+
port: relay.port,
|
|
3212
3251
|
running: true,
|
|
3213
|
-
...
|
|
3252
|
+
...relay.startedAt ? { startedAt: relay.startedAt } : {}
|
|
3214
3253
|
};
|
|
3215
3254
|
};
|
|
3216
3255
|
const status = defineCommand({
|
|
@@ -3232,7 +3271,7 @@ const status = defineCommand({
|
|
|
3232
3271
|
const result = await collectStatus({ deep: Boolean(args.deep) });
|
|
3233
3272
|
if (args.json) console.log(JSON.stringify(result, null, 2));
|
|
3234
3273
|
else console.log(renderStatus(result).join("\n"));
|
|
3235
|
-
process.exitCode =
|
|
3274
|
+
process.exitCode = resolveExitCode(result);
|
|
3236
3275
|
}
|
|
3237
3276
|
});
|
|
3238
3277
|
|