Dosu LogoDosu Logo
Ask
Join our Discord
assistant-ui's SpacePublic
assistant-ui
Documentsassistant-ui's Space
model-selector
model-selector
Type
External
Status
Published
Created
Mar 17, 2026
Updated
Aug 22, 2026
Updated by
Dosu Bot
Source
apps/docs/content/docs/ui/model-selector.mdx

import { ModelSelectorSample } from "@/components/pages/docs/samples/model-selector";
import * as ModelSelectorRadixSamples from "@/components/pages/docs/samples/model-selector.radix";
import { Flavored, FlavorSwitcher } from "@/components/pages/docs/contexts/flavor.server";

A picker that lets users switch between AI models and choose a reasoning effort (thinking) level. It is built on Popover + Command, so search, provider grouping, and filtering compose in without being built in. The default export integrates with assistant-ui's ModelContext system, so the selection reaches your backend on every request with no extra wiring.

<Flavored radix={<ModelSelectorRadixSamples.ModelSelectorSample />} base={} />

Getting Started#

Add model-selector#

<InstallCommand shadcn={["model-selector"]} />

Use in your application#

Place the ModelSelector inside your thread component, typically in the composer area. Each model needs an id and a display name; everything else is optional:

import { ModelSelector } from "@/components/assistant-ui/model-selector";

const ComposerAction: FC = () => {
  return (
    <div className="flex items-center gap-1">
      <ModelSelector
        models={[
          { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", description: "Fast and efficient" },
          { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", description: "Balanced performance" },
          { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", description: "Most capable", efforts: true },
        ]}
        defaultValue="gpt-5.6-luna"
        defaultEffort="medium"
        size="sm"
      />
    </div>
  );
};

Read the selection in your API route#

The selected model's id arrives as config.modelName, and the effort level as config.reasoningEffort:

export async function POST(req: Request) {
  const { messages, config } = await req.json();

  const result = streamText({
    model: openai(config?.modelName ?? "gpt-5.6-luna"),
    providerOptions: {
      openai:
        config?.reasoningEffort !== undefined
          ? { reasoningEffort: config.reasoningEffort }
          : {},
    },
    messages: await convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse();
}

config.reasoningEffort is only present when the selected model supports the chosen level, so the route only forwards it when it exists. See How It Works.

Reasoning Efforts#

A model that declares efforts shows a "Thinking" row at the bottom of the popover. efforts: true enables the default Low / Medium / High levels; pass a list of { id, name } objects to define your own:

{
  id: "gpt-5.6-sol",
  name: "GPT-5.6 Sol",
  efforts: [
    { id: "minimal", name: "Minimal" },
    { id: "high", name: "High" },
  ],
}

Omit efforts for models without configurable reasoning. The row is hidden while such a model is selected.

Sticky Selection#

The effort selection survives model switches. Switching to a model that doesn't support the current level omits reasoningEffort from the request instead of resetting the user's choice, and the level applies again when the user switches back. The exported resolveModelEffort helper applies the same rule if you build your own runtime integration around ModelSelector.Root; see resolveModelEffort.

Custom Effort UI#

ModelSelector.Effort lays the levels out as horizontal segments, which overflows the popover width once a model has more than a few. For those cases, or for a different layout such as a slider or a sub-dropdown, build your own control with the useModelSelectorEfforts hook. It exposes the selected model's levels and the active selection:

import { useModelSelectorEfforts } from "@/components/assistant-ui/model-selector";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

function EffortDropdown() {
  const { efforts, effort, setEffort } = useModelSelectorEfforts();
  if (!efforts?.length) return null;

  return (
    <div className="flex items-center justify-between gap-3 border-t px-3 py-2">
      <span className="text-muted-foreground text-xs">Thinking</span>
      <DropdownMenu>
        <DropdownMenuTrigger className="text-xs">
          {efforts.find((e) => e.id === effort)?.name ?? "Select"}
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuRadioGroup value={effort} onValueChange={setEffort}>
            {efforts.map((option) => (
              <DropdownMenuRadioItem key={option.id} value={option.id}>
                {option.name}
              </DropdownMenuRadioItem>
            ))}
          </DropdownMenuRadioGroup>
        </DropdownMenuContent>
      </DropdownMenu>
    </div>
  );
}

Render it inside ModelSelector.Content in place of ModelSelector.Effort. The same hook supports any shape that reads the levels and writes the selection.

Provider Logos#

Each model's icon accepts any ReactNode and renders in the trigger and the dropdown items. The optional logos registry item ships OpenAILogo, ClaudeLogo, and GeminiLogo marks to plug in:

<InstallCommand shadcn={["logos"]} />

import { ClaudeLogo, GeminiLogo, OpenAILogo } from "@/components/assistant-ui/logos";

<ModelSelector
  models={[
    { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", icon: <OpenAILogo /> },
    { id: "claude-opus-4.5", name: "Claude Opus 4.5", icon: <ClaudeLogo /> },
    { id: "gemini-3-pro", name: "Gemini 3 Pro", icon: <GeminiLogo /> },
  ]}
/>;

Icons are opt-in per model — omit icon and the entry renders text-only.

Search#

Search is opt-in. Pass searchable to the default component:

<ModelSelector models={models} searchable />

The same prop works on ModelSelector.Content when it renders its default children. Or compose ModelSelector.Search into a custom layout. Matching runs against each model's id, name, and keywords; add the provider name to keywords so typing "openai" finds its models.

Composition#

All parts are exported individually. The default popover content is List + Effort; replace it to add search, provider groups, or anything else:

import {
  ModelSelectorRoot,
  ModelSelectorTrigger,
  ModelSelectorContent,
  ModelSelectorSearch,
  ModelSelectorList,
  ModelSelectorEmpty,
  ModelSelectorGroup,
  ModelSelectorItem,
  ModelSelectorEffort,
} from "@/components/assistant-ui/model-selector";

<ModelSelectorRoot
  models={models}
  value={modelId}
  onValueChange={setModelId}
  effort={effort}
  onEffortChange={setEffort}
  <ModelSelectorTrigger variant="outline" />
  <ModelSelectorContent>
    <ModelSelectorSearch placeholder="Search models..." />
    <ModelSelectorList>
      <ModelSelectorEmpty />
      <ModelSelectorGroup heading="OpenAI">
        {openaiModels.map((model) => (
          <ModelSelectorItem key={model.id} model={model} />
        ))}
      </ModelSelectorGroup>
      <ModelSelectorGroup heading="Anthropic">
        {anthropicModels.map((model) => (
          <ModelSelectorItem key={model.id} model={model} />
        ))}
      </ModelSelectorGroup>
    </ModelSelectorList>
    <ModelSelectorEffort label="Thinking" />
  </ModelSelectorContent>
</ModelSelectorRoot>
ComponentDescription
ModelSelectorDefault export with runtime integration
ModelSelector.RootPresentational root (no runtime, controlled state)
ModelSelector.TriggerCVA-styled trigger showing the current selection
ModelSelector.ValueSelected model name, icon, and active effort
ModelSelector.ContentPopover content wrapping a Command
ModelSelector.SearchSearch input that filters the list
ModelSelector.FocusAnchorVisually hidden input that anchors keyboard navigation when there is no search box
ModelSelector.ListList of model items (renders all models by default)
ModelSelector.EmptyEmpty state shown when search has no matches
ModelSelector.GroupLabeled group of items (e.g. by provider)
ModelSelector.SeparatorDivider between groups or items
ModelSelector.ItemIndividual model option
ModelSelector.EffortThinking level row for the selected model

ModelSelector.List is a Command list, so filtering and keyboard navigation work across groups automatically. Custom sorting is plain code: order the models before rendering items.

Keyboard navigation needs a focused input to drive it. When content is unfiltered (searchable={false} on ModelSelector.Content, or the default children), ModelSelector.Content renders a visually hidden ModelSelector.FocusAnchor automatically, so custom layouts without a search box stay keyboard-operable. In a custom layout that renders neither ModelSelector.Search nor searchable={false}, place ModelSelector.FocusAnchor yourself to keep the list reachable from the keyboard.

`ModelSelector.Content` wraps a Command whose root keydown handler claims Enter to select the highlighted model and the arrow keys to move through the list. Interactive elements composed inside it (filter chips, custom effort controls) should stop propagation for the keys they handle in their own `onKeyDown` so the focused control responds instead. `ModelSelector.Effort` does this for Home / End (which cmdk would otherwise use to jump to the first / last model), lets its radiogroup own ArrowLeft / ArrowRight, and hands ArrowUp / ArrowDown back to the model list by refocusing cmdk's input, so the highlight only moves while a following Enter can act on it.

Variants#

Use the variant prop to change the trigger's visual style.

<ModelSelector variant="outline" /> // Border (default)
<ModelSelector variant="ghost" /> // No background
<ModelSelector variant="muted" /> // Solid background
VariantDescription
outlineBorder with transparent background (default)
ghostNo background, subtle hover
mutedSolid secondary background

Sizes#

Use the size prop to control the trigger dimensions.

<ModelSelector size="sm" /> // Compact (h-8, text-xs)
<ModelSelector size="default" /> // Standard (h-9)
<ModelSelector size="lg" /> // Large (h-10)

Keyboard Navigation#

The picker is fully operable from the keyboard, including when search is disabled.

KeyAction
ArrowDown / ArrowUpOpen the popover from the focused trigger; move between models once open, returning focus to the list from the Thinking row
EnterSelect the highlighted model and close
EscapeClose the popover and return focus to the trigger
TabMove from the model list to the Thinking row
ArrowLeft / ArrowRightMove between reasoning effort levels (Thinking row)
Home / EndJump to the first / last model (list) or effort level (Thinking row)

When searchable is set, typing filters the list; otherwise the keys above drive selection directly.

Accessibility#

The picker implements the WAI-ARIA combobox pattern over Popover + Command.

  • The trigger is role="combobox" with aria-haspopup="listbox"; the popover primitive manages aria-expanded and aria-controls.
  • The model list is a Command (cmdk) listbox: each item is role="option" with aria-selected, and the active item is tracked with aria-activedescendant.
  • Keyboard navigation works without a visible search box: ModelSelector.Content renders a visually hidden input (ModelSelector.FocusAnchor) that anchors cmdk's focus so the list stays reachable. Pass searchable to surface a real search input instead.
  • The Thinking row (ModelSelector.Effort) is a role="radiogroup" of role="radio" toggles with roving tabindex, so it is a single tab stop and ArrowLeft / ArrowRight move focus and select in one step. ArrowUp / ArrowDown return focus to the model list.

How It Works#

The default ModelSelector export registers the selection with assistant-ui's ModelContext system:

  1. The component calls aui.modelContext.register() with config.modelName, plus config.reasoningEffort when the selected model supports the chosen level
  2. The AssistantChatTransport includes config in the request body of every chat request
  3. Your API route reads config.modelName and config.reasoningEffort

This works out of the box with @assistant-ui/ai-sdk. ModelSelector.Root performs no registration; it is purely presentational, with controlled and uncontrolled props for the value, effort, and open state.

API Reference#

ModelSelector#

<ParametersTable
type="ModelSelectorProps"
parameters={[
{
name: "models",
type: "ModelOption[]",
required: true,
description: "Array of available models to display.",
},
{
name: "defaultValue",
type: "string",
description: "Initial model ID for uncontrolled usage. Defaults to the first model, captured on first render; if models loads asynchronously, control the value instead.",
},
{
name: "value",
type: "string",
description: "Controlled selected model ID.",
},
{
name: "onValueChange",
type: "(value: string) => void",
description: "Callback when selected model changes.",
},
{
name: "defaultEffort",
type: "string",
description: "Initial effort level ID for uncontrolled usage.",
},
{
name: "effort",
type: "string",
description: "Controlled effort level ID.",
},
{
name: "onEffortChange",
type: "(effort: string) => void",
description: "Callback when effort level changes.",
},
{
name: "searchable",
type: "boolean",
default: "false",
description: "Render a search input above the model list.",
},
{
name: "variant",
type: '"outline" | "ghost" | "muted"',
default: '"outline"',
description: "Visual style of the trigger button.",
},
{
name: "size",
type: '"sm" | "default" | "lg"',
default: '"default"',
description: "Size of the trigger button.",
},
{
name: "align",
type: '"start" | "center" | "end"',
default: '"start"',
description: "Alignment of the dropdown relative to the trigger.",
},
{
name: "contentClassName",
type: "string",
description: "Additional class name for the dropdown content.",
},
]}
/>

ModelOption#

<ParametersTable
type="ModelOption"
parameters={[
{
name: "id",
type: "string",
required: true,
description: "Unique identifier sent to the backend as modelName.",
},
{
name: "name",
type: "string",
required: true,
description: "Display name shown in trigger and dropdown.",
},
{
name: "description",
type: "string",
description: "Optional subtitle shown below the model name.",
},
{
name: "icon",
type: "React.ReactNode",
description: "Optional icon displayed before the model name.",
},
{
name: "disabled",
type: "boolean",
description: "Disable selection of this model.",
},
{
name: "keywords",
type: "string[]",
description: "Extra search terms matched by ModelSelector.Search (e.g. the provider name).",
},
{
name: "efforts",
type: "boolean | ModelSelectorEffortOption[]",
description: "Reasoning effort levels. true enables the default Low/Medium/High; pass a custom { id, name } list to override. Omit for models without configurable reasoning.",
},
]}
/>

useModelSelectorEfforts#

const { efforts, effort, setEffort } = useModelSelectorEfforts();

The selected model's effort levels and the active selection, for building a custom effort UI inside ModelSelector.Content. efforts is undefined for models without configurable reasoning.

resolveModelEffort#

resolveModelEffort(models, modelId, effort); // => string | undefined

Returns the effort ID when the given model supports it, otherwise undefined. This is the sticky selection rule the default component applies before registering the selection.

Related#

  • Model Context: How registered context (instructions, tools, config) reaches your backend
Documents
AG-UI Message History Management
AGENTS
AGENTS
ai-sdk-assistant-ui
CHANGELOG
CHANGELOG
CHANGELOG
README
AG-UI Streaming State Management
AGENTS
ai-sdk-assistant-ui
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
index
README
README
AI SDK Integration
AGENTS
ai-sdk
ai-sdk
assistant-runtime-provider
CHANGELOG
CHANGELOG
form-demo
hooks
index
index
migration
migration
overview
part-2
README
README
README
README
README
README
SKILL
SPEC_AssistantFrame
v4-legacy
v5-legacy
AI SDK Version Compatibility
migration
migration
v4-legacy
v5-legacy
Audio Content Handling
CHANGELOG
CHANGELOG
README
Bedrock Tool Call Streaming
CHANGELOG
Chat History Loading
Chat Transport
AGENTS
ai-sdk
ai-sdk-assistant-ui
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
child-scopes
expo
external-store
form-demo
hooks
index
index
migration
overview
pick-a-runtime
primitives
README
README
README
README
README
README
README
SKILL
SPEC_AssistantFrame
stockbroker
Code Block Rendering
index
markdown
Composer Attachment Handling
attachment
CHANGELOG
CHANGELOG
CHANGELOG
composer
form-demo
index
migration
overview
part-2
perplexity
primitives
primitives
README
README
SKILL
Data Stream Decoding
README
External History Adapter
External Store Adapter
adapters
ai-sdk
external-store
README
sibling-scopes
File Message Part Handling
file
LangChain Message Processing
AGENTS
CHANGELOG
CHANGELOG
CHANGELOG
external-store
migration
part-2
SKILL
LangGraph File Attachments
AGENTS
attachment
CHANGELOG
CHANGELOG
CHANGELOG
external-store
index
primitives
README
LangGraph Runtime API
AGENTS
ai-sdk-assistant-ui
CHANGELOG
CHANGELOG
CHANGELOG
expo
hooks
hooks
index
index
langgraph
migration
overview
pick-a-runtime
README
README
SKILL
Math Delimiter Preprocessing
CHANGELOG
Model Context Provider System
ai-sdk
hooks
model-selector
README
sibling-scopes
Plain Text Stream Encoder
Popover and Command UI Components
chatgpt
claude
grok
modal
model-selector
perplexity
README
SKILL
React Store Notification Manager
quickstart
README
SPEC
why-store
React Version Compatibility
Reasoning Disclosure State Machine
reasoning
SKILL
Remote Thread List Runtime
AGENTS
ai-sdk
hooks
hooks
overview
README
README
README
Resumable Streams
AGENTS
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
child-scopes
Run Cancellation and Abort Signal Propagation
AGENTS
CHANGELOG
CHANGELOG
README
README
Safari Browser Compatibility
CHANGELOG
index
migration
SPEC_AssistantFrame
Shimmer Text Animation
index
StreamdownTextPrimitive
CHANGELOG
Tap React Hook Lifecycle Management
CHANGELOG
child-scopes
methods
quickstart
README
README
why-store
Thread List Runtime API
adapters
ai-sdk
hooks
hooks
langgraph
README
rendering-lists
thread-list
Thread List UI Components
ai-sdk
langgraph
primitives
primitives
rendering-lists
thread-list
Thread Switch Stability
AGENTS
CHANGELOG
CHANGELOG
CHANGELOG
index
primitives
Tool Approval Gates
CHANGELOG
part-3
stockbroker
tool-fallback
Tool Approval Permission Model
CHANGELOG
CHANGELOG
part-3
README
SPEC_AssistantFrame
stockbroker
Tool Call Stream Lifecycle
CHANGELOG
tool-fallback
Top-Anchor Reserve
URL-Based Thread Routing
langgraph
README
SKILL
thread-list
Viewport Auto-Scroll
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CHANGELOG
CLAUDE
CODE_OF_CONDUCT
CONTRIBUTING
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README
README_LANGGRAPH
SECURITY
SPEC_ModelContextRegistry
action-bar
action-bar-more
adapters
api-reference
artifacts
assistant-if
assistant-modal
assistant-modal
assistant-runtime
assistant-sidebar
attachment
attachment-runtime
authorization
branch-picker
composer-runtime
composition
context-display
custom-backend
custom-backend
error
events
heat-graph
image
index
index
index
index
index
index
index
index
introduction
mem0
mermaid
message
message-part
message-part-runtime
message-runtime
message-timing
meta
part-1
part-grouping
quote
scopes
scrollbar
selection-toolbar
simplify-review
sources
state
streamdown
suggestion
syntax-highlighting
thread
thread
thread-list
thread-list-item
thread-list-item-more
thread-list-item-runtime
thread-list-runtime
thread-runtime
tool-group
tw-shimmer