diff --git a/packages/coding-agent/src/config/model-registry.ts b/packages/coding-agent/src/config/model-registry.ts index 79a37904b..e97bb5737 100644 --- a/packages/coding-agent/src/config/model-registry.ts +++ b/packages/coding-agent/src/config/model-registry.ts @@ -839,26 +839,27 @@ export class ModelRegistry { for (const customModel of customModels) { const existingIndex = merged.findIndex(m => m.provider === customModel.provider && m.id === customModel.id); if (existingIndex >= 0) { + const existingModel = merged[existingIndex]; merged[existingIndex] = enrichModelThinking({ - ...merged[existingIndex], + ...existingModel, id: customModel.id, provider: customModel.provider, api: customModel.api, baseUrl: customModel.baseUrl, - name: customModel.name ?? merged[existingIndex].name, - reasoning: customModel.reasoning ?? merged[existingIndex].reasoning, - thinking: customModel.thinking ?? merged[existingIndex].thinking, - input: customModel.input ?? merged[existingIndex].input, - cost: customModel.cost ?? merged[existingIndex].cost, - contextWindow: customModel.contextWindow ?? merged[existingIndex].contextWindow, - maxTokens: customModel.maxTokens ?? merged[existingIndex].maxTokens, - headers: customModel.headers - ? { ...merged[existingIndex].headers, ...customModel.headers } - : merged[existingIndex].headers, - compat: mergeCompat(merged[existingIndex].compat, customModel.compat), - contextPromotionTarget: - customModel.contextPromotionTarget ?? merged[existingIndex].contextPromotionTarget, - premiumMultiplier: customModel.premiumMultiplier ?? merged[existingIndex].premiumMultiplier, + name: customModel.name ?? existingModel.name, + reasoning: customModel.reasoning ?? existingModel.reasoning, + thinking: customModel.thinking ?? existingModel.thinking, + input: customModel.input ?? existingModel.input, + cost: customModel.cost ?? existingModel.cost, + contextWindow: customModel.contextWindow ?? existingModel.contextWindow, + maxTokens: customModel.maxTokens ?? existingModel.maxTokens, + // Same-id custom definitions replace bundled transport behavior. Provider-level + // headers/compat were already folded into customModel during parsing; do not + // re-merge bundled transport metadata here. + headers: customModel.headers, + compat: customModel.compat, + contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget, + premiumMultiplier: customModel.premiumMultiplier ?? existingModel.premiumMultiplier, } as Model); } else { merged.push(finalizeCustomModel(customModel, { useDefaults: true })); diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index c40cc8e92..2a2c1e3ec 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -322,6 +322,25 @@ describe("ModelRegistry", () => { expect(sonnetModels[0].baseUrl).toBe("https://my-proxy.example.com/v1"); }); + test("custom same-id replacement does not keep bundled headers", () => { + writeRawModelsJson({ + "github-copilot": { + baseUrl: "https://proxy.example.com/v1", + headers: { "X-Proxy": "proxy" }, + apiKey: "TEST_KEY", + api: "openai-completions", + models: [{ id: "gpt-4o" }], + }, + }); + + const registry = new ModelRegistry(authStorage, modelsJsonPath); + const model = registry.find("github-copilot", "gpt-4o"); + + expect(model?.headers).toEqual({ "X-Proxy": "proxy" }); + expect(model?.headers?.["User-Agent"]).toBeUndefined(); + expect(model?.headers?.["Editor-Version"]).toBeUndefined(); + }); + test("custom provider with same name as built-in does not affect other built-in providers", () => { writeModelsJson({ anthropic: providerConfig("https://my-proxy.example.com/v1", [{ id: "claude-custom" }]), @@ -638,7 +657,7 @@ describe("ModelRegistry", () => { expect(discovered?.headers?.["X-Model"]).toBeUndefined(); }); - test("provider compat overlays preserve bundled model compat", () => { + test("same-id replacement uses configured compat without bundled compat leak", () => { writeRawModelsJson({ "minimax-code": { baseUrl: "https://proxy.example.com/v1", @@ -653,8 +672,8 @@ describe("ModelRegistry", () => { const registry = new ModelRegistry(authStorage, modelsJsonPath); const model = registry.find("minimax-code", "MiniMax-M2.5"); - expect(model?.compat?.thinkingFormat).toBe("zai"); - expect(model?.compat?.reasoningContentField).toBe("reasoning_content"); + expect(model?.compat?.thinkingFormat).toBeUndefined(); + expect(model?.compat?.reasoningContentField).toBeUndefined(); expect(model?.compat?.extraBody).toEqual({ source: "proxy" }); });