From d25f8b739609fe0510711b7442e33713b1aba241 Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sat, 25 Jul 2026 01:39:05 +0300 Subject: [PATCH 1/4] refactor(api): use canonical provider identifiers --- src/api/__tests__/index.spec.ts | 54 ++++++++++++++++++++++- src/api/index.ts | 76 ++++++++++++++++++--------------- 2 files changed, 93 insertions(+), 37 deletions(-) diff --git a/src/api/__tests__/index.spec.ts b/src/api/__tests__/index.spec.ts index b86e73dac6..08350e2858 100644 --- a/src/api/__tests__/index.spec.ts +++ b/src/api/__tests__/index.spec.ts @@ -1,5 +1,7 @@ // npx vitest run src/api/__tests__/index.spec.ts +import fs from "node:fs" + // Mock vscode first to avoid import errors vitest.mock("vscode", () => ({ workspace: { @@ -9,15 +11,22 @@ vitest.mock("vscode", () => ({ }, })) -import type { ProviderSettings } from "@roo-code/types" +import { + providerIdentifiers, + retiredProviderIdentifiers, + type ProviderSettings, + type ProviderNameWithRetired, +} from "@roo-code/types" import { buildApiHandler } from "../index" +import { AnthropicHandler } from "../providers/anthropic" import { KenariHandler } from "../providers/kenari" +import { OpenRouterHandler } from "../providers/openrouter" describe("buildApiHandler", () => { it("returns a KenariHandler for the kenari provider", () => { const configuration: ProviderSettings = { - apiProvider: "kenari", + apiProvider: providerIdentifiers.kenari, kenariApiKey: "test-key", kenariModelId: "glm-5-2", } @@ -26,4 +35,45 @@ describe("buildApiHandler", () => { expect(handler).toBeInstanceOf(KenariHandler) }) + + it.each([ + [providerIdentifiers.anthropic, AnthropicHandler], + [providerIdentifiers.openrouter, OpenRouterHandler], + ] as const)("returns the expected handler for %s", (apiProvider, Handler) => { + const handler = buildApiHandler({ apiProvider }) + + 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") + }) + + it("falls back to Anthropic for an unsupported provider value", () => { + const handler = buildApiHandler({ + apiProvider: "unsupported-provider" as ProviderNameWithRetired, + }) + + expect(handler).toBeInstanceOf(AnthropicHandler) + }) + + it("uses canonical identifiers instead of provider literals in the handler factory", () => { + const factorySource = fs.readFileSync(new URL("../index.ts", import.meta.url), "utf8") + + expect(factorySource).toContain("providerIdentifiers.anthropic") + expect(factorySource).toContain("retiredProviderIdentifiers.roo") + expect(factorySource).not.toMatch(/case\s+["']/) + }) }) 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) From 186ebb9542b8669fb54a5699812ce043be613cfc Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sat, 25 Jul 2026 02:11:30 +0300 Subject: [PATCH 2/4] test(api): cover canonical provider routing --- src/api/__tests__/index.spec.ts | 120 ++++++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 30 deletions(-) diff --git a/src/api/__tests__/index.spec.ts b/src/api/__tests__/index.spec.ts index 08350e2858..5bd318cbd9 100644 --- a/src/api/__tests__/index.spec.ts +++ b/src/api/__tests__/index.spec.ts @@ -1,7 +1,5 @@ // npx vitest run src/api/__tests__/index.spec.ts -import fs from "node:fs" - // Mock vscode first to avoid import errors vitest.mock("vscode", () => ({ workspace: { @@ -11,40 +9,110 @@ vitest.mock("vscode", () => ({ }, })) -import { - providerIdentifiers, - retiredProviderIdentifiers, - type ProviderSettings, - type ProviderNameWithRetired, -} 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") -import { buildApiHandler } from "../index" -import { AnthropicHandler } from "../providers/anthropic" -import { KenariHandler } from "../providers/kenari" -import { OpenRouterHandler } from "../providers/openrouter" + return Object.fromEntries(Object.keys(providers).map((name) => [name, class {}])) +}) -describe("buildApiHandler", () => { - it("returns a KenariHandler for the kenari provider", () => { - const configuration: ProviderSettings = { - apiProvider: providerIdentifiers.kenari, - kenariApiKey: "test-key", - kenariModelId: "glm-5-2", - } +vitest.mock("../providers/native-ollama", () => ({ + NativeOllamaHandler: class {}, +})) - const handler = buildApiHandler(configuration) +import { providerIdentifiers, retiredProviderIdentifiers, type ProviderNameWithRetired } from "@roo-code/types" - expect(handler).toBeInstanceOf(KenariHandler) - }) +import { buildApiHandler } from "../index" +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" +describe("buildApiHandler", () => { it.each([ [providerIdentifiers.anthropic, AnthropicHandler], [providerIdentifiers.openrouter, OpenRouterHandler], + [providerIdentifiers.bedrock, AwsBedrockHandler], + [providerIdentifiers.openai, OpenAiHandler], + [providerIdentifiers.ollama, NativeOllamaHandler], + [providerIdentifiers.lmstudio, LmStudioHandler], + [providerIdentifiers.gemini, GeminiHandler], + [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], ] as const)("returns the expected handler for %s", (apiProvider, Handler) => { const handler = buildApiHandler({ apiProvider }) expect(handler).toBeInstanceOf(Handler) }) + it.each([ + ["non-Claude models", "gemini-2.5-pro", VertexHandler], + ["Claude models", "claude-3-7-sonnet", 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({ @@ -68,12 +136,4 @@ describe("buildApiHandler", () => { expect(handler).toBeInstanceOf(AnthropicHandler) }) - - it("uses canonical identifiers instead of provider literals in the handler factory", () => { - const factorySource = fs.readFileSync(new URL("../index.ts", import.meta.url), "utf8") - - expect(factorySource).toContain("providerIdentifiers.anthropic") - expect(factorySource).toContain("retiredProviderIdentifiers.roo") - expect(factorySource).not.toMatch(/case\s+["']/) - }) }) From 692f4578aa0096d603d8d88125ff796a0ab2e75a Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sat, 25 Jul 2026 02:22:00 +0300 Subject: [PATCH 3/4] test(api): enforce exhaustive provider routing --- src/api/__tests__/index.spec.ts | 85 +++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/src/api/__tests__/index.spec.ts b/src/api/__tests__/index.spec.ts index 5bd318cbd9..20bf9baabb 100644 --- a/src/api/__tests__/index.spec.ts +++ b/src/api/__tests__/index.spec.ts @@ -21,7 +21,12 @@ vitest.mock("../providers/native-ollama", () => ({ NativeOllamaHandler: class {}, })) -import { providerIdentifiers, retiredProviderIdentifiers, type ProviderNameWithRetired } from "@roo-code/types" +import { + providerIdentifiers, + retiredProviderIdentifiers, + type ProviderName, + type ProviderNameWithRetired, +} from "@roo-code/types" import { buildApiHandler } from "../index" import { @@ -61,41 +66,51 @@ import { } 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.each([ - [providerIdentifiers.anthropic, AnthropicHandler], - [providerIdentifiers.openrouter, OpenRouterHandler], - [providerIdentifiers.bedrock, AwsBedrockHandler], - [providerIdentifiers.openai, OpenAiHandler], - [providerIdentifiers.ollama, NativeOllamaHandler], - [providerIdentifiers.lmstudio, LmStudioHandler], - [providerIdentifiers.gemini, GeminiHandler], - [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], - ] as const)("returns the expected handler for %s", (apiProvider, Handler) => { + it.each(expectedHandlerEntries)("returns the expected handler for %s", (apiProvider, Handler) => { const handler = buildApiHandler({ apiProvider }) expect(handler).toBeInstanceOf(Handler) From a47f30a8e0c1cdb9be7235d91377abbcaf6117ad Mon Sep 17 00:00:00 2001 From: gubin-dev Date: Sat, 25 Jul 2026 02:23:03 +0300 Subject: [PATCH 4/4] test(api): avoid concrete Vertex model IDs --- src/api/__tests__/index.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/__tests__/index.spec.ts b/src/api/__tests__/index.spec.ts index 20bf9baabb..3617c3cd6d 100644 --- a/src/api/__tests__/index.spec.ts +++ b/src/api/__tests__/index.spec.ts @@ -117,8 +117,9 @@ describe("buildApiHandler", () => { }) it.each([ - ["non-Claude models", "gemini-2.5-pro", VertexHandler], - ["Claude models", "claude-3-7-sonnet", AnthropicVertexHandler], + ["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,