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 CHANGED
@@ -36,7 +36,7 @@ Routing:
36
36
 
37
37
  | Requested model | Upstream model |
38
38
  | --- | --- |
39
- | contains `opus` | `claude-opus-4.8` |
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.log`; old `.log` files are cleaned according to `logRetentionDays`.
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.log
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.
@@ -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-4.8
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 logPath = path.join(logsDir, "copilot-relay.log");
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
- logPath,
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
- const formatLogValue = (value) => typeof value === "string" ? value : inspect(value, { depth: null });
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(paths.logPath, `${line}\n`);
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 cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
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.unlink(filePath);
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-4.8"
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-4.8",
453
+ opusModel: "claude-opus-5",
393
454
  port: 4142,
394
455
  thinkEffort: defaultReasoningEffort,
395
456
  upstreamTimeoutSeconds: 180,