Skip to content
Open
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
107 changes: 62 additions & 45 deletions packages/types/src/__tests__/provider-settings.test.ts
Original file line number Diff line number Diff line change
@@ -1,107 +1,124 @@
import { getApiProtocol } from "../provider-settings.js"
import { ANTHROPIC_API_PROTOCOL, getApiProtocol, OPENAI_API_PROTOCOL, providerIdentifiers } from "../index.js"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every protocol assertion here uses the imported constant as the expected value (e.g. .toBe(ANTHROPIC_API_PROTOCOL)), so the tests verify internal consistency but don't pin the wire value "anthropic". If both the constant and its use site agreed on a wrong value, all tests would still pass. Worth adding a small companion test that asserts ANTHROPIC_API_PROTOCOL === "anthropic" and OPENAI_API_PROTOCOL === "openai" to anchor the wire values?


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)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimax is in ANTHROPIC_STYLE_PROVIDERS alongside anthropic and bedrock, but there's no test calling getApiProtocol(providerIdentifiers.minimax, ...). Could we add a case to this block so a mutation removing minimax from the list would be caught?

})

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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vercelAiGateway call here is identical to the first assertion in the "should return 'anthropic' for vercel-ai-gateway..." test at line 47 — is zooGateway (line 41) the only new coverage this test adds? If so, trimming the duplicate would make the intent clearer.

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)
})
})
})
43 changes: 33 additions & 10 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

/**
Expand Down Expand Up @@ -581,25 +585,39 @@ 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,
]

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 +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
}

/**
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/providers/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Comment on lines +9 to +10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ANTHROPIC_MODEL_ID_PREFIX and CLAUDE_MODEL_ID_FRAGMENT are each used exactly once in provider-settings.ts and have no callers elsewhere in src/. Is there a reason to export them as public @roo-code/types surface rather than keeping them unexported — like ANTHROPIC_MODEL_GATEWAY_PROVIDERS on line 594 of provider-settings.ts?


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