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