From 84cdb1ca3d29a29e7c8f7d5493953b2d7defeb51 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Wed, 5 Aug 2026 15:29:20 +0300 Subject: [PATCH 1/4] refactor(webview): complete provider identifier migration --- .../src/components/settings/ModelInfoView.tsx | 2 +- .../src/components/settings/ModelPicker.tsx | 10 +- .../settings/__tests__/ModelInfoView.spec.tsx | 13 +++ .../src/components/settings/constants.ts | 103 +++++++++--------- 4 files changed, 74 insertions(+), 54 deletions(-) diff --git a/webview-ui/src/components/settings/ModelInfoView.tsx b/webview-ui/src/components/settings/ModelInfoView.tsx index fff55eda55..34feecb2bb 100644 --- a/webview-ui/src/components/settings/ModelInfoView.tsx +++ b/webview-ui/src/components/settings/ModelInfoView.tsx @@ -79,7 +79,7 @@ export const ModelInfoView = ({ supportsLabel={t("settings:modelInfo.supportsPromptCache")} doesNotSupportLabel={t("settings:modelInfo.noPromptCache")} />, - apiProvider === "gemini" && ( + apiProvider === providerIdentifiers.gemini && ( {selectedModelId.includes("pro-preview") ? t("settings:modelInfo.gemini.billingEstimate") diff --git a/webview-ui/src/components/settings/ModelPicker.tsx b/webview-ui/src/components/settings/ModelPicker.tsx index e32806343d..64707f6313 100644 --- a/webview-ui/src/components/settings/ModelPicker.tsx +++ b/webview-ui/src/components/settings/ModelPicker.tsx @@ -3,7 +3,13 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { Trans } from "react-i18next" import { ChevronsUpDown, Check, X, Info } from "lucide-react" -import { type ProviderSettings, type ModelInfo, type OrganizationAllowList, isRetiredProvider } from "@roo-code/types" +import { + type ProviderSettings, + type ModelInfo, + type OrganizationAllowList, + isRetiredProvider, + providerIdentifiers, +} from "@roo-code/types" import { useAppTranslation } from "@src/i18n/TranslationContext" import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" @@ -305,7 +311,7 @@ export const ModelPicker = ({ hidePricing={hidePricing} /> )} - {!hidePricing && apiConfiguration.apiProvider !== "mimo" && ( + {!hidePricing && apiConfiguration.apiProvider !== providerIdentifiers.mimo && (
{ } describe("ModelInfoView service tier pricing", () => { + it("shows Gemini billing guidance for the canonical Gemini provider", () => { + render( + , + ) + + expect(screen.getByText("settings:modelInfo.gemini.billingEstimate")).toBeInTheDocument() + }) + it("shows OpenAI Native tier prices with per-field fallback to Standard pricing", () => { const modelInfo: ModelInfo = { ...baseModelInfo, diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 15061e333d..7e5e10db6b 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -1,6 +1,7 @@ import { type ProviderName, type ModelInfo, + providerIdentifiers, anthropicModels, bedrockModels, deepSeekModels, @@ -22,57 +23,57 @@ import { } from "@roo-code/types" export const MODELS_BY_PROVIDER: Partial>> = { - anthropic: anthropicModels, - bedrock: bedrockModels, - deepseek: deepSeekModels, - moonshot: moonshotModels, - gemini: geminiModels, - mistral: mistralModels, - "openai-native": openAiNativeModels, - "openai-codex": openAiCodexModels, - "qwen-code": qwenCodeModels, - vertex: vertexModels, - xai: xaiModels, - sambanova: sambaNovaModels, - zai: internationalZAiModels, - fireworks: fireworksModels, - friendli: friendliModels, - minimax: minimaxModels, - mimo: mimoModels, - baseten: basetenModels, + [providerIdentifiers.anthropic]: anthropicModels, + [providerIdentifiers.bedrock]: bedrockModels, + [providerIdentifiers.deepseek]: deepSeekModels, + [providerIdentifiers.moonshot]: moonshotModels, + [providerIdentifiers.gemini]: geminiModels, + [providerIdentifiers.mistral]: mistralModels, + [providerIdentifiers.openaiNative]: openAiNativeModels, + [providerIdentifiers.openaiCodex]: openAiCodexModels, + [providerIdentifiers.qwenCode]: qwenCodeModels, + [providerIdentifiers.vertex]: vertexModels, + [providerIdentifiers.xai]: xaiModels, + [providerIdentifiers.sambanova]: sambaNovaModels, + [providerIdentifiers.zai]: internationalZAiModels, + [providerIdentifiers.fireworks]: fireworksModels, + [providerIdentifiers.friendli]: friendliModels, + [providerIdentifiers.minimax]: minimaxModels, + [providerIdentifiers.mimo]: mimoModels, + [providerIdentifiers.baseten]: basetenModels, } -export const PROVIDERS = [ - { value: "openrouter", label: "OpenRouter", proxy: false }, - { value: "anthropic", label: "Anthropic", proxy: false }, - { value: "gemini", label: "Google Gemini", proxy: false }, - { value: "deepseek", label: "DeepSeek", proxy: false }, - { value: "moonshot", label: "Moonshot", proxy: false }, - { value: "kimi-code", label: "Kimi Code", proxy: false }, - { value: "openai-native", label: "OpenAI", proxy: false }, - { value: "openai-codex", label: "OpenAI - ChatGPT Plus/Pro", proxy: false }, - { value: "openai", label: "OpenAI Compatible", proxy: true }, - { value: "qwen-code", label: "Qwen Code", proxy: false }, - { value: "vertex", label: "GCP Vertex AI", proxy: false }, - { value: "bedrock", label: "Amazon Bedrock", proxy: false }, - { value: "vscode-lm", label: "VS Code LM API", proxy: false }, - { value: "mistral", label: "Mistral", proxy: false }, - { value: "lmstudio", label: "LM Studio", proxy: true }, - { value: "ollama", label: "Ollama", proxy: true }, - { value: "requesty", label: "Requesty", proxy: false }, - { value: "xai", label: "xAI (Grok)", proxy: false }, - { value: "litellm", label: "LiteLLM", proxy: true }, - { value: "sambanova", label: "SambaNova", proxy: false }, - { value: "zai", label: "Z.ai", proxy: false }, - { value: "fireworks", label: "Fireworks AI", proxy: false }, - { value: "friendli", label: "Friendli", proxy: false }, - { value: "vercel-ai-gateway", label: "Vercel AI Gateway", proxy: false }, - { value: "opencode-go", label: "Opencode Go", proxy: false }, - { value: "kenari", label: "Kenari", proxy: false }, - { value: "zoo-gateway", label: "Zoo Gateway", proxy: false }, - { value: "minimax", label: "MiniMax", proxy: false }, - { value: "mimo", label: "Xiaomi MiMo", proxy: false }, - { value: "baseten", label: "Baseten", proxy: false }, - { value: "unbound", label: "Unbound", proxy: false }, - { value: "poe", label: "Poe", proxy: false }, +export const PROVIDERS: Array<{ value: string; label: string; proxy: boolean }> = [ + { value: providerIdentifiers.openrouter, label: "OpenRouter", proxy: false }, + { value: providerIdentifiers.anthropic, label: "Anthropic", proxy: false }, + { value: providerIdentifiers.gemini, label: "Google Gemini", proxy: false }, + { value: providerIdentifiers.deepseek, label: "DeepSeek", proxy: false }, + { value: providerIdentifiers.moonshot, label: "Moonshot", proxy: false }, + { value: providerIdentifiers.kimiCode, label: "Kimi Code", proxy: false }, + { value: providerIdentifiers.openaiNative, label: "OpenAI", proxy: false }, + { value: providerIdentifiers.openaiCodex, label: "OpenAI - ChatGPT Plus/Pro", proxy: false }, + { value: providerIdentifiers.openai, label: "OpenAI Compatible", proxy: true }, + { value: providerIdentifiers.qwenCode, label: "Qwen Code", proxy: false }, + { value: providerIdentifiers.vertex, label: "GCP Vertex AI", proxy: false }, + { value: providerIdentifiers.bedrock, label: "Amazon Bedrock", proxy: false }, + { value: providerIdentifiers.vscodeLm, label: "VS Code LM API", proxy: false }, + { value: providerIdentifiers.mistral, label: "Mistral", proxy: false }, + { value: providerIdentifiers.lmstudio, label: "LM Studio", proxy: true }, + { value: providerIdentifiers.ollama, label: "Ollama", proxy: true }, + { value: providerIdentifiers.requesty, label: "Requesty", proxy: false }, + { value: providerIdentifiers.xai, label: "xAI (Grok)", proxy: false }, + { value: providerIdentifiers.litellm, label: "LiteLLM", proxy: true }, + { value: providerIdentifiers.sambanova, label: "SambaNova", proxy: false }, + { value: providerIdentifiers.zai, label: "Z.ai", proxy: false }, + { value: providerIdentifiers.fireworks, label: "Fireworks AI", proxy: false }, + { value: providerIdentifiers.friendli, label: "Friendli", proxy: false }, + { value: providerIdentifiers.vercelAiGateway, label: "Vercel AI Gateway", proxy: false }, + { value: providerIdentifiers.opencodeGo, label: "Opencode Go", proxy: false }, + { value: providerIdentifiers.kenari, label: "Kenari", proxy: false }, + { value: providerIdentifiers.zooGateway, label: "Zoo Gateway", proxy: false }, + { value: providerIdentifiers.minimax, label: "MiniMax", proxy: false }, + { value: providerIdentifiers.mimo, label: "Xiaomi MiMo", proxy: false }, + { value: providerIdentifiers.baseten, label: "Baseten", proxy: false }, + { value: providerIdentifiers.unbound, label: "Unbound", proxy: false }, + { value: providerIdentifiers.poe, label: "Poe", proxy: false }, ].sort((a, b) => a.label.localeCompare(b.label)) From 74d9f569a54d174f575642f2716d30f9e8fed699 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Wed, 5 Aug 2026 18:38:55 +0300 Subject: [PATCH 2/4] test(webview): type selected model hook mocks --- .../hooks/__tests__/useSelectedModel.spec.ts | 913 ++++++++++-------- 1 file changed, 524 insertions(+), 389 deletions(-) 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 5fca23ba8e..f0c7a11543 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -3,7 +3,6 @@ import React from "react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { renderHook } from "@testing-library/react" -import type { Mock } from "vitest" import { ProviderSettings, @@ -25,17 +24,54 @@ import { moonshotModels, kimiCodeDefaultModelInfo, providerIdentifiers, + type RouterModels, } from "@roo-code/types" import { useSelectedModel } from "../useSelectedModel" -import { useRouterModels } from "../useRouterModels" -import { useOpenRouterModelProviders } from "../useOpenRouterModelProviders" - -vi.mock("../useRouterModels") -vi.mock("../useOpenRouterModelProviders") - -const mockUseRouterModels = useRouterModels as Mock -const mockUseOpenRouterModelProviders = useOpenRouterModelProviders as Mock +import type { useRouterModels } from "../useRouterModels" +import type { useOpenRouterModelProviders } from "../useOpenRouterModelProviders" + +type RouterModelsResult = Pick, "data" | "isLoading" | "isError"> +type OpenRouterModelProvidersResult = Pick< + ReturnType, + "data" | "isLoading" | "isError" +> +type OpenRouterModelProvider = NonNullable[string] + +const { mockUseRouterModels, mockUseOpenRouterModelProviders } = vi.hoisted(() => ({ + mockUseRouterModels: vi.fn<(...args: Parameters) => RouterModelsResult>(), + mockUseOpenRouterModelProviders: + vi.fn<(...args: Parameters) => OpenRouterModelProvidersResult>(), +})) + +vi.mock("../useRouterModels", () => ({ useRouterModels: mockUseRouterModels })) +vi.mock("../useOpenRouterModelProviders", () => ({ useOpenRouterModelProviders: mockUseOpenRouterModelProviders })) + +const emptyRouterModels = { + [providerIdentifiers.openrouter]: {}, + [providerIdentifiers.vercelAiGateway]: {}, + [providerIdentifiers.zooGateway]: {}, + [providerIdentifiers.litellm]: {}, + [providerIdentifiers.requesty]: {}, + [providerIdentifiers.unbound]: {}, + [providerIdentifiers.poe]: {}, + [providerIdentifiers.deepseek]: {}, + [providerIdentifiers.moonshot]: {}, + [providerIdentifiers.opencodeGo]: {}, + [providerIdentifiers.kenari]: {}, + [providerIdentifiers.kimiCode]: {}, + [providerIdentifiers.ollama]: {}, + [providerIdentifiers.lmstudio]: {}, +} satisfies RouterModels + +const routerModelsResult = (result: Omit & { data?: Partial }) => + ({ + ...result, + data: result.data ? { ...emptyRouterModels, ...result.data } : undefined, + }) satisfies RouterModelsResult + +const openRouterModelProvidersResult = (result: OpenRouterModelProvidersResult) => + result satisfies OpenRouterModelProvidersResult const createWrapper = () => { const queryClient = new QueryClient({ @@ -59,7 +95,7 @@ describe("useSelectedModel", () => { supportsPromptCache: false, } - const specificProviderInfo: ModelInfo = { + const specificProviderInfo: OpenRouterModelProvider = { maxTokens: 8192, // Different value that should override contextWindow: 16384, // Different value that should override supportsImages: true, // Different value that should override @@ -67,27 +103,32 @@ describe("useSelectedModel", () => { inputPrice: 0.001, outputPrice: 0.002, description: "Provider-specific description", + label: "Test provider", } - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: { - "test-model": baseModelInfo, + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: { + "test-model": baseModelInfo, + }, + requesty: {}, + litellm: {}, }, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: { - "test-provider": specificProviderInfo, - }, - isLoading: false, - isError: false, - } as any) + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: { + "test-provider": specificProviderInfo, + }, + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openrouter, @@ -107,11 +148,12 @@ describe("useSelectedModel", () => { inputPrice: 0.001, outputPrice: 0.002, description: "Provider-specific description", + label: "Test provider", }) }) it("should fall back to default when configured model doesn't exist in available models", () => { - const specificProviderInfo: ModelInfo = { + const specificProviderInfo: OpenRouterModelProvider = { maxTokens: 8192, contextWindow: 16384, supportsImages: true, @@ -119,36 +161,41 @@ describe("useSelectedModel", () => { inputPrice: 0.001, outputPrice: 0.002, description: "Provider-specific description", + label: "Test provider", } - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: { - "anthropic/claude-sonnet-4.5": { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 3.0, - outputPrice: 15.0, - cacheWritesPrice: 3.75, - cacheReadsPrice: 0.3, + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: { + "anthropic/claude-sonnet-4.5": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + }, }, + requesty: {}, + litellm: {}, }, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: { - "test-provider": specificProviderInfo, - }, - isLoading: false, - isError: false, - } as any) + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: { + "test-provider": specificProviderInfo, + }, + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openrouter, @@ -177,7 +224,7 @@ describe("useSelectedModel", () => { }) }) - it("should demonstrate the merging behavior validates the comment about missing fields", () => { + it("should merge provider values with base-only pricing fields", () => { const baseModelInfo: ModelInfo = { maxTokens: 4096, contextWindow: 8192, @@ -187,31 +234,38 @@ describe("useSelectedModel", () => { cacheReadsPrice: 0.01, } - const specificProviderInfo: Partial = { + const specificProviderInfo = { inputPrice: 0.001, outputPrice: 0.002, description: "Provider-specific description", maxTokens: 8192, // Override this one supportsImages: true, // Override this one + contextWindow: 8192, + supportsPromptCache: false, + label: "Test provider", } - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: { - "test-model": baseModelInfo, + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: { + "test-model": baseModelInfo, + }, + requesty: {}, + litellm: {}, }, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: { "test-provider": specificProviderInfo as ModelInfo }, - isLoading: false, - isError: false, - } as any) + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: { "test-provider": specificProviderInfo }, + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openrouter, @@ -224,9 +278,9 @@ describe("useSelectedModel", () => { expect(result.current.id).toBe("test-model") expect(result.current.info).toEqual({ - // Fields from base model that provider doesn't have - contextWindow: 8192, // From base (provider doesn't override) - supportsPromptCache: false, // From base (provider doesn't override) + // Values shared by the base model and provider + contextWindow: 8192, + supportsPromptCache: false, cacheWritesPrice: 0.1, // From base (provider doesn't have) cacheReadsPrice: 0.01, // From base (provider doesn't have) @@ -238,6 +292,7 @@ describe("useSelectedModel", () => { inputPrice: 0.001, // From provider (base doesn't have) outputPrice: 0.002, // From provider (base doesn't have) description: "Provider-specific description", // From provider (base doesn't have) + label: "Test provider", // From provider (base doesn't have) }) }) @@ -249,21 +304,25 @@ describe("useSelectedModel", () => { supportsPromptCache: false, } - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: { "test-model": baseModelInfo }, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: { "test-model": baseModelInfo }, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openrouter, @@ -278,33 +337,37 @@ describe("useSelectedModel", () => { }) it("should fall back to default when configured model and provider don't exist", () => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: { - "anthropic/claude-sonnet-4.5": { - // Default model - using correct default model name - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 3.0, - outputPrice: 15.0, - cacheWritesPrice: 3.75, - cacheReadsPrice: 0.3, + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: { + "anthropic/claude-sonnet-4.5": { + // Default model - using correct default model name + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + }, }, + requesty: {}, + litellm: {}, }, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openrouter, @@ -333,17 +396,21 @@ describe("useSelectedModel", () => { describe("loading and error states", () => { it("should set loading when router models are loading for the default OpenRouter provider", () => { - mockUseRouterModels.mockReturnValue({ - data: undefined, - isLoading: true, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: undefined, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: undefined, + isLoading: true, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: undefined, + isLoading: false, + isError: false, + }), + ) const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(), { wrapper }) @@ -352,17 +419,21 @@ describe("useSelectedModel", () => { }) it("should set loading when OpenRouter provider metadata is loading for the default provider", () => { - mockUseRouterModels.mockReturnValue({ - data: { openrouter: {}, requesty: {}, litellm: {} }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: undefined, - isLoading: true, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { openrouter: {}, requesty: {}, litellm: {} }, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: undefined, + isLoading: true, + isError: false, + }), + ) const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(), { wrapper }) @@ -371,17 +442,21 @@ describe("useSelectedModel", () => { }) it("should set error when router models error for the default OpenRouter provider", () => { - mockUseRouterModels.mockReturnValue({ - data: undefined, - isLoading: false, - isError: true, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: undefined, + isLoading: false, + isError: true, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(), { wrapper }) @@ -392,17 +467,21 @@ describe("useSelectedModel", () => { describe("default behavior", () => { it("should return OpenRouter default when no configuration is provided", () => { - mockUseRouterModels.mockReturnValue({ - data: undefined, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: undefined, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: undefined, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: undefined, + isLoading: false, + isError: false, + }), + ) const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(), { wrapper }) @@ -415,17 +494,21 @@ describe("useSelectedModel", () => { describe("anthropic provider with 1M context", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue({ - data: undefined, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: undefined, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: undefined, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: undefined, + isLoading: false, + isError: false, + }), + ) }) it("should apply 1M pricing tier for Claude Sonnet 4.6 when enabled", () => { @@ -480,21 +563,25 @@ describe("useSelectedModel", () => { describe("bedrock provider with 1M context", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) }) it("should enable 1M context window for Bedrock Claude Sonnet 4 when awsBedrock1MContext is true", () => { @@ -542,21 +629,25 @@ describe("useSelectedModel", () => { describe("bedrock provider with custom ARN", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) }) it("should enable supportsPromptCache for custom-arn model", () => { @@ -588,23 +679,27 @@ describe("useSelectedModel", () => { describe("litellm provider", () => { beforeEach(() => { - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) }) it("should use litellmDefaultModelInfo as fallback when routerModels.litellm is empty", () => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -622,15 +717,17 @@ describe("useSelectedModel", () => { }) it("should return an empty model ID when the list is empty and no model is configured", () => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -650,22 +747,24 @@ describe("useSelectedModel", () => { // Primary user-visible scenario: a "Sync Models" click momentarily empties the // router-models list before the refreshed list arrives. The selection must be held // across that transition rather than reset. - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: { - "my-custom-model": { - maxTokens: 4096, - contextWindow: 8192, - supportsImages: false, - supportsPromptCache: false, + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: { + "my-custom-model": { + maxTokens: 4096, + contextWindow: 8192, + supportsImages: false, + supportsPromptCache: false, + }, }, }, - }, - isLoading: false, - isError: false, - } as any) + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -679,15 +778,17 @@ describe("useSelectedModel", () => { expect(result.current.id).toBe("my-custom-model") // Simulate the list emptying mid-sync. - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + }), + ) rerender() // Selection is preserved through the empty window. @@ -695,22 +796,24 @@ describe("useSelectedModel", () => { }) it("should use litellmDefaultModelInfo when selected model not found in routerModels", () => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: { - "existing-model": { - maxTokens: 4096, - contextWindow: 8192, - supportsImages: false, - supportsPromptCache: false, + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: { + "existing-model": { + maxTokens: 4096, + contextWindow: 8192, + supportsImages: false, + supportsPromptCache: false, + }, }, }, - }, - isLoading: false, - isError: false, - } as any) + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -736,17 +839,19 @@ describe("useSelectedModel", () => { description: "Custom LiteLLM model", } - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: { - "custom-model": customModelInfo, + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: { + "custom-model": customModelInfo, + }, }, - }, - isLoading: false, - isError: false, - } as any) + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -764,11 +869,13 @@ describe("useSelectedModel", () => { describe("kenari provider", () => { beforeEach(() => { - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) }) it("should return routerModels info for the configured kenari model", () => { @@ -780,18 +887,20 @@ describe("useSelectedModel", () => { description: "GLM 5.2 via Kenari", } - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - kenari: { - "glm-5-2": customModelInfo, + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + kenari: { + "glm-5-2": customModelInfo, + }, }, - }, - isLoading: false, - isError: false, - } as any) + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.kenari, @@ -807,16 +916,18 @@ describe("useSelectedModel", () => { }) it("should use kenariDefaultModelInfo as fallback when routerModels.kenari is empty", () => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - kenari: {}, - }, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + kenari: {}, + }, + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.kenari, @@ -836,21 +947,25 @@ describe("useSelectedModel", () => { describe("openai provider", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) }) it("should use openAiModelInfoSaneDefaults when no custom model info is provided", () => { @@ -917,21 +1032,25 @@ describe("useSelectedModel", () => { describe("minimax provider", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) }) it("should return default minimax model when no custom model is specified", () => { @@ -964,21 +1083,25 @@ describe("useSelectedModel", () => { describe("vscode-lm provider", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) }) it("resolves a listed family's contextWindow to its maxInputTokens", () => { @@ -1035,21 +1158,25 @@ describe("useSelectedModel", () => { describe("friendli provider", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) }) it("should return default Friendli model when no custom model is specified", () => { @@ -1087,11 +1214,13 @@ describe("useSelectedModel", () => { description: "Configured Kimi Code model", } - mockUseRouterModels.mockReturnValue({ - data: { "kimi-code": { "kimi-for-coding": modelInfo } }, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { "kimi-code": { "kimi-for-coding": modelInfo } }, + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.kimiCode, @@ -1126,22 +1255,26 @@ describe("useSelectedModel", () => { describe("moonshot provider", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - moonshot: {}, - }, - isLoading: false, - isError: false, - } as any) - - mockUseOpenRouterModelProviders.mockReturnValue({ - data: {}, - isLoading: false, - isError: false, - } as any) + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + moonshot: {}, + }, + isLoading: false, + isError: false, + }), + ) + + mockUseOpenRouterModelProviders.mockReturnValue( + openRouterModelProvidersResult({ + data: {}, + isLoading: false, + isError: false, + }), + ) }) it("should return default moonshot model when no custom model is specified", () => { @@ -1167,18 +1300,20 @@ describe("useSelectedModel", () => { outputPrice: 5.0, } - mockUseRouterModels.mockReturnValue({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - moonshot: { - "kimi-k2-0905-preview": routerModelInfo, + mockUseRouterModels.mockReturnValue( + routerModelsResult({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + moonshot: { + "kimi-k2-0905-preview": routerModelInfo, + }, }, - }, - isLoading: false, - isError: false, - } as any) + isLoading: false, + isError: false, + }), + ) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.moonshot, From 3219853b4f7e5a61691e51d93ebdeb4419a2834c Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Wed, 5 Aug 2026 19:28:31 +0300 Subject: [PATCH 3/4] test(webview): keep selected model spec out of provider migration --- .../hooks/__tests__/useSelectedModel.spec.ts | 913 ++++++++---------- 1 file changed, 389 insertions(+), 524 deletions(-) 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 f0c7a11543..5fca23ba8e 100644 --- a/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts +++ b/webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts @@ -3,6 +3,7 @@ import React from "react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { renderHook } from "@testing-library/react" +import type { Mock } from "vitest" import { ProviderSettings, @@ -24,54 +25,17 @@ import { moonshotModels, kimiCodeDefaultModelInfo, providerIdentifiers, - type RouterModels, } from "@roo-code/types" import { useSelectedModel } from "../useSelectedModel" -import type { useRouterModels } from "../useRouterModels" -import type { useOpenRouterModelProviders } from "../useOpenRouterModelProviders" - -type RouterModelsResult = Pick, "data" | "isLoading" | "isError"> -type OpenRouterModelProvidersResult = Pick< - ReturnType, - "data" | "isLoading" | "isError" -> -type OpenRouterModelProvider = NonNullable[string] - -const { mockUseRouterModels, mockUseOpenRouterModelProviders } = vi.hoisted(() => ({ - mockUseRouterModels: vi.fn<(...args: Parameters) => RouterModelsResult>(), - mockUseOpenRouterModelProviders: - vi.fn<(...args: Parameters) => OpenRouterModelProvidersResult>(), -})) - -vi.mock("../useRouterModels", () => ({ useRouterModels: mockUseRouterModels })) -vi.mock("../useOpenRouterModelProviders", () => ({ useOpenRouterModelProviders: mockUseOpenRouterModelProviders })) - -const emptyRouterModels = { - [providerIdentifiers.openrouter]: {}, - [providerIdentifiers.vercelAiGateway]: {}, - [providerIdentifiers.zooGateway]: {}, - [providerIdentifiers.litellm]: {}, - [providerIdentifiers.requesty]: {}, - [providerIdentifiers.unbound]: {}, - [providerIdentifiers.poe]: {}, - [providerIdentifiers.deepseek]: {}, - [providerIdentifiers.moonshot]: {}, - [providerIdentifiers.opencodeGo]: {}, - [providerIdentifiers.kenari]: {}, - [providerIdentifiers.kimiCode]: {}, - [providerIdentifiers.ollama]: {}, - [providerIdentifiers.lmstudio]: {}, -} satisfies RouterModels - -const routerModelsResult = (result: Omit & { data?: Partial }) => - ({ - ...result, - data: result.data ? { ...emptyRouterModels, ...result.data } : undefined, - }) satisfies RouterModelsResult - -const openRouterModelProvidersResult = (result: OpenRouterModelProvidersResult) => - result satisfies OpenRouterModelProvidersResult +import { useRouterModels } from "../useRouterModels" +import { useOpenRouterModelProviders } from "../useOpenRouterModelProviders" + +vi.mock("../useRouterModels") +vi.mock("../useOpenRouterModelProviders") + +const mockUseRouterModels = useRouterModels as Mock +const mockUseOpenRouterModelProviders = useOpenRouterModelProviders as Mock const createWrapper = () => { const queryClient = new QueryClient({ @@ -95,7 +59,7 @@ describe("useSelectedModel", () => { supportsPromptCache: false, } - const specificProviderInfo: OpenRouterModelProvider = { + const specificProviderInfo: ModelInfo = { maxTokens: 8192, // Different value that should override contextWindow: 16384, // Different value that should override supportsImages: true, // Different value that should override @@ -103,32 +67,27 @@ describe("useSelectedModel", () => { inputPrice: 0.001, outputPrice: 0.002, description: "Provider-specific description", - label: "Test provider", } - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: { - "test-model": baseModelInfo, - }, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: { - "test-provider": specificProviderInfo, + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: { + "test-model": baseModelInfo, }, - isLoading: false, - isError: false, - }), - ) + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: { + "test-provider": specificProviderInfo, + }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openrouter, @@ -148,12 +107,11 @@ describe("useSelectedModel", () => { inputPrice: 0.001, outputPrice: 0.002, description: "Provider-specific description", - label: "Test provider", }) }) it("should fall back to default when configured model doesn't exist in available models", () => { - const specificProviderInfo: OpenRouterModelProvider = { + const specificProviderInfo: ModelInfo = { maxTokens: 8192, contextWindow: 16384, supportsImages: true, @@ -161,41 +119,36 @@ describe("useSelectedModel", () => { inputPrice: 0.001, outputPrice: 0.002, description: "Provider-specific description", - label: "Test provider", } - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: { - "anthropic/claude-sonnet-4.5": { - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 3.0, - outputPrice: 15.0, - cacheWritesPrice: 3.75, - cacheReadsPrice: 0.3, - }, + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: { + "anthropic/claude-sonnet-4.5": { + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, }, - requesty: {}, - litellm: {}, }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: { - "test-provider": specificProviderInfo, - }, - isLoading: false, - isError: false, - }), - ) + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: { + "test-provider": specificProviderInfo, + }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openrouter, @@ -224,7 +177,7 @@ describe("useSelectedModel", () => { }) }) - it("should merge provider values with base-only pricing fields", () => { + it("should demonstrate the merging behavior validates the comment about missing fields", () => { const baseModelInfo: ModelInfo = { maxTokens: 4096, contextWindow: 8192, @@ -234,38 +187,31 @@ describe("useSelectedModel", () => { cacheReadsPrice: 0.01, } - const specificProviderInfo = { + const specificProviderInfo: Partial = { inputPrice: 0.001, outputPrice: 0.002, description: "Provider-specific description", maxTokens: 8192, // Override this one supportsImages: true, // Override this one - contextWindow: 8192, - supportsPromptCache: false, - label: "Test provider", } - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: { - "test-model": baseModelInfo, - }, - requesty: {}, - litellm: {}, + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: { + "test-model": baseModelInfo, }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: { "test-provider": specificProviderInfo }, - isLoading: false, - isError: false, - }), - ) + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: { "test-provider": specificProviderInfo as ModelInfo }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openrouter, @@ -278,9 +224,9 @@ describe("useSelectedModel", () => { expect(result.current.id).toBe("test-model") expect(result.current.info).toEqual({ - // Values shared by the base model and provider - contextWindow: 8192, - supportsPromptCache: false, + // Fields from base model that provider doesn't have + contextWindow: 8192, // From base (provider doesn't override) + supportsPromptCache: false, // From base (provider doesn't override) cacheWritesPrice: 0.1, // From base (provider doesn't have) cacheReadsPrice: 0.01, // From base (provider doesn't have) @@ -292,7 +238,6 @@ describe("useSelectedModel", () => { inputPrice: 0.001, // From provider (base doesn't have) outputPrice: 0.002, // From provider (base doesn't have) description: "Provider-specific description", // From provider (base doesn't have) - label: "Test provider", // From provider (base doesn't have) }) }) @@ -304,25 +249,21 @@ describe("useSelectedModel", () => { supportsPromptCache: false, } - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: { "test-model": baseModelInfo }, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: { "test-model": baseModelInfo }, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openrouter, @@ -337,37 +278,33 @@ describe("useSelectedModel", () => { }) it("should fall back to default when configured model and provider don't exist", () => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: { - "anthropic/claude-sonnet-4.5": { - // Default model - using correct default model name - maxTokens: 8192, - contextWindow: 200_000, - supportsImages: true, - supportsPromptCache: true, - inputPrice: 3.0, - outputPrice: 15.0, - cacheWritesPrice: 3.75, - cacheReadsPrice: 0.3, - }, + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: { + "anthropic/claude-sonnet-4.5": { + // Default model - using correct default model name + maxTokens: 8192, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, }, - requesty: {}, - litellm: {}, }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.openrouter, @@ -396,21 +333,17 @@ describe("useSelectedModel", () => { describe("loading and error states", () => { it("should set loading when router models are loading for the default OpenRouter provider", () => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: undefined, - isLoading: true, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: undefined, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: undefined, + isLoading: true, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: undefined, + isLoading: false, + isError: false, + } as any) const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(), { wrapper }) @@ -419,21 +352,17 @@ describe("useSelectedModel", () => { }) it("should set loading when OpenRouter provider metadata is loading for the default provider", () => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { openrouter: {}, requesty: {}, litellm: {} }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: undefined, - isLoading: true, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { openrouter: {}, requesty: {}, litellm: {} }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: undefined, + isLoading: true, + isError: false, + } as any) const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(), { wrapper }) @@ -442,21 +371,17 @@ describe("useSelectedModel", () => { }) it("should set error when router models error for the default OpenRouter provider", () => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: undefined, - isLoading: false, - isError: true, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: undefined, + isLoading: false, + isError: true, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(), { wrapper }) @@ -467,21 +392,17 @@ describe("useSelectedModel", () => { describe("default behavior", () => { it("should return OpenRouter default when no configuration is provided", () => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: undefined, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: undefined, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: undefined, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: undefined, + isLoading: false, + isError: false, + } as any) const wrapper = createWrapper() const { result } = renderHook(() => useSelectedModel(), { wrapper }) @@ -494,21 +415,17 @@ describe("useSelectedModel", () => { describe("anthropic provider with 1M context", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: undefined, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: undefined, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: undefined, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: undefined, + isLoading: false, + isError: false, + } as any) }) it("should apply 1M pricing tier for Claude Sonnet 4.6 when enabled", () => { @@ -563,25 +480,21 @@ describe("useSelectedModel", () => { describe("bedrock provider with 1M context", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) }) it("should enable 1M context window for Bedrock Claude Sonnet 4 when awsBedrock1MContext is true", () => { @@ -629,25 +542,21 @@ describe("useSelectedModel", () => { describe("bedrock provider with custom ARN", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) }) it("should enable supportsPromptCache for custom-arn model", () => { @@ -679,27 +588,23 @@ describe("useSelectedModel", () => { describe("litellm provider", () => { beforeEach(() => { - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) }) it("should use litellmDefaultModelInfo as fallback when routerModels.litellm is empty", () => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -717,17 +622,15 @@ describe("useSelectedModel", () => { }) it("should return an empty model ID when the list is empty and no model is configured", () => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -747,24 +650,22 @@ describe("useSelectedModel", () => { // Primary user-visible scenario: a "Sync Models" click momentarily empties the // router-models list before the refreshed list arrives. The selection must be held // across that transition rather than reset. - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: { - "my-custom-model": { - maxTokens: 4096, - contextWindow: 8192, - supportsImages: false, - supportsPromptCache: false, - }, + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: { + "my-custom-model": { + maxTokens: 4096, + contextWindow: 8192, + supportsImages: false, + supportsPromptCache: false, }, }, - isLoading: false, - isError: false, - }), - ) + }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -778,17 +679,15 @@ describe("useSelectedModel", () => { expect(result.current.id).toBe("my-custom-model") // Simulate the list emptying mid-sync. - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) rerender() // Selection is preserved through the empty window. @@ -796,24 +695,22 @@ describe("useSelectedModel", () => { }) it("should use litellmDefaultModelInfo when selected model not found in routerModels", () => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: { - "existing-model": { - maxTokens: 4096, - contextWindow: 8192, - supportsImages: false, - supportsPromptCache: false, - }, + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: { + "existing-model": { + maxTokens: 4096, + contextWindow: 8192, + supportsImages: false, + supportsPromptCache: false, }, }, - isLoading: false, - isError: false, - }), - ) + }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -839,19 +736,17 @@ describe("useSelectedModel", () => { description: "Custom LiteLLM model", } - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: { - "custom-model": customModelInfo, - }, + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: { + "custom-model": customModelInfo, }, - isLoading: false, - isError: false, - }), - ) + }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.litellm, @@ -869,13 +764,11 @@ describe("useSelectedModel", () => { describe("kenari provider", () => { beforeEach(() => { - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) }) it("should return routerModels info for the configured kenari model", () => { @@ -887,20 +780,18 @@ describe("useSelectedModel", () => { description: "GLM 5.2 via Kenari", } - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - kenari: { - "glm-5-2": customModelInfo, - }, + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + kenari: { + "glm-5-2": customModelInfo, }, - isLoading: false, - isError: false, - }), - ) + }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.kenari, @@ -916,18 +807,16 @@ describe("useSelectedModel", () => { }) it("should use kenariDefaultModelInfo as fallback when routerModels.kenari is empty", () => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - kenari: {}, - }, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + kenari: {}, + }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.kenari, @@ -947,25 +836,21 @@ describe("useSelectedModel", () => { describe("openai provider", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) }) it("should use openAiModelInfoSaneDefaults when no custom model info is provided", () => { @@ -1032,25 +917,21 @@ describe("useSelectedModel", () => { describe("minimax provider", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) }) it("should return default minimax model when no custom model is specified", () => { @@ -1083,25 +964,21 @@ describe("useSelectedModel", () => { describe("vscode-lm provider", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) }) it("resolves a listed family's contextWindow to its maxInputTokens", () => { @@ -1158,25 +1035,21 @@ describe("useSelectedModel", () => { describe("friendli provider", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) }) it("should return default Friendli model when no custom model is specified", () => { @@ -1214,13 +1087,11 @@ describe("useSelectedModel", () => { description: "Configured Kimi Code model", } - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { "kimi-code": { "kimi-for-coding": modelInfo } }, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { "kimi-code": { "kimi-for-coding": modelInfo } }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.kimiCode, @@ -1255,26 +1126,22 @@ describe("useSelectedModel", () => { describe("moonshot provider", () => { beforeEach(() => { - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - moonshot: {}, - }, - isLoading: false, - isError: false, - }), - ) - - mockUseOpenRouterModelProviders.mockReturnValue( - openRouterModelProvidersResult({ - data: {}, - isLoading: false, - isError: false, - }), - ) + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + moonshot: {}, + }, + isLoading: false, + isError: false, + } as any) + + mockUseOpenRouterModelProviders.mockReturnValue({ + data: {}, + isLoading: false, + isError: false, + } as any) }) it("should return default moonshot model when no custom model is specified", () => { @@ -1300,20 +1167,18 @@ describe("useSelectedModel", () => { outputPrice: 5.0, } - mockUseRouterModels.mockReturnValue( - routerModelsResult({ - data: { - openrouter: {}, - requesty: {}, - litellm: {}, - moonshot: { - "kimi-k2-0905-preview": routerModelInfo, - }, + mockUseRouterModels.mockReturnValue({ + data: { + openrouter: {}, + requesty: {}, + litellm: {}, + moonshot: { + "kimi-k2-0905-preview": routerModelInfo, }, - isLoading: false, - isError: false, - }), - ) + }, + isLoading: false, + isError: false, + } as any) const apiConfiguration: ProviderSettings = { apiProvider: providerIdentifiers.moonshot, From ac0b8f2d35032113db386af62e88d7cd1f5c130c Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Thu, 6 Aug 2026 00:07:39 +0300 Subject: [PATCH 4/4] refactor(providers): canonicalize model router calls --- src/api/providers/fetchers/lmstudio.ts | 4 +- .../providers/fetchers/modelEndpointCache.ts | 6 +- src/api/providers/kimi-code.ts | 3 +- src/api/providers/lm-studio.ts | 9 +- src/api/providers/openrouter.ts | 9 +- src/api/providers/poe.ts | 3 +- src/api/providers/requesty.ts | 10 +- src/api/providers/unbound.ts | 13 +- src/core/webview/webviewMessageHandler.ts | 116 +++++++++++------- 9 files changed, 111 insertions(+), 62 deletions(-) diff --git a/src/api/providers/fetchers/lmstudio.ts b/src/api/providers/fetchers/lmstudio.ts index 73cb60e88e..842fe9d08d 100644 --- a/src/api/providers/fetchers/lmstudio.ts +++ b/src/api/providers/fetchers/lmstudio.ts @@ -1,7 +1,7 @@ import axios from "axios" import { LLM, LLMInfo, LLMInstanceInfo, LMStudioClient } from "@lmstudio/sdk" -import { type ModelInfo, lMStudioDefaultModelInfo } from "@roo-code/types" +import { type ModelInfo, lMStudioDefaultModelInfo, providerIdentifiers } from "@roo-code/types" import { flushModels, getModels } from "./modelCache" @@ -19,7 +19,7 @@ export const forceFullModelDetailsLoad = async (baseUrl: string, modelId: string const client = new LMStudioClient({ baseUrl: lmsUrl }) await client.llm.model(modelId) // Flush and refresh cache to get updated model details - await flushModels({ provider: "lmstudio", baseUrl }, true) + await flushModels({ provider: providerIdentifiers.lmstudio, baseUrl }, true) // Mark this model as having full details loaded. modelsWithLoadedDetails.add(modelId) diff --git a/src/api/providers/fetchers/modelEndpointCache.ts b/src/api/providers/fetchers/modelEndpointCache.ts index 06d6234f91..4e85213a6c 100644 --- a/src/api/providers/fetchers/modelEndpointCache.ts +++ b/src/api/providers/fetchers/modelEndpointCache.ts @@ -4,7 +4,7 @@ import fs from "fs/promises" import NodeCache from "node-cache" import sanitize from "sanitize-filename" -import type { ModelRecord } from "@roo-code/types" +import { providerIdentifiers, type ModelRecord } from "@roo-code/types" import { ContextProxy } from "../../../core/config/ContextProxy" import { RouterName } from "../../../shared/api" @@ -44,7 +44,7 @@ export const getModelEndpoints = async ({ }): Promise => { // OpenRouter is the only provider that supports model endpoints, but you // can see how we'd extend this to other providers in the future. - if (router !== "openrouter" || !modelId || !endpoint) { + if (router !== providerIdentifiers.openrouter || !modelId || !endpoint) { return {} } @@ -61,7 +61,7 @@ export const getModelEndpoints = async ({ // Copy model-level capabilities from the parent model to each endpoint // These are capabilities that don't vary by provider (tools, reasoning, etc.) if (Object.keys(modelProviders).length > 0) { - const parentModels = await getModels({ provider: "openrouter" }) + const parentModels = await getModels({ provider: providerIdentifiers.openrouter }) const parentModel = parentModels[modelId] if (parentModel) { diff --git a/src/api/providers/kimi-code.ts b/src/api/providers/kimi-code.ts index 0a50ce6ec3..76f674ccc7 100644 --- a/src/api/providers/kimi-code.ts +++ b/src/api/providers/kimi-code.ts @@ -4,6 +4,7 @@ import { KIMI_CODE_BASE_URL, kimiCodeDefaultModelId, kimiCodeDefaultModelInfo, + providerIdentifiers, type ModelInfo, type ModelRecord, } from "@roo-code/types" @@ -63,7 +64,7 @@ export class KimiCodeHandler extends OpenAiHandler { if (!this.modelDiscoveryAttempted) { this.modelDiscoveryAttempted = true try { - this.models = await getModels({ provider: "kimi-code", apiKey: accessToken }) + this.models = await getModels({ provider: providerIdentifiers.kimiCode, apiKey: accessToken }) } catch (error) { // Model discovery is best-effort; preserve the configured ID and fallback metadata. console.debug("[KimiCode] Model discovery failed; using fallback model metadata", { diff --git a/src/api/providers/lm-studio.ts b/src/api/providers/lm-studio.ts index 79f5355ef8..be752ac887 100644 --- a/src/api/providers/lm-studio.ts +++ b/src/api/providers/lm-studio.ts @@ -2,7 +2,12 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" import axios from "axios" -import { type ModelInfo, openAiModelInfoSaneDefaults, LMSTUDIO_DEFAULT_TEMPERATURE } from "@roo-code/types" +import { + type ModelInfo, + openAiModelInfoSaneDefaults, + LMSTUDIO_DEFAULT_TEMPERATURE, + providerIdentifiers, +} from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" @@ -171,7 +176,7 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan override getModel(): { id: string; info: ModelInfo } { const models = getModelsFromCache({ - provider: "lmstudio", + provider: providerIdentifiers.lmstudio, baseUrl: this.options.lmStudioBaseUrl, }) if (models && this.options.lmStudioModelId && models[this.options.lmStudioModelId]) { diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 3e59b4360b..e0d4d53324 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -10,6 +10,7 @@ import { OPENROUTER_DEFAULT_PROVIDER_NAME, OPEN_ROUTER_PROMPT_CACHING_MODELS, DEEP_SEEK_DEFAULT_TEMPERATURE, + providerIdentifiers, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -164,9 +165,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH private async loadDynamicModels(): Promise { try { const [models, endpoints] = await Promise.all([ - getModels({ provider: "openrouter" }), + getModels({ provider: providerIdentifiers.openrouter }), getModelEndpoints({ - router: "openrouter", + router: providerIdentifiers.openrouter, modelId: this.options.openRouterModelId, endpoint: this.options.openRouterSpecificProvider, }), @@ -535,9 +536,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH public async fetchModel() { const [models, endpoints] = await Promise.all([ - getModels({ provider: "openrouter" }), + getModels({ provider: providerIdentifiers.openrouter }), getModelEndpoints({ - router: "openrouter", + router: providerIdentifiers.openrouter, modelId: this.options.openRouterModelId, endpoint: this.options.openRouterSpecificProvider, }), diff --git a/src/api/providers/poe.ts b/src/api/providers/poe.ts index 1e5315b1ba..9d1d7bc9dc 100644 --- a/src/api/providers/poe.ts +++ b/src/api/providers/poe.ts @@ -9,6 +9,7 @@ import { type ModelInfo, type ReasoningEffortExtended, ApiProviderError, + providerIdentifiers, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -39,7 +40,7 @@ export class PoeHandler extends BaseProvider implements SingleCompletionHandler override getModel() { const id = this.options.apiModelId ?? poeDefaultModelId const cached = getModelsFromCache({ - provider: "poe", + provider: providerIdentifiers.poe, apiKey: this.options.poeApiKey, baseUrl: this.options.poeBaseUrl, }) diff --git a/src/api/providers/requesty.ts b/src/api/providers/requesty.ts index 5753660de5..4731a66c21 100644 --- a/src/api/providers/requesty.ts +++ b/src/api/providers/requesty.ts @@ -1,7 +1,13 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -import { type ModelInfo, type ModelRecord, requestyDefaultModelId, requestyDefaultModelInfo } from "@roo-code/types" +import { + type ModelInfo, + type ModelRecord, + providerIdentifiers, + requestyDefaultModelId, + requestyDefaultModelInfo, +} from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" import { calculateApiCostOpenAI } from "../../shared/cost" @@ -74,7 +80,7 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan } public async fetchModel() { - this.models = await getModels({ provider: "requesty", baseUrl: this.baseURL }) + this.models = await getModels({ provider: providerIdentifiers.requesty, baseUrl: this.baseURL }) return this.getModel() } diff --git a/src/api/providers/unbound.ts b/src/api/providers/unbound.ts index c3ec9c44fc..0ffc65c248 100644 --- a/src/api/providers/unbound.ts +++ b/src/api/providers/unbound.ts @@ -1,7 +1,13 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -import { type ModelInfo, type ModelRecord, unboundDefaultModelId, unboundDefaultModelInfo } from "@roo-code/types" +import { + type ModelInfo, + type ModelRecord, + providerIdentifiers, + unboundDefaultModelId, + unboundDefaultModelInfo, +} from "@roo-code/types" import type { ApiHandlerOptions } from "../../shared/api" import { calculateApiCostOpenAI } from "../../shared/cost" @@ -68,7 +74,10 @@ export class UnboundHandler extends BaseProvider implements SingleCompletionHand } public async fetchModel() { - this.models = await getModels({ provider: "unbound", apiKey: this.options.unboundApiKey }) + this.models = await getModels({ + provider: providerIdentifiers.unbound, + apiKey: this.options.unboundApiKey, + }) return this.getModel() } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 7009343573..1458e1e50d 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1045,20 +1045,20 @@ export const webviewMessageHandler = async ( const routerModels: Record = providerFilter ? ({} as Record) : { - openrouter: {}, - "vercel-ai-gateway": {}, - "zoo-gateway": {}, - litellm: {}, - requesty: {}, - unbound: {}, - ollama: {}, - lmstudio: {}, - poe: {}, - deepseek: {}, - moonshot: {}, - "opencode-go": {}, - kenari: {}, - "kimi-code": {}, + [providerIdentifiers.openrouter]: {}, + [providerIdentifiers.vercelAiGateway]: {}, + [providerIdentifiers.zooGateway]: {}, + [providerIdentifiers.litellm]: {}, + [providerIdentifiers.requesty]: {}, + [providerIdentifiers.unbound]: {}, + [providerIdentifiers.ollama]: {}, + [providerIdentifiers.lmstudio]: {}, + [providerIdentifiers.poe]: {}, + [providerIdentifiers.deepseek]: {}, + [providerIdentifiers.moonshot]: {}, + [providerIdentifiers.opencodeGo]: {}, + [providerIdentifiers.kenari]: {}, + [providerIdentifiers.kimiCode]: {}, } const safeGetModels = async (options: GetModelsOptions): Promise => { @@ -1076,27 +1076,33 @@ export const webviewMessageHandler = async ( // Base candidates (only those handled by this aggregate fetcher) const candidates: { key: RouterName; options: GetModelsOptions }[] = [ - { key: "openrouter", options: { provider: "openrouter" } }, { - key: "requesty", + key: providerIdentifiers.openrouter, + options: { provider: providerIdentifiers.openrouter }, + }, + { + key: providerIdentifiers.requesty, options: { - provider: "requesty", + provider: providerIdentifiers.requesty, apiKey: apiConfiguration.requestyApiKey, baseUrl: apiConfiguration.requestyBaseUrl, }, }, { - key: "unbound", + key: providerIdentifiers.unbound, options: { - provider: "unbound", + provider: providerIdentifiers.unbound, apiKey: apiConfiguration.unboundApiKey, }, }, - { key: "vercel-ai-gateway", options: { provider: "vercel-ai-gateway" } }, { - key: "zoo-gateway", + key: providerIdentifiers.vercelAiGateway, + options: { provider: providerIdentifiers.vercelAiGateway }, + }, + { + key: providerIdentifiers.zooGateway, options: { - provider: "zoo-gateway", + provider: providerIdentifiers.zooGateway, apiKey: apiConfiguration.zooSessionToken, baseUrl: apiConfiguration.zooGatewayBaseUrl, }, @@ -1113,12 +1119,15 @@ export const webviewMessageHandler = async ( // If explicit credentials are provided in message.values (from Refresh Models button), // flush the cache first to ensure we fetch fresh data with the new credentials if (message?.values?.litellmApiKey || message?.values?.litellmBaseUrl) { - await flushModels({ provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl }, true) + await flushModels( + { provider: providerIdentifiers.litellm, apiKey: litellmApiKey, baseUrl: litellmBaseUrl }, + true, + ) } candidates.push({ - key: "litellm", - options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl }, + key: providerIdentifiers.litellm, + options: { provider: providerIdentifiers.litellm, apiKey: litellmApiKey, baseUrl: litellmBaseUrl }, }) } @@ -1128,12 +1137,15 @@ export const webviewMessageHandler = async ( if (poeApiKey) { if (message?.values?.poeApiKey || message?.values?.poeBaseUrl) { - await flushModels({ provider: "poe", apiKey: poeApiKey, baseUrl: poeBaseUrl }, true) + await flushModels( + { provider: providerIdentifiers.poe, apiKey: poeApiKey, baseUrl: poeBaseUrl }, + true, + ) } candidates.push({ - key: "poe", - options: { provider: "poe", apiKey: poeApiKey, baseUrl: poeBaseUrl }, + key: providerIdentifiers.poe, + options: { provider: providerIdentifiers.poe, apiKey: poeApiKey, baseUrl: poeBaseUrl }, }) } @@ -1143,12 +1155,19 @@ export const webviewMessageHandler = async ( if (deepSeekApiKey) { if (message?.values?.deepSeekApiKey || message?.values?.deepSeekBaseUrl) { - await flushModels({ provider: "deepseek", apiKey: deepSeekApiKey, baseUrl: deepSeekBaseUrl }, true) + await flushModels( + { provider: providerIdentifiers.deepseek, apiKey: deepSeekApiKey, baseUrl: deepSeekBaseUrl }, + true, + ) } candidates.push({ - key: "deepseek", - options: { provider: "deepseek", apiKey: deepSeekApiKey, baseUrl: deepSeekBaseUrl }, + key: providerIdentifiers.deepseek, + options: { + provider: providerIdentifiers.deepseek, + apiKey: deepSeekApiKey, + baseUrl: deepSeekBaseUrl, + }, }) } @@ -1158,12 +1177,19 @@ export const webviewMessageHandler = async ( if (moonshotApiKey) { if (message?.values?.moonshotApiKey || message?.values?.moonshotBaseUrl) { - await flushModels({ provider: "moonshot", apiKey: moonshotApiKey, baseUrl: moonshotBaseUrl }, true) + await flushModels( + { provider: providerIdentifiers.moonshot, apiKey: moonshotApiKey, baseUrl: moonshotBaseUrl }, + true, + ) } candidates.push({ - key: "moonshot", - options: { provider: "moonshot", apiKey: moonshotApiKey, baseUrl: moonshotBaseUrl }, + key: providerIdentifiers.moonshot, + options: { + provider: providerIdentifiers.moonshot, + apiKey: moonshotApiKey, + baseUrl: moonshotBaseUrl, + }, }) } @@ -1176,12 +1202,12 @@ export const webviewMessageHandler = async ( // Refresh the cache when a new key is explicitly provided (e.g. the Refresh Models button). if (message?.values?.opencodeGoApiKey) { - await flushModels({ provider: "opencode-go", apiKey: opencodeGoApiKey }, true) + await flushModels({ provider: providerIdentifiers.opencodeGo, apiKey: opencodeGoApiKey }, true) } candidates.push({ - key: "opencode-go", - options: { provider: "opencode-go", apiKey: opencodeGoApiKey }, + key: providerIdentifiers.opencodeGo, + options: { provider: providerIdentifiers.opencodeGo, apiKey: opencodeGoApiKey }, }) // Kenari's /models endpoint is public — it returns the full model list with no @@ -1193,15 +1219,15 @@ export const webviewMessageHandler = async ( // Refresh the cache when a new key is explicitly provided (e.g. the Refresh Models button). if (message?.values?.kenariApiKey) { - await flushModels({ provider: "kenari", apiKey: kenariApiKey }, true) + await flushModels({ provider: providerIdentifiers.kenari, apiKey: kenariApiKey }, true) } candidates.push({ - key: "kenari", - options: { provider: "kenari", apiKey: kenariApiKey }, + key: providerIdentifiers.kenari, + options: { provider: providerIdentifiers.kenari, apiKey: kenariApiKey }, }) - if (!providerFilter || providerFilter === "kimi-code") { + if (!providerFilter || providerFilter === providerIdentifiers.kimiCode) { const { kimiCodeOAuthManager } = await import("../../integrations/kimi-code/oauth") const kimiCodeAuthMethod = message?.values?.kimiCodeAuthMethod ?? apiConfiguration.kimiCodeAuthMethod ?? "oauth" @@ -1211,8 +1237,8 @@ export const webviewMessageHandler = async ( : await kimiCodeOAuthManager.getAccessToken() if (kimiCodeApiKey) { candidates.push({ - key: "kimi-code", - options: { provider: "kimi-code", apiKey: kimiCodeApiKey }, + key: providerIdentifiers.kimiCode, + options: { provider: providerIdentifiers.kimiCode, apiKey: kimiCodeApiKey }, }) } } @@ -1276,7 +1302,7 @@ export const webviewMessageHandler = async ( const apiKey = message.values?.apiKey ?? ollamaApiConfig.ollamaApiKey const logBaseUrl = baseUrl || "http://localhost:11434" const ollamaOptions = { - provider: "ollama" as const, + provider: providerIdentifiers.ollama, baseUrl, apiKey, } @@ -1324,7 +1350,7 @@ export const webviewMessageHandler = async ( lmStudioModels = await getLMStudioModels(requestedBaseUrl) } else { const lmStudioOptions = { - provider: "lmstudio" as const, + provider: providerIdentifiers.lmstudio, baseUrl: lmStudioApiConfig.lmStudioBaseUrl, } // Flush cache and refresh to ensure fresh models.