Skip to content
Merged
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
118 changes: 67 additions & 51 deletions packages/types/src/__tests__/provider-settings.test.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,123 @@
import { getApiProtocol } from "../provider-settings.js"
import { ANTHROPIC_API_PROTOCOL, getApiProtocol, OPENAI_API_PROTOCOL, providerIdentifiers } from "../index.js"
Comment thread
WebMad marked this conversation as resolved.

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")
})
it("preserves API protocol wire values", () => {
expect(ANTHROPIC_API_PROTOCOL).toBe("anthropic")
expect(OPENAI_API_PROTOCOL).toBe("openai")
})

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")
})
describe("Anthropic-style providers", () => {
it.each([providerIdentifiers.anthropic, providerIdentifiers.bedrock, providerIdentifiers.minimax])(
"should return 'anthropic' for %s provider",
(provider) => {
expect(getApiProtocol(provider)).toBe(ANTHROPIC_API_PROTOCOL)
expect(getApiProtocol(provider, "gpt-4")).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", () => {
describe("Gateway providers", () => {
it("uses the canonical Zoo Gateway identifier for Anthropic model protocol selection", () => {
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)
})
})
})
44 changes: 34 additions & 10 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
minimaxModels,
mimoModels,
isOpencodeGoAnthropicFormatModel,
ANTHROPIC_API_PROTOCOL,
OPENAI_API_PROTOCOL,
} from "./providers/index.js"

/**
Expand Down Expand Up @@ -581,25 +583,42 @@ export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
*/

// 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,
]

const ANTHROPIC_MODEL_ID_PREFIX = "anthropic/"
const CLAUDE_MODEL_ID_FRAGMENT = "claude"

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
Expand All @@ -609,11 +628,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
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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 anthropicModels = {
"claude-sonnet-4-6": {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
18 changes: 15 additions & 3 deletions src/shared/__tests__/checkExistApiConfig.spec.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand Down Expand Up @@ -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)
})

Comment thread
coderabbitai[bot] marked this conversation as resolved.
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)
})
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
13 changes: 9 additions & 4 deletions src/shared/checkExistApiConfig.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}

Expand Down
Loading