fix: improve OpenRouter model validation to support newer models

- Changed validation threshold from >1 to >10 models to avoid false negatives
- Added pattern matching for known model providers (OpenAI, Google, X-AI, etc.)
- Allows newer models like gpt-5.1, gemini-3-pro-preview, and grok-4.1-fast
- Added comprehensive tests for the new validation logic

Fixes #9597
This commit is contained in:
Roo Code 2025-11-26 04:20:01 +00:00
parent 099ea6dc6a
commit e3c1bc0280
2 changed files with 143 additions and 8 deletions

View file

@ -4,6 +4,20 @@ import { RouterModels } from "@roo/api"
import { getModelValidationError, validateApiConfigurationExcludingModelErrors } from "../validate"
// Mock i18next
vi.mock("i18next", () => ({
default: {
t: vi.fn((key: string, options?: any) => {
// Return the key as-is for testing (this matches what the actual code expects)
if (options?.modelId) {
return key // For validation.modelAvailability, just return the key
}
return key
}),
language: "en",
},
}))
describe("Model Validation Functions", () => {
const mockRouterModels: RouterModels = {
openrouter: {
@ -47,6 +61,25 @@ describe("Model Validation Functions", () => {
chutes: {},
}
// Mock router models with many models (>10) to trigger validation
const mockRouterModelsWithManyModels: RouterModels = {
...mockRouterModels,
openrouter: {
...mockRouterModels.openrouter,
"model-1": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
"model-2": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
"model-3": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
"model-4": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
"model-5": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
"model-6": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
"model-7": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
"model-8": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
"model-9": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
"model-10": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
"model-11": { maxTokens: 1000, contextWindow: 10000, supportsImages: false, supportsPromptCache: false },
},
}
const allowAllOrganization: OrganizationAllowList = {
allowAll: true,
providers: {},
@ -73,14 +106,92 @@ describe("Model Validation Functions", () => {
expect(result).toBeUndefined()
})
it("returns error for invalid OpenRouter model", () => {
it("returns error for invalid OpenRouter model when model list has many models", () => {
const config: ProviderSettings = {
apiProvider: "openrouter",
openRouterModelId: "invalid-model",
}
// Should return error when we have many models (>10) in cache
const result = getModelValidationError(config, mockRouterModelsWithManyModels, allowAllOrganization)
expect(result).toBe("settings:validation.modelAvailability")
})
it("allows newer OpenRouter models like gpt-5.1 even when not in cache", () => {
const config: ProviderSettings = {
apiProvider: "openrouter",
openRouterModelId: "openai/gpt-5.1",
}
// Should allow gpt-5.1 even when not in the model list
const result = getModelValidationError(config, mockRouterModelsWithManyModels, allowAllOrganization)
expect(result).toBeUndefined()
})
it("allows newer OpenRouter models like gemini-3-pro-preview even when not in cache", () => {
const config: ProviderSettings = {
apiProvider: "openrouter",
openRouterModelId: "google/gemini-3-pro-preview",
}
// Should allow gemini-3 models even when not in the model list
const result = getModelValidationError(config, mockRouterModelsWithManyModels, allowAllOrganization)
expect(result).toBeUndefined()
})
it("allows newer OpenRouter models like grok-4.1-fast:free even when not in cache", () => {
const config: ProviderSettings = {
apiProvider: "openrouter",
openRouterModelId: "x-ai/grok-4.1-fast:free",
}
// Should allow grok-4 models even when not in the model list
const result = getModelValidationError(config, mockRouterModelsWithManyModels, allowAllOrganization)
expect(result).toBeUndefined()
})
it("allows known pattern models for OpenRouter even when not in cache", () => {
const testCases = [
"openai/gpt-5",
"openai/gpt-5.2",
"google/gemini-4",
"x-ai/grok-5",
"anthropic/claude-4-opus",
"meta-llama/llama-4",
"mistralai/mistral-large-2",
]
testCases.forEach((modelId) => {
const config: ProviderSettings = {
apiProvider: "openrouter",
openRouterModelId: modelId,
}
const result = getModelValidationError(config, mockRouterModelsWithManyModels, allowAllOrganization)
expect(result).toBeUndefined()
})
})
it("still validates unknown pattern models for OpenRouter", () => {
const config: ProviderSettings = {
apiProvider: "openrouter",
openRouterModelId: "unknown-provider/unknown-model",
}
// Should still validate models that don't match known patterns
const result = getModelValidationError(config, mockRouterModelsWithManyModels, allowAllOrganization)
expect(result).toBe("settings:validation.modelAvailability")
})
it("does not validate when model list is small (<=10 models)", () => {
const config: ProviderSettings = {
apiProvider: "openrouter",
openRouterModelId: "any-model-id",
}
// Should not validate when we have few models in cache (initial state)
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
expect(result).toBe("validation.modelAvailability")
expect(result).toBeUndefined()
})
it("returns error for model not allowed by organization", () => {
@ -130,7 +241,7 @@ describe("Model Validation Functions", () => {
}
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
expect(result).toBe("validation.modelId")
expect(result).toBe("settings:validation.modelId")
})
it("handles undefined model IDs gracefully", () => {
@ -140,7 +251,7 @@ describe("Model Validation Functions", () => {
}
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
expect(result).toBe("validation.modelId")
expect(result).toBe("settings:validation.modelId")
})
})
@ -164,7 +275,7 @@ describe("Model Validation Functions", () => {
}
const result = validateApiConfigurationExcludingModelErrors(config, mockRouterModels, allowAllOrganization)
expect(result).toBe("validation.apiKey")
expect(result).toBe("settings:validation.apiKey")
})
it("excludes model-specific errors", () => {
@ -196,7 +307,7 @@ describe("Model Validation Functions", () => {
it("returns undefined for valid IO Intelligence model", () => {
const config: ProviderSettings = {
apiProvider: "io-intelligence",
glamaModelId: "valid-model",
ioIntelligenceModelId: "valid-model",
}
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)
@ -206,7 +317,7 @@ describe("Model Validation Functions", () => {
it("returns error for invalid IO Intelligence model", () => {
const config: ProviderSettings = {
apiProvider: "io-intelligence",
glamaModelId: "invalid-model",
ioIntelligenceModelId: "invalid-model",
}
const result = getModelValidationError(config, mockRouterModels, allowAllOrganization)

View file

@ -276,7 +276,31 @@ function validateDynamicProviderModelId(
const models = routerModels?.[provider]
if (models && Object.keys(models).length > 1 && !Object.keys(models).includes(modelId)) {
// Only validate if we have a substantial model list (more than 10 models)
// This prevents false negatives for newer models that haven't been added to the cache yet
// OpenRouter frequently adds new models that are immediately usable but may not appear
// in the /models endpoint right away
if (models && Object.keys(models).length > 10 && !Object.keys(models).includes(modelId)) {
// For OpenRouter specifically, we'll be more lenient with model validation
// to support newer models like gpt-5.1, gemini-3-pro-preview, grok-4.1-fast:free
if (provider === "openrouter") {
// Only show a warning for models that don't match any known pattern
// Allow models that follow common naming patterns even if not in cache
const knownPatterns = [
/^openai\/gpt-\d+(\.\d+)?/, // Matches gpt-5, gpt-5.1, etc.
/^google\/gemini-\d+/, // Matches gemini-3, gemini-3-pro, etc.
/^x-ai\/grok-\d+/, // Matches grok-4, grok-4.1, etc.
/^anthropic\/claude-/, // Claude models
/^meta-llama\//, // Meta Llama models
/^mistralai\//, // Mistral models
]
// If the model matches a known pattern, allow it
if (knownPatterns.some((pattern) => pattern.test(modelId))) {
return undefined
}
}
return i18next.t("settings:validation.modelAvailability", { modelId })
}