copilot-relay 0.2.2 → 0.2.4
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 +13 -4
- package/config.default.yaml +8 -1
- package/dist/main.js +69 -8
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Routing:
|
|
|
36
36
|
|
|
37
37
|
| Requested model | Upstream model |
|
|
38
38
|
| --- | --- |
|
|
39
|
-
| contains `opus` | `claude-opus-
|
|
39
|
+
| contains `opus` | `claude-opus-5` |
|
|
40
40
|
| `gpt-5.6-sol[1m]`, plain `gpt-5.6-sol`, or another non-Opus alias | `gpt-5.6-sol` |
|
|
41
41
|
|
|
42
42
|
The relay advertises `gpt-5.6-sol[1m]` to Claude Code. The `[1m]` selector
|
|
@@ -80,6 +80,11 @@ upstreamTimeoutSeconds: 180
|
|
|
80
80
|
webSearchBackend:
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
copilot-relay writes the resolved config back to this file, so every key is
|
|
84
|
+
present after the first start. Shipped defaults therefore apply to fresh
|
|
85
|
+
installs only — once a value is in your `config.yaml` it is never rewritten by
|
|
86
|
+
an upgrade. To pick up a changed default, edit the key yourself.
|
|
87
|
+
|
|
83
88
|
`logLevel` controls verbosity:
|
|
84
89
|
|
|
85
90
|
| Level | Logs |
|
|
@@ -119,13 +124,17 @@ Upstream failures are logged at `error` with full request and response context i
|
|
|
119
124
|
Unsupported Claude API requests are logged at `error` with the local method/path
|
|
120
125
|
and detailed request payload.
|
|
121
126
|
|
|
122
|
-
Logs are written to `~/.copilot-relay/logs/copilot-relay
|
|
127
|
+
Logs are written to `~/.copilot-relay/logs/copilot-relay.<local-date>.log`. The
|
|
128
|
+
active file rotates at local midnight, and files older than `logRetentionDays`
|
|
129
|
+
local calendar days are deleted. Rotation is what makes retention take effect:
|
|
130
|
+
without it the single log file's mtime was refreshed by every append, so nothing
|
|
131
|
+
ever aged out.
|
|
123
132
|
|
|
124
133
|
Quick inspection:
|
|
125
134
|
|
|
126
135
|
```sh
|
|
127
|
-
tail -f ~/.copilot-relay/logs/copilot-relay.log
|
|
128
|
-
grep -n "Failed to create\\|Startup preflight failed" ~/.copilot-relay/logs/copilot-relay
|
|
136
|
+
tail -f ~/.copilot-relay/logs/copilot-relay.$(date +%F).log
|
|
137
|
+
grep -n "Failed to create\\|Startup preflight failed" ~/.copilot-relay/logs/copilot-relay.*.log
|
|
129
138
|
```
|
|
130
139
|
|
|
131
140
|
See [`docs/troubleshooting.md`](docs/troubleshooting.md) for common debugging workflows.
|
package/config.default.yaml
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
# Edit the user config file, not this template, for local changes.
|
|
5
5
|
# Most values hot-reload while the service is running. host and port still
|
|
6
6
|
# require restart because the listening socket is already bound.
|
|
7
|
+
#
|
|
8
|
+
# Values here apply to a fresh install only. copilot-relay writes the resolved
|
|
9
|
+
# config back to disk, so once a key exists in your config.yaml it is yours:
|
|
10
|
+
# changing a shipped default never rewrites it.
|
|
7
11
|
|
|
8
12
|
# Local host for the Claude Code-compatible HTTP server.
|
|
9
13
|
host: 127.0.0.1
|
|
@@ -29,6 +33,9 @@ claudeSetup: true
|
|
|
29
33
|
logLevel: info
|
|
30
34
|
|
|
31
35
|
# Number of days to keep files in ~/.copilot-relay/logs.
|
|
36
|
+
# Counted in local calendar days: 3 keeps today plus the previous two days.
|
|
37
|
+
# The active log file is copilot-relay.<local-date>.log and rotates at local
|
|
38
|
+
# midnight, which is what makes this setting take effect at all.
|
|
32
39
|
logRetentionDays: 3
|
|
33
40
|
|
|
34
41
|
# Default upstream thinking/reasoning effort sent to Copilot.
|
|
@@ -48,4 +55,4 @@ webSearchBackend:
|
|
|
48
55
|
gptModel: gpt-5.6-sol
|
|
49
56
|
|
|
50
57
|
# Upstream Copilot model used for requests containing "opus".
|
|
51
|
-
opusModel: claude-opus-
|
|
58
|
+
opusModel: claude-opus-5
|
package/dist/main.js
CHANGED
|
@@ -57,8 +57,26 @@ const configPath = path.join(appDir, "config.yaml");
|
|
|
57
57
|
const copilotTokenPath = path.join(appDir, "copilot_token.json");
|
|
58
58
|
const githubTokenPath = path.join(appDir, "github_token");
|
|
59
59
|
const logsDir = path.join(appDir, "logs");
|
|
60
|
-
const
|
|
60
|
+
const logFileBaseName = "copilot-relay";
|
|
61
|
+
const legacyLogPath = path.join(logsDir, `${logFileBaseName}.log`);
|
|
61
62
|
const pidPath = path.join(appDir, "copilot-relay.pid");
|
|
63
|
+
/**
|
|
64
|
+
* Local-date stamp used in log file names.
|
|
65
|
+
*
|
|
66
|
+
* Local rather than UTC on purpose: `logRetentionDays` is a human-facing "how
|
|
67
|
+
* many days do I keep" setting, and a UTC stamp would roll the active file over
|
|
68
|
+
* in the middle of the local afternoon for anyone west of Greenwich.
|
|
69
|
+
*/
|
|
70
|
+
const formatLogDate = (date) => [
|
|
71
|
+
date.getFullYear(),
|
|
72
|
+
String(date.getMonth() + 1).padStart(2, "0"),
|
|
73
|
+
String(date.getDate()).padStart(2, "0")
|
|
74
|
+
].join("-");
|
|
75
|
+
/**
|
|
76
|
+
* Path of the log file that owns `date`. Resolved per write rather than cached,
|
|
77
|
+
* so a long-running relay rotates at local midnight with no timer to drift.
|
|
78
|
+
*/
|
|
79
|
+
const getLogPath = (date = /* @__PURE__ */ new Date()) => path.join(logsDir, `${logFileBaseName}.${formatLogDate(date)}.log`);
|
|
62
80
|
const paths = {
|
|
63
81
|
appDir,
|
|
64
82
|
configPath,
|
|
@@ -70,7 +88,8 @@ const paths = {
|
|
|
70
88
|
path.join(homeDir, ".copilot-tennel.yaml")
|
|
71
89
|
],
|
|
72
90
|
legacyCopilotTokenPaths,
|
|
73
|
-
|
|
91
|
+
legacyLogPath,
|
|
92
|
+
logFileBaseName,
|
|
74
93
|
logsDir,
|
|
75
94
|
pidPath
|
|
76
95
|
};
|
|
@@ -103,8 +122,10 @@ async function ensureFile(filePath, options = {}) {
|
|
|
103
122
|
//#endregion
|
|
104
123
|
//#region src/lib/log.ts
|
|
105
124
|
const logCleanupCheckIntervalMs = 3600 * 1e3;
|
|
125
|
+
const millisecondsPerDay = 1440 * 60 * 1e3;
|
|
106
126
|
let logRetentionDays = 3;
|
|
107
127
|
let nextLogCleanupCheckAt = 0;
|
|
128
|
+
const datedLogFilePattern = /* @__PURE__ */ new RegExp(`^${paths.logFileBaseName}\\.(\\d{4})-(\\d{2})-(\\d{2})\\.log$`);
|
|
108
129
|
const consolaLevelByName = {
|
|
109
130
|
error: 0,
|
|
110
131
|
info: 3,
|
|
@@ -120,7 +141,22 @@ const setLogLevel = (level) => {
|
|
|
120
141
|
currentLogLevel = consolaLevelByName[level];
|
|
121
142
|
consola.level = consolaLevelByName[level];
|
|
122
143
|
};
|
|
123
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Render one logged value as a single physical line.
|
|
146
|
+
*
|
|
147
|
+
* `breakLength: Infinity` matters as much as the depth cap. The previous
|
|
148
|
+
* `inspect(value, { depth: null })` pretty-printed each payload across
|
|
149
|
+
* thousands of physical lines, which was both the dominant source of log volume
|
|
150
|
+
* and the reason the `grep` recipes in docs/logging.md returned a fragment of an
|
|
151
|
+
* object instead of the matching entry. One entry is now one line.
|
|
152
|
+
*/
|
|
153
|
+
const formatLogValue = (value) => typeof value === "string" ? value : inspect(value, {
|
|
154
|
+
breakLength: Infinity,
|
|
155
|
+
compact: true,
|
|
156
|
+
depth: 6,
|
|
157
|
+
maxArrayLength: 100,
|
|
158
|
+
maxStringLength: 4e3
|
|
159
|
+
});
|
|
124
160
|
const writeLogFile = async (level, values) => {
|
|
125
161
|
await fs.mkdir(paths.logsDir, { recursive: true });
|
|
126
162
|
await cleanupLogsIfDue();
|
|
@@ -129,7 +165,7 @@ const writeLogFile = async (level, values) => {
|
|
|
129
165
|
level,
|
|
130
166
|
values.map(formatLogValue).join(" ")
|
|
131
167
|
].join(" ");
|
|
132
|
-
await fs.appendFile(
|
|
168
|
+
await fs.appendFile(getLogPath(), `${line}\n`);
|
|
133
169
|
};
|
|
134
170
|
const cleanupLogsIfDue = async () => {
|
|
135
171
|
const now = Date.now();
|
|
@@ -144,14 +180,31 @@ const wrapFileLog = (level, fn) => ((...args) => {
|
|
|
144
180
|
consola.error = wrapFileLog("error", consola.error.bind(consola));
|
|
145
181
|
consola.info = wrapFileLog("info", consola.info.bind(consola));
|
|
146
182
|
consola.debug = wrapFileLog("debug", consola.debug.bind(consola));
|
|
183
|
+
/**
|
|
184
|
+
* Calendar day a log file belongs to, or undefined when its name carries no
|
|
185
|
+
* date stamp.
|
|
186
|
+
*
|
|
187
|
+
* The filename stamp is preferred over mtime because mtime is rewritten by
|
|
188
|
+
* backups, `cp`, and editors touching the file, any of which would silently
|
|
189
|
+
* extend or shorten the retention window.
|
|
190
|
+
*/
|
|
191
|
+
const parseLogFileDate = (fileName) => {
|
|
192
|
+
const match = datedLogFilePattern.exec(fileName);
|
|
193
|
+
if (!match) return;
|
|
194
|
+
const [, year, month, day] = match;
|
|
195
|
+
const date = new Date(Number(year), Number(month) - 1, Number(day));
|
|
196
|
+
return date.getFullYear() === Number(year) && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day) ? date : void 0;
|
|
197
|
+
};
|
|
147
198
|
const cleanupLogs = async (retentionDays) => {
|
|
148
199
|
logRetentionDays = retentionDays;
|
|
149
200
|
await fs.mkdir(paths.logsDir, { recursive: true });
|
|
150
|
-
const
|
|
201
|
+
const startOfToday = /* @__PURE__ */ new Date();
|
|
202
|
+
startOfToday.setHours(0, 0, 0, 0);
|
|
203
|
+
const cutoff = startOfToday.getTime() - (retentionDays - 1) * millisecondsPerDay;
|
|
151
204
|
const entries = await fs.readdir(paths.logsDir, { withFileTypes: true });
|
|
152
205
|
await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".log")).map(async (entry) => {
|
|
153
206
|
const filePath = `${paths.logsDir}/${entry.name}`;
|
|
154
|
-
if ((await fs.stat(filePath)).mtimeMs < cutoff) await fs.
|
|
207
|
+
if ((parseLogFileDate(entry.name)?.getTime() ?? (await fs.stat(filePath)).mtimeMs) < cutoff) await fs.rm(filePath, { force: true });
|
|
155
208
|
}));
|
|
156
209
|
};
|
|
157
210
|
const log = consola;
|
|
@@ -346,7 +399,7 @@ const runtimeState = {};
|
|
|
346
399
|
const defaultReasoningEffort = "max";
|
|
347
400
|
const defaultModelRouting = {
|
|
348
401
|
gptModel: "gpt-5.6-sol",
|
|
349
|
-
opusModel: "claude-opus-
|
|
402
|
+
opusModel: "claude-opus-5"
|
|
350
403
|
};
|
|
351
404
|
const isReasoningEffort = (value) => value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
|
|
352
405
|
const exactGpt56SolPattern = /^gpt-5\.6-sol(?:\[1m\])*$/i;
|
|
@@ -382,6 +435,14 @@ const logLevels = [
|
|
|
382
435
|
"info",
|
|
383
436
|
"debug"
|
|
384
437
|
];
|
|
438
|
+
/**
|
|
439
|
+
* Values used when a key is absent from the user's config.
|
|
440
|
+
*
|
|
441
|
+
* readAppConfig() writes the resolved config back to disk, so an existing
|
|
442
|
+
* install has every key materialized and never consults these again. Changing
|
|
443
|
+
* one therefore affects fresh installs only, by design: copilot-relay does not
|
|
444
|
+
* rewrite a value the user's config already holds.
|
|
445
|
+
*/
|
|
385
446
|
const defaultConfig = {
|
|
386
447
|
claudeSetup: true,
|
|
387
448
|
copilotBaseUrl: "https://api.githubcopilot.com",
|
|
@@ -389,7 +450,7 @@ const defaultConfig = {
|
|
|
389
450
|
host: "127.0.0.1",
|
|
390
451
|
logLevel: "info",
|
|
391
452
|
logRetentionDays: 3,
|
|
392
|
-
opusModel: "claude-opus-
|
|
453
|
+
opusModel: "claude-opus-5",
|
|
393
454
|
port: 4142,
|
|
394
455
|
thinkEffort: defaultReasoningEffort,
|
|
395
456
|
upstreamTimeoutSeconds: 180,
|