Skip to content
Draft
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
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