copilot-relay 0.2.0 → 0.2.2

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
@@ -37,7 +37,17 @@ Routing:
37
37
  | Requested model | Upstream model |
38
38
  | --- | --- |
39
39
  | contains `opus` | `claude-opus-4.8` |
40
- | anything else | `gpt-5.6-sol` |
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,7 +58,11 @@ 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
 
package/dist/main.js CHANGED
@@ -349,8 +349,24 @@ const defaultModelRouting = {
349
349
  opusModel: "claude-opus-4.8"
350
350
  };
351
351
  const isReasoningEffort = (value) => value === "none" || value === "low" || value === "medium" || value === "high" || value === "xhigh" || value === "max";
352
- const getModelRouting = () => runtimeState.modelRouting ?? defaultModelRouting;
352
+ const exactGpt56SolPattern = /^gpt-5\.6-sol(?:\[1m\])*$/i;
353
+ const claudeContextModelId = "gpt-5.6-sol[1m]";
354
+ const copilotModelId = "gpt-5.6-sol";
355
+ const normalizeClaudeModelId = (model) => exactGpt56SolPattern.test(model) ? claudeContextModelId : model;
356
+ const normalizeCopilotModelId = (model) => exactGpt56SolPattern.test(model) ? copilotModelId : model;
357
+ const getConfiguredModelRouting = () => runtimeState.modelRouting ?? defaultModelRouting;
358
+ const getModelRouting = () => {
359
+ const routing = getConfiguredModelRouting();
360
+ return {
361
+ gptModel: normalizeCopilotModelId(routing.gptModel),
362
+ opusModel: normalizeCopilotModelId(routing.opusModel)
363
+ };
364
+ };
353
365
  const getExposedModelIds = () => {
366
+ const routing = getConfiguredModelRouting();
367
+ return [normalizeClaudeModelId(routing.gptModel), routing.opusModel];
368
+ };
369
+ const getUpstreamModelIds = () => {
354
370
  const routing = getModelRouting();
355
371
  return [routing.gptModel, routing.opusModel];
356
372
  };
@@ -806,13 +822,30 @@ const stopExistingRelay = async (config) => {
806
822
 
807
823
  //#endregion
808
824
  //#region src/lib/claude-settings.ts
825
+ const knownModelEnvKeys = [
826
+ "ANTHROPIC_MODEL",
827
+ "ANTHROPIC_DEFAULT_SONNET_MODEL",
828
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL",
829
+ "ANTHROPIC_DEFAULT_OPUS_MODEL",
830
+ "ANTHROPIC_DEFAULT_FABLE_MODEL",
831
+ "ANTHROPIC_SMALL_FAST_MODEL",
832
+ "CLAUDE_CODE_SUBAGENT_MODEL"
833
+ ];
834
+ const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
835
+ const hasMalformedModelOverride = (parsed) => {
836
+ if (parsed.model !== void 0 && typeof parsed.model !== "string") return true;
837
+ const env = parsed.env;
838
+ if (!isRecord$1(env)) return false;
839
+ return knownModelEnvKeys.some((key) => env[key] !== void 0 && typeof env[key] !== "string");
840
+ };
841
+ const hasPrimaryModelOverride = (parsed, env) => typeof parsed.model === "string" || typeof env.ANTHROPIC_MODEL === "string";
809
842
  /**
810
843
  * 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.
844
+ * running proxy. Preserves unrelated keys and model choices while normalizing
845
+ * managed gpt-5.6-sol overrides. Sets a dummy auth token only if none is present.
813
846
  */
814
847
  async function applyClaudeConfig(input) {
815
- const { configPath: configPath$1, baseUrl } = input;
848
+ const { configPath: configPath$1, baseUrl, gptModel } = input;
816
849
  let raw = "";
817
850
  let created = false;
818
851
  try {
@@ -822,9 +855,19 @@ async function applyClaudeConfig(input) {
822
855
  else throw error;
823
856
  }
824
857
  let parsed = {};
858
+ if (!created && raw.trim().length === 0) return {
859
+ configPath: configPath$1,
860
+ changed: false,
861
+ created: false
862
+ };
825
863
  if (raw.trim().length > 0) try {
826
- parsed = JSON.parse(raw);
827
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) parsed = {};
864
+ const value = JSON.parse(raw);
865
+ if (!isRecord$1(value) || value.env !== void 0 && !isRecord$1(value.env) || hasMalformedModelOverride(value)) return {
866
+ configPath: configPath$1,
867
+ changed: false,
868
+ created: false
869
+ };
870
+ parsed = value;
828
871
  } catch {
829
872
  return {
830
873
  configPath: configPath$1,
@@ -832,12 +875,19 @@ async function applyClaudeConfig(input) {
832
875
  created: false
833
876
  };
834
877
  }
835
- const env = typeof parsed.env === "object" && parsed.env !== null && !Array.isArray(parsed.env) ? { ...parsed.env } : {};
878
+ const env = isRecord$1(parsed.env) ? { ...parsed.env } : {};
836
879
  const previousBaseUrl = typeof env.ANTHROPIC_BASE_URL === "string" ? env.ANTHROPIC_BASE_URL : void 0;
880
+ const hadPrimaryModelOverride = hasPrimaryModelOverride(parsed, env);
881
+ const claudeGptModel = normalizeClaudeModelId(gptModel);
882
+ const shouldSeedTopLevelModel = !hadPrimaryModelOverride;
883
+ const nextModel = typeof parsed.model === "string" ? normalizeClaudeModelId(parsed.model) : void 0;
884
+ for (const key of knownModelEnvKeys) if (typeof env[key] === "string") env[key] = normalizeClaudeModelId(env[key]);
837
885
  env.ANTHROPIC_BASE_URL = baseUrl;
838
886
  if (typeof env.ANTHROPIC_AUTH_TOKEN !== "string" || !env.ANTHROPIC_AUTH_TOKEN) env.ANTHROPIC_AUTH_TOKEN = "dummy";
839
887
  const next = {
840
888
  ...parsed,
889
+ ...nextModel !== void 0 && { model: nextModel },
890
+ ...shouldSeedTopLevelModel && { model: claudeGptModel },
841
891
  env
842
892
  };
843
893
  const serialized = `${JSON.stringify(next, null, 2)}\n`;
@@ -1539,7 +1589,7 @@ async function shouldRetryWithResponses(response, signal, timeoutMs) {
1539
1589
 
1540
1590
  //#endregion
1541
1591
  //#region src/lib/preflight.ts
1542
- const getUpstreamModelIds = async (config) => {
1592
+ const fetchUpstreamModelIds = async (config) => {
1543
1593
  const provider = getCopilotProviderContext(config);
1544
1594
  const signal = createCopilotRequestSignal(void 0, config.upstreamTimeoutMs);
1545
1595
  const response = await fetchCopilot(provider, "/models", {
@@ -1554,8 +1604,8 @@ const getUpstreamModelIds = async (config) => {
1554
1604
  return new Set((payload.data ?? []).map((model) => model.id).filter((id) => typeof id === "string" && id.length > 0));
1555
1605
  };
1556
1606
  const ensureRequiredModels = async (config) => {
1557
- const upstreamModels = await getUpstreamModelIds(config);
1558
- const requiredModels = getExposedModelIds();
1607
+ const upstreamModels = await fetchUpstreamModelIds(config);
1608
+ const requiredModels = getUpstreamModelIds();
1559
1609
  const missingModels = requiredModels.filter((model) => !upstreamModels.has(model));
1560
1610
  if (missingModels.length > 0) throw new Error(`Required Copilot model(s) unavailable upstream: ${missingModels.join(", ")}`);
1561
1611
  log.info(`Upstream models available: ${requiredModels.join(", ")}`);
@@ -1589,7 +1639,7 @@ const validateModelRequest = async (config, model, thinkEffort) => {
1589
1639
  const validateUpstream = async (config, thinkEffort) => {
1590
1640
  log.info("Running upstream preflight");
1591
1641
  await ensureRequiredModels(config);
1592
- for (const model of getExposedModelIds()) await validateModelRequest(config, model, thinkEffort);
1642
+ for (const model of getUpstreamModelIds()) await validateModelRequest(config, model, thinkEffort);
1593
1643
  };
1594
1644
 
1595
1645
  //#endregion
@@ -1865,7 +1915,7 @@ function translateToClaude(response, toolNameMapper = createClaudeToolNameMapper
1865
1915
  id: response.id,
1866
1916
  type: "message",
1867
1917
  role: "assistant",
1868
- model: response.model,
1918
+ model: normalizeClaudeModelId(response.model),
1869
1919
  content: [
1870
1920
  ...allThinkingBlocks,
1871
1921
  ...allTextBlocks,
@@ -1945,7 +1995,7 @@ function translateChunkToClaudeEvents(chunk, state, toolNameMapper = createClaud
1945
1995
  type: "message",
1946
1996
  role: "assistant",
1947
1997
  content: [],
1948
- model: chunk.model,
1998
+ model: normalizeClaudeModelId(chunk.model),
1949
1999
  stop_reason: null,
1950
2000
  stop_sequence: null,
1951
2001
  usage: {
@@ -2083,7 +2133,7 @@ const isClaudeCodeWebSearchTool = (tool) => tool.name === claudeCodeWebSearchToo
2083
2133
  const isClaudeWebSearchTool = (tool) => isAnthropicNativeWebSearchTool(tool) || isClaudeCodeWebSearchTool(tool);
2084
2134
  const hasClaudeWebSearch = (payload) => payload.tools?.some(isClaudeWebSearchTool) ?? false;
2085
2135
  const isClaudeWebSearchToolName = (name) => name === "web_search" || name === claudeCodeWebSearchToolName;
2086
- const getWebSearchBackendModel = (config) => config.webSearchBackend?.trim() || getModelRouting().gptModel;
2136
+ const getWebSearchBackendModel = (config) => normalizeCopilotModelId(config.webSearchBackend?.trim() || getModelRouting().gptModel);
2087
2137
  const prepareClaudeWebSearchDecisionPayload = (payload) => {
2088
2138
  if (!hasClaudeWebSearch(payload)) return payload;
2089
2139
  return {
@@ -2257,7 +2307,7 @@ const createClaudeWebSearchResponse = (search) => {
2257
2307
  type: "message",
2258
2308
  role: "assistant",
2259
2309
  content,
2260
- model: search.model,
2310
+ model: normalizeClaudeModelId(search.model),
2261
2311
  stop_reason: "end_turn",
2262
2312
  stop_sequence: null,
2263
2313
  usage: {
@@ -2845,7 +2895,8 @@ async function startRelay(appConfig) {
2845
2895
  if (appConfig.claudeSetup) try {
2846
2896
  const claudeResult = await applyClaudeConfig({
2847
2897
  baseUrl,
2848
- configPath: claudeConfigPath$1
2898
+ configPath: claudeConfigPath$1,
2899
+ gptModel: appConfig.gptModel
2849
2900
  });
2850
2901
  if (claudeResult.changed) {
2851
2902
  log.info(`claude settings ${claudeResult.created ? "created" : "updated"}: ${claudeResult.configPath}`);