/* 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 (
{maybeProviderInfo.description}
For more information, see the{" "} provider details .
{model.model}
{model.description}
{model.roles.length > 0 && (Capabilities: