diff --git a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts index 9433bb8148..5fca23ba8e 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -8,6 +8,7 @@ import type { Mock } from "vitest" import { ProviderSettings, ModelInfo, + anthropicModels, BEDROCK_1M_CONTEXT_MODEL_IDS, litellmDefaultModelInfo, kenariDefaultModelId, @@ -22,6 +23,8 @@ import { vscodeLlmDefaultModelId, moonshotDefaultModelId, moonshotModels, + kimiCodeDefaultModelInfo, + providerIdentifiers, } from "@roo-code/types" import { useSelectedModel } from "../useSelectedModel" @@ -87,7 +90,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "test-model", openRouterSpecificProvider: "test-provider", } @@ -148,7 +151,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "test-model", // This model doesn't exist in available models openRouterSpecificProvider: "test-provider", } @@ -211,7 +214,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "test-model", openRouterSpecificProvider: "test-provider", } @@ -263,7 +266,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "test-model", } @@ -304,7 +307,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "openrouter", + apiProvider: providerIdentifiers.openrouter, openRouterModelId: "non-existent-model", openRouterSpecificProvider: "non-existent-provider", } @@ -404,7 +407,7 @@ describe("useSelectedModel", () => { const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(), { wrapper }) - expect(result.current.provider).toBe("openrouter") + expect(result.current.provider).toBe(providerIdentifiers.openrouter) expect(result.current.id).toBe(openRouterDefaultModelId) expect(result.current.info).toBeUndefined() }) @@ -427,7 +430,7 @@ describe("useSelectedModel", () => { it("should apply 1M pricing tier for Claude Sonnet 4.6 when enabled", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, apiModelId: "claude-sonnet-4-6", anthropicBeta1MContext: true, } @@ -440,6 +443,39 @@ describe("useSelectedModel", () => { expect(result.current.info?.inputPrice).toBe(6.0) expect(result.current.info?.outputPrice).toBe(22.5) }) + + it("should apply 1M pricing tier for Claude Opus 4.6 when enabled", () => { + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.anthropic, + apiModelId: "claude-opus-4-6", + anthropicBeta1MContext: true, + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.id).toBe("claude-opus-4-6") + expect(result.current.info?.contextWindow).toBe(1_000_000) + expect(result.current.info?.inputPrice).toBe(10.0) + expect(result.current.info?.outputPrice).toBe(37.5) + }) + + it.each([providerIdentifiers.anthropic, providerIdentifiers.geminiCli, providerIdentifiers.fakeAi] as const)( + "should explicitly resolve configured models for %s", + (apiProvider) => { + const apiConfiguration: ProviderSettings = { + apiProvider, + apiModelId: "claude-sonnet-4-6", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe(apiProvider) + expect(result.current.id).toBe("claude-sonnet-4-6") + expect(result.current.info).toEqual(anthropicModels["claude-sonnet-4-6"]) + }, + ) }) describe("bedrock provider with 1M context", () => { @@ -463,7 +499,7 @@ describe("useSelectedModel", () => { it("should enable 1M context window for Bedrock Claude Sonnet 4 when awsBedrock1MContext is true", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "bedrock", + apiProvider: providerIdentifiers.bedrock, apiModelId: BEDROCK_1M_CONTEXT_MODEL_IDS[0], awsBedrock1MContext: true, } @@ -477,7 +513,7 @@ describe("useSelectedModel", () => { it("should use default context window for Bedrock Claude Sonnet 4 when awsBedrock1MContext is false", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "bedrock", + apiProvider: providerIdentifiers.bedrock, apiModelId: BEDROCK_1M_CONTEXT_MODEL_IDS[0], awsBedrock1MContext: false, } @@ -491,7 +527,7 @@ describe("useSelectedModel", () => { it("should not affect context window for non-Claude Sonnet 4 Bedrock models", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "bedrock", + apiProvider: providerIdentifiers.bedrock, apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0", awsBedrock1MContext: true, } @@ -525,7 +561,7 @@ describe("useSelectedModel", () => { it("should enable supportsPromptCache for custom-arn model", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "bedrock", + apiProvider: providerIdentifiers.bedrock, apiModelId: "custom-arn", } @@ -538,7 +574,7 @@ describe("useSelectedModel", () => { it("should enable supportsImages for custom-arn model", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "bedrock", + apiProvider: providerIdentifiers.bedrock, apiModelId: "custom-arn", } @@ -571,14 +607,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "litellm", + apiProvider: providerIdentifiers.litellm, litellmModelId: "some-model", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("litellm") + expect(result.current.provider).toBe(providerIdentifiers.litellm) // Should preserve configured model ID since "some-model" doesn't exist in empty litellm models expect(result.current.id).toBe("some-model") // Should use litellmDefaultModelInfo as fallback @@ -597,14 +633,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "litellm", + apiProvider: providerIdentifiers.litellm, // litellmModelId intentionally omitted } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("litellm") + expect(result.current.provider).toBe(providerIdentifiers.litellm) // LiteLLM has no inherent default; with nothing configured the ID is empty rather than a phantom model expect(result.current.id).toBe("") expect(result.current.info).toEqual(litellmDefaultModelInfo) @@ -632,7 +668,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "litellm", + apiProvider: providerIdentifiers.litellm, litellmModelId: "my-custom-model", } @@ -677,14 +713,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "litellm", + apiProvider: providerIdentifiers.litellm, litellmModelId: "non-existing-model", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("litellm") + expect(result.current.provider).toBe(providerIdentifiers.litellm) // Falls back to default model ID expect(result.current.id).toBe("claude-3-7-sonnet-20250219") // Should use litellmDefaultModelInfo as fallback since default model also not in router models @@ -713,14 +749,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "litellm", + apiProvider: providerIdentifiers.litellm, litellmModelId: "custom-model", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("litellm") + expect(result.current.provider).toBe(providerIdentifiers.litellm) expect(result.current.id).toBe("custom-model") expect(result.current.info).toEqual(customModelInfo) }) @@ -758,14 +794,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "kenari", + apiProvider: providerIdentifiers.kenari, kenariModelId: "glm-5-2", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("kenari") + expect(result.current.provider).toBe(providerIdentifiers.kenari) expect(result.current.id).toBe("glm-5-2") expect(result.current.info).toEqual(customModelInfo) }) @@ -783,14 +819,14 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "kenari", + apiProvider: providerIdentifiers.kenari, kenariModelId: "some-model", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("kenari") + expect(result.current.provider).toBe(providerIdentifiers.kenari) // Falls back to the kenari default model ID when the router list is empty expect(result.current.id).toBe(kenariDefaultModelId) // Should use kenariDefaultModelInfo as fallback @@ -819,14 +855,14 @@ describe("useSelectedModel", () => { it("should use openAiModelInfoSaneDefaults when no custom model info is provided", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "openai", + apiProvider: providerIdentifiers.openai, openAiModelId: "gpt-4o", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("openai") + expect(result.current.provider).toBe(providerIdentifiers.openai) expect(result.current.id).toBe("gpt-4o") expect(result.current.info).toEqual(openAiModelInfoSaneDefaults) }) @@ -843,7 +879,7 @@ describe("useSelectedModel", () => { } const apiConfiguration: ProviderSettings = { - apiProvider: "openai", + apiProvider: providerIdentifiers.openai, openAiModelId: "custom-model", openAiCustomModelInfo: customModelInfo, } @@ -851,7 +887,7 @@ describe("useSelectedModel", () => { const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("openai") + expect(result.current.provider).toBe(providerIdentifiers.openai) expect(result.current.id).toBe("custom-model") expect(result.current.info).toEqual(customModelInfo) }) @@ -865,7 +901,7 @@ describe("useSelectedModel", () => { } const apiConfiguration: ProviderSettings = { - apiProvider: "openai", + apiProvider: providerIdentifiers.openai, openAiModelId: "custom-model-no-tools", openAiCustomModelInfo: customModelInfo, } @@ -873,7 +909,7 @@ describe("useSelectedModel", () => { const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("openai") + expect(result.current.provider).toBe(providerIdentifiers.openai) expect(result.current.id).toBe("custom-model-no-tools") expect(result.current.info).toEqual(customModelInfo) }) @@ -900,27 +936,27 @@ describe("useSelectedModel", () => { it("should return default minimax model when no custom model is specified", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "minimax", + apiProvider: providerIdentifiers.minimax, } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("minimax") + expect(result.current.provider).toBe(providerIdentifiers.minimax) expect(result.current.id).toBe(minimaxDefaultModelId) expect(result.current.info).toEqual(minimaxModels[minimaxDefaultModelId]) }) it("should use custom model ID and info when model exists in minimaxModels", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "minimax", + apiProvider: providerIdentifiers.minimax, apiModelId: "MiniMax-M2.7", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("minimax") + expect(result.current.provider).toBe(providerIdentifiers.minimax) expect(result.current.id).toBe("MiniMax-M2.7") expect(result.current.info).toEqual(minimaxModels["MiniMax-M2.7"]) }) @@ -948,14 +984,14 @@ describe("useSelectedModel", () => { it("resolves a listed family's contextWindow to its maxInputTokens", () => { const family = vscodeLlmDefaultModelId const apiConfiguration: ProviderSettings = { - apiProvider: "vscode-lm", + apiProvider: providerIdentifiers.vscodeLm, vsCodeLmModelSelector: { vendor: "copilot", family }, } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("vscode-lm") + expect(result.current.provider).toBe(providerIdentifiers.vscodeLm) expect(result.current.id).toBe(`copilot/${family}`) // The bar and the condense gate share one source of truth: contextWindow === maxInputTokens. expect(result.current.info?.contextWindow).toBe(vscodeLlmModels[family].maxInputTokens) @@ -967,14 +1003,14 @@ describe("useSelectedModel", () => { // the advertised window would be caught here. const family = "claude-opus-4.8" const apiConfiguration: ProviderSettings = { - apiProvider: "vscode-lm", + apiProvider: providerIdentifiers.vscodeLm, vsCodeLmModelSelector: { vendor: "copilot", family }, } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("vscode-lm") + expect(result.current.provider).toBe(providerIdentifiers.vscodeLm) expect(result.current.id).toBe(`copilot/${family}`) expect(result.current.info?.contextWindow).toBe(vscodeLlmModels[family].maxInputTokens) // 197897 expect(result.current.info?.contextWindow).not.toBe(vscodeLlmModels[family].contextWindow) // NOT 679560 @@ -983,7 +1019,7 @@ describe("useSelectedModel", () => { it("falls back to the default model's window for an unlisted family (NOT 128000)", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "vscode-lm", + apiProvider: providerIdentifiers.vscodeLm, vsCodeLmModelSelector: { vendor: "copilot", family: "totally-unknown-family" }, } @@ -1018,32 +1054,76 @@ describe("useSelectedModel", () => { it("should return default Friendli model when no custom model is specified", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "friendli", + apiProvider: providerIdentifiers.friendli, } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("friendli") + expect(result.current.provider).toBe(providerIdentifiers.friendli) expect(result.current.id).toBe(friendliDefaultModelId) expect(result.current.info).toEqual(friendliModels[friendliDefaultModelId]) }) it("should use custom model ID and info when model exists in friendliModels", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "friendli", + apiProvider: providerIdentifiers.friendli, apiModelId: "zai-org/GLM-5.1", } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("friendli") + expect(result.current.provider).toBe(providerIdentifiers.friendli) expect(result.current.id).toBe("zai-org/GLM-5.1") expect(result.current.info).toEqual(friendliModels["zai-org/GLM-5.1"]) }) }) + describe("Kimi Code provider", () => { + it("should resolve the configured model from router models", () => { + const modelInfo: ModelInfo = { + ...kimiCodeDefaultModelInfo, + description: "Configured Kimi Code model", + } + + mockUseRouterModels.mockReturnValue({ + data: { "kimi-code": { "kimi-for-coding": modelInfo } }, + isLoading: false, + isError: false, + } as any) + + const apiConfiguration: ProviderSettings = { + apiProvider: providerIdentifiers.kimiCode, + apiModelId: "kimi-for-coding", + } + + const wrapper = createWrapper() + const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) + + expect(result.current.provider).toBe(providerIdentifiers.kimiCode) + expect(result.current.id).toBe("kimi-for-coding") + expect(result.current.info).toEqual(modelInfo) + }) + }) + + it("should reject providers unsupported by model selection", () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined) + const preventExpectedError = (event: ErrorEvent) => event.preventDefault() + window.addEventListener("error", preventExpectedError) + const apiConfiguration = { + apiProvider: "unsupported-provider", + } as unknown as ProviderSettings + + const wrapper = createWrapper() + + expect(() => renderHook(() => useSelectedModel(apiConfiguration), { wrapper })).toThrow( + "Unsupported provider: unsupported-provider", + ) + window.removeEventListener("error", preventExpectedError) + consoleError.mockRestore() + }) + describe("moonshot provider", () => { beforeEach(() => { mockUseRouterModels.mockReturnValue({ @@ -1066,13 +1146,13 @@ describe("useSelectedModel", () => { it("should return default moonshot model when no custom model is specified", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "moonshot", + apiProvider: providerIdentifiers.moonshot, } const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(apiConfiguration), { wrapper }) - expect(result.current.provider).toBe("moonshot") + expect(result.current.provider).toBe(providerIdentifiers.moonshot) expect(result.current.id).toBe(moonshotDefaultModelId) expect(result.current.info).toEqual(moonshotModels[moonshotDefaultModelId]) }) @@ -1101,7 +1181,7 @@ describe("useSelectedModel", () => { } as any) const apiConfiguration: ProviderSettings = { - apiProvider: "moonshot", + apiProvider: providerIdentifiers.moonshot, } const wrapper = createWrapper() @@ -1114,7 +1194,7 @@ describe("useSelectedModel", () => { it("should fallback to default when model ID is not in static or router models", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "moonshot", + apiProvider: providerIdentifiers.moonshot, apiModelId: "non-existent-model", } @@ -1127,7 +1207,7 @@ describe("useSelectedModel", () => { it("should use getValidatedModelId to return apiModelId when valid", () => { const apiConfiguration: ProviderSettings = { - apiProvider: "moonshot", + apiProvider: providerIdentifiers.moonshot, apiModelId: "kimi-k2-turbo-preview", } diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index 50886d4538..ec513ce885 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -36,6 +36,7 @@ import { isDynamicProvider, isRetiredProvider, getProviderDefaultModelId, + providerIdentifiers, } from "@roo-code/types" import { useRouterModels } from "./useRouterModels" @@ -148,7 +149,7 @@ function getSelectedModel({ // this gives a better UX than showing the default model const defaultModelId = getProviderDefaultModelId(provider) switch (provider) { - case "openrouter": { + case providerIdentifiers.openrouter: { const id = getValidatedModelId(apiConfiguration.openRouterModelId, routerModels.openrouter, defaultModelId) let info = routerModels.openrouter?.[id] const specificProvider = apiConfiguration.openRouterSpecificProvider @@ -164,17 +165,17 @@ function getSelectedModel({ return { id, info } } - case "requesty": { + case providerIdentifiers.requesty: { const id = getValidatedModelId(apiConfiguration.requestyModelId, routerModels.requesty, defaultModelId) const routerInfo = routerModels.requesty?.[id] return { id, info: routerInfo } } - case "unbound": { + case providerIdentifiers.unbound: { const id = getValidatedModelId(apiConfiguration.unboundModelId, routerModels.unbound, defaultModelId) const routerInfo = routerModels.unbound?.[id] return { id, info: routerInfo } } - case "litellm": { + case providerIdentifiers.litellm: { // When the model list is empty (not yet loaded or still loading), // preserve the configured model ID. LiteLLM is a proxy with no inherent // default model, so we never substitute a hardcoded default here -- when @@ -187,17 +188,17 @@ function getSelectedModel({ const routerInfo = routerModels.litellm?.[id] return { id, info: routerInfo ?? litellmDefaultModelInfo } } - case "xai": { + case providerIdentifiers.xai: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = xaiModels[id as keyof typeof xaiModels] return info ? { id, info } : { id, info: undefined } } - case "baseten": { + case providerIdentifiers.baseten: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = basetenModels[id as keyof typeof basetenModels] return { id, info } } - case "bedrock": { + case providerIdentifiers.bedrock: { const id = apiConfiguration.apiModelId ?? defaultModelId const baseInfo = bedrockModels[id as keyof typeof bedrockModels] @@ -221,7 +222,7 @@ function getSelectedModel({ return { id, info: baseInfo } } - case "vertex": { + case providerIdentifiers.vertex: { const id = apiConfiguration.apiModelId ?? defaultModelId const baseInfo = vertexModels[id as keyof typeof vertexModels] @@ -244,12 +245,12 @@ function getSelectedModel({ return { id, info: baseInfo } } - case "gemini": { + case providerIdentifiers.gemini: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = geminiModels[id as keyof typeof geminiModels] return { id, info } } - case "deepseek": { + case providerIdentifiers.deepseek: { const availableModels = routerModels.deepseek ? { ...deepSeekModels, ...routerModels.deepseek } : deepSeekModels @@ -258,7 +259,7 @@ function getSelectedModel({ const staticInfo = deepSeekModels[id as keyof typeof deepSeekModels] return { id, info: routerInfo ?? staticInfo } } - case "moonshot": { + case providerIdentifiers.moonshot: { const availableModels = routerModels.moonshot ? { ...moonshotModels, ...routerModels.moonshot } : moonshotModels @@ -267,23 +268,23 @@ function getSelectedModel({ const staticInfo = moonshotModels[id as keyof typeof moonshotModels] return { id, info: routerInfo ?? staticInfo } } - case "kimi-code": { + case providerIdentifiers.kimiCode: { const configuredId = apiConfiguration.apiModelId const availableModels = routerModels["kimi-code"] const id = configuredId || defaultModelId return { id, info: availableModels?.[id] ?? kimiCodeDefaultModelInfo } } - case "minimax": { + case providerIdentifiers.minimax: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = minimaxModels[id as keyof typeof minimaxModels] return { id, info } } - case "mimo": { + case providerIdentifiers.mimo: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = mimoModels[id as keyof typeof mimoModels] ?? mimoModels["mimo-v2.5-pro"] return { id, info } } - case "zai": { + case providerIdentifiers.zai: { const isChina = apiConfiguration.zaiApiLine === "china_coding" const models = isChina ? mainlandZAiModels : internationalZAiModels const defaultModelId = getProviderDefaultModelId(provider, { isChina }) @@ -291,23 +292,23 @@ function getSelectedModel({ const info = models[id as keyof typeof models] return { id, info } } - case "openai-native": { + case providerIdentifiers.openaiNative: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = openAiNativeModels[id as keyof typeof openAiNativeModels] return { id, info } } - case "mistral": { + case providerIdentifiers.mistral: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = mistralModels[id as keyof typeof mistralModels] return { id, info } } - case "openai": { + case providerIdentifiers.openai: { const id = apiConfiguration.openAiModelId ?? "" const customInfo = apiConfiguration?.openAiCustomModelInfo const info = customInfo ?? openAiModelInfoSaneDefaults return { id, info } } - case "ollama": { + case providerIdentifiers.ollama: { const id = apiConfiguration.ollamaModelId ?? "" const info = ollamaModels && ollamaModels[apiConfiguration.ollamaModelId!] @@ -323,7 +324,7 @@ function getSelectedModel({ info: adjustedInfo || undefined, } } - case "lmstudio": { + case providerIdentifiers.lmstudio: { const id = apiConfiguration.lmStudioModelId ?? "" const modelInfo = lmStudioModels && lmStudioModels[apiConfiguration.lmStudioModelId!] return { @@ -331,7 +332,7 @@ function getSelectedModel({ info: modelInfo ? { ...lMStudioDefaultModelInfo, ...modelInfo } : undefined, } } - case "vscode-lm": { + case providerIdentifiers.vscodeLm: { const id = apiConfiguration?.vsCodeLmModelSelector ? `${apiConfiguration.vsCodeLmModelSelector.vendor}/${apiConfiguration.vsCodeLmModelSelector.family}` : vscodeLlmDefaultModelId @@ -351,37 +352,37 @@ function getSelectedModel({ } return { id, info } } - case "sambanova": { + case providerIdentifiers.sambanova: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = sambaNovaModels[id as keyof typeof sambaNovaModels] return { id, info } } - case "fireworks": { + case providerIdentifiers.fireworks: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = fireworksModels[id as keyof typeof fireworksModels] return { id, info } } - case "friendli": { + case providerIdentifiers.friendli: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = friendliModels[id as keyof typeof friendliModels] return { id, info } } - case "poe": { + case providerIdentifiers.poe: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = routerModels.poe?.[id] return { id, info } } - case "qwen-code": { + case providerIdentifiers.qwenCode: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = qwenCodeModels[id as keyof typeof qwenCodeModels] return { id, info } } - case "openai-codex": { + case providerIdentifiers.openaiCodex: { const id = apiConfiguration.apiModelId ?? defaultModelId const info = openAiCodexModels[id as keyof typeof openAiCodexModels] return { id, info } } - case "vercel-ai-gateway": { + case providerIdentifiers.vercelAiGateway: { const id = getValidatedModelId( apiConfiguration.vercelAiGatewayModelId, routerModels["vercel-ai-gateway"], @@ -390,7 +391,7 @@ function getSelectedModel({ const info = routerModels["vercel-ai-gateway"]?.[id] return { id, info } } - case "opencode-go": { + case providerIdentifiers.opencodeGo: { const id = getValidatedModelId( apiConfiguration.opencodeGoModelId, routerModels["opencode-go"], @@ -401,14 +402,14 @@ function getSelectedModel({ const info = routerModels["opencode-go"]?.[id] ?? opencodeGoDefaultModelInfo return { id, info } } - case "kenari": { + case providerIdentifiers.kenari: { const id = getValidatedModelId(apiConfiguration.kenariModelId, routerModels["kenari"], defaultModelId) // Fall back to the provider's default ModelInfo so capability-driven UI // keeps working when the /models list is empty or unavailable. const info = routerModels["kenari"]?.[id] ?? kenariDefaultModelInfo return { id, info } } - case "zoo-gateway": { + case providerIdentifiers.zooGateway: { const id = getValidatedModelId( apiConfiguration.zooGatewayModelId, routerModels["zoo-gateway"], @@ -417,16 +418,15 @@ function getSelectedModel({ const info = routerModels["zoo-gateway"]?.[id] return { id, info } } - // case "anthropic": - // case "fake-ai": - default: { - provider satisfies "anthropic" | "gemini-cli" | "fake-ai" + case providerIdentifiers.anthropic: + case providerIdentifiers.geminiCli: + case providerIdentifiers.fakeAi: { const id = apiConfiguration.apiModelId ?? defaultModelId const baseInfo = anthropicModels[id as keyof typeof anthropicModels] // Apply 1M context beta tier pricing for supported Claude 4 models if ( - provider === "anthropic" && + provider === providerIdentifiers.anthropic && (id === "claude-sonnet-4-20250514" || id === "claude-sonnet-4-5" || id === "claude-sonnet-4-6" || @@ -461,5 +461,9 @@ function getSelectedModel({ return { id, info: baseInfo } } + default: { + provider satisfies never + throw new Error(`Unsupported provider: ${provider}`) + } } } diff --git a/webview-ui/src/utils/__tests__/validate.spec.ts b/webview-ui/src/utils/__tests__/validate.spec.ts index 8aebe3f8e5..6ce9bf5245 100644 --- a/webview-ui/src/utils/__tests__/validate.spec.ts +++ b/webview-ui/src/utils/__tests__/validate.spec.ts @@ -1,4 +1,4 @@ -import type { ProviderSettings, OrganizationAllowList, RouterModels } from "@roo-code/types" +import { type ProviderSettings, type OrganizationAllowList, type RouterModels } from "@roo-code/types" // Mock i18next to return translation keys with interpolated values vi.mock("i18next", () => ({ diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index 0a812e9d48..b750d8833b 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -11,6 +11,7 @@ import { isDynamicProvider, isFauxProvider, isCustomProvider, + providerIdentifiers, } from "@roo-code/types" export function validateApiConfiguration( @@ -42,117 +43,117 @@ function validateModelsAndKeysProvided( zooCodeIsAuthenticated?: boolean, ): string | undefined { switch (apiConfiguration.apiProvider) { - case "openrouter": + case providerIdentifiers.openrouter: if (!apiConfiguration.openRouterApiKey) { return i18next.t("settings:validation.apiKey") } break - case "requesty": + case providerIdentifiers.requesty: if (!apiConfiguration.requestyApiKey) { return i18next.t("settings:validation.apiKey") } break - case "unbound": + case providerIdentifiers.unbound: if (!apiConfiguration.unboundApiKey) { return i18next.t("settings:validation.apiKey") } break - case "litellm": + case providerIdentifiers.litellm: if (!apiConfiguration.litellmApiKey) { return i18next.t("settings:validation.apiKey") } break - case "anthropic": + case providerIdentifiers.anthropic: if (!apiConfiguration.apiKey) { return i18next.t("settings:validation.apiKey") } break - case "bedrock": + case providerIdentifiers.bedrock: if (!apiConfiguration.awsRegion) { return i18next.t("settings:validation.awsRegion") } break - case "vertex": + case providerIdentifiers.vertex: if (!apiConfiguration.vertexProjectId || !apiConfiguration.vertexRegion) { return i18next.t("settings:validation.googleCloud") } break - case "gemini": + case providerIdentifiers.gemini: if (!apiConfiguration.geminiApiKey) { return i18next.t("settings:validation.apiKey") } break - case "openai-native": + case providerIdentifiers.openaiNative: if (!apiConfiguration.openAiNativeApiKey) { return i18next.t("settings:validation.apiKey") } break - case "mistral": + case providerIdentifiers.mistral: if (!apiConfiguration.mistralApiKey) { return i18next.t("settings:validation.apiKey") } break - case "openai": + case providerIdentifiers.openai: if (!apiConfiguration.openAiBaseUrl || !apiConfiguration.openAiApiKey || !apiConfiguration.openAiModelId) { return i18next.t("settings:validation.openAi") } break - case "ollama": + case providerIdentifiers.ollama: if (!apiConfiguration.ollamaModelId) { return i18next.t("settings:validation.modelId") } break - case "lmstudio": + case providerIdentifiers.lmstudio: if (!apiConfiguration.lmStudioModelId) { return i18next.t("settings:validation.modelId") } break - case "vscode-lm": + case providerIdentifiers.vscodeLm: if (!apiConfiguration.vsCodeLmModelSelector) { return i18next.t("settings:validation.modelSelector") } break - case "fireworks": + case providerIdentifiers.fireworks: if (!apiConfiguration.fireworksApiKey) { return i18next.t("settings:validation.apiKey") } break - case "friendli": + case providerIdentifiers.friendli: if (!apiConfiguration.friendliApiKey) { return i18next.t("settings:validation.apiKey") } break - case "qwen-code": + case providerIdentifiers.qwenCode: if (!apiConfiguration.qwenCodeOauthPath) { return i18next.t("settings:validation.qwenCodeOauthPath") } break - case "kimi-code": + case providerIdentifiers.kimiCode: if ((apiConfiguration.kimiCodeAuthMethod ?? "oauth") === "api-key" && !apiConfiguration.kimiCodeApiKey) { return i18next.t("settings:validation.apiKey") } break - case "vercel-ai-gateway": + case providerIdentifiers.vercelAiGateway: if (!apiConfiguration.vercelAiGatewayApiKey) { return i18next.t("settings:validation.apiKey") } break - case "opencode-go": + case providerIdentifiers.opencodeGo: if (!apiConfiguration.opencodeGoApiKey) { return i18next.t("settings:validation.apiKey") } break - case "kenari": + case providerIdentifiers.kenari: if (!apiConfiguration.kenariApiKey) { return i18next.t("settings:validation.apiKey") } break - case "zoo-gateway": + case providerIdentifiers.zooGateway: if (!apiConfiguration.zooSessionToken && !zooCodeIsAuthenticated) { return i18next.t("settings:validation.zooGatewaySignIn") } break - case "baseten": + case providerIdentifiers.baseten: if (!apiConfiguration.basetenApiKey) { return i18next.t("settings:validation.apiKey") } @@ -206,7 +207,7 @@ function validateProviderAgainstOrganizationSettings( } function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: ProviderName): string | undefined { - if (provider === "vscode-lm") { + if (provider === providerIdentifiers.vscodeLm) { return apiConfiguration.vsCodeLmModelSelector?.id } @@ -317,7 +318,7 @@ export function validateApiConfigurationExcludingModelErrors( _routerModels?: RouterModels, // Keeping this for compatibility with the old function. organizationAllowList?: OrganizationAllowList, ): string | undefined { - if (apiConfiguration.apiProvider !== "zoo-gateway") { + if (apiConfiguration.apiProvider !== providerIdentifiers.zooGateway) { const keysAndIdsPresentErrorMessage = validateModelsAndKeysProvided(apiConfiguration) if (keysAndIdsPresentErrorMessage) {