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
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { providerIdentifiers } from "@roo-code/types"

import {
PROVIDER_SERVICE_CONFIG,
PROVIDER_DEFAULT_MODEL_IDS,
getProviderServiceConfig,
getProviderModelConfig,
getDefaultModelIdForProvider,
getStaticModelsForProvider,
isStaticModelProvider,
PROVIDERS_WITH_CUSTOM_MODEL_UI,
shouldUseGenericModelPicker,
handleModelChangeSideEffects,
} from "../providerModelConfig"

describe("providerModelConfig", () => {
describe("PROVIDER_SERVICE_CONFIG", () => {
it("uses canonical provider identifiers as registry keys", () => {
expect(PROVIDER_SERVICE_CONFIG[providerIdentifiers.openaiNative]?.serviceName).toBe("OpenAI")
expect(PROVIDER_SERVICE_CONFIG[providerIdentifiers.vscodeLm]?.serviceName).toBe("VS Code LM")
})

it("contains service config for anthropic", () => {
expect(PROVIDER_SERVICE_CONFIG.anthropic).toEqual({
serviceName: "Anthropic",
Expand Down Expand Up @@ -63,10 +72,10 @@ describe("providerModelConfig", () => {

describe("PROVIDER_DEFAULT_MODEL_IDS", () => {
it("contains default model IDs for static providers", () => {
expect(PROVIDER_DEFAULT_MODEL_IDS.anthropic).toBeDefined()
expect(PROVIDER_DEFAULT_MODEL_IDS.bedrock).toBeDefined()
expect(PROVIDER_DEFAULT_MODEL_IDS.gemini).toBeDefined()
expect(PROVIDER_DEFAULT_MODEL_IDS["openai-native"]).toBeDefined()
expect(PROVIDER_DEFAULT_MODEL_IDS[providerIdentifiers.anthropic]).toBeDefined()
expect(PROVIDER_DEFAULT_MODEL_IDS[providerIdentifiers.bedrock]).toBeDefined()
expect(PROVIDER_DEFAULT_MODEL_IDS[providerIdentifiers.gemini]).toBeDefined()
expect(PROVIDER_DEFAULT_MODEL_IDS[providerIdentifiers.openaiNative]).toBeDefined()
})
})

Expand Down Expand Up @@ -129,6 +138,23 @@ describe("providerModelConfig", () => {
})
})

describe("getProviderModelConfig", () => {
it("selects the Z.ai default for the configured API line", () => {
const config = getProviderModelConfig(providerIdentifiers.zai, {
apiProvider: providerIdentifiers.zai,
zaiApiLine: "china_coding",
})

expect(config).toEqual({
field: "apiModelId",
default: getDefaultModelIdForProvider(providerIdentifiers.zai, {
apiProvider: providerIdentifiers.zai,
zaiApiLine: "china_coding",
}),
})
})
})

describe("getStaticModelsForProvider", () => {
it("returns models for anthropic provider", () => {
const models = getStaticModelsForProvider("anthropic")
Expand Down Expand Up @@ -164,10 +190,10 @@ describe("providerModelConfig", () => {

describe("PROVIDERS_WITH_CUSTOM_MODEL_UI", () => {
it("includes providers that have their own model selection UI", () => {
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain("openrouter")
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain("ollama")
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain("lmstudio")
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain("vscode-lm")
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain(providerIdentifiers.openrouter)
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain(providerIdentifiers.ollama)
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain(providerIdentifiers.lmstudio)
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).toContain(providerIdentifiers.vscodeLm)
})

it("does not include static providers using generic picker", () => {
Expand Down Expand Up @@ -196,4 +222,34 @@ describe("providerModelConfig", () => {
expect(shouldUseGenericModelPicker("openai")).toBe(false)
})
})

it("uses the canonical Bedrock identifier when clearing a custom ARN", () => {
const setApiConfigurationField = vi.fn()

handleModelChangeSideEffects(providerIdentifiers.bedrock, "anthropic.claude", setApiConfigurationField)

expect(setApiConfigurationField).toHaveBeenCalledWith("awsCustomArn", "")
})

it("preserves the custom ARN while resetting shared settings for Bedrock's custom ARN model", () => {
const setApiConfigurationField = vi.fn()

handleModelChangeSideEffects(providerIdentifiers.bedrock, "custom-arn", setApiConfigurationField)

expect(setApiConfigurationField).not.toHaveBeenCalledWith("awsCustomArn", expect.anything())
expect(setApiConfigurationField).toHaveBeenCalledWith("reasoningEffort", undefined)
expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxTokens", undefined)
expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", undefined)
})

it("preserves the custom ARN while resetting shared settings for a non-Bedrock provider", () => {
const setApiConfigurationField = vi.fn()

handleModelChangeSideEffects(providerIdentifiers.anthropic, "claude-sonnet", setApiConfigurationField)

expect(setApiConfigurationField).not.toHaveBeenCalledWith("awsCustomArn", expect.anything())
expect(setApiConfigurationField).toHaveBeenCalledWith("reasoningEffort", undefined)
expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxTokens", undefined)
expect(setApiConfigurationField).toHaveBeenCalledWith("modelMaxThinkingTokens", undefined)
})
})
183 changes: 95 additions & 88 deletions webview-ui/src/components/settings/utils/providerModelConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ProviderName, ModelInfo, ProviderSettings } from "@roo-code/types"
import {
providerIdentifiers,
anthropicDefaultModelId,
bedrockDefaultModelId,
deepSeekDefaultModelId,
Expand Down Expand Up @@ -39,51 +40,54 @@ export interface ProviderServiceConfig {
}

export const PROVIDER_SERVICE_CONFIG: Partial<Record<ProviderName, ProviderServiceConfig>> = {
anthropic: { serviceName: "Anthropic", serviceUrl: "https://console.anthropic.com" },
bedrock: { serviceName: "Amazon Bedrock", serviceUrl: "https://aws.amazon.com/bedrock" },
deepseek: { serviceName: "DeepSeek", serviceUrl: "https://platform.deepseek.com" },
moonshot: { serviceName: "Moonshot", serviceUrl: "https://platform.moonshot.cn" },
"kimi-code": { serviceName: "Kimi Code", serviceUrl: "https://www.kimi.com/code" },
gemini: { serviceName: "Google Gemini", serviceUrl: "https://ai.google.dev" },
mistral: { serviceName: "Mistral", serviceUrl: "https://console.mistral.ai" },
"openai-native": { serviceName: "OpenAI", serviceUrl: "https://platform.openai.com" },
"qwen-code": { serviceName: "Qwen Code", serviceUrl: "https://dashscope.console.aliyun.com" },
vertex: { serviceName: "GCP Vertex AI", serviceUrl: "https://console.cloud.google.com/vertex-ai" },
xai: { serviceName: "xAI", serviceUrl: "https://x.ai" },
sambanova: { serviceName: "SambaNova", serviceUrl: "https://sambanova.ai" },
zai: { serviceName: "Z.ai", serviceUrl: "https://z.ai" },
fireworks: { serviceName: "Fireworks AI", serviceUrl: "https://fireworks.ai" },
friendli: { serviceName: "Friendli", serviceUrl: "https://friendli.ai" },
minimax: { serviceName: "MiniMax", serviceUrl: "https://minimax.chat" },
mimo: { serviceName: "Xiaomi MiMo", serviceUrl: "https://platform.xiaomimimo.com" },
baseten: { serviceName: "Baseten", serviceUrl: "https://baseten.co" },
ollama: { serviceName: "Ollama", serviceUrl: "https://ollama.ai" },
lmstudio: { serviceName: "LM Studio", serviceUrl: "https://lmstudio.ai/docs" },
"vscode-lm": {
[providerIdentifiers.anthropic]: { serviceName: "Anthropic", serviceUrl: "https://console.anthropic.com" },
[providerIdentifiers.bedrock]: { serviceName: "Amazon Bedrock", serviceUrl: "https://aws.amazon.com/bedrock" },
[providerIdentifiers.deepseek]: { serviceName: "DeepSeek", serviceUrl: "https://platform.deepseek.com" },
[providerIdentifiers.moonshot]: { serviceName: "Moonshot", serviceUrl: "https://platform.moonshot.cn" },
[providerIdentifiers.kimiCode]: { serviceName: "Kimi Code", serviceUrl: "https://www.kimi.com/code" },
[providerIdentifiers.gemini]: { serviceName: "Google Gemini", serviceUrl: "https://ai.google.dev" },
[providerIdentifiers.mistral]: { serviceName: "Mistral", serviceUrl: "https://console.mistral.ai" },
[providerIdentifiers.openaiNative]: { serviceName: "OpenAI", serviceUrl: "https://platform.openai.com" },
[providerIdentifiers.qwenCode]: { serviceName: "Qwen Code", serviceUrl: "https://dashscope.console.aliyun.com" },
[providerIdentifiers.vertex]: {
serviceName: "GCP Vertex AI",
serviceUrl: "https://console.cloud.google.com/vertex-ai",
},
[providerIdentifiers.xai]: { serviceName: "xAI", serviceUrl: "https://x.ai" },
[providerIdentifiers.sambanova]: { serviceName: "SambaNova", serviceUrl: "https://sambanova.ai" },
[providerIdentifiers.zai]: { serviceName: "Z.ai", serviceUrl: "https://z.ai" },
[providerIdentifiers.fireworks]: { serviceName: "Fireworks AI", serviceUrl: "https://fireworks.ai" },
[providerIdentifiers.friendli]: { serviceName: "Friendli", serviceUrl: "https://friendli.ai" },
[providerIdentifiers.minimax]: { serviceName: "MiniMax", serviceUrl: "https://minimax.chat" },
[providerIdentifiers.mimo]: { serviceName: "Xiaomi MiMo", serviceUrl: "https://platform.xiaomimimo.com" },
[providerIdentifiers.baseten]: { serviceName: "Baseten", serviceUrl: "https://baseten.co" },
[providerIdentifiers.ollama]: { serviceName: "Ollama", serviceUrl: "https://ollama.ai" },
[providerIdentifiers.lmstudio]: { serviceName: "LM Studio", serviceUrl: "https://lmstudio.ai/docs" },
[providerIdentifiers.vscodeLm]: {
serviceName: "VS Code LM",
serviceUrl: "https://code.visualstudio.com/api/extension-guides/language-model",
},
}

export const PROVIDER_DEFAULT_MODEL_IDS: Partial<Record<ProviderName, string>> = {
anthropic: anthropicDefaultModelId,
bedrock: bedrockDefaultModelId,
deepseek: deepSeekDefaultModelId,
moonshot: moonshotDefaultModelId,
"kimi-code": kimiCodeDefaultModelId,
gemini: geminiDefaultModelId,
mistral: mistralDefaultModelId,
"openai-native": openAiNativeDefaultModelId,
"qwen-code": qwenCodeDefaultModelId,
vertex: vertexDefaultModelId,
xai: xaiDefaultModelId,
sambanova: sambaNovaDefaultModelId,
zai: internationalZAiDefaultModelId,
fireworks: fireworksDefaultModelId,
friendli: friendliDefaultModelId,
minimax: minimaxDefaultModelId,
mimo: mimoDefaultModelId,
baseten: basetenDefaultModelId,
[providerIdentifiers.anthropic]: anthropicDefaultModelId,
[providerIdentifiers.bedrock]: bedrockDefaultModelId,
[providerIdentifiers.deepseek]: deepSeekDefaultModelId,
[providerIdentifiers.moonshot]: moonshotDefaultModelId,
[providerIdentifiers.kimiCode]: kimiCodeDefaultModelId,
[providerIdentifiers.gemini]: geminiDefaultModelId,
[providerIdentifiers.mistral]: mistralDefaultModelId,
[providerIdentifiers.openaiNative]: openAiNativeDefaultModelId,
[providerIdentifiers.qwenCode]: qwenCodeDefaultModelId,
[providerIdentifiers.vertex]: vertexDefaultModelId,
[providerIdentifiers.xai]: xaiDefaultModelId,
[providerIdentifiers.sambanova]: sambaNovaDefaultModelId,
[providerIdentifiers.zai]: internationalZAiDefaultModelId,
[providerIdentifiers.fireworks]: fireworksDefaultModelId,
[providerIdentifiers.friendli]: friendliDefaultModelId,
[providerIdentifiers.minimax]: minimaxDefaultModelId,
[providerIdentifiers.mimo]: mimoDefaultModelId,
[providerIdentifiers.baseten]: basetenDefaultModelId,
}

export const getProviderServiceConfig = (provider: ProviderName): ProviderServiceConfig => {
Expand All @@ -92,7 +96,7 @@ export const getProviderServiceConfig = (provider: ProviderName): ProviderServic

export const getDefaultModelIdForProvider = (provider: ProviderName, apiConfiguration?: ProviderSettings): string => {
// Handle Z.ai's China/International entrypoint distinction
if (provider === "zai" && apiConfiguration) {
if (provider === providerIdentifiers.zai && apiConfiguration) {
return apiConfiguration.zaiApiLine === "china_coding"
? mainlandZAiDefaultModelId
: internationalZAiDefaultModelId
Expand All @@ -109,44 +113,47 @@ export type ProviderModelConfig = {
// Minimal per-provider config used by ApiOptions for model-id field wiring.
// Kept in this file to keep ApiOptions.tsx from growing a second registry.
const PROVIDER_MODEL_CONFIG: Partial<Record<ProviderName, ProviderModelConfig>> = {
openrouter: { field: "openRouterModelId", default: openRouterDefaultModelId },
requesty: { field: "requestyModelId", default: requestyDefaultModelId },
unbound: { field: "unboundModelId", default: unboundDefaultModelId },
litellm: { field: "litellmModelId", default: litellmDefaultModelId },
anthropic: { field: "apiModelId", default: anthropicDefaultModelId },
"openai-codex": { field: "apiModelId", default: openAiCodexDefaultModelId },
"qwen-code": { field: "apiModelId", default: qwenCodeDefaultModelId },
"openai-native": { field: "apiModelId", default: openAiNativeDefaultModelId },
gemini: { field: "apiModelId", default: geminiDefaultModelId },
deepseek: { field: "apiModelId", default: deepSeekDefaultModelId },
moonshot: { field: "apiModelId", default: moonshotDefaultModelId },
"kimi-code": { field: "apiModelId", default: kimiCodeDefaultModelId },
minimax: { field: "apiModelId", default: minimaxDefaultModelId },
mimo: { field: "apiModelId", default: mimoDefaultModelId },
mistral: { field: "apiModelId", default: mistralDefaultModelId },
xai: { field: "apiModelId", default: xaiDefaultModelId },
baseten: { field: "apiModelId", default: basetenDefaultModelId },
bedrock: { field: "apiModelId", default: bedrockDefaultModelId },
vertex: { field: "apiModelId", default: vertexDefaultModelId },
sambanova: { field: "apiModelId", default: sambaNovaDefaultModelId },
zai: { field: "apiModelId" },
fireworks: { field: "apiModelId", default: fireworksDefaultModelId },
friendli: { field: "apiModelId", default: friendliDefaultModelId },
poe: { field: "apiModelId", default: poeDefaultModelId },
"vercel-ai-gateway": { field: "vercelAiGatewayModelId", default: vercelAiGatewayDefaultModelId },
"opencode-go": { field: "opencodeGoModelId", default: opencodeGoDefaultModelId },
kenari: { field: "kenariModelId", default: kenariDefaultModelId },
"zoo-gateway": { field: "zooGatewayModelId", default: zooGatewayDefaultModelId },
openai: { field: "openAiModelId" },
ollama: { field: "ollamaModelId" },
lmstudio: { field: "lmStudioModelId" },
[providerIdentifiers.openrouter]: { field: "openRouterModelId", default: openRouterDefaultModelId },
[providerIdentifiers.requesty]: { field: "requestyModelId", default: requestyDefaultModelId },
[providerIdentifiers.unbound]: { field: "unboundModelId", default: unboundDefaultModelId },
[providerIdentifiers.litellm]: { field: "litellmModelId", default: litellmDefaultModelId },
[providerIdentifiers.anthropic]: { field: "apiModelId", default: anthropicDefaultModelId },
[providerIdentifiers.openaiCodex]: { field: "apiModelId", default: openAiCodexDefaultModelId },
[providerIdentifiers.qwenCode]: { field: "apiModelId", default: qwenCodeDefaultModelId },
[providerIdentifiers.openaiNative]: { field: "apiModelId", default: openAiNativeDefaultModelId },
[providerIdentifiers.gemini]: { field: "apiModelId", default: geminiDefaultModelId },
[providerIdentifiers.deepseek]: { field: "apiModelId", default: deepSeekDefaultModelId },
[providerIdentifiers.moonshot]: { field: "apiModelId", default: moonshotDefaultModelId },
[providerIdentifiers.kimiCode]: { field: "apiModelId", default: kimiCodeDefaultModelId },
[providerIdentifiers.minimax]: { field: "apiModelId", default: minimaxDefaultModelId },
[providerIdentifiers.mimo]: { field: "apiModelId", default: mimoDefaultModelId },
[providerIdentifiers.mistral]: { field: "apiModelId", default: mistralDefaultModelId },
[providerIdentifiers.xai]: { field: "apiModelId", default: xaiDefaultModelId },
[providerIdentifiers.baseten]: { field: "apiModelId", default: basetenDefaultModelId },
[providerIdentifiers.bedrock]: { field: "apiModelId", default: bedrockDefaultModelId },
[providerIdentifiers.vertex]: { field: "apiModelId", default: vertexDefaultModelId },
[providerIdentifiers.sambanova]: { field: "apiModelId", default: sambaNovaDefaultModelId },
[providerIdentifiers.zai]: { field: "apiModelId" },
[providerIdentifiers.fireworks]: { field: "apiModelId", default: fireworksDefaultModelId },
[providerIdentifiers.friendli]: { field: "apiModelId", default: friendliDefaultModelId },
[providerIdentifiers.poe]: { field: "apiModelId", default: poeDefaultModelId },
[providerIdentifiers.vercelAiGateway]: {
field: "vercelAiGatewayModelId",
default: vercelAiGatewayDefaultModelId,
},
[providerIdentifiers.opencodeGo]: { field: "opencodeGoModelId", default: opencodeGoDefaultModelId },
[providerIdentifiers.kenari]: { field: "kenariModelId", default: kenariDefaultModelId },
[providerIdentifiers.zooGateway]: { field: "zooGatewayModelId", default: zooGatewayDefaultModelId },
[providerIdentifiers.openai]: { field: "openAiModelId" },
[providerIdentifiers.ollama]: { field: "ollamaModelId" },
[providerIdentifiers.lmstudio]: { field: "lmStudioModelId" },
}

export function getProviderModelConfig(provider: string, apiConfiguration?: ProviderSettings) {
const config = PROVIDER_MODEL_CONFIG[provider as ProviderName]
if (!config) return undefined

if (provider === "zai") {
if (provider === providerIdentifiers.zai) {
return {
...config,
default: getDefaultModelIdForProvider(provider as ProviderName, apiConfiguration),
Expand All @@ -158,8 +165,8 @@ export function getProviderModelConfig(provider: string, apiConfiguration?: Prov

// Custom mapping for doc URL slugs. Default is provider key.
const PROVIDER_DOCS_SLUGS: Partial<Record<ProviderName, string>> = {
"openai-native": "openai",
openai: "openai-compatible",
[providerIdentifiers.openaiNative]: "openai",
[providerIdentifiers.openai]: "openai-compatible",
}

export function getProviderDocsSlug(provider: string) {
Expand All @@ -173,7 +180,7 @@ export const getStaticModelsForProvider = (
const models = MODELS_BY_PROVIDER[provider] ?? {}

// Add custom-arn option for Bedrock
if (provider === "bedrock") {
if (provider === providerIdentifiers.bedrock) {
return {
...models,
"custom-arn": {
Expand All @@ -200,18 +207,18 @@ export const isStaticModelProvider = (provider: ProviderName): boolean => {
* and should not use the generic ModelPicker in ApiOptions
*/
export const PROVIDERS_WITH_CUSTOM_MODEL_UI: ProviderName[] = [
"openrouter",
"requesty",
"unbound",
"openai", // OpenAI Compatible
"openai-codex", // OpenAI Codex has custom UI with auth and rate limits
"kimi-code",
"litellm",
"vercel-ai-gateway",
"ollama",
"lmstudio",
"vscode-lm",
"moonshot", // Moonshot has custom ModelPicker inside Moonshot.tsx
providerIdentifiers.openrouter,
providerIdentifiers.requesty,
providerIdentifiers.unbound,
providerIdentifiers.openai, // OpenAI Compatible
providerIdentifiers.openaiCodex, // OpenAI Codex has custom UI with auth and rate limits
providerIdentifiers.kimiCode,
providerIdentifiers.litellm,
providerIdentifiers.vercelAiGateway,
providerIdentifiers.ollama,
providerIdentifiers.lmstudio,
providerIdentifiers.vscodeLm,
providerIdentifiers.moonshot, // Moonshot has custom ModelPicker inside Moonshot.tsx
]

/**
Expand All @@ -231,7 +238,7 @@ export const handleModelChangeSideEffects = <K extends keyof ProviderSettings>(
setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void,
): void => {
// Bedrock: Clear custom ARN if not using custom ARN option
if (provider === "bedrock" && modelId !== "custom-arn") {
if (provider === providerIdentifiers.bedrock && modelId !== "custom-arn") {
setApiConfigurationField("awsCustomArn" as K, "" as ProviderSettings[K])
}

Expand Down
Loading