mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: standardize model selectors across all providers (#10294)
Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: Roo Code <roomote@roocode.com>
This commit is contained in:
parent
a060915d18
commit
e356d058e9
8 changed files with 574 additions and 283 deletions
|
|
@ -41,6 +41,14 @@ import {
|
|||
minimaxDefaultModelId,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import {
|
||||
getProviderServiceConfig,
|
||||
getDefaultModelIdForProvider,
|
||||
getStaticModelsForProvider,
|
||||
shouldUseGenericModelPicker,
|
||||
handleModelChangeSideEffects,
|
||||
} from "./utils/providerModelConfig"
|
||||
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { validateApiConfigurationExcludingModelErrors, getModelValidationError } from "@src/utils/validate"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
|
|
@ -104,7 +112,7 @@ import {
|
|||
|
||||
import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants"
|
||||
import { inputEventTransform, noTransform } from "./transforms"
|
||||
import { ModelInfoView } from "./ModelInfoView"
|
||||
import { ModelPicker } from "./ModelPicker"
|
||||
import { ApiErrorMessage } from "./ApiErrorMessage"
|
||||
import { ThinkingBudget } from "./ThinkingBudget"
|
||||
import { Verbosity } from "./Verbosity"
|
||||
|
|
@ -174,7 +182,6 @@ const ApiOptions = ({
|
|||
[customHeaders, apiConfiguration?.openAiHeaders, setApiConfigurationField],
|
||||
)
|
||||
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false)
|
||||
const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false)
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
|
|
@ -273,32 +280,6 @@ const ApiOptions = ({
|
|||
setErrorMessage(apiValidationResult)
|
||||
}, [apiConfiguration, routerModels, organizationAllowList, setErrorMessage])
|
||||
|
||||
const selectedProviderModels = useMemo(() => {
|
||||
const models = MODELS_BY_PROVIDER[selectedProvider]
|
||||
|
||||
if (!models) return []
|
||||
|
||||
const filteredModels = filterModels(models, selectedProvider, organizationAllowList)
|
||||
|
||||
// Include the currently selected model even if deprecated (so users can see what they have selected)
|
||||
// But filter out other deprecated models from being newly selectable
|
||||
const availableModels = filteredModels
|
||||
? Object.entries(filteredModels)
|
||||
.filter(([modelId, modelInfo]) => {
|
||||
// Always include the currently selected model
|
||||
if (modelId === selectedModelId) return true
|
||||
// Filter out deprecated models that aren't currently selected
|
||||
return !modelInfo.deprecated
|
||||
})
|
||||
.map(([modelId]) => ({
|
||||
value: modelId,
|
||||
label: modelId,
|
||||
}))
|
||||
: []
|
||||
|
||||
return availableModels
|
||||
}, [selectedProvider, organizationAllowList, selectedModelId])
|
||||
|
||||
const onProviderChange = useCallback(
|
||||
(value: ProviderName) => {
|
||||
setApiConfigurationField("apiProvider", value)
|
||||
|
|
@ -660,11 +641,7 @@ const ApiOptions = ({
|
|||
)}
|
||||
|
||||
{selectedProvider === "lmstudio" && (
|
||||
<LMStudio
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
simplifySettings={fromWelcomeView}
|
||||
/>
|
||||
<LMStudio apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
|
||||
)}
|
||||
|
||||
{selectedProvider === "deepseek" && (
|
||||
|
|
@ -797,69 +774,33 @@ const ApiOptions = ({
|
|||
<Featherless apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
|
||||
)}
|
||||
|
||||
{/* Skip generic model picker for claude-code/openai-codex since they have their own model pickers */}
|
||||
{selectedProviderModels.length > 0 &&
|
||||
selectedProvider !== "claude-code" &&
|
||||
selectedProvider !== "openai-codex" && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block font-medium mb-1">{t("settings:providers.model")}</label>
|
||||
<Select
|
||||
value={selectedModelId === "custom-arn" ? "custom-arn" : selectedModelId}
|
||||
onValueChange={(value) => {
|
||||
setApiConfigurationField("apiModelId", value)
|
||||
{/* Generic model picker for providers with static models */}
|
||||
{shouldUseGenericModelPicker(selectedProvider) && (
|
||||
<>
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId={getDefaultModelIdForProvider(selectedProvider, apiConfiguration)}
|
||||
models={getStaticModelsForProvider(selectedProvider, t("settings:labels.useCustomArn"))}
|
||||
modelIdKey="apiModelId"
|
||||
serviceName={getProviderServiceConfig(selectedProvider).serviceName}
|
||||
serviceUrl={getProviderServiceConfig(selectedProvider).serviceUrl}
|
||||
organizationAllowList={organizationAllowList}
|
||||
errorMessage={modelValidationError}
|
||||
simplifySettings={fromWelcomeView}
|
||||
onModelChange={(modelId) =>
|
||||
handleModelChangeSideEffects(selectedProvider, modelId, setApiConfigurationField)
|
||||
}
|
||||
/>
|
||||
|
||||
// Clear custom ARN if not using custom ARN option.
|
||||
if (value !== "custom-arn" && selectedProvider === "bedrock") {
|
||||
setApiConfigurationField("awsCustomArn", "")
|
||||
}
|
||||
|
||||
// Clear reasoning effort when switching models to allow the new model's default to take effect
|
||||
// This is especially important for GPT-5 models which default to "medium"
|
||||
if (selectedProvider === "openai-native") {
|
||||
setApiConfigurationField("reasoningEffort", undefined)
|
||||
}
|
||||
}}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t("settings:common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{selectedProviderModels.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
{selectedProvider === "bedrock" && (
|
||||
<SelectItem value="custom-arn">{t("settings:labels.useCustomArn")}</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Show error if a deprecated model is selected */}
|
||||
{selectedModelInfo?.deprecated && (
|
||||
<ApiErrorMessage errorMessage={t("settings:validation.modelDeprecated")} />
|
||||
)}
|
||||
|
||||
{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
|
||||
<BedrockCustomArn
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Only show model info if not deprecated */}
|
||||
{!selectedModelInfo?.deprecated && (
|
||||
<ModelInfoView
|
||||
apiProvider={selectedProvider}
|
||||
selectedModelId={selectedModelId}
|
||||
modelInfo={selectedModelInfo}
|
||||
isDescriptionExpanded={isDescriptionExpanded}
|
||||
setIsDescriptionExpanded={setIsDescriptionExpanded}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectedProvider === "bedrock" && selectedModelId === "custom-arn" && (
|
||||
<BedrockCustomArn
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!fromWelcomeView && (
|
||||
<ThinkingBudget
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ type ModelIdKey = keyof Pick<
|
|||
| "ioIntelligenceModelId"
|
||||
| "vercelAiGatewayModelId"
|
||||
| "apiModelId"
|
||||
| "ollamaModelId"
|
||||
| "lmStudioModelId"
|
||||
| "lmStudioDraftModelId"
|
||||
| "vsCodeLmModelSelector"
|
||||
>
|
||||
|
||||
interface ModelPickerProps {
|
||||
|
|
@ -55,6 +59,14 @@ interface ModelPickerProps {
|
|||
errorMessage?: string
|
||||
simplifySettings?: boolean
|
||||
hidePricing?: boolean
|
||||
/** Label for the model picker field - defaults to "Model" */
|
||||
label?: string
|
||||
/** Transform model ID string to the value stored in configuration (for compound types like VSCodeLM selector) */
|
||||
valueTransform?: (modelId: string) => unknown
|
||||
/** Transform stored configuration value back to display string */
|
||||
displayTransform?: (value: unknown) => string
|
||||
/** Callback when model changes - useful for side effects like clearing related fields */
|
||||
onModelChange?: (modelId: string) => void
|
||||
}
|
||||
|
||||
export const ModelPicker = ({
|
||||
|
|
@ -69,6 +81,10 @@ export const ModelPicker = ({
|
|||
errorMessage,
|
||||
simplifySettings,
|
||||
hidePricing,
|
||||
label,
|
||||
valueTransform,
|
||||
displayTransform,
|
||||
onModelChange,
|
||||
}: ModelPickerProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
|
|
@ -81,6 +97,16 @@ export const ModelPicker = ({
|
|||
|
||||
const { id: selectedModelId, info: selectedModelInfo } = useSelectedModel(apiConfiguration)
|
||||
|
||||
// Get the display value for the current selection
|
||||
// If displayTransform is provided, use it to convert the stored value to a display string
|
||||
const displayValue = useMemo(() => {
|
||||
if (displayTransform) {
|
||||
const storedValue = apiConfiguration[modelIdKey]
|
||||
return storedValue ? displayTransform(storedValue) : undefined
|
||||
}
|
||||
return selectedModelId
|
||||
}, [displayTransform, apiConfiguration, modelIdKey, selectedModelId])
|
||||
|
||||
const modelIds = useMemo(() => {
|
||||
const filteredModels = filterModels(models, apiConfiguration.apiProvider, organizationAllowList)
|
||||
|
||||
|
|
@ -113,7 +139,13 @@ export const ModelPicker = ({
|
|||
}
|
||||
|
||||
setOpen(false)
|
||||
setApiConfigurationField(modelIdKey, modelId)
|
||||
|
||||
// Apply value transform if provided (e.g., for VSCodeLM selector)
|
||||
const valueToStore = valueTransform ? valueTransform(modelId) : modelId
|
||||
setApiConfigurationField(modelIdKey, valueToStore as ProviderSettings[ModelIdKey])
|
||||
|
||||
// Call the optional change callback
|
||||
onModelChange?.(modelId)
|
||||
|
||||
// Clear any existing timeout
|
||||
if (selectTimeoutRef.current) {
|
||||
|
|
@ -123,7 +155,7 @@ export const ModelPicker = ({
|
|||
// Delay to ensure the popover is closed before setting the search value.
|
||||
selectTimeoutRef.current = setTimeout(() => setSearchValue(""), 100)
|
||||
},
|
||||
[modelIdKey, setApiConfigurationField],
|
||||
[modelIdKey, setApiConfigurationField, valueTransform, onModelChange],
|
||||
)
|
||||
|
||||
const onOpenChange = useCallback((open: boolean) => {
|
||||
|
|
@ -173,7 +205,7 @@ export const ModelPicker = ({
|
|||
return (
|
||||
<>
|
||||
<div>
|
||||
<label className="block font-medium mb-1">{t("settings:modelPicker.label")}</label>
|
||||
<label className="block font-medium mb-1">{label ?? t("settings:modelPicker.label")}</label>
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
|
@ -182,7 +214,7 @@ export const ModelPicker = ({
|
|||
aria-expanded={open}
|
||||
className="w-full justify-between"
|
||||
data-testid="model-picker-button">
|
||||
<div className="truncate">{selectedModelId ?? t("settings:common.select")}</div>
|
||||
<div className="truncate">{displayValue ?? t("settings:common.select")}</div>
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
|
@ -227,7 +259,7 @@ export const ModelPicker = ({
|
|||
<Check
|
||||
className={cn(
|
||||
"size-4 p-0.5 ml-auto",
|
||||
model === selectedModelId ? "opacity-100" : "opacity-0",
|
||||
model === displayValue ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,17 @@ vi.mock("@src/components/ui", () => ({
|
|||
CollapsibleContent: ({ children }: any) => <div>{children}</div>,
|
||||
Slider: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
||||
// Add Popover components for ModelPicker
|
||||
Popover: ({ children }: any) => <div>{children}</div>,
|
||||
PopoverTrigger: ({ children }: any) => <div>{children}</div>,
|
||||
PopoverContent: ({ children }: any) => <div>{children}</div>,
|
||||
// Add Command components for ModelPicker
|
||||
Command: ({ children }: any) => <div>{children}</div>,
|
||||
CommandInput: ({ ...props }: any) => <input {...props} />,
|
||||
CommandList: ({ children }: any) => <div>{children}</div>,
|
||||
CommandEmpty: ({ children }: any) => <div>{children}</div>,
|
||||
CommandGroup: ({ children }: any) => <div>{children}</div>,
|
||||
CommandItem: ({ children, ...props }: any) => <div {...props}>{children}</div>,
|
||||
}))
|
||||
|
||||
describe("ApiOptions Provider Filtering", () => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useCallback, useState, useMemo, useEffect } from "react"
|
|||
import { useEvent } from "react-use"
|
||||
import { Trans } from "react-i18next"
|
||||
import { Checkbox } from "vscrui"
|
||||
import { VSCodeLink, VSCodeRadio, VSCodeRadioGroup, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import type { ProviderSettings, ExtensionMessage, ModelRecord } from "@roo-code/types"
|
||||
|
||||
|
|
@ -11,11 +11,11 @@ import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
|
|||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
import { ModelPicker } from "../ModelPicker"
|
||||
|
||||
type LMStudioProps = {
|
||||
apiConfiguration: ProviderSettings
|
||||
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
|
||||
simplifySettings?: boolean
|
||||
}
|
||||
|
||||
export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudioProps) => {
|
||||
|
|
@ -57,46 +57,50 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi
|
|||
}, [])
|
||||
|
||||
// Check if the selected model exists in the fetched models
|
||||
const modelNotAvailable = useMemo(() => {
|
||||
const modelNotAvailableError = useMemo(() => {
|
||||
const selectedModel = apiConfiguration?.lmStudioModelId
|
||||
if (!selectedModel) return false
|
||||
if (!selectedModel) return undefined
|
||||
|
||||
// Check if model exists in local LM Studio models
|
||||
if (Object.keys(lmStudioModels).length > 0 && selectedModel in lmStudioModels) {
|
||||
return false // Model is available locally
|
||||
return undefined // Model is available locally
|
||||
}
|
||||
|
||||
// If we have router models data for LM Studio
|
||||
if (routerModels.data?.lmstudio) {
|
||||
const availableModels = Object.keys(routerModels.data.lmstudio)
|
||||
// Show warning if model is not in the list (regardless of how many models there are)
|
||||
return !availableModels.includes(selectedModel)
|
||||
if (!availableModels.includes(selectedModel)) {
|
||||
return t("settings:validation.modelAvailability", { modelId: selectedModel })
|
||||
}
|
||||
}
|
||||
|
||||
// If neither source has loaded yet, don't show warning
|
||||
return false
|
||||
}, [apiConfiguration?.lmStudioModelId, routerModels.data, lmStudioModels])
|
||||
return undefined
|
||||
}, [apiConfiguration?.lmStudioModelId, routerModels.data, lmStudioModels, t])
|
||||
|
||||
// Check if the draft model exists
|
||||
const draftModelNotAvailable = useMemo(() => {
|
||||
const draftModelNotAvailableError = useMemo(() => {
|
||||
const draftModel = apiConfiguration?.lmStudioDraftModelId
|
||||
if (!draftModel) return false
|
||||
if (!draftModel) return undefined
|
||||
|
||||
// Check if model exists in local LM Studio models
|
||||
if (Object.keys(lmStudioModels).length > 0 && draftModel in lmStudioModels) {
|
||||
return false // Model is available locally
|
||||
return undefined // Model is available locally
|
||||
}
|
||||
|
||||
// If we have router models data for LM Studio
|
||||
if (routerModels.data?.lmstudio) {
|
||||
const availableModels = Object.keys(routerModels.data.lmstudio)
|
||||
// Show warning if model is not in the list (regardless of how many models there are)
|
||||
return !availableModels.includes(draftModel)
|
||||
if (!availableModels.includes(draftModel)) {
|
||||
return t("settings:validation.modelAvailability", { modelId: draftModel })
|
||||
}
|
||||
}
|
||||
|
||||
// If neither source has loaded yet, don't show warning
|
||||
return false
|
||||
}, [apiConfiguration?.lmStudioDraftModelId, routerModels.data, lmStudioModels])
|
||||
return undefined
|
||||
}, [apiConfiguration?.lmStudioDraftModelId, routerModels.data, lmStudioModels, t])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -108,38 +112,17 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi
|
|||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.lmStudio.baseUrl")}</label>
|
||||
</VSCodeTextField>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.lmStudioModelId || ""}
|
||||
onInput={handleInputChange("lmStudioModelId")}
|
||||
placeholder={t("settings:placeholders.modelId.lmStudio")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.lmStudio.modelId")}</label>
|
||||
</VSCodeTextField>
|
||||
{modelNotAvailable && (
|
||||
<div className="flex flex-col gap-2 text-vscode-errorForeground text-sm">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div className="codicon codicon-close" />
|
||||
<div>
|
||||
{t("settings:validation.modelAvailability", { modelId: apiConfiguration?.lmStudioModelId })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{Object.keys(lmStudioModels).length > 0 && (
|
||||
<VSCodeRadioGroup
|
||||
value={
|
||||
(apiConfiguration?.lmStudioModelId || "") in lmStudioModels
|
||||
? apiConfiguration?.lmStudioModelId
|
||||
: ""
|
||||
}
|
||||
onChange={handleInputChange("lmStudioModelId")}>
|
||||
{Object.keys(lmStudioModels).map((model) => (
|
||||
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.lmStudioModelId === model}>
|
||||
{model}
|
||||
</VSCodeRadio>
|
||||
))}
|
||||
</VSCodeRadioGroup>
|
||||
)}
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId=""
|
||||
models={lmStudioModels}
|
||||
modelIdKey="lmStudioModelId"
|
||||
serviceName="LM Studio"
|
||||
serviceUrl="https://lmstudio.ai/docs"
|
||||
errorMessage={modelNotAvailableError}
|
||||
hidePricing
|
||||
/>
|
||||
<Checkbox
|
||||
checked={apiConfiguration?.lmStudioSpeculativeDecodingEnabled === true}
|
||||
onChange={(checked) => {
|
||||
|
|
@ -149,61 +132,21 @@ export const LMStudio = ({ apiConfiguration, setApiConfigurationField }: LMStudi
|
|||
</Checkbox>
|
||||
{apiConfiguration?.lmStudioSpeculativeDecodingEnabled && (
|
||||
<>
|
||||
<div>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.lmStudioDraftModelId || ""}
|
||||
onInput={handleInputChange("lmStudioDraftModelId")}
|
||||
placeholder={t("settings:placeholders.modelId.lmStudioDraft")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">
|
||||
{t("settings:providers.lmStudio.draftModelId")}
|
||||
</label>
|
||||
</VSCodeTextField>
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.lmStudio.draftModelDesc")}
|
||||
</div>
|
||||
{draftModelNotAvailable && (
|
||||
<div className="flex flex-col gap-2 text-vscode-errorForeground text-sm mt-2">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div className="codicon codicon-close" />
|
||||
<div>
|
||||
{t("settings:validation.modelAvailability", {
|
||||
modelId: apiConfiguration?.lmStudioDraftModelId,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId=""
|
||||
models={lmStudioModels}
|
||||
modelIdKey="lmStudioDraftModelId"
|
||||
serviceName="LM Studio"
|
||||
serviceUrl="https://lmstudio.ai/docs"
|
||||
label={t("settings:providers.lmStudio.draftModelId")}
|
||||
errorMessage={draftModelNotAvailableError}
|
||||
hidePricing
|
||||
/>
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.lmStudio.draftModelDesc")}
|
||||
</div>
|
||||
{Object.keys(lmStudioModels).length > 0 && (
|
||||
<>
|
||||
<div className="font-medium">{t("settings:providers.lmStudio.selectDraftModel")}</div>
|
||||
<VSCodeRadioGroup
|
||||
value={
|
||||
(apiConfiguration?.lmStudioDraftModelId || "") in lmStudioModels
|
||||
? apiConfiguration?.lmStudioDraftModelId
|
||||
: ""
|
||||
}
|
||||
onChange={handleInputChange("lmStudioDraftModelId")}>
|
||||
{Object.keys(lmStudioModels).map((model) => (
|
||||
<VSCodeRadio key={`draft-${model}`} value={model}>
|
||||
{model}
|
||||
</VSCodeRadio>
|
||||
))}
|
||||
</VSCodeRadioGroup>
|
||||
{Object.keys(lmStudioModels).length === 0 && (
|
||||
<div
|
||||
className="text-sm rounded-xs p-2"
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-inputValidation-infoBackground)",
|
||||
border: "1px solid var(--vscode-inputValidation-infoBorder)",
|
||||
color: "var(--vscode-inputValidation-infoForeground)",
|
||||
}}>
|
||||
{t("settings:providers.lmStudio.noModelsFound")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useState, useCallback, useMemo, useEffect } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
|
||||
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import type { ProviderSettings, ExtensionMessage, ModelRecord } from "@roo-code/types"
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ import { useRouterModels } from "@src/components/ui/hooks/useRouterModels"
|
|||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
import { ModelPicker } from "../ModelPicker"
|
||||
|
||||
type OllamaProps = {
|
||||
apiConfiguration: ProviderSettings
|
||||
|
|
@ -54,25 +55,27 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
|
|||
}, [])
|
||||
|
||||
// Check if the selected model exists in the fetched models
|
||||
const modelNotAvailable = useMemo(() => {
|
||||
const modelNotAvailableError = useMemo(() => {
|
||||
const selectedModel = apiConfiguration?.ollamaModelId
|
||||
if (!selectedModel) return false
|
||||
if (!selectedModel) return undefined
|
||||
|
||||
// Check if model exists in local ollama models
|
||||
if (Object.keys(ollamaModels).length > 0 && selectedModel in ollamaModels) {
|
||||
return false // Model is available locally
|
||||
return undefined // Model is available locally
|
||||
}
|
||||
|
||||
// If we have router models data for Ollama
|
||||
if (routerModels.data?.ollama) {
|
||||
const availableModels = Object.keys(routerModels.data.ollama)
|
||||
// Show warning if model is not in the list (regardless of how many models there are)
|
||||
return !availableModels.includes(selectedModel)
|
||||
if (!availableModels.includes(selectedModel)) {
|
||||
return t("settings:validation.modelAvailability", { modelId: selectedModel })
|
||||
}
|
||||
}
|
||||
|
||||
// If neither source has loaded yet, don't show warning
|
||||
return false
|
||||
}, [apiConfiguration?.ollamaModelId, routerModels.data, ollamaModels])
|
||||
return undefined
|
||||
}, [apiConfiguration?.ollamaModelId, routerModels.data, ollamaModels, t])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -97,40 +100,21 @@ export const Ollama = ({ apiConfiguration, setApiConfigurationField }: OllamaPro
|
|||
</div>
|
||||
</VSCodeTextField>
|
||||
)}
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.ollamaModelId || ""}
|
||||
onInput={handleInputChange("ollamaModelId")}
|
||||
placeholder={t("settings:placeholders.modelId.ollama")}
|
||||
className="w-full">
|
||||
<label className="block font-medium mb-1">{t("settings:providers.ollama.modelId")}</label>
|
||||
</VSCodeTextField>
|
||||
{modelNotAvailable && (
|
||||
<div className="flex flex-col gap-2 text-vscode-errorForeground text-sm">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div className="codicon codicon-close" />
|
||||
<div>
|
||||
{t("settings:validation.modelAvailability", { modelId: apiConfiguration?.ollamaModelId })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{Object.keys(ollamaModels).length > 0 && (
|
||||
<VSCodeRadioGroup
|
||||
value={
|
||||
(apiConfiguration?.ollamaModelId || "") in ollamaModels ? apiConfiguration?.ollamaModelId : ""
|
||||
}
|
||||
onChange={handleInputChange("ollamaModelId")}>
|
||||
{Object.keys(ollamaModels).map((model) => (
|
||||
<VSCodeRadio key={model} value={model} checked={apiConfiguration?.ollamaModelId === model}>
|
||||
{model}
|
||||
</VSCodeRadio>
|
||||
))}
|
||||
</VSCodeRadioGroup>
|
||||
)}
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId=""
|
||||
models={ollamaModels}
|
||||
modelIdKey="ollamaModelId"
|
||||
serviceName="Ollama"
|
||||
serviceUrl="https://ollama.ai"
|
||||
errorMessage={modelNotAvailableError}
|
||||
hidePricing
|
||||
/>
|
||||
<VSCodeTextField
|
||||
value={apiConfiguration?.ollamaNumCtx?.toString() || ""}
|
||||
onInput={(e: any) => {
|
||||
const value = e.target?.value
|
||||
onInput={(e) => {
|
||||
const value = (e.target as HTMLInputElement)?.value
|
||||
if (value === "") {
|
||||
setApiConfigurationField("ollamaNumCtx", undefined)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import { useState, useCallback } from "react"
|
||||
import { useState, useCallback, useMemo } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { LanguageModelChatSelector } from "vscode"
|
||||
|
||||
import type { ProviderSettings, ExtensionMessage } from "@roo-code/types"
|
||||
import type { ProviderSettings, ExtensionMessage, ModelInfo } from "@roo-code/types"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
|
||||
|
||||
import { inputEventTransform } from "../transforms"
|
||||
import { ModelPicker } from "../ModelPicker"
|
||||
|
||||
type VSCodeLMProps = {
|
||||
apiConfiguration: ProviderSettings
|
||||
|
|
@ -19,17 +18,6 @@ export const VSCodeLM = ({ apiConfiguration, setApiConfigurationField }: VSCodeL
|
|||
|
||||
const [vsCodeLmModels, setVsCodeLmModels] = useState<LanguageModelChatSelector[]>([])
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
<K extends keyof ProviderSettings, E>(
|
||||
field: K,
|
||||
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
|
||||
) =>
|
||||
(event: E | Event) => {
|
||||
setApiConfigurationField(field, transform(event as E))
|
||||
},
|
||||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
const onMessage = useCallback((event: MessageEvent) => {
|
||||
const message: ExtensionMessage = event.data
|
||||
|
||||
|
|
@ -45,40 +33,59 @@ export const VSCodeLM = ({ apiConfiguration, setApiConfigurationField }: VSCodeL
|
|||
|
||||
useEvent("message", onMessage)
|
||||
|
||||
// Convert VSCode LM models array to Record format for ModelPicker
|
||||
const modelsRecord = useMemo((): Record<string, ModelInfo> => {
|
||||
return vsCodeLmModels.reduce(
|
||||
(acc, model) => {
|
||||
const modelId = `${model.vendor}/${model.family}`
|
||||
acc[modelId] = {
|
||||
maxTokens: 0,
|
||||
contextWindow: 0,
|
||||
supportsPromptCache: false,
|
||||
description: `${model.vendor} - ${model.family}`,
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, ModelInfo>,
|
||||
)
|
||||
}, [vsCodeLmModels])
|
||||
|
||||
// Transform string model ID to { vendor, family } object for storage
|
||||
const valueTransform = useCallback((modelId: string) => {
|
||||
const [vendor, family] = modelId.split("/")
|
||||
return { vendor, family }
|
||||
}, [])
|
||||
|
||||
// Transform stored { vendor, family } object back to display string
|
||||
const displayTransform = useCallback((value: unknown) => {
|
||||
if (!value) return ""
|
||||
const selector = value as { vendor?: string; family?: string }
|
||||
return selector.vendor && selector.family ? `${selector.vendor}/${selector.family}` : ""
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label className="block font-medium mb-1">{t("settings:providers.vscodeLmModel")}</label>
|
||||
{vsCodeLmModels.length > 0 ? (
|
||||
<Select
|
||||
value={
|
||||
apiConfiguration?.vsCodeLmModelSelector
|
||||
? `${apiConfiguration.vsCodeLmModelSelector.vendor ?? ""}/${apiConfiguration.vsCodeLmModelSelector.family ?? ""}`
|
||||
: ""
|
||||
}
|
||||
onValueChange={handleInputChange("vsCodeLmModelSelector", (value) => {
|
||||
const [vendor, family] = value.split("/")
|
||||
return { vendor, family }
|
||||
})}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t("settings:common.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vsCodeLmModels.map((model) => (
|
||||
<SelectItem
|
||||
key={`${model.vendor}/${model.family}`}
|
||||
value={`${model.vendor}/${model.family}`}>
|
||||
{`${model.vendor} - ${model.family}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
{vsCodeLmModels.length > 0 ? (
|
||||
<ModelPicker
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
defaultModelId=""
|
||||
models={modelsRecord}
|
||||
modelIdKey="vsCodeLmModelSelector"
|
||||
serviceName="VS Code LM"
|
||||
serviceUrl="https://code.visualstudio.com/api/extension-guides/language-model"
|
||||
valueTransform={valueTransform}
|
||||
displayTransform={displayTransform}
|
||||
hidePricing
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<label className="block font-medium mb-1">{t("settings:providers.vscodeLmModel")}</label>
|
||||
<div className="text-sm text-vscode-descriptionForeground">
|
||||
{t("settings:providers.vscodeLmDescription")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-vscode-errorForeground">{t("settings:providers.vscodeLmWarning")}</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,200 @@
|
|||
import {
|
||||
PROVIDER_SERVICE_CONFIG,
|
||||
PROVIDER_DEFAULT_MODEL_IDS,
|
||||
getProviderServiceConfig,
|
||||
getDefaultModelIdForProvider,
|
||||
getStaticModelsForProvider,
|
||||
isStaticModelProvider,
|
||||
PROVIDERS_WITH_CUSTOM_MODEL_UI,
|
||||
shouldUseGenericModelPicker,
|
||||
} from "../providerModelConfig"
|
||||
|
||||
describe("providerModelConfig", () => {
|
||||
describe("PROVIDER_SERVICE_CONFIG", () => {
|
||||
it("contains service config for anthropic", () => {
|
||||
expect(PROVIDER_SERVICE_CONFIG.anthropic).toEqual({
|
||||
serviceName: "Anthropic",
|
||||
serviceUrl: "https://console.anthropic.com",
|
||||
})
|
||||
})
|
||||
|
||||
it("contains service config for bedrock", () => {
|
||||
expect(PROVIDER_SERVICE_CONFIG.bedrock).toEqual({
|
||||
serviceName: "Amazon Bedrock",
|
||||
serviceUrl: "https://aws.amazon.com/bedrock",
|
||||
})
|
||||
})
|
||||
|
||||
it("contains service config for ollama", () => {
|
||||
expect(PROVIDER_SERVICE_CONFIG.ollama).toEqual({
|
||||
serviceName: "Ollama",
|
||||
serviceUrl: "https://ollama.ai",
|
||||
})
|
||||
})
|
||||
|
||||
it("contains service config for lmstudio", () => {
|
||||
expect(PROVIDER_SERVICE_CONFIG.lmstudio).toEqual({
|
||||
serviceName: "LM Studio",
|
||||
serviceUrl: "https://lmstudio.ai/docs",
|
||||
})
|
||||
})
|
||||
|
||||
it("contains service config for vscode-lm", () => {
|
||||
expect(PROVIDER_SERVICE_CONFIG["vscode-lm"]).toEqual({
|
||||
serviceName: "VS Code LM",
|
||||
serviceUrl: "https://code.visualstudio.com/api/extension-guides/language-model",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getProviderServiceConfig", () => {
|
||||
it("returns correct config for known provider", () => {
|
||||
const config = getProviderServiceConfig("gemini")
|
||||
expect(config.serviceName).toBe("Google Gemini")
|
||||
expect(config.serviceUrl).toBe("https://ai.google.dev")
|
||||
})
|
||||
|
||||
it("returns fallback config for unknown provider", () => {
|
||||
const config = getProviderServiceConfig("unknown-provider" as any)
|
||||
expect(config.serviceName).toBe("unknown-provider")
|
||||
expect(config.serviceUrl).toBe("")
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getDefaultModelIdForProvider", () => {
|
||||
it("returns default model ID for known provider", () => {
|
||||
const defaultId = getDefaultModelIdForProvider("anthropic")
|
||||
expect(defaultId).toBeDefined()
|
||||
expect(typeof defaultId).toBe("string")
|
||||
expect(defaultId.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("returns empty string for unknown provider", () => {
|
||||
const defaultId = getDefaultModelIdForProvider("unknown" as any)
|
||||
expect(defaultId).toBe("")
|
||||
})
|
||||
|
||||
it("returns international default for Z.ai without apiConfiguration", () => {
|
||||
const defaultId = getDefaultModelIdForProvider("zai")
|
||||
expect(defaultId).toBeDefined()
|
||||
expect(typeof defaultId).toBe("string")
|
||||
expect(defaultId.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("returns mainland default for Z.ai with china_coding entrypoint", () => {
|
||||
const defaultId = getDefaultModelIdForProvider("zai", {
|
||||
apiProvider: "zai",
|
||||
zaiApiLine: "china_coding",
|
||||
})
|
||||
expect(defaultId).toBeDefined()
|
||||
expect(typeof defaultId).toBe("string")
|
||||
// Mainland model IDs should contain 'mainland' or be different from international
|
||||
expect(defaultId.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("returns international default for Z.ai with international_coding entrypoint", () => {
|
||||
const defaultId = getDefaultModelIdForProvider("zai", {
|
||||
apiProvider: "zai",
|
||||
zaiApiLine: "international_coding",
|
||||
})
|
||||
expect(defaultId).toBeDefined()
|
||||
expect(typeof defaultId).toBe("string")
|
||||
expect(defaultId.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("uses mainland or international defaults based on zaiApiLine setting", () => {
|
||||
// Verify the function correctly routes to appropriate defaults
|
||||
const chinaDefault = getDefaultModelIdForProvider("zai", {
|
||||
apiProvider: "zai",
|
||||
zaiApiLine: "china_coding",
|
||||
})
|
||||
const internationalDefault = getDefaultModelIdForProvider("zai", {
|
||||
apiProvider: "zai",
|
||||
zaiApiLine: "international_coding",
|
||||
})
|
||||
// Both should return valid model IDs (they may or may not be the same)
|
||||
expect(chinaDefault).toBeDefined()
|
||||
expect(internationalDefault).toBeDefined()
|
||||
expect(chinaDefault.length).toBeGreaterThan(0)
|
||||
expect(internationalDefault.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getStaticModelsForProvider", () => {
|
||||
it("returns models for anthropic provider", () => {
|
||||
const models = getStaticModelsForProvider("anthropic")
|
||||
expect(Object.keys(models).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it("adds custom-arn option for bedrock provider", () => {
|
||||
const models = getStaticModelsForProvider("bedrock", "Use Custom ARN")
|
||||
expect(models["custom-arn"]).toBeDefined()
|
||||
expect(models["custom-arn"].description).toBe("Use Custom ARN")
|
||||
})
|
||||
|
||||
it("returns empty object for providers without static models", () => {
|
||||
const models = getStaticModelsForProvider("openrouter")
|
||||
expect(Object.keys(models).length).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isStaticModelProvider", () => {
|
||||
it("returns true for providers with static models", () => {
|
||||
expect(isStaticModelProvider("anthropic")).toBe(true)
|
||||
expect(isStaticModelProvider("bedrock")).toBe(true)
|
||||
expect(isStaticModelProvider("gemini")).toBe(true)
|
||||
expect(isStaticModelProvider("openai-native")).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false for providers without static models", () => {
|
||||
expect(isStaticModelProvider("openrouter")).toBe(false)
|
||||
expect(isStaticModelProvider("ollama")).toBe(false)
|
||||
expect(isStaticModelProvider("lmstudio")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
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("claude-code")
|
||||
})
|
||||
|
||||
it("does not include static providers using generic picker", () => {
|
||||
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).not.toContain("anthropic")
|
||||
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).not.toContain("gemini")
|
||||
expect(PROVIDERS_WITH_CUSTOM_MODEL_UI).not.toContain("bedrock")
|
||||
})
|
||||
})
|
||||
|
||||
describe("shouldUseGenericModelPicker", () => {
|
||||
it("returns true for static providers without custom UI", () => {
|
||||
expect(shouldUseGenericModelPicker("anthropic")).toBe(true)
|
||||
expect(shouldUseGenericModelPicker("bedrock")).toBe(true)
|
||||
expect(shouldUseGenericModelPicker("gemini")).toBe(true)
|
||||
expect(shouldUseGenericModelPicker("deepseek")).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false for providers with custom model UI", () => {
|
||||
expect(shouldUseGenericModelPicker("openrouter")).toBe(false)
|
||||
expect(shouldUseGenericModelPicker("ollama")).toBe(false)
|
||||
expect(shouldUseGenericModelPicker("lmstudio")).toBe(false)
|
||||
expect(shouldUseGenericModelPicker("vscode-lm")).toBe(false)
|
||||
})
|
||||
|
||||
it("returns false for providers without static models", () => {
|
||||
expect(shouldUseGenericModelPicker("openai")).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
173
webview-ui/src/components/settings/utils/providerModelConfig.ts
Normal file
173
webview-ui/src/components/settings/utils/providerModelConfig.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import type { ProviderName, ModelInfo, ProviderSettings } from "@roo-code/types"
|
||||
import {
|
||||
anthropicDefaultModelId,
|
||||
bedrockDefaultModelId,
|
||||
cerebrasDefaultModelId,
|
||||
deepSeekDefaultModelId,
|
||||
doubaoDefaultModelId,
|
||||
moonshotDefaultModelId,
|
||||
geminiDefaultModelId,
|
||||
mistralDefaultModelId,
|
||||
openAiNativeDefaultModelId,
|
||||
qwenCodeDefaultModelId,
|
||||
vertexDefaultModelId,
|
||||
xaiDefaultModelId,
|
||||
groqDefaultModelId,
|
||||
sambaNovaDefaultModelId,
|
||||
internationalZAiDefaultModelId,
|
||||
mainlandZAiDefaultModelId,
|
||||
fireworksDefaultModelId,
|
||||
featherlessDefaultModelId,
|
||||
minimaxDefaultModelId,
|
||||
basetenDefaultModelId,
|
||||
} from "@roo-code/types"
|
||||
|
||||
import { MODELS_BY_PROVIDER } from "../constants"
|
||||
|
||||
export interface ProviderServiceConfig {
|
||||
serviceName: string
|
||||
serviceUrl: string
|
||||
}
|
||||
|
||||
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" },
|
||||
cerebras: { serviceName: "Cerebras", serviceUrl: "https://cerebras.ai" },
|
||||
deepseek: { serviceName: "DeepSeek", serviceUrl: "https://platform.deepseek.com" },
|
||||
doubao: { serviceName: "Doubao", serviceUrl: "https://www.volcengine.com/product/doubao" },
|
||||
moonshot: { serviceName: "Moonshot", serviceUrl: "https://platform.moonshot.cn" },
|
||||
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" },
|
||||
groq: { serviceName: "Groq", serviceUrl: "https://console.groq.com" },
|
||||
sambanova: { serviceName: "SambaNova", serviceUrl: "https://sambanova.ai" },
|
||||
zai: { serviceName: "Z.ai", serviceUrl: "https://z.ai" },
|
||||
fireworks: { serviceName: "Fireworks AI", serviceUrl: "https://fireworks.ai" },
|
||||
featherless: { serviceName: "Featherless AI", serviceUrl: "https://featherless.ai" },
|
||||
minimax: { serviceName: "MiniMax", serviceUrl: "https://minimax.chat" },
|
||||
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": {
|
||||
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,
|
||||
cerebras: cerebrasDefaultModelId,
|
||||
deepseek: deepSeekDefaultModelId,
|
||||
doubao: doubaoDefaultModelId,
|
||||
moonshot: moonshotDefaultModelId,
|
||||
gemini: geminiDefaultModelId,
|
||||
mistral: mistralDefaultModelId,
|
||||
"openai-native": openAiNativeDefaultModelId,
|
||||
"qwen-code": qwenCodeDefaultModelId,
|
||||
vertex: vertexDefaultModelId,
|
||||
xai: xaiDefaultModelId,
|
||||
groq: groqDefaultModelId,
|
||||
sambanova: sambaNovaDefaultModelId,
|
||||
zai: internationalZAiDefaultModelId,
|
||||
fireworks: fireworksDefaultModelId,
|
||||
featherless: featherlessDefaultModelId,
|
||||
minimax: minimaxDefaultModelId,
|
||||
baseten: basetenDefaultModelId,
|
||||
}
|
||||
|
||||
export const getProviderServiceConfig = (provider: ProviderName): ProviderServiceConfig => {
|
||||
return PROVIDER_SERVICE_CONFIG[provider] ?? { serviceName: provider, serviceUrl: "" }
|
||||
}
|
||||
|
||||
export const getDefaultModelIdForProvider = (provider: ProviderName, apiConfiguration?: ProviderSettings): string => {
|
||||
// Handle Z.ai's China/International entrypoint distinction
|
||||
if (provider === "zai" && apiConfiguration) {
|
||||
return apiConfiguration.zaiApiLine === "china_coding"
|
||||
? mainlandZAiDefaultModelId
|
||||
: internationalZAiDefaultModelId
|
||||
}
|
||||
|
||||
return PROVIDER_DEFAULT_MODEL_IDS[provider] ?? ""
|
||||
}
|
||||
|
||||
export const getStaticModelsForProvider = (
|
||||
provider: ProviderName,
|
||||
customArnLabel?: string,
|
||||
): Record<string, ModelInfo> => {
|
||||
const models = MODELS_BY_PROVIDER[provider] ?? {}
|
||||
|
||||
// Add custom-arn option for Bedrock
|
||||
if (provider === "bedrock") {
|
||||
return {
|
||||
...models,
|
||||
"custom-arn": {
|
||||
maxTokens: 0,
|
||||
contextWindow: 0,
|
||||
supportsPromptCache: false,
|
||||
description: customArnLabel ?? "Use Custom ARN",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return models
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a provider uses static models from MODELS_BY_PROVIDER
|
||||
*/
|
||||
export const isStaticModelProvider = (provider: ProviderName): boolean => {
|
||||
return provider in MODELS_BY_PROVIDER
|
||||
}
|
||||
|
||||
/**
|
||||
* List of providers that have their own custom model selection UI
|
||||
* and should not use the generic ModelPicker in ApiOptions
|
||||
*/
|
||||
export const PROVIDERS_WITH_CUSTOM_MODEL_UI: ProviderName[] = [
|
||||
"openrouter",
|
||||
"requesty",
|
||||
"unbound",
|
||||
"deepinfra",
|
||||
"claude-code",
|
||||
"openai", // OpenAI Compatible
|
||||
"litellm",
|
||||
"io-intelligence",
|
||||
"vercel-ai-gateway",
|
||||
"roo",
|
||||
"chutes",
|
||||
"ollama",
|
||||
"lmstudio",
|
||||
"vscode-lm",
|
||||
"huggingface",
|
||||
]
|
||||
|
||||
/**
|
||||
* Checks if a provider should use the generic ModelPicker
|
||||
*/
|
||||
export const shouldUseGenericModelPicker = (provider: ProviderName): boolean => {
|
||||
return isStaticModelProvider(provider) && !PROVIDERS_WITH_CUSTOM_MODEL_UI.includes(provider)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles provider-specific side effects when a model is changed.
|
||||
* Centralizes provider-specific logic to keep it out of the ApiOptions template.
|
||||
*/
|
||||
export const handleModelChangeSideEffects = <K extends keyof ProviderSettings>(
|
||||
provider: ProviderName,
|
||||
modelId: string,
|
||||
setApiConfigurationField: (field: K, value: ProviderSettings[K]) => void,
|
||||
): void => {
|
||||
// Bedrock: Clear custom ARN if not using custom ARN option
|
||||
if (provider === "bedrock" && modelId !== "custom-arn") {
|
||||
setApiConfigurationField("awsCustomArn" as K, "" as ProviderSettings[K])
|
||||
}
|
||||
|
||||
// All providers: Clear reasoning effort when switching models to allow
|
||||
// the new model's default to take effect. Different models within the
|
||||
// same provider can have different reasoning effort defaults/options.
|
||||
setApiConfigurationField("reasoningEffort" as K, undefined as ProviderSettings[K])
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue