/** * Provider Model Selector Component * Per-provider model selection using model-catalog.ts * Includes tier badges, broken/deprecated status indicators */ import { useMemo } from 'react'; import { AlertCircle, AlertTriangle } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Badge } from '@/components/ui/badge'; import { SearchableSelect } from '@/components/ui/searchable-select'; import { Skeleton } from '@/components/ui/skeleton'; import type { CliproxyProviderRoutingHints } from '@/lib/api-client'; import { getCodexEffortDisplay } from '@/lib/codex-effort'; import { getResolvedCatalogModels } from '@/lib/model-catalogs'; import { cn } from '@/lib/utils'; /** Model entry from catalog */ export interface ModelEntry { id: string; name: string; tier?: 'free' | 'paid'; description?: string; broken?: boolean; issueUrl?: string; deprecated?: boolean; deprecationReason?: string; /** Whether model supports 1M extended context window */ extendedContext?: boolean; /** Optional preset mapping for different tiers (if different from id) */ presetMapping?: { default: string; opus: string; sonnet: string; haiku: string; }; } /** Provider catalog */ export interface ProviderCatalog { provider: string; displayName: string; models: ModelEntry[]; defaultModel: string; } interface ProviderModelSelectorProps { /** Provider catalog data */ catalog: ProviderCatalog | undefined; /** Loading state */ isLoading?: boolean; /** Currently selected model */ value: string | undefined; /** Callback when model changes */ onChange: (model: string) => void; /** Disabled state */ disabled?: boolean; /** Placeholder text */ placeholder?: string; /** Custom className */ className?: string; } function PaidBadge({ label }: { label: string }) { return ( {label} ); } function StatusBadges({ model, brokenLabel, deprecatedLabel, }: { model: Pick; brokenLabel: string; deprecatedLabel: string; }) { return ( <> {model.broken && ( {brokenLabel} )} {model.deprecated && ( {deprecatedLabel} )} ); } function CodexEffortBadge({ modelId }: { modelId: string | undefined }) { const codexEffort = getCodexEffortDisplay(modelId); if (!codexEffort) return null; return ( {codexEffort.label} ); } export function ProviderModelSelector({ catalog, isLoading, value, onChange, disabled, placeholder, className, }: ProviderModelSelectorProps) { const { t } = useTranslation(); const resolvedPlaceholder = placeholder ?? t('providerModelSelector.selectModel'); const groupedModels = useMemo(() => { if (!catalog?.models) return { free: [], paid: [] }; return { free: catalog.models.filter((model) => !model.tier || model.tier === 'free'), paid: catalog.models.filter((model) => model.tier === 'paid'), }; }, [catalog]); const selectedModel = useMemo( () => catalog?.models.find((model) => model.id === value), [catalog, value] ); if (isLoading) { return ; } if (!catalog || catalog.models.length === 0) { return (
{t('providerModelSelector.noModelsForProvider')}
); } return (
{t('providerModelSelector.freeTier')} ), }, { key: 'paid', label: ( {t('providerModelSelector.paidTier')} ), }, ]} options={[...groupedModels.free, ...groupedModels.paid].map((model) => ({ value: model.id, groupKey: model.tier === 'paid' ? 'paid' : 'free', searchText: `${model.name} ${model.id}`, keywords: [model.tier ?? 'free'], triggerContent: (
{model.name} {model.tier === 'paid' && }
), itemContent: (
{model.name}
), }))} /> {selectedModel?.broken && (

{t('providerModelSelector.modelKnownIssues')}

{selectedModel.issueUrl && ( {t('providerModelSelector.viewIssueDetails')} )}
)} {selectedModel?.deprecated && (

{t('providerModelSelector.modelDeprecated')}

{selectedModel.deprecationReason && (

{selectedModel.deprecationReason}

)}
)} {selectedModel?.description && !selectedModel.broken && !selectedModel.deprecated && (

{selectedModel.description}

)}
); } /** Model Mapping Selector - For Opus/Sonnet/Haiku mapping */ interface ModelMappingSelectorProps { catalog: ProviderCatalog | undefined; label: string; value: string | undefined; onChange: (model: string) => void; disabled?: boolean; } export function ModelMappingSelector({ catalog, label, value, onChange, disabled, }: ModelMappingSelectorProps) { const { t } = useTranslation(); if (!catalog) return null; return (
({ value: model.id, searchText: `${model.name} ${model.id}`, triggerContent: {model.id}, itemContent: (
{model.name} {model.tier === 'paid' && }
), }))} />
); } /** Flexible Model Selector - Combines catalog recommendations with full model list */ interface FlexibleModelSelectorProps { label: string; description?: string; value: string | undefined; onChange: (model: string) => void; catalog?: ProviderCatalog; allModels: { id: string; owned_by: string }[]; routing?: CliproxyProviderRoutingHints; disabled?: boolean; } function normalizeModelValue( value: string | undefined, routing?: CliproxyProviderRoutingHints ): string { if (!value) return ''; if (!routing?.prefix) return value; const prefix = `${routing.prefix}/`; return value.startsWith(prefix) ? value.slice(prefix.length) : value; } function getPreferredOptionValue( modelId: string, routingHint: CliproxyProviderRoutingHints['models'][number] | undefined ): string { if (!routingHint?.pinnedAvailable) { return modelId; } return routingHint.recommendedModelId; } export function FlexibleModelSelector({ label, description, value, onChange, catalog, allModels, routing, disabled, }: FlexibleModelSelectorProps) { const { t } = useTranslation(); const isCodexProvider = catalog?.provider === 'codex'; const resolvedCatalogModels = useMemo( () => getResolvedCatalogModels(catalog, allModels), [allModels, catalog] ); const catalogModelIds = new Set(resolvedCatalogModels.map((model) => model.id)); const routingHints = useMemo( () => new Map((routing?.models ?? []).map((hint) => [hint.modelId.toLowerCase(), hint] as const)), [routing] ); const selectedRoutingHint = useMemo( () => routingHints.get(normalizeModelValue(value, routing).toLowerCase()), [routing, routingHints, value] ); const recommendedOptions = resolvedCatalogModels.map((model) => ({ value: getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase())), groupKey: 'recommended', searchText: `${model.id} ${model.name} ${routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? ''}`, keywords: [model.tier ?? '', catalog?.provider ?? ''], triggerContent: (
{getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))} {routingHints.get(model.id.toLowerCase())?.pinnedAvailable ? ( {routingHints.get(model.id.toLowerCase())?.prefix} ) : null} {isCodexProvider && }
), itemContent: (
{getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))} {model.tier === 'paid' && } {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? ( Shadowed ) : null} {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'prefix-only' ? ( Prefix only ) : null} {isCodexProvider && }
), })); const allModelOptions = allModels .filter((model) => !catalogModelIds.has(model.id)) .map((model) => ({ value: getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase())), groupKey: 'all', searchText: `${model.id} ${routingHints.get(model.id.toLowerCase())?.recommendedModelId ?? ''}`, keywords: [model.owned_by], triggerContent: (
{getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))} {routingHints.get(model.id.toLowerCase())?.pinnedAvailable ? ( {routingHints.get(model.id.toLowerCase())?.prefix} ) : null} {isCodexProvider && }
), itemContent: (
{getPreferredOptionValue(model.id, routingHints.get(model.id.toLowerCase()))} {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'shadowed' ? ( Shadowed ) : null} {routingHints.get(model.id.toLowerCase())?.unprefixedStatus === 'prefix-only' ? ( Prefix only ) : null} {isCodexProvider && }
), })); const selectedValueMissing = Boolean(value) && !recommendedOptions.some((option) => option.value === value) && !allModelOptions.some((option) => option.value === value); const legacySelectedOption = value ? { value, groupKey: 'current', searchText: value, triggerContent: (
{value} Current
), itemContent: (
{value} Current
), } : null; const hasAvailableModels = recommendedOptions.length + allModelOptions.length > 0; return (
{description &&

{description}

}
Current value, }, ] : []), { key: 'recommended', label: ( {t('providerModelSelector.recommended')} ), }, { key: 'all', label: ( {t('providerModelSelector.allModelsCount', { count: allModels.length })} ), }, ]} options={[ ...(selectedValueMissing && legacySelectedOption ? [legacySelectedOption] : []), ...recommendedOptions, ...allModelOptions, ]} /> {selectedRoutingHint ? (
{selectedRoutingHint.pinnedAvailable ? 'Preferred pinned model:' : 'Pinned route status:'}{' '} {selectedRoutingHint.pinnedAvailable ? selectedRoutingHint.recommendedModelId : selectedRoutingHint.pinnedModelId}

{selectedRoutingHint.summary}

) : null} {value && !selectedRoutingHint && normalizeModelValue(value, routing) !== value ? (
Pinned model is not currently advertised by the proxy: {value}
) : null}
); }