diff --git a/packages/types/src/__tests__/provider-settings.test.ts b/packages/types/src/__tests__/provider-settings.test.ts index 724fc20f34..57f2ffbc52 100644 --- a/packages/types/src/__tests__/provider-settings.test.ts +++ b/packages/types/src/__tests__/provider-settings.test.ts @@ -1,107 +1,124 @@ -import { getApiProtocol } from "../provider-settings.js" +import { ANTHROPIC_API_PROTOCOL, getApiProtocol, OPENAI_API_PROTOCOL, providerIdentifiers } from "../index.js" describe("getApiProtocol", () => { describe("Anthropic-style providers", () => { it("should return 'anthropic' for anthropic provider", () => { - expect(getApiProtocol("anthropic")).toBe("anthropic") - expect(getApiProtocol("anthropic", "gpt-4")).toBe("anthropic") + expect(getApiProtocol(providerIdentifiers.anthropic)).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.anthropic, "gpt-4")).toBe(ANTHROPIC_API_PROTOCOL) }) it("should return 'anthropic' for bedrock provider", () => { - expect(getApiProtocol("bedrock")).toBe("anthropic") - expect(getApiProtocol("bedrock", "gpt-4")).toBe("anthropic") - expect(getApiProtocol("bedrock", "claude-3-opus")).toBe("anthropic") + expect(getApiProtocol(providerIdentifiers.bedrock)).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.bedrock, "gpt-4")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.bedrock, "claude-3-opus")).toBe(ANTHROPIC_API_PROTOCOL) }) }) describe("Vertex provider with Claude models", () => { it("should return 'anthropic' for vertex provider with claude models", () => { - expect(getApiProtocol("vertex", "claude-3-opus")).toBe("anthropic") - expect(getApiProtocol("vertex", "Claude-3-Sonnet")).toBe("anthropic") - expect(getApiProtocol("vertex", "CLAUDE-instant")).toBe("anthropic") - expect(getApiProtocol("vertex", "anthropic/claude-3-haiku")).toBe("anthropic") + expect(getApiProtocol(providerIdentifiers.vertex, "claude-3-opus")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "Claude-3-Sonnet")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "CLAUDE-instant")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "anthropic/claude-3-haiku")).toBe(ANTHROPIC_API_PROTOCOL) }) it("should return 'openai' for vertex provider with non-claude models", () => { - expect(getApiProtocol("vertex", "gpt-4")).toBe("openai") - expect(getApiProtocol("vertex", "gemini-pro")).toBe("openai") - expect(getApiProtocol("vertex", "llama-2")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.vertex, "gpt-4")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "gemini-pro")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "llama-2")).toBe(OPENAI_API_PROTOCOL) }) it("should return 'openai' for vertex provider without model", () => { - expect(getApiProtocol("vertex")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.vertex)).toBe(OPENAI_API_PROTOCOL) }) }) describe("Vercel AI Gateway provider", () => { + it("uses canonical gateway identifiers for Anthropic model protocol selection", () => { + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "anthropic/claude-3-opus")).toBe( + ANTHROPIC_API_PROTOCOL, + ) + expect(getApiProtocol(providerIdentifiers.zooGateway, "anthropic/claude-3-opus")).toBe( + ANTHROPIC_API_PROTOCOL, + ) + }) + it("should return 'anthropic' for vercel-ai-gateway provider with anthropic models", () => { - expect(getApiProtocol("vercel-ai-gateway", "anthropic/claude-3-opus")).toBe("anthropic") - expect(getApiProtocol("vercel-ai-gateway", "anthropic/claude-3.5-sonnet")).toBe("anthropic") - expect(getApiProtocol("vercel-ai-gateway", "ANTHROPIC/claude-sonnet-4")).toBe("anthropic") - expect(getApiProtocol("vercel-ai-gateway", "anthropic/claude-opus-4.1")).toBe("anthropic") + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "anthropic/claude-3-opus")).toBe( + ANTHROPIC_API_PROTOCOL, + ) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "anthropic/claude-3.5-sonnet")).toBe( + ANTHROPIC_API_PROTOCOL, + ) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "ANTHROPIC/claude-sonnet-4")).toBe( + ANTHROPIC_API_PROTOCOL, + ) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "anthropic/claude-opus-4.1")).toBe( + ANTHROPIC_API_PROTOCOL, + ) }) it("should return 'openai' for vercel-ai-gateway provider with non-anthropic models", () => { - expect(getApiProtocol("vercel-ai-gateway", "openai/gpt-4")).toBe("openai") - expect(getApiProtocol("vercel-ai-gateway", "google/gemini-pro")).toBe("openai") - expect(getApiProtocol("vercel-ai-gateway", "meta/llama-3")).toBe("openai") - expect(getApiProtocol("vercel-ai-gateway", "mistral/mixtral")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "openai/gpt-4")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "google/gemini-pro")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "meta/llama-3")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vercelAiGateway, "mistral/mixtral")).toBe(OPENAI_API_PROTOCOL) }) it("should return 'openai' for vercel-ai-gateway provider without model", () => { - expect(getApiProtocol("vercel-ai-gateway")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.vercelAiGateway)).toBe(OPENAI_API_PROTOCOL) }) }) describe("Opencode Go provider", () => { it("should return 'anthropic' for opencode-go Anthropic-format models (Qwen/MiniMax)", () => { - expect(getApiProtocol("opencode-go", "qwen3.7-max")).toBe("anthropic") - expect(getApiProtocol("opencode-go", "qwen3.7-plus")).toBe("anthropic") - expect(getApiProtocol("opencode-go", "qwen3.6-plus")).toBe("anthropic") - expect(getApiProtocol("opencode-go", "minimax-m3")).toBe("anthropic") - expect(getApiProtocol("opencode-go", "minimax-m2.7")).toBe("anthropic") - expect(getApiProtocol("opencode-go", "minimax-m2.5")).toBe("anthropic") + expect(getApiProtocol(providerIdentifiers.opencodeGo, "qwen3.7-max")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "qwen3.7-plus")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "qwen3.6-plus")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "minimax-m3")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "minimax-m2.7")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "minimax-m2.5")).toBe(ANTHROPIC_API_PROTOCOL) }) it("should return 'openai' for opencode-go OpenAI-format models (GLM/DeepSeek/etc.)", () => { - expect(getApiProtocol("opencode-go", "glm-5.2")).toBe("openai") - expect(getApiProtocol("opencode-go", "deepseek-v4-pro")).toBe("openai") - expect(getApiProtocol("opencode-go", "kimi-k2.5")).toBe("openai") - expect(getApiProtocol("opencode-go", "mimo-v2.5")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.opencodeGo, "glm-5.2")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "deepseek-v4-pro")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "kimi-k2.5")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.opencodeGo, "mimo-v2.5")).toBe(OPENAI_API_PROTOCOL) }) it("should return 'openai' for opencode-go without a model", () => { - expect(getApiProtocol("opencode-go")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.opencodeGo)).toBe(OPENAI_API_PROTOCOL) }) it("should return 'openai' for opencode-go with an unknown model id", () => { - expect(getApiProtocol("opencode-go", "some-future-model")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.opencodeGo, "some-future-model")).toBe(OPENAI_API_PROTOCOL) }) }) describe("Other providers", () => { it("should return 'openai' for non-anthropic providers regardless of model", () => { - expect(getApiProtocol("openrouter", "claude-3-opus")).toBe("openai") - expect(getApiProtocol("openai", "claude-3-sonnet")).toBe("openai") - expect(getApiProtocol("litellm", "claude-instant")).toBe("openai") - expect(getApiProtocol("ollama", "claude-model")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.openrouter, "claude-3-opus")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.openai, "claude-3-sonnet")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.litellm, "claude-instant")).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.ollama, "claude-model")).toBe(OPENAI_API_PROTOCOL) }) }) describe("Edge cases", () => { it("should return 'openai' when provider is undefined", () => { - expect(getApiProtocol(undefined)).toBe("openai") - expect(getApiProtocol(undefined, "claude-3-opus")).toBe("openai") + expect(getApiProtocol(undefined)).toBe(OPENAI_API_PROTOCOL) + expect(getApiProtocol(undefined, "claude-3-opus")).toBe(OPENAI_API_PROTOCOL) }) it("should handle empty strings", () => { - expect(getApiProtocol("vertex", "")).toBe("openai") + expect(getApiProtocol(providerIdentifiers.vertex, "")).toBe(OPENAI_API_PROTOCOL) }) it("should be case-insensitive for claude detection", () => { - expect(getApiProtocol("vertex", "CLAUDE-3-OPUS")).toBe("anthropic") - expect(getApiProtocol("vertex", "claude-3-opus")).toBe("anthropic") - expect(getApiProtocol("vertex", "ClAuDe-InStAnT")).toBe("anthropic") + expect(getApiProtocol(providerIdentifiers.vertex, "CLAUDE-3-OPUS")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "claude-3-opus")).toBe(ANTHROPIC_API_PROTOCOL) + expect(getApiProtocol(providerIdentifiers.vertex, "ClAuDe-InStAnT")).toBe(ANTHROPIC_API_PROTOCOL) }) }) }) diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 3898c65bcc..122a7b7119 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -29,6 +29,10 @@ import { minimaxModels, mimoModels, isOpencodeGoAnthropicFormatModel, + ANTHROPIC_API_PROTOCOL, + ANTHROPIC_MODEL_ID_PREFIX, + CLAUDE_MODEL_ID_FRAGMENT, + OPENAI_API_PROTOCOL, } from "./providers/index.js" /** @@ -581,25 +585,39 @@ export const modelIdKeysByProvider: Record = { */ // Providers that use Anthropic-style API protocol. -export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "bedrock", "minimax"] +export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = [ + providerIdentifiers.anthropic, + providerIdentifiers.bedrock, + providerIdentifiers.minimax, +] + +const ANTHROPIC_MODEL_GATEWAY_PROVIDERS: ProviderName[] = [ + providerIdentifiers.vercelAiGateway, + providerIdentifiers.zooGateway, +] export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => { if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) { - return "anthropic" + return ANTHROPIC_API_PROTOCOL } - if (provider && provider === "vertex" && modelId && modelId.toLowerCase().includes("claude")) { - return "anthropic" + if ( + provider && + provider === providerIdentifiers.vertex && + modelId && + modelId.toLowerCase().includes(CLAUDE_MODEL_ID_FRAGMENT) + ) { + return ANTHROPIC_API_PROTOCOL } // Vercel AI Gateway and Zoo Gateway use the anthropic protocol for anthropic models. if ( provider && - ["vercel-ai-gateway", "zoo-gateway"].includes(provider) && + ANTHROPIC_MODEL_GATEWAY_PROVIDERS.includes(provider) && modelId && - modelId.toLowerCase().startsWith("anthropic/") + modelId.toLowerCase().startsWith(ANTHROPIC_MODEL_ID_PREFIX) ) { - return "anthropic" + return ANTHROPIC_API_PROTOCOL } // Opencode Go routes a subset of its models (Qwen, MiniMax) through the @@ -609,11 +627,16 @@ export const getApiProtocol = (provider: ProviderName | undefined, modelId?: str // models must use the anthropic protocol so token/cost aggregation adds the // cache tokens back into the input total — otherwise the cached prefix is // dropped from `contextTokens`, undercounting context-window usage. - if (provider && provider === "opencode-go" && modelId && isOpencodeGoAnthropicFormatModel(modelId)) { - return "anthropic" + if ( + provider && + provider === providerIdentifiers.opencodeGo && + modelId && + isOpencodeGoAnthropicFormatModel(modelId) + ) { + return ANTHROPIC_API_PROTOCOL } - return "openai" + return OPENAI_API_PROTOCOL } /** diff --git a/packages/types/src/providers/anthropic.ts b/packages/types/src/providers/anthropic.ts index f5f7e71116..13d53f3732 100644 --- a/packages/types/src/providers/anthropic.ts +++ b/packages/types/src/providers/anthropic.ts @@ -5,6 +5,9 @@ import type { ModelInfo } from "../model.js" export type AnthropicModelId = keyof typeof anthropicModels export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-4-5" +export const ANTHROPIC_API_PROTOCOL = "anthropic" +export const ANTHROPIC_MODEL_ID_PREFIX = "anthropic/" +export const CLAUDE_MODEL_ID_FRAGMENT = "claude" export const anthropicModels = { "claude-sonnet-4-6": { diff --git a/packages/types/src/providers/openai.ts b/packages/types/src/providers/openai.ts index 1601961964..5f114e41f6 100644 --- a/packages/types/src/providers/openai.ts +++ b/packages/types/src/providers/openai.ts @@ -3,6 +3,7 @@ import type { ModelInfo } from "../model.js" // https://openai.com/api/pricing/ export type OpenAiNativeModelId = keyof typeof openAiNativeModels +export const OPENAI_API_PROTOCOL = "openai" export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5.6-sol" export const openAiNativeModels = { diff --git a/src/shared/__tests__/checkExistApiConfig.spec.ts b/src/shared/__tests__/checkExistApiConfig.spec.ts index 5c32e567e2..ab92beea36 100644 --- a/src/shared/__tests__/checkExistApiConfig.spec.ts +++ b/src/shared/__tests__/checkExistApiConfig.spec.ts @@ -1,6 +1,6 @@ // npx vitest run src/shared/__tests__/checkExistApiConfig.spec.ts -import type { ProviderSettings } from "@roo-code/types" +import { providerIdentifiers, type ProviderSettings } from "@roo-code/types" import { checkExistKey } from "../checkExistApiConfig" @@ -66,16 +66,20 @@ describe("checkExistKey", () => { expect(checkExistKey(config)).toBe(true) }) + it("recognizes keyless providers through their canonical identifiers", () => { + expect(checkExistKey({ apiProvider: providerIdentifiers.fakeAi })).toBe(true) + }) + it("should return true for openai-codex provider without API key", () => { const config: ProviderSettings = { - apiProvider: "openai-codex", + apiProvider: providerIdentifiers.openaiCodex, } expect(checkExistKey(config)).toBe(true) }) it("should return true for qwen-code provider without API key", () => { const config: ProviderSettings = { - apiProvider: "qwen-code", + apiProvider: providerIdentifiers.qwenCode, } expect(checkExistKey(config)).toBe(true) }) @@ -95,6 +99,10 @@ describe("checkExistKey", () => { expect(checkExistKey(config)).toBe(true) }) + it("recognizes OAuth authentication through the canonical Kimi Code identifier", () => { + expect(checkExistKey({ apiProvider: providerIdentifiers.kimiCode, kimiCodeAuthMethod: "oauth" })).toBe(true) + }) + it("should return true for kimi-code provider without auth method (defaults to OAuth)", () => { const config: ProviderSettings = { apiProvider: "kimi-code", @@ -128,6 +136,10 @@ describe("checkExistKey", () => { expect(checkExistKey(config, false)).toBe(false) }) + it("recognizes session authentication through the canonical Zoo Gateway identifier", () => { + expect(checkExistKey({ apiProvider: providerIdentifiers.zooGateway }, true)).toBe(true) + }) + it("should return true for zoo-gateway when profile has zooSessionToken", () => { const config: ProviderSettings = { apiProvider: "zoo-gateway", diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts index 9ce55a9ac3..3560199b91 100644 --- a/src/shared/checkExistApiConfig.ts +++ b/src/shared/checkExistApiConfig.ts @@ -1,4 +1,4 @@ -import { SECRET_STATE_KEYS, GLOBAL_SECRET_KEYS, ProviderSettings } from "@roo-code/types" +import { SECRET_STATE_KEYS, GLOBAL_SECRET_KEYS, providerIdentifiers, ProviderSettings } from "@roo-code/types" /** * Returns whether a provider profile is sufficiently configured to leave the @@ -14,17 +14,22 @@ export function checkExistKey(config: ProviderSettings | undefined, zooCodeIsAut } // Special case for fake-ai, openai-codex, and qwen-code providers which don't need any configuration. - if (config.apiProvider && ["fake-ai", "openai-codex", "qwen-code"].includes(config.apiProvider)) { + const configurationFreeProviders: ProviderSettings["apiProvider"][] = [ + providerIdentifiers.fakeAi, + providerIdentifiers.openaiCodex, + providerIdentifiers.qwenCode, + ] + if (config.apiProvider && configurationFreeProviders.includes(config.apiProvider)) { return true } - if (config.apiProvider === "kimi-code" && (config.kimiCodeAuthMethod ?? "oauth") === "oauth") { + if (config.apiProvider === providerIdentifiers.kimiCode && (config.kimiCodeAuthMethod ?? "oauth") === "oauth") { return true } // Zoo Gateway uses session auth (profile token and/or global Zoo Code login), // not a traditional API key listed in SECRET_STATE_KEYS. - if (config.apiProvider === "zoo-gateway") { + if (config.apiProvider === providerIdentifiers.zooGateway) { return Boolean(config.zooSessionToken) || Boolean(zooCodeIsAuthenticated) }