From 1cd51f6689ef02e1a66f1acff7a58a451cd5e58b Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 20 Mar 2026 13:40:47 +0000 Subject: [PATCH] fix: preserve custom model IDs across all providers instead of silently replacing them Previously, when a user provided a custom model ID not in the predefined list, many providers would silently replace it with the default model ID. This affected: Anthropic, Anthropic Vertex, Gemini, MiniMax, OpenAI Codex, OpenAI Native, Vertex, xAI, and BaseOpenAiCompatibleProvider (which also affects SambaNova, Baseten, Fireworks, Z.ai). Now, all providers preserve the user's custom model ID and only fall back to the default model's *info* (capabilities/pricing metadata) when the model is not in the predefined list. This matches the pattern already used by DeepSeek, Moonshot, Mistral, and QwenCode. Closes #11964 --- .../__tests__/anthropic-vertex.spec.ts | 12 ++++++ src/api/providers/__tests__/anthropic.spec.ts | 11 ++++++ .../base-openai-compatible-provider.spec.ts | 39 +++++++++++++++++++ src/api/providers/__tests__/gemini.spec.ts | 10 +++-- src/api/providers/__tests__/minimax.spec.ts | 11 ++++++ .../providers/__tests__/openai-codex.spec.ts | 7 +++- .../providers/__tests__/openai-native.spec.ts | 11 ++++++ src/api/providers/__tests__/vertex.spec.ts | 12 ++++++ src/api/providers/__tests__/xai.spec.ts | 8 ++++ src/api/providers/anthropic-vertex.ts | 5 +-- src/api/providers/anthropic.ts | 5 +-- .../base-openai-compatible-provider.ts | 10 ++--- src/api/providers/gemini.ts | 5 +-- src/api/providers/minimax.ts | 5 +-- src/api/providers/openai-codex.ts | 7 ++-- src/api/providers/openai-native.ts | 8 ++-- src/api/providers/vertex.ts | 5 +-- src/api/providers/xai.ts | 7 +--- 18 files changed, 139 insertions(+), 39 deletions(-) diff --git a/src/api/providers/__tests__/anthropic-vertex.spec.ts b/src/api/providers/__tests__/anthropic-vertex.spec.ts index f1ada38a63..d1b8e831b3 100644 --- a/src/api/providers/__tests__/anthropic-vertex.spec.ts +++ b/src/api/providers/__tests__/anthropic-vertex.spec.ts @@ -841,6 +841,18 @@ describe("VertexHandler", () => { expect(modelInfo.info.contextWindow).toBe(200_000) }) + it("should preserve custom model ID and use default model info for unknown models", () => { + const customHandler = new AnthropicVertexHandler({ + apiModelId: "claude-custom-vertex-model", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = customHandler.getModel() + expect(modelInfo.id).toBe("claude-custom-vertex-model") + expect(modelInfo.info).toBeDefined() + expect(modelInfo.info.contextWindow).toBeDefined() + }) + it("honors custom maxTokens for thinking models", () => { const handler = new AnthropicVertexHandler({ apiKey: "test-api-key", diff --git a/src/api/providers/__tests__/anthropic.spec.ts b/src/api/providers/__tests__/anthropic.spec.ts index 3731f3a068..c3ecaef6aa 100644 --- a/src/api/providers/__tests__/anthropic.spec.ts +++ b/src/api/providers/__tests__/anthropic.spec.ts @@ -268,6 +268,17 @@ describe("AnthropicHandler", () => { expect(model.info.supportsPromptCache).toBe(true) }) + it("should preserve custom model ID and use default model info for unknown models", () => { + const customHandler = new AnthropicHandler({ + apiKey: "test-api-key", + apiModelId: "claude-custom-model-v1", + }) + const model = customHandler.getModel() + expect(model.id).toBe("claude-custom-model-v1") + expect(model.info).toBeDefined() + expect(model.info.contextWindow).toBeDefined() + }) + it("honors custom maxTokens for thinking models", () => { const handler = new AnthropicHandler({ apiKey: "test-api-key", diff --git a/src/api/providers/__tests__/base-openai-compatible-provider.spec.ts b/src/api/providers/__tests__/base-openai-compatible-provider.spec.ts index 6f8d121e69..c7baf2a8c1 100644 --- a/src/api/providers/__tests__/base-openai-compatible-provider.spec.ts +++ b/src/api/providers/__tests__/base-openai-compatible-provider.spec.ts @@ -545,4 +545,43 @@ describe("BaseOpenAiCompatibleProvider", () => { expect(endChunks).toHaveLength(0) }) }) + + describe("getModel", () => { + it("should return the default model when no apiModelId is set", () => { + const model = handler.getModel() + expect(model.id).toBe("test-model") + expect(model.info).toBeDefined() + }) + + it("should preserve custom model ID and use default model info for unknown models", () => { + const customHandler = new (class extends BaseOpenAiCompatibleProvider<"test-model"> { + constructor() { + const testModels: Record<"test-model", ModelInfo> = { + "test-model": { + maxTokens: 4096, + contextWindow: 128000, + supportsImages: false, + supportsPromptCache: false, + inputPrice: 0.5, + outputPrice: 1.5, + }, + } + + super({ + providerName: "TestProvider", + baseURL: "https://test.example.com/v1", + defaultProviderModelId: "test-model", + providerModels: testModels, + apiKey: "test-key", + apiModelId: "custom-unknown-model", + }) + } + })() + + const model = customHandler.getModel() + expect(model.id).toBe("custom-unknown-model") + expect(model.info).toBeDefined() + expect(model.info.contextWindow).toBe(128000) + }) + }) }) diff --git a/src/api/providers/__tests__/gemini.spec.ts b/src/api/providers/__tests__/gemini.spec.ts index 47ee79dd0d..9ed00e3e00 100644 --- a/src/api/providers/__tests__/gemini.spec.ts +++ b/src/api/providers/__tests__/gemini.spec.ts @@ -165,13 +165,17 @@ describe("GeminiHandler", () => { expect(modelInfo.info).toBeDefined() }) - it("should return default model if invalid model specified", () => { + it("should preserve custom model ID and use default model info for unknown models", () => { const invalidHandler = new GeminiHandler({ - apiModelId: "invalid-model", + apiModelId: "gemini-custom-model", geminiApiKey: "test-key", }) const modelInfo = invalidHandler.getModel() - expect(modelInfo.id).toBe(geminiDefaultModelId) // Default model + expect(modelInfo.id).toBe("gemini-custom-model") + expect(modelInfo.info).toBeDefined() + // Should fall back to default model's info + const defaultHandler = new GeminiHandler({ geminiApiKey: "test-key" }) + expect(modelInfo.info.contextWindow).toBe(defaultHandler.getModel().info.contextWindow) }) it("should exclude apply_diff and include edit in tool preferences", () => { diff --git a/src/api/providers/__tests__/minimax.spec.ts b/src/api/providers/__tests__/minimax.spec.ts index e5df368f50..bb4f4f639c 100644 --- a/src/api/providers/__tests__/minimax.spec.ts +++ b/src/api/providers/__tests__/minimax.spec.ts @@ -134,6 +134,17 @@ describe("MiniMaxHandler", () => { expect(model.info.cacheWritesPrice).toBe(0.375) expect(model.info.cacheReadsPrice).toBe(0.03) }) + + it("should preserve custom model ID and use default model info for unknown models", () => { + const handlerWithCustom = new MiniMaxHandler({ + apiModelId: "MiniMax-M2.7-custom", + minimaxApiKey: "test-minimax-api-key", + }) + const model = handlerWithCustom.getModel() + expect(model.id).toBe("MiniMax-M2.7-custom") + expect(model.info).toBeDefined() + expect(model.info).toEqual(minimaxModels[minimaxDefaultModelId]) + }) }) describe("China MiniMax", () => { diff --git a/src/api/providers/__tests__/openai-codex.spec.ts b/src/api/providers/__tests__/openai-codex.spec.ts index dcc0c4d035..94511ecb8e 100644 --- a/src/api/providers/__tests__/openai-codex.spec.ts +++ b/src/api/providers/__tests__/openai-codex.spec.ts @@ -16,12 +16,15 @@ describe("OpenAiCodexHandler.getModel", () => { }, ) - it("should fall back to default model when an invalid model id is provided", () => { + it("should preserve custom model ID and use default model info for unknown models", () => { const handler = new OpenAiCodexHandler({ apiModelId: "not-a-real-model" }) const model = handler.getModel() - expect(model.id).toBe("gpt-5.3-codex") + expect(model.id).toBe("not-a-real-model") expect(model.info).toBeDefined() + // Should fall back to default model's info + const defaultHandler = new OpenAiCodexHandler({}) + expect(model.info.contextWindow).toBe(defaultHandler.getModel().info.contextWindow) }) it("should use Spark-specific limits and capabilities", () => { diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 6887da4d20..a3fa5b236a 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -324,6 +324,17 @@ describe("OpenAiNativeHandler", () => { expect(modelInfo.id).toBe("gpt-5.1-codex-max") // Default model expect(modelInfo.info).toBeDefined() }) + + it("should preserve custom model ID and use default model info for unknown models", () => { + const customHandler = new OpenAiNativeHandler({ + ...mockOptions, + apiModelId: "gpt-custom-model", + }) + const modelInfo = customHandler.getModel() + expect(modelInfo.id).toBe("gpt-custom-model") + expect(modelInfo.info).toBeDefined() + expect(modelInfo.info.contextWindow).toBeDefined() + }) }) describe("GPT-5 models", () => { diff --git a/src/api/providers/__tests__/vertex.spec.ts b/src/api/providers/__tests__/vertex.spec.ts index 3361176f1f..9843391d5d 100644 --- a/src/api/providers/__tests__/vertex.spec.ts +++ b/src/api/providers/__tests__/vertex.spec.ts @@ -138,6 +138,18 @@ describe("VertexHandler", () => { expect(modelInfo.info.contextWindow).toBe(1048576) }) + it("should preserve custom model ID and use default model info for unknown models", () => { + const customHandler = new VertexHandler({ + apiModelId: "gemini-custom-vertex-model", + vertexProjectId: "test-project", + vertexRegion: "us-central1", + }) + const modelInfo = customHandler.getModel() + expect(modelInfo.id).toBe("gemini-custom-vertex-model") + expect(modelInfo.info).toBeDefined() + expect(modelInfo.info.contextWindow).toBeDefined() + }) + it("should exclude apply_diff and include edit in tool preferences", () => { const testHandler = new VertexHandler({ apiModelId: "gemini-2.0-flash-001", diff --git a/src/api/providers/__tests__/xai.spec.ts b/src/api/providers/__tests__/xai.spec.ts index c622c9d4fc..b346d6e3ec 100644 --- a/src/api/providers/__tests__/xai.spec.ts +++ b/src/api/providers/__tests__/xai.spec.ts @@ -80,6 +80,14 @@ describe("XAIHandler", () => { expect(model.info).toEqual(xaiModels[testModelId]) }) + it("should preserve custom model ID and use default model info for unknown models", () => { + const customHandler = new XAIHandler({ apiModelId: "grok-custom-model" }) + const model = customHandler.getModel() + expect(model.id).toBe("grok-custom-model") + expect(model.info).toBeDefined() + expect(model.info).toEqual(xaiModels[xaiDefaultModelId]) + }) + it("should include reasoning_effort parameter for mini models", async () => { const miniModelHandler = new XAIHandler({ apiModelId: "grok-3-mini", diff --git a/src/api/providers/anthropic-vertex.ts b/src/api/providers/anthropic-vertex.ts index 3ed5dd45cc..59448138a9 100644 --- a/src/api/providers/anthropic-vertex.ts +++ b/src/api/providers/anthropic-vertex.ts @@ -206,9 +206,8 @@ export class AnthropicVertexHandler extends BaseProvider implements SingleComple } getModel() { - const modelId = this.options.apiModelId - let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId - let info: ModelInfo = vertexModels[id] + const id = this.options.apiModelId ?? vertexDefaultModelId + let info: ModelInfo = vertexModels[id as VertexModelId] || vertexModels[vertexDefaultModelId] // Check if 1M context beta should be enabled for supported models const supports1MContext = VERTEX_1M_CONTEXT_MODEL_IDS.includes( diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index 1786a105a5..80c24c4e94 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -333,9 +333,8 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa } getModel() { - const modelId = this.options.apiModelId - let id = modelId && modelId in anthropicModels ? (modelId as AnthropicModelId) : anthropicDefaultModelId - let info: ModelInfo = anthropicModels[id] + const id = this.options.apiModelId ?? anthropicDefaultModelId + let info: ModelInfo = anthropicModels[id as AnthropicModelId] || anthropicModels[anthropicDefaultModelId] // If 1M context beta is enabled for supported models, update the model info if ( diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index fc3d769ae2..7f38c8c1b8 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -250,11 +250,11 @@ export abstract class BaseOpenAiCompatibleProvider } override getModel() { - const id = - this.options.apiModelId && this.options.apiModelId in this.providerModels - ? (this.options.apiModelId as ModelName) - : this.defaultProviderModelId + const id = this.options.apiModelId ?? this.defaultProviderModelId - return { id, info: this.providerModels[id] } + return { + id, + info: this.providerModels[id as ModelName] || this.providerModels[this.defaultProviderModelId as ModelName], + } } } diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index a49073ea33..4c010a6bb1 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -347,9 +347,8 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl } override getModel() { - const modelId = this.options.apiModelId - let id = modelId && modelId in geminiModels ? (modelId as GeminiModelId) : geminiDefaultModelId - let info: ModelInfo = geminiModels[id] + const id = this.options.apiModelId ?? geminiDefaultModelId + let info: ModelInfo = geminiModels[id as GeminiModelId] || geminiModels[geminiDefaultModelId] const params = getModelParams({ format: "gemini", diff --git a/src/api/providers/minimax.ts b/src/api/providers/minimax.ts index bfcf4e3be4..45a75c20a6 100644 --- a/src/api/providers/minimax.ts +++ b/src/api/providers/minimax.ts @@ -270,9 +270,8 @@ export class MiniMaxHandler extends BaseProvider implements SingleCompletionHand } getModel() { - const modelId = this.options.apiModelId - const id = modelId && modelId in minimaxModels ? (modelId as MinimaxModelId) : minimaxDefaultModelId - const info = minimaxModels[id] + const id = this.options.apiModelId ?? minimaxDefaultModelId + const info = minimaxModels[id as MinimaxModelId] || minimaxModels[minimaxDefaultModelId] const params = getModelParams({ format: "anthropic", diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 9dfb37bc72..6606a82f92 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -1115,11 +1115,10 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion } override getModel() { - const modelId = this.options.apiModelId + const id = this.options.apiModelId ?? openAiCodexDefaultModelId - let id = modelId && modelId in openAiCodexModels ? (modelId as OpenAiCodexModelId) : openAiCodexDefaultModelId - - const info: ModelInfo = openAiCodexModels[id] + const info: ModelInfo = + openAiCodexModels[id as OpenAiCodexModelId] || openAiCodexModels[openAiCodexDefaultModelId] const params = getModelParams({ format: "openai", diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 6ce9382763..17c6408a2b 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -1433,12 +1433,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio // Removed isResponsesApiModel method as ALL models now use the Responses API override getModel() { - const modelId = this.options.apiModelId + const id = this.options.apiModelId ?? openAiNativeDefaultModelId - let id = - modelId && modelId in openAiNativeModels ? (modelId as OpenAiNativeModelId) : openAiNativeDefaultModelId - - const info: ModelInfo = openAiNativeModels[id] + const info: ModelInfo = + openAiNativeModels[id as OpenAiNativeModelId] || openAiNativeModels[openAiNativeDefaultModelId] const params = getModelParams({ format: "openai", diff --git a/src/api/providers/vertex.ts b/src/api/providers/vertex.ts index fd318d9b19..84560ce056 100644 --- a/src/api/providers/vertex.ts +++ b/src/api/providers/vertex.ts @@ -13,9 +13,8 @@ export class VertexHandler extends GeminiHandler implements SingleCompletionHand } override getModel() { - const modelId = this.options.apiModelId - let id = modelId && modelId in vertexModels ? (modelId as VertexModelId) : vertexDefaultModelId - let info: ModelInfo = vertexModels[id] + const id = this.options.apiModelId ?? vertexDefaultModelId + let info: ModelInfo = vertexModels[id as VertexModelId] || vertexModels[vertexDefaultModelId] const params = getModelParams({ format: "gemini", modelId: id, diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 8b973d41c4..d602a618cc 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -37,12 +37,9 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler } override getModel() { - const id = - this.options.apiModelId && this.options.apiModelId in xaiModels - ? (this.options.apiModelId as XAIModelId) - : xaiDefaultModelId + const id = this.options.apiModelId ?? xaiDefaultModelId - const info = xaiModels[id] + const info = xaiModels[id as XAIModelId] || xaiModels[xaiDefaultModelId] const params = getModelParams({ format: "openai", modelId: id,