/* Copyright 2026 Marimo. All rights reserved. */ import type { Role } from "@marimo-team/llm-info"; import { useAtomValue } from "jotai"; import { BotIcon, BrainIcon, ChevronDownIcon, CircleHelpIcon, } from "lucide-react"; import React from "react"; import { type SupportedRole, useModelChange } from "@/core/ai/config"; import { AiModelId, isKnownAIProvider, type ProviderId, type QualifiedModelId, } from "@/core/ai/ids/ids"; import { type AiModel, AiModelRegistry } from "@/core/ai/model-registry"; import { aiAtom, completionAtom } from "@/core/config/config"; import { capitalize } from "@/utils/strings"; import { useOpenSettingsToTab } from "../app-config/state"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "../ui/dropdown-menu"; import { Tooltip } from "../ui/tooltip"; import { AiProviderIcon } from "./ai-provider-icon"; import { getCurrentRoleTooltip, getTagColour } from "./display-helpers"; interface AIModelDropdownProps { value?: string; placeholder?: string; onSelect?: (modelId: QualifiedModelId) => void; triggerClassName?: string; customDropdownContent?: React.ReactNode; iconSize?: "medium" | "small"; showAddCustomModelDocs?: boolean; displayIconOnly?: boolean; forRole: SupportedRole; } export const AIModelDropdown = ({ value, placeholder, onSelect, triggerClassName, customDropdownContent, iconSize = "medium", showAddCustomModelDocs = false, forRole, displayIconOnly = false, }: AIModelDropdownProps) => { const [isOpen, setIsOpen] = React.useState(false); const ai = useAtomValue(aiAtom); const completion = useAtomValue(completionAtom); const { saveModelChange } = useModelChange(); const { handleClick } = useOpenSettingsToTab(); // Only include autocompleteModel if copilot is set to "custom" const autocompleteModel = completion.copilot === "custom" ? ai?.models?.autocomplete_model : undefined; const aiModelRegistry = AiModelRegistry.create({ // We add all the custom models and the models used in the editor. // If they among the known models, they won't overwrite them. customModels: [ ...(ai?.models?.custom_models ?? []), ai?.models?.chat_model, autocompleteModel, ai?.models?.edit_model, ].filter(Boolean), displayedModels: ai?.models?.displayed_models, }); const modelsByProvider = aiModelRegistry.getListModelsByProvider(); const activeModel = forRole === "autocomplete" ? ai?.models?.autocomplete_model : forRole === "chat" ? ai?.models?.chat_model : forRole === "edit" ? ai?.models?.edit_model : undefined; // If value is provided, use it, otherwise use the active model const currentValue = value ? AiModelId.parse(value) : activeModel ? AiModelId.parse(activeModel) : undefined; const iconSizeClass = iconSize === "medium" ? "h-4 w-4" : "h-3 w-3"; const renderModelWithRole = (modelId: AiModelId, role: Role) => { const maybeModelMatch = aiModelRegistry.getModel(modelId.id); return (
{maybeModelMatch?.name || modelId.shortModelId} {modelId.id}
{role}
); }; const handleSelect = (modelId: QualifiedModelId) => { if (onSelect) { onSelect(modelId); } else { saveModelChange(modelId, forRole); } setIsOpen(false); }; return (
{currentValue ? ( <> {displayIconOnly ? null : ( {isKnownAIProvider(currentValue.providerId) ? currentValue.shortModelId : currentValue.id} )} ) : ( {placeholder} )}
{activeModel && forRole && renderModelWithRole(AiModelId.parse(activeModel), forRole)} {activeModel && forRole && } {modelsByProvider.map(([provider, models]) => ( ))} {customDropdownContent} {showAddCustomModelDocs && ( <> handleClick("ai", "ai-models")} > Add custom model )}
); }; const ProviderDropdownContent = ({ provider, onSelect, models, iconSizeClass, }: { provider: ProviderId; onSelect: (modelId: QualifiedModelId) => void; models: AiModel[]; iconSizeClass: string; }) => { const iconProvider = isKnownAIProvider(provider) ? provider : "openai-compatible"; const maybeProviderInfo = AiModelRegistry.getProviderInfo(provider); if (models.length === 0) { return null; } return (

{getProviderLabel(provider)}

{maybeProviderInfo && ( <>

{maybeProviderInfo.description}

For more information, see the{" "} provider details .

)} {models.map((model) => { const qualifiedModelId: QualifiedModelId = `${provider}/${model.model}`; return (
{ onSelect(qualifiedModelId); }} >
); })}
); }; const AiModelDropdownItem = ({ model, provider, }: { model: AiModel; provider: ProviderId; }) => { const iconProvider = isKnownAIProvider(provider) ? provider : "openai-compatible"; return ( <>
{model.name}
{model.thinking && ( )}
{model.custom && ( )} ); }; export const AiModelInfoDisplay = ({ model, provider, }: { model: AiModel; provider: ProviderId; }) => { return (

{model.name}

{model.model}

{model.description}

{model.roles.length > 0 && (

Capabilities:

{model.roles.map((role) => ( {role} ))}
)} {model.thinking && (
Supports thinking mode
)}
{getProviderLabel(provider)}
); }; export function getProviderLabel(provider: ProviderId): string { const providerInfo = AiModelRegistry.getProviderInfo(provider); if (providerInfo) { return providerInfo.name; } return capitalize(provider); }