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 CHANGED
@@ -36,8 +36,18 @@ Routing:
36
36
 
37
37
  | Requested model | Upstream model |
38
38
  | --- | --- |
39
- | contains `opus` | `claude-opus-4.8` |
40
- | anything else | `gpt-5.6-sol` |
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` writes `ANTHROPIC_BASE_URL` and a dummy `ANTHROPIC_AUTH_TOKEN` into `~/.claude/settings.json`.
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.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.
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.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
115
138
  ```
116
139
 
117
140
  See [`docs/troubleshooting.md`](docs/troubleshooting.md) for common debugging workflows.
@@ -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-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,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-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
- const getModelRouting = () => runtimeState.modelRouting ?? defaultModelRouting;
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-4.8",
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) ?? defaultConfig.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 all unrelated keys (model overrides, plugins,
812
- * marketplaces, etc.). Sets a dummy auth token only if none is present.
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
- parsed = JSON.parse(raw);
827
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) parsed = {};
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 = typeof parsed.env === "object" && parsed.env !== null && !Array.isArray(parsed.env) ? { ...parsed.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 getUpstreamModelIds = async (config) => {
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 getUpstreamModelIds(config);
1558
- const requiredModels = getExposedModelIds();
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 getExposedModelIds()) await validateModelRequest(config, model, thinkEffort);
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}`);