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
This commit is contained in:
Roo Code 2026-03-20 13:40:47 +00:00
parent 137d3f4fd8
commit 1cd51f6689
18 changed files with 139 additions and 39 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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)
})
})
})

View file

@ -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", () => {

View file

@ -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", () => {

View file

@ -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", () => {

View file

@ -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", () => {

View file

@ -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",

View file

@ -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",

View file

@ -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(

View file

@ -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 (

View file

@ -250,11 +250,11 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
}
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],
}
}
}

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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,

View file

@ -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,