Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 136 additions & 10 deletions src/api/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, unknown>>("../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<Exclude<ProviderName, typeof providerIdentifiers.vertex>, HandlerConstructor>

const expectedHandlerEntries = Object.entries(expectedHandlers) as Array<
[Exclude<ProviderName, typeof providerIdentifiers.vertex>, 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)
})
})
76 changes: 41 additions & 35 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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())
}

Expand All @@ -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)
Expand Down
Loading