diff --git a/src/api/__tests__/index.spec.ts b/src/api/__tests__/index.spec.ts index b86e73dac6..3617c3cd6d 100644 --- a/src/api/__tests__/index.spec.ts +++ b/src/api/__tests__/index.spec.ts @@ -9,21 +9,147 @@ vitest.mock("vscode", () => ({ }, })) -import type { ProviderSettings } from "@roo-code/types" +// Handler constructors can require credentials or initialize SDK clients. Replace them +// with inert classes so these tests exercise only the factory's routing behavior. +vitest.mock("../providers", async () => { + const providers = await vitest.importActual>("../providers") + + return Object.fromEntries(Object.keys(providers).map((name) => [name, class {}])) +}) + +vitest.mock("../providers/native-ollama", () => ({ + NativeOllamaHandler: class {}, +})) + +import { + providerIdentifiers, + retiredProviderIdentifiers, + type ProviderName, + type ProviderNameWithRetired, +} from "@roo-code/types" import { buildApiHandler } from "../index" -import { KenariHandler } from "../providers/kenari" +import { + AnthropicHandler, + AnthropicVertexHandler, + AwsBedrockHandler, + BasetenHandler, + DeepSeekHandler, + FakeAIHandler, + FireworksHandler, + FriendliHandler, + GeminiHandler, + KenariHandler, + KimiCodeHandler, + LiteLLMHandler, + LmStudioHandler, + MiniMaxHandler, + MimoHandler, + MistralHandler, + MoonshotHandler, + OpenAiCodexHandler, + OpenAiHandler, + OpenAiNativeHandler, + OpencodeGoHandler, + OpenRouterHandler, + PoeHandler, + QwenCodeHandler, + RequestyHandler, + SambaNovaHandler, + UnboundHandler, + VercelAiGatewayHandler, + VertexHandler, + VsCodeLmHandler, + XAIHandler, + ZAiHandler, + ZooGatewayHandler, +} from "../providers" +import { NativeOllamaHandler } from "../providers/native-ollama" + +type HandlerConstructor = new (...args: never[]) => object + +const expectedHandlers = { + [providerIdentifiers.anthropic]: AnthropicHandler, + [providerIdentifiers.openrouter]: OpenRouterHandler, + [providerIdentifiers.bedrock]: AwsBedrockHandler, + [providerIdentifiers.openai]: OpenAiHandler, + [providerIdentifiers.ollama]: NativeOllamaHandler, + [providerIdentifiers.lmstudio]: LmStudioHandler, + [providerIdentifiers.gemini]: GeminiHandler, + // Gemini CLI currently relies on the factory's default Anthropic handler. + [providerIdentifiers.geminiCli]: AnthropicHandler, + [providerIdentifiers.openaiCodex]: OpenAiCodexHandler, + [providerIdentifiers.openaiNative]: OpenAiNativeHandler, + [providerIdentifiers.deepseek]: DeepSeekHandler, + [providerIdentifiers.qwenCode]: QwenCodeHandler, + [providerIdentifiers.moonshot]: MoonshotHandler, + [providerIdentifiers.kimiCode]: KimiCodeHandler, + [providerIdentifiers.vscodeLm]: VsCodeLmHandler, + [providerIdentifiers.mistral]: MistralHandler, + [providerIdentifiers.requesty]: RequestyHandler, + [providerIdentifiers.unbound]: UnboundHandler, + [providerIdentifiers.fakeAi]: FakeAIHandler, + [providerIdentifiers.xai]: XAIHandler, + [providerIdentifiers.litellm]: LiteLLMHandler, + [providerIdentifiers.sambanova]: SambaNovaHandler, + [providerIdentifiers.mimo]: MimoHandler, + [providerIdentifiers.zai]: ZAiHandler, + [providerIdentifiers.fireworks]: FireworksHandler, + [providerIdentifiers.friendli]: FriendliHandler, + [providerIdentifiers.vercelAiGateway]: VercelAiGatewayHandler, + [providerIdentifiers.opencodeGo]: OpencodeGoHandler, + [providerIdentifiers.kenari]: KenariHandler, + [providerIdentifiers.zooGateway]: ZooGatewayHandler, + [providerIdentifiers.minimax]: MiniMaxHandler, + [providerIdentifiers.baseten]: BasetenHandler, + [providerIdentifiers.poe]: PoeHandler, +} satisfies Record, HandlerConstructor> + +const expectedHandlerEntries = Object.entries(expectedHandlers) as Array< + [Exclude, HandlerConstructor] +> describe("buildApiHandler", () => { - it("returns a KenariHandler for the kenari provider", () => { - const configuration: ProviderSettings = { - apiProvider: "kenari", - kenariApiKey: "test-key", - kenariModelId: "glm-5-2", - } + it.each(expectedHandlerEntries)("returns the expected handler for %s", (apiProvider, Handler) => { + const handler = buildApiHandler({ apiProvider }) + + expect(handler).toBeInstanceOf(Handler) + }) + + it.each([ + ["an unspecified model", undefined, VertexHandler], + ["non-Claude models", "non-claude-test-model", VertexHandler], + ["Claude models", "claude-test-model", AnthropicVertexHandler], + ] as const)("returns the expected Vertex handler for %s", (_description, apiModelId, Handler) => { + const handler = buildApiHandler({ + apiProvider: providerIdentifiers.vertex, + apiModelId, + }) + + expect(handler).toBeInstanceOf(Handler) + }) + + it("preserves the dedicated removal error for the retired Roo provider", () => { + expect(() => + buildApiHandler({ + apiProvider: retiredProviderIdentifiers.roo, + }), + ).toThrow("Roo Code Router has been removed") + }) + + it("rejects other retired providers", () => { + expect(() => + buildApiHandler({ + apiProvider: retiredProviderIdentifiers.cerebras, + }), + ).toThrow("this provider is no longer supported") + }) - const handler = buildApiHandler(configuration) + it("falls back to Anthropic for an unsupported provider value", () => { + const handler = buildApiHandler({ + apiProvider: "unsupported-provider" as ProviderNameWithRetired, + }) - expect(handler).toBeInstanceOf(KenariHandler) + expect(handler).toBeInstanceOf(AnthropicHandler) }) }) diff --git a/src/api/index.ts b/src/api/index.ts index fccbf48867..c9992c9c39 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,7 +1,13 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" -import { isRetiredProvider, type ProviderSettings, type ModelInfo } from "@roo-code/types" +import { + isRetiredProvider, + providerIdentifiers, + retiredProviderIdentifiers, + type ProviderSettings, + type ModelInfo, +} from "@roo-code/types" import { getRouterRemovalMessage } from "../core/config/routerRemoval" import { ApiStream } from "./transform/stream" @@ -140,7 +146,7 @@ export interface ApiHandler { export function buildApiHandler(configuration: ProviderSettings): ApiHandler { const { apiProvider, ...options } = configuration - if (apiProvider === "roo") { + if (apiProvider === retiredProviderIdentifiers.roo) { throw new Error(getRouterRemovalMessage()) } @@ -151,73 +157,73 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { } switch (apiProvider) { - case "anthropic": + case providerIdentifiers.anthropic: return new AnthropicHandler(options) - case "openrouter": + case providerIdentifiers.openrouter: return new OpenRouterHandler(options) - case "bedrock": + case providerIdentifiers.bedrock: return new AwsBedrockHandler(options) - case "vertex": + case providerIdentifiers.vertex: return options.apiModelId?.startsWith("claude") ? new AnthropicVertexHandler(options) : new VertexHandler(options) - case "openai": + case providerIdentifiers.openai: return new OpenAiHandler(options) - case "ollama": + case providerIdentifiers.ollama: return new NativeOllamaHandler(options) - case "lmstudio": + case providerIdentifiers.lmstudio: return new LmStudioHandler(options) - case "gemini": + case providerIdentifiers.gemini: return new GeminiHandler(options) - case "openai-codex": + case providerIdentifiers.openaiCodex: return new OpenAiCodexHandler(options) - case "openai-native": + case providerIdentifiers.openaiNative: return new OpenAiNativeHandler(options) - case "deepseek": + case providerIdentifiers.deepseek: return new DeepSeekHandler(options) - case "qwen-code": + case providerIdentifiers.qwenCode: return new QwenCodeHandler(options) - case "moonshot": + case providerIdentifiers.moonshot: return new MoonshotHandler(options) - case "kimi-code": + case providerIdentifiers.kimiCode: return new KimiCodeHandler(options) - case "vscode-lm": + case providerIdentifiers.vscodeLm: return new VsCodeLmHandler(options) - case "mistral": + case providerIdentifiers.mistral: return new MistralHandler(options) - case "requesty": + case providerIdentifiers.requesty: return new RequestyHandler(options) - case "unbound": + case providerIdentifiers.unbound: return new UnboundHandler(options) - case "fake-ai": + case providerIdentifiers.fakeAi: return new FakeAIHandler(options) - case "xai": + case providerIdentifiers.xai: return new XAIHandler(options) - case "litellm": + case providerIdentifiers.litellm: return new LiteLLMHandler(options) - case "sambanova": + case providerIdentifiers.sambanova: return new SambaNovaHandler(options) - case "mimo": + case providerIdentifiers.mimo: return new MimoHandler(options) - case "zai": + case providerIdentifiers.zai: return new ZAiHandler(options) - case "fireworks": + case providerIdentifiers.fireworks: return new FireworksHandler(options) - case "friendli": + case providerIdentifiers.friendli: return new FriendliHandler(options) - case "vercel-ai-gateway": + case providerIdentifiers.vercelAiGateway: return new VercelAiGatewayHandler(options) - case "opencode-go": + case providerIdentifiers.opencodeGo: return new OpencodeGoHandler(options) - case "kenari": + case providerIdentifiers.kenari: return new KenariHandler(options) - case "zoo-gateway": + case providerIdentifiers.zooGateway: return new ZooGatewayHandler(options) - case "minimax": + case providerIdentifiers.minimax: return new MiniMaxHandler(options) - case "baseten": + case providerIdentifiers.baseten: return new BasetenHandler(options) - case "poe": + case providerIdentifiers.poe: return new PoeHandler(options) default: return new AnthropicHandler(options)