Skip to content
Closed
68 changes: 68 additions & 0 deletions packages/types/src/__tests__/all-model-capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { modelCapabilityPresets } from "../providers/all-model-capabilities.js"
import type { ModelCapabilityPreset } from "../providers/all-model-capabilities.js"

describe("modelCapabilityPresets", () => {
it("should be a non-empty array", () => {
expect(Array.isArray(modelCapabilityPresets)).toBe(true)
expect(modelCapabilityPresets.length).toBeGreaterThan(0)
})

it("every preset should have a provider, modelId, and info with required fields", () => {
for (const preset of modelCapabilityPresets) {
expect(typeof preset.provider).toBe("string")
expect(preset.provider.length).toBeGreaterThan(0)

expect(typeof preset.modelId).toBe("string")
expect(preset.modelId.length).toBeGreaterThan(0)

expect(preset.info).toBeDefined()
expect(typeof preset.info.contextWindow).toBe("number")
expect(preset.info.contextWindow).toBeGreaterThan(0)
// supportsPromptCache is a required field in ModelInfo
expect(typeof preset.info.supportsPromptCache).toBe("boolean")
}
})

it("should include models from multiple providers", () => {
const providers = new Set(modelCapabilityPresets.map((p: ModelCapabilityPreset) => p.provider))
expect(providers.size).toBeGreaterThan(5)
})

it("should include well-known models", () => {
const modelIds = modelCapabilityPresets.map((p: ModelCapabilityPreset) => p.modelId)

// Check for some well-known models
expect(modelIds.some((id: string) => id.includes("claude"))).toBe(true)
expect(modelIds.some((id: string) => id.includes("gpt"))).toBe(true)
expect(modelIds.some((id: string) => id.includes("deepseek"))).toBe(true)
expect(modelIds.some((id: string) => id.includes("gemini"))).toBe(true)
})

it("should have unique provider/modelId combinations", () => {
const keys = modelCapabilityPresets.map((p: ModelCapabilityPreset) => `${p.provider}/${p.modelId}`)
const uniqueKeys = new Set(keys)
expect(uniqueKeys.size).toBe(keys.length)
})

it("each preset should include known providers", () => {
const knownProviders = [
"Anthropic",
"OpenAI",
"DeepSeek",
"Gemini",
"MiniMax",
"Mistral",
"Moonshot (Kimi)",
"Qwen",
"SambaNova",
"xAI",
"ZAi (GLM)",
]

const providers = new Set(modelCapabilityPresets.map((p: ModelCapabilityPreset) => p.provider))

for (const known of knownProviders) {
expect(providers.has(known)).toBe(true)
}
})
})
69 changes: 69 additions & 0 deletions packages/types/src/providers/all-model-capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Aggregated model capabilities from all providers.
*
* This map is used by the OpenAI Compatible provider to let users select
* a known model's capabilities (context window, max tokens, image support,
* prompt caching, etc.) so Roo can communicate optimally with local or
* third-party endpoints that serve these models.
*/
import type { ModelInfo } from "../model.js"

import { anthropicModels } from "./anthropic.js"
import { deepSeekModels } from "./deepseek.js"
import { geminiModels } from "./gemini.js"
import { minimaxModels } from "./minimax.js"
import { mistralModels } from "./mistral.js"
import { moonshotModels } from "./moonshot.js"
import { openAiNativeModels } from "./openai.js"
import { sambaNovaModels } from "./sambanova.js"
import { xaiModels } from "./xai.js"
import { internationalZAiModels } from "./zai.js"
import { qwenCodeModels } from "./qwen-code.js"

/**
* A single entry in the capability presets list.
*/
export interface ModelCapabilityPreset {
/** The provider this model originally belongs to */
provider: string
/** The model ID as known by its native provider */
modelId: string
/** The model's capability info */
info: ModelInfo
}

/**
* Helper to build preset entries from a provider's model record.
*/
function buildPresets(provider: string, models: Record<string, ModelInfo>): ModelCapabilityPreset[] {
return Object.entries(models).map(([modelId, info]) => ({
provider,
modelId,
info,
}))
}

/**
* All known model capability presets, aggregated from every provider.
*
* We intentionally exclude cloud-only routing providers (OpenRouter, Requesty,
* LiteLLM, Roo, Unbound, Vercel AI Gateway) and platform-locked providers
* (Bedrock, Vertex, VSCode LM, OpenAI Codex, Baseten, Fireworks) since those
* models are either duplicates of the originals or have platform-specific
* model IDs that don't map to local inference.
*
* The user can always choose "Custom" and configure capabilities manually.
*/
export const modelCapabilityPresets: ModelCapabilityPreset[] = [
...buildPresets("Anthropic", anthropicModels),
...buildPresets("OpenAI", openAiNativeModels),
...buildPresets("DeepSeek", deepSeekModels),
...buildPresets("Gemini", geminiModels),
...buildPresets("MiniMax", minimaxModels),
...buildPresets("Mistral", mistralModels),
...buildPresets("Moonshot (Kimi)", moonshotModels),
...buildPresets("Qwen", qwenCodeModels),
...buildPresets("SambaNova", sambaNovaModels),
...buildPresets("xAI", xaiModels),
...buildPresets("ZAi (GLM)", internationalZAiModels),
]
1 change: 1 addition & 0 deletions packages/types/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from "./xai.js"
export * from "./vercel-ai-gateway.js"
export * from "./zai.js"
export * from "./minimax.js"
export * from "./all-model-capabilities.js"

import { anthropicDefaultModelId } from "./anthropic.js"
import { basetenDefaultModelId } from "./baseten.js"
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/providers/moonshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const moonshotModels = {
outputPrice: 3.0, // $3.00 per million tokens
cacheReadsPrice: 0.1, // $0.10 per million tokens (cache hit)
supportsTemperature: true,
preserveReasoning: true,
defaultTemperature: 1.0,
description:
"Kimi K2.5 is the latest generation of Moonshot AI's Kimi series, featuring improved reasoning capabilities and enhanced performance across diverse tasks.",
Expand Down
166 changes: 164 additions & 2 deletions webview-ui/src/components/settings/providers/OpenAICompatible.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useCallback, useEffect } from "react"
import { useState, useCallback, useEffect, useMemo } from "react"
import { useEvent } from "react-use"
import { Checkbox } from "vscrui"
import { ChevronsUpDown, Check } from "lucide-react"
import { VSCodeButton, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"

import {
Expand All @@ -11,10 +12,24 @@ import {
type ExtensionMessage,
azureOpenAiDefaultApiVersion,
openAiModelInfoSaneDefaults,
modelCapabilityPresets,
} from "@roo-code/types"

import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Button, StandardTooltip } from "@src/components/ui"
import {
Button,
StandardTooltip,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
} from "@src/components/ui"
import { cn } from "@src/lib/utils"

import { convertHeadersToObject } from "../utils/headers"
import { inputEventTransform, noTransform } from "../transforms"
Expand Down Expand Up @@ -44,9 +59,74 @@ export const OpenAICompatible = ({
const { t } = useAppTranslation()

const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion)
const [presetPickerOpen, setPresetPickerOpen] = useState(false)
const [selectedPresetId, setSelectedPresetId] = useState<string | null>(null)

const [openAiModels, setOpenAiModels] = useState<Record<string, ModelInfo> | null>(null)

// Compute applied capability flags for the selected preset
const appliedCapabilityFlags = useMemo(() => {
if (!selectedPresetId) return null
const preset = modelCapabilityPresets.find((p) => `${p.provider}/${p.modelId}` === selectedPresetId)
if (!preset) return null
const flags: string[] = []
if (preset.info.preserveReasoning)
flags.push(t("settings:providers.customModel.capabilityPreset.flags.reasoning"))
if (preset.info.supportsImages) flags.push(t("settings:providers.customModel.capabilityPreset.flags.images"))
if (preset.info.supportsPromptCache)
flags.push(t("settings:providers.customModel.capabilityPreset.flags.promptCache"))
if (preset.info.supportsTemperature)
flags.push(t("settings:providers.customModel.capabilityPreset.flags.temperature"))
if (preset.info.defaultTemperature !== undefined)
flags.push(
t("settings:providers.customModel.capabilityPreset.flags.defaultTemp", {
temp: preset.info.defaultTemperature,
}),
)
return flags.length > 0 ? flags : null
}, [selectedPresetId, t])

// Group presets by provider for organized display
const groupedPresets = useMemo(() => {
const groups: Record<string, typeof modelCapabilityPresets> = {}
for (const preset of modelCapabilityPresets) {
if (!groups[preset.provider]) {
groups[preset.provider] = []
}
groups[preset.provider].push(preset)
}
return groups
}, [])

const handlePresetSelect = useCallback(
(presetKey: string) => {
if (presetKey === "custom") {
setSelectedPresetId(null)
setApiConfigurationField("openAiCustomModelInfo", openAiModelInfoSaneDefaults)
setApiConfigurationField("openAiR1FormatEnabled", false)
setApiConfigurationField("modelTemperature", null)
} else {
const preset = modelCapabilityPresets.find((p) => `${p.provider}/${p.modelId}` === presetKey)
if (preset) {
setSelectedPresetId(presetKey)
setApiConfigurationField("openAiCustomModelInfo", { ...preset.info })

// Auto-enable/disable R1 format based on whether model uses reasoning/thinking blocks
setApiConfigurationField("openAiR1FormatEnabled", !!preset.info.preserveReasoning)

// Auto-apply default temperature when the model specifies one (e.g. Kimi K2 models require temperature=1.0)
if (preset.info.defaultTemperature !== undefined) {
setApiConfigurationField("modelTemperature", preset.info.defaultTemperature)
} else {
setApiConfigurationField("modelTemperature", null)
}
}
}
setPresetPickerOpen(false)
},
[setApiConfigurationField],
)

const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
const headers = apiConfiguration?.openAiHeaders || {}
return Object.entries(headers)
Expand Down Expand Up @@ -278,6 +358,88 @@ export const OpenAICompatible = ({
)}
</div>
<div className="flex flex-col gap-3">
<div>
<label className="block font-medium mb-1">
{t("settings:providers.customModel.capabilityPreset.label")}
</label>
<div className="text-sm text-vscode-descriptionForeground mb-2">
{t("settings:providers.customModel.capabilityPreset.description")}
</div>
<Popover open={presetPickerOpen} onOpenChange={setPresetPickerOpen}>
<PopoverTrigger asChild>
<Button
variant="combobox"
role="combobox"
aria-expanded={presetPickerOpen}
className="w-full justify-between">
<span className="truncate">
{selectedPresetId ?? t("settings:providers.customModel.capabilityPreset.custom")}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
<Command>
<CommandInput
placeholder={t("settings:providers.customModel.capabilityPreset.searchPlaceholder")}
/>
<CommandList>
<CommandEmpty>
{t("settings:providers.customModel.capabilityPreset.noResults")}
</CommandEmpty>
<CommandGroup heading={t("settings:providers.customModel.capabilityPreset.custom")}>
<CommandItem value="custom" onSelect={() => handlePresetSelect("custom")}>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedPresetId === null ? "opacity-100" : "opacity-0",
)}
/>
{t("settings:providers.customModel.capabilityPreset.custom")}
</CommandItem>
</CommandGroup>
{Object.entries(groupedPresets).map(([provider, presets]) => (
<CommandGroup key={provider} heading={provider}>
{presets.map((preset) => {
const presetKey = `${preset.provider}/${preset.modelId}`
return (
<CommandItem
key={presetKey}
value={presetKey}
onSelect={() => handlePresetSelect(presetKey)}>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedPresetId === presetKey
? "opacity-100"
: "opacity-0",
)}
/>
{preset.modelId}
{preset.info.description && (
<span className="ml-2 text-xs text-vscode-descriptionForeground truncate">
{preset.info.contextWindow
? `${Math.round(preset.info.contextWindow / 1000)}K ctx`
: ""}
</span>
)}
</CommandItem>
)
})}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
{appliedCapabilityFlags && (
<div className="text-xs text-vscode-descriptionForeground mt-1">
{t("settings:providers.customModel.capabilityPreset.appliedFlags")}:{" "}
{appliedCapabilityFlags.join(", ")}
</div>
)}
</div>

<div className="text-sm text-vscode-descriptionForeground whitespace-pre-line">
{t("settings:providers.customModel.capabilities")}
</div>
Expand Down
Loading
Loading