copilot-relay 0.2.1 → 0.2.3
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 +29 -6
- package/config.default.yaml +8 -1
- package/dist/main.js +166 -25
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,8 +36,18 @@ Routing:
|
|
|
36
36
|
|
|
37
37
|
| Requested model | Upstream model |
|
|
38
38
|
| --- | --- |
|
|
39
|
-
| contains `opus` | `claude-opus-
|
|
40
|
-
|
|
|
39
|
+
| contains `opus` | `claude-opus-5` |
|
|
40
|
+
| `gpt-5.6-sol[1m]`, plain `gpt-5.6-sol`, or another non-Opus alias | `gpt-5.6-sol` |
|
|
41
|
+
|
|
42
|
+
The relay advertises `gpt-5.6-sol[1m]` to Claude Code. The `[1m]` selector
|
|
43
|
+
changes Claude Code's client-side context budgeting only; every Copilot request
|
|
44
|
+
still uses `gpt-5.6-sol`, and the relay cannot enlarge GitHub Copilot's upstream
|
|
45
|
+
capacity. An explicit CLI or API model override that bypasses the managed Claude
|
|
46
|
+
settings is outside this guarantee.
|
|
47
|
+
|
|
48
|
+
Claude Code can still show its built-in Haiku and Sonnet picker entries. The
|
|
49
|
+
managed `model` field sets the startup default but does not restrict
|
|
50
|
+
`availableModels`; selecting any non-Opus alias still follows the GPT route.
|
|
41
51
|
|
|
42
52
|
## Install & run
|
|
43
53
|
|
|
@@ -48,13 +58,18 @@ npx copilot-relay@latest restart
|
|
|
48
58
|
npx copilot-relay@latest stop
|
|
49
59
|
```
|
|
50
60
|
|
|
51
|
-
`start`
|
|
61
|
+
With `claudeSetup: true`, `start` manages `ANTHROPIC_BASE_URL`, a dummy
|
|
62
|
+
`ANTHROPIC_AUTH_TOKEN` when auth is absent, and the configured GPT default via
|
|
63
|
+
the top-level `model` field in `~/.claude/settings.json`. Exact
|
|
64
|
+
`gpt-5.6-sol` model overrides are normalized to the Claude-facing
|
|
65
|
+
`gpt-5.6-sol[1m]` identity; unrelated model choices are preserved.
|
|
52
66
|
|
|
53
67
|
## Config
|
|
54
68
|
|
|
55
69
|
Config lives at `~/.copilot-relay/config.yaml` and is hot-reloaded:
|
|
56
70
|
|
|
57
71
|
```yaml
|
|
72
|
+
configVersion: 2
|
|
58
73
|
host: 127.0.0.1
|
|
59
74
|
port: 4142
|
|
60
75
|
copilotBaseUrl: https://api.githubcopilot.com
|
|
@@ -66,6 +81,10 @@ upstreamTimeoutSeconds: 180
|
|
|
66
81
|
webSearchBackend:
|
|
67
82
|
```
|
|
68
83
|
|
|
84
|
+
`configVersion` is managed by copilot-relay. It gates one-time migrations that
|
|
85
|
+
update a shipped default on installs which already persisted the previous value;
|
|
86
|
+
do not edit it.
|
|
87
|
+
|
|
69
88
|
`logLevel` controls verbosity:
|
|
70
89
|
|
|
71
90
|
| Level | Logs |
|
|
@@ -105,13 +124,17 @@ Upstream failures are logged at `error` with full request and response context i
|
|
|
105
124
|
Unsupported Claude API requests are logged at `error` with the local method/path
|
|
106
125
|
and detailed request payload.
|
|
107
126
|
|
|
108
|
-
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.
|
|
109
132
|
|
|
110
133
|
Quick inspection:
|
|
111
134
|
|
|
112
135
|
```sh
|
|
113
|
-
tail -f ~/.copilot-relay/logs/copilot-relay.log
|
|
114
|
-
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
|
|
115
138
|
```
|
|
116
139
|
|
|
117
140
|
See [`docs/troubleshooting.md`](docs/troubleshooting.md) for common debugging workflows.
|
package/config.default.yaml
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
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
7
|
|
|
8
|
+
# Config schema version. Managed by copilot-relay; do not edit.
|
|
9
|
+
# Used to apply one-time migrations when a shipped default changes.
|
|
10
|
+
configVersion: 2
|
|
11
|
+
|
|
8
12
|
# Local host for the Claude Code-compatible HTTP server.
|
|
9
13
|
host: 127.0.0.1
|
|
10
14
|
|
|
@@ -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,11 +399,27 @@ 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
|
-
const
|
|
405
|
+
const exactGpt56SolPattern = /^gpt-5\.6-sol(?:\[1m\])*$/i;
|
|
406
|
+
const claudeContextModelId = "gpt-5.6-sol[1m]";
|
|
407
|
+
const copilotModelId = "gpt-5.6-sol";
|
|
408
|
+
const normalizeClaudeModelId = (model) => exactGpt56SolPattern.test(model) ? claudeContextModelId : model;
|
|
409
|
+
const normalizeCopilotModelId = (model) => exactGpt56SolPattern.test(model) ? copilotModelId : model;
|
|
410
|
+
const getConfiguredModelRouting = () => runtimeState.modelRouting ?? defaultModelRouting;
|
|
411
|
+
const getModelRouting = () => {
|
|
412
|
+
const routing = getConfiguredModelRouting();
|
|
413
|
+
return {
|
|
414
|
+
gptModel: normalizeCopilotModelId(routing.gptModel),
|
|
415
|
+
opusModel: normalizeCopilotModelId(routing.opusModel)
|
|
416
|
+
};
|
|
417
|
+
};
|
|
353
418
|
const getExposedModelIds = () => {
|
|
419
|
+
const routing = getConfiguredModelRouting();
|
|
420
|
+
return [normalizeClaudeModelId(routing.gptModel), routing.opusModel];
|
|
421
|
+
};
|
|
422
|
+
const getUpstreamModelIds = () => {
|
|
354
423
|
const routing = getModelRouting();
|
|
355
424
|
return [routing.gptModel, routing.opusModel];
|
|
356
425
|
};
|
|
@@ -366,14 +435,31 @@ const logLevels = [
|
|
|
366
435
|
"info",
|
|
367
436
|
"debug"
|
|
368
437
|
];
|
|
438
|
+
/**
|
|
439
|
+
* Schema version of ~/.copilot-relay/config.yaml.
|
|
440
|
+
*
|
|
441
|
+
* Bumped whenever a shipped default changes in a way that must reach installs
|
|
442
|
+
* that already persisted the previous value. See supersededOpusModels.
|
|
443
|
+
*/
|
|
444
|
+
const currentConfigVersion = 2;
|
|
445
|
+
/**
|
|
446
|
+
* Opus defaults shipped by earlier releases.
|
|
447
|
+
*
|
|
448
|
+
* readAppConfig() writes the resolved config back to disk, so every install
|
|
449
|
+
* that ever ran an older build has the superseded default sitting in its
|
|
450
|
+
* config.yaml. Without this set, bumping defaultConfig.opusModel would only
|
|
451
|
+
* ever reach fresh installs.
|
|
452
|
+
*/
|
|
453
|
+
const supersededOpusModels = new Set(["claude-opus-4.8"]);
|
|
369
454
|
const defaultConfig = {
|
|
370
455
|
claudeSetup: true,
|
|
456
|
+
configVersion: currentConfigVersion,
|
|
371
457
|
copilotBaseUrl: "https://api.githubcopilot.com",
|
|
372
458
|
gptModel: "gpt-5.6-sol",
|
|
373
459
|
host: "127.0.0.1",
|
|
374
460
|
logLevel: "info",
|
|
375
461
|
logRetentionDays: 3,
|
|
376
|
-
opusModel: "claude-opus-
|
|
462
|
+
opusModel: "claude-opus-5",
|
|
377
463
|
port: 4142,
|
|
378
464
|
thinkEffort: defaultReasoningEffort,
|
|
379
465
|
upstreamTimeoutSeconds: 180,
|
|
@@ -457,6 +543,10 @@ const parseConfigYaml = (content) => {
|
|
|
457
543
|
case "claude_setup":
|
|
458
544
|
config.claudeSetup = unquoteYamlScalar(value);
|
|
459
545
|
break;
|
|
546
|
+
case "configVersion":
|
|
547
|
+
case "config_version":
|
|
548
|
+
config.configVersion = unquoteYamlScalar(value);
|
|
549
|
+
break;
|
|
460
550
|
case "copilotBaseUrl":
|
|
461
551
|
case "copilot_base_url":
|
|
462
552
|
config.copilotBaseUrl = unquoteYamlScalar(value);
|
|
@@ -511,6 +601,9 @@ const serializeConfig = (config) => [
|
|
|
511
601
|
"#",
|
|
512
602
|
"# This file is hot-reloaded while copilot-relay is running.",
|
|
513
603
|
"",
|
|
604
|
+
"# Config schema version. Managed by copilot-relay; do not edit.",
|
|
605
|
+
`configVersion: ${config.configVersion}`,
|
|
606
|
+
"",
|
|
514
607
|
"# Local host for the Claude Code-compatible HTTP server.",
|
|
515
608
|
`host: ${config.host}`,
|
|
516
609
|
"",
|
|
@@ -552,6 +645,17 @@ const serializeConfig = (config) => [
|
|
|
552
645
|
`opusModel: ${config.opusModel}`,
|
|
553
646
|
""
|
|
554
647
|
].join("\n");
|
|
648
|
+
/**
|
|
649
|
+
* Resolve opusModel, applying the one-time superseded-default migration.
|
|
650
|
+
*
|
|
651
|
+
* Gated on configVersion so it runs exactly once. Without that gate the rewrite
|
|
652
|
+
* would re-run on every start, which would make a superseded model impossible
|
|
653
|
+
* to pin deliberately - the user's edit would be reverted underneath them.
|
|
654
|
+
*/
|
|
655
|
+
const resolveOpusModel = (rawOpusModel, configVersion) => {
|
|
656
|
+
if (rawOpusModel === void 0) return defaultConfig.opusModel;
|
|
657
|
+
return configVersion < currentConfigVersion && supersededOpusModels.has(rawOpusModel) ? defaultConfig.opusModel : rawOpusModel;
|
|
658
|
+
};
|
|
555
659
|
async function readAppConfig() {
|
|
556
660
|
const raw = await readRawConfig();
|
|
557
661
|
const claudeSetup = normalizeBoolean(raw.claudeSetup);
|
|
@@ -561,14 +665,16 @@ async function readAppConfig() {
|
|
|
561
665
|
const port = normalizePort(raw.port);
|
|
562
666
|
const thinkEffort = normalizeThinkEffort(raw.thinkEffort);
|
|
563
667
|
const upstreamTimeoutSeconds = normalizeUpstreamTimeoutSeconds(raw.upstreamTimeoutSeconds);
|
|
668
|
+
const configVersion = normalizePositiveInteger(raw.configVersion) ?? 1;
|
|
564
669
|
const config = {
|
|
565
670
|
claudeSetup: claudeSetup ?? defaultConfig.claudeSetup,
|
|
671
|
+
configVersion: currentConfigVersion,
|
|
566
672
|
copilotBaseUrl: normalizeString(raw.copilotBaseUrl) ?? defaultConfig.copilotBaseUrl,
|
|
567
673
|
gptModel: normalizeString(raw.gptModel) ?? defaultConfig.gptModel,
|
|
568
674
|
host: host ?? defaultConfig.host,
|
|
569
675
|
logLevel: logLevel ?? defaultConfig.logLevel,
|
|
570
676
|
logRetentionDays: logRetentionDays$1 ?? defaultConfig.logRetentionDays,
|
|
571
|
-
opusModel: normalizeString(raw.opusModel)
|
|
677
|
+
opusModel: resolveOpusModel(normalizeString(raw.opusModel), configVersion),
|
|
572
678
|
port: port ?? defaultConfig.port,
|
|
573
679
|
thinkEffort: thinkEffort ?? defaultConfig.thinkEffort,
|
|
574
680
|
upstreamTimeoutSeconds: upstreamTimeoutSeconds ?? defaultConfig.upstreamTimeoutSeconds,
|
|
@@ -806,13 +912,30 @@ const stopExistingRelay = async (config) => {
|
|
|
806
912
|
|
|
807
913
|
//#endregion
|
|
808
914
|
//#region src/lib/claude-settings.ts
|
|
915
|
+
const knownModelEnvKeys = [
|
|
916
|
+
"ANTHROPIC_MODEL",
|
|
917
|
+
"ANTHROPIC_DEFAULT_SONNET_MODEL",
|
|
918
|
+
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
|
|
919
|
+
"ANTHROPIC_DEFAULT_OPUS_MODEL",
|
|
920
|
+
"ANTHROPIC_DEFAULT_FABLE_MODEL",
|
|
921
|
+
"ANTHROPIC_SMALL_FAST_MODEL",
|
|
922
|
+
"CLAUDE_CODE_SUBAGENT_MODEL"
|
|
923
|
+
];
|
|
924
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
925
|
+
const hasMalformedModelOverride = (parsed) => {
|
|
926
|
+
if (parsed.model !== void 0 && typeof parsed.model !== "string") return true;
|
|
927
|
+
const env = parsed.env;
|
|
928
|
+
if (!isRecord$1(env)) return false;
|
|
929
|
+
return knownModelEnvKeys.some((key) => env[key] !== void 0 && typeof env[key] !== "string");
|
|
930
|
+
};
|
|
931
|
+
const hasPrimaryModelOverride = (parsed, env) => typeof parsed.model === "string" || typeof env.ANTHROPIC_MODEL === "string";
|
|
809
932
|
/**
|
|
810
933
|
* Update `~/.claude/settings.json` so its env block points Claude Code at the
|
|
811
|
-
* running proxy. Preserves
|
|
812
|
-
*
|
|
934
|
+
* running proxy. Preserves unrelated keys and model choices while normalizing
|
|
935
|
+
* managed gpt-5.6-sol overrides. Sets a dummy auth token only if none is present.
|
|
813
936
|
*/
|
|
814
937
|
async function applyClaudeConfig(input) {
|
|
815
|
-
const { configPath: configPath$1, baseUrl } = input;
|
|
938
|
+
const { configPath: configPath$1, baseUrl, gptModel } = input;
|
|
816
939
|
let raw = "";
|
|
817
940
|
let created = false;
|
|
818
941
|
try {
|
|
@@ -822,9 +945,19 @@ async function applyClaudeConfig(input) {
|
|
|
822
945
|
else throw error;
|
|
823
946
|
}
|
|
824
947
|
let parsed = {};
|
|
948
|
+
if (!created && raw.trim().length === 0) return {
|
|
949
|
+
configPath: configPath$1,
|
|
950
|
+
changed: false,
|
|
951
|
+
created: false
|
|
952
|
+
};
|
|
825
953
|
if (raw.trim().length > 0) try {
|
|
826
|
-
|
|
827
|
-
if (
|
|
954
|
+
const value = JSON.parse(raw);
|
|
955
|
+
if (!isRecord$1(value) || value.env !== void 0 && !isRecord$1(value.env) || hasMalformedModelOverride(value)) return {
|
|
956
|
+
configPath: configPath$1,
|
|
957
|
+
changed: false,
|
|
958
|
+
created: false
|
|
959
|
+
};
|
|
960
|
+
parsed = value;
|
|
828
961
|
} catch {
|
|
829
962
|
return {
|
|
830
963
|
configPath: configPath$1,
|
|
@@ -832,12 +965,19 @@ async function applyClaudeConfig(input) {
|
|
|
832
965
|
created: false
|
|
833
966
|
};
|
|
834
967
|
}
|
|
835
|
-
const env =
|
|
968
|
+
const env = isRecord$1(parsed.env) ? { ...parsed.env } : {};
|
|
836
969
|
const previousBaseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
|
|
970
|
+
const hadPrimaryModelOverride = hasPrimaryModelOverride(parsed, env);
|
|
971
|
+
const claudeGptModel = normalizeClaudeModelId(gptModel);
|
|
972
|
+
const shouldSeedTopLevelModel = !hadPrimaryModelOverride;
|
|
973
|
+
const nextModel = typeof parsed.model === "string" ? normalizeClaudeModelId(parsed.model) : void 0;
|
|
974
|
+
for (const key of knownModelEnvKeys) if (typeof env[key] === "string") env[key] = normalizeClaudeModelId(env[key]);
|
|
837
975
|
env.ANTHROPIC_BASE_URL = baseUrl;
|
|
838
976
|
if (typeof env.ANTHROPIC_AUTH_TOKEN !== "string" || !env.ANTHROPIC_AUTH_TOKEN) env.ANTHROPIC_AUTH_TOKEN = "dummy";
|
|
839
977
|
const next = {
|
|
840
978
|
...parsed,
|
|
979
|
+
...nextModel !== void 0 && { model: nextModel },
|
|
980
|
+
...shouldSeedTopLevelModel && { model: claudeGptModel },
|
|
841
981
|
env
|
|
842
982
|
};
|
|
843
983
|
const serialized = `${JSON.stringify(next, null, 2)}\n`;
|
|
@@ -1539,7 +1679,7 @@ async function shouldRetryWithResponses(response, signal, timeoutMs) {
|
|
|
1539
1679
|
|
|
1540
1680
|
//#endregion
|
|
1541
1681
|
//#region src/lib/preflight.ts
|
|
1542
|
-
const
|
|
1682
|
+
const fetchUpstreamModelIds = async (config) => {
|
|
1543
1683
|
const provider = getCopilotProviderContext(config);
|
|
1544
1684
|
const signal = createCopilotRequestSignal(void 0, config.upstreamTimeoutMs);
|
|
1545
1685
|
const response = await fetchCopilot(provider, "/models", {
|
|
@@ -1554,8 +1694,8 @@ const getUpstreamModelIds = async (config) => {
|
|
|
1554
1694
|
return new Set((payload.data ?? []).map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0));
|
|
1555
1695
|
};
|
|
1556
1696
|
const ensureRequiredModels = async (config) => {
|
|
1557
|
-
const upstreamModels = await
|
|
1558
|
-
const requiredModels =
|
|
1697
|
+
const upstreamModels = await fetchUpstreamModelIds(config);
|
|
1698
|
+
const requiredModels = getUpstreamModelIds();
|
|
1559
1699
|
const missingModels = requiredModels.filter((model) => !upstreamModels.has(model));
|
|
1560
1700
|
if (missingModels.length > 0) throw new Error(`Required Copilot model(s) unavailable upstream: ${missingModels.join(", ")}`);
|
|
1561
1701
|
log.info(`Upstream models available: ${requiredModels.join(", ")}`);
|
|
@@ -1589,7 +1729,7 @@ const validateModelRequest = async (config, model, thinkEffort) => {
|
|
|
1589
1729
|
const validateUpstream = async (config, thinkEffort) => {
|
|
1590
1730
|
log.info("Running upstream preflight");
|
|
1591
1731
|
await ensureRequiredModels(config);
|
|
1592
|
-
for (const model of
|
|
1732
|
+
for (const model of getUpstreamModelIds()) await validateModelRequest(config, model, thinkEffort);
|
|
1593
1733
|
};
|
|
1594
1734
|
|
|
1595
1735
|
//#endregion
|
|
@@ -1865,7 +2005,7 @@ function translateToClaude(response, toolNameMapper = createClaudeToolNameMapper
|
|
|
1865
2005
|
id: response.id,
|
|
1866
2006
|
type: "message",
|
|
1867
2007
|
role: "assistant",
|
|
1868
|
-
model: response.model,
|
|
2008
|
+
model: normalizeClaudeModelId(response.model),
|
|
1869
2009
|
content: [
|
|
1870
2010
|
...allThinkingBlocks,
|
|
1871
2011
|
...allTextBlocks,
|
|
@@ -1945,7 +2085,7 @@ function translateChunkToClaudeEvents(chunk, state, toolNameMapper = createClaud
|
|
|
1945
2085
|
type: "message",
|
|
1946
2086
|
role: "assistant",
|
|
1947
2087
|
content: [],
|
|
1948
|
-
model: chunk.model,
|
|
2088
|
+
model: normalizeClaudeModelId(chunk.model),
|
|
1949
2089
|
stop_reason: null,
|
|
1950
2090
|
stop_sequence: null,
|
|
1951
2091
|
usage: {
|
|
@@ -2083,7 +2223,7 @@ const isClaudeCodeWebSearchTool = (tool) => tool.name === claudeCodeWebSearchToo
|
|
|
2083
2223
|
const isClaudeWebSearchTool = (tool) => isAnthropicNativeWebSearchTool(tool) || isClaudeCodeWebSearchTool(tool);
|
|
2084
2224
|
const hasClaudeWebSearch = (payload) => payload.tools?.some(isClaudeWebSearchTool) ?? false;
|
|
2085
2225
|
const isClaudeWebSearchToolName = (name) => name === "web_search" || name === claudeCodeWebSearchToolName;
|
|
2086
|
-
const getWebSearchBackendModel = (config) => config.webSearchBackend?.trim() || getModelRouting().gptModel;
|
|
2226
|
+
const getWebSearchBackendModel = (config) => normalizeCopilotModelId(config.webSearchBackend?.trim() || getModelRouting().gptModel);
|
|
2087
2227
|
const prepareClaudeWebSearchDecisionPayload = (payload) => {
|
|
2088
2228
|
if (!hasClaudeWebSearch(payload)) return payload;
|
|
2089
2229
|
return {
|
|
@@ -2257,7 +2397,7 @@ const createClaudeWebSearchResponse = (search) => {
|
|
|
2257
2397
|
type: "message",
|
|
2258
2398
|
role: "assistant",
|
|
2259
2399
|
content,
|
|
2260
|
-
model: search.model,
|
|
2400
|
+
model: normalizeClaudeModelId(search.model),
|
|
2261
2401
|
stop_reason: "end_turn",
|
|
2262
2402
|
stop_sequence: null,
|
|
2263
2403
|
usage: {
|
|
@@ -2845,7 +2985,8 @@ async function startRelay(appConfig) {
|
|
|
2845
2985
|
if (appConfig.claudeSetup) try {
|
|
2846
2986
|
const claudeResult = await applyClaudeConfig({
|
|
2847
2987
|
baseUrl,
|
|
2848
|
-
configPath: claudeConfigPath$1
|
|
2988
|
+
configPath: claudeConfigPath$1,
|
|
2989
|
+
gptModel: appConfig.gptModel
|
|
2849
2990
|
});
|
|
2850
2991
|
if (claudeResult.changed) {
|
|
2851
2992
|
log.info(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);
|