diff --git a/webview-ui/src/utils/__tests__/validate.test.ts b/webview-ui/src/utils/__tests__/validate.spec.ts similarity index 54% rename from webview-ui/src/utils/__tests__/validate.test.ts rename to webview-ui/src/utils/__tests__/validate.spec.ts index 0bd7a15962b..409061a0427 100644 --- a/webview-ui/src/utils/__tests__/validate.test.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -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) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 947b18ac3b0..5c7816a5831 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -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 }) }