Models
The model lives in the composer rail, one tap away with context at a glance.
Installation
npx shadcn@latest add "@assistant-ui/elements-composer"First time? Set up a runtime
Runtime components read their state from an assistant-ui runtime. Add one to an existing project:
npx assistant-ui@latest initThen wrap your app in a runtime provider:
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";
export default function App() {
const runtime = useChatRuntime({
transport: new AssistantChatTransport({ api: "/api/chat" }),
});
return (
<AssistantRuntimeProvider runtime={runtime}>
{/* your components */}
</AssistantRuntimeProvider>
);
}The installation guide covers new projects, templates, and API routes.
npx shadcn@latest add "@assistant-ui/elements-composer"Props-driven: no runtime or provider required.
A small pill in the composer's action row names the active model and opens a short list to switch it, without leaving the message being written. With a runtime the choice is registered into assistant-ui's model context system so it actually reaches the next request; standalone you just hold the selection as state.
Getting started
There is no dedicated primitive for a model trigger, because "the active model" is not thread or message state the way branches or attachments are; it is whatever your app registers through the general-purpose model context system. Read the registered name to show it, and register a new one to change it.
Read the active model
import { useAuiState } from "@assistant-ui/react";
const modelName = useAuiState((s) => s.modelContext.modelName);s.modelContext.modelName is derived from every provider currently registered via aui.modelContext.register(...), merged by priority; with nothing registered it is undefined.
Register a change
"use client";
import { useEffect, useState } from "react";
import { useAui, useAuiState } from "@assistant-ui/react";
import { ComposerModelTrigger, ComposerMenu, ComposerModelItem } from "@/components/assistant-ui/elements/composer";
const models = [
{ name: "Fast", meta: "$0.25/M" },
{ name: "Frontier", meta: "$3/M" },
];
export function ModelPicker() {
const aui = useAui();
const [selected, setSelected] = useState(models[0]);
const [open, setOpen] = useState(false);
useEffect(() => {
return aui.modelContext.register({
getModelContext: () => ({ config: { modelName: selected.name } }),
});
}, [aui, selected]);
return (
<div className="relative">
<ComposerModelTrigger model={selected.name} open={open} onClick={() => setOpen((v) => !v)} />
<ComposerMenu open={open}>
{models.map((entry) => (
<ComposerModelItem
key={entry.name}
entry={entry}
selected={entry.name === selected.name}
onClick={() => {
setSelected(entry);
setOpen(false);
}}
/>
))}
</ComposerMenu>
</div>
);
}The effect re-registers on every change to selected and cleans up the previous registration automatically, since register returns an unsubscribe. This is the same pattern the Model selector kit uses internally; install that element instead of building the effect yourself if you also want a searchable, provider-grouped list with reasoning-effort controls. For the full list rather than this compact trigger, see the model-picker design.
Standalone, the trigger and menu are fully controlled: you hold which model is selected and whether the menu is open, and the two catalog pieces only render that state.
Hold the selection
"use client";
import { useState } from "react";
import { ComposerModelTrigger, ComposerMenu, ComposerModelItem, type ComposerModel } from "@/components/assistant-ui/elements/composer";
const models: ComposerModel[] = [
{ name: "Fast", meta: "$0.25/M" },
{ name: "Frontier", meta: "$3/M" },
];
export function ModelPicker() {
const [selected, setSelected] = useState(models[0]);
const [open, setOpen] = useState(false);
return (
<div className="relative">
<ComposerModelTrigger model={selected.name} open={open} onClick={() => setOpen((v) => !v)} />
<ComposerMenu open={open} align="end">
{models.map((entry) => (
<ComposerModelItem
key={entry.name}
entry={entry}
selected={entry.name === selected.name}
onClick={() => {
setSelected(entry);
setOpen(false);
}}
/>
))}
</ComposerMenu>
</div>
);
}Close it yourself
Neither ComposerMenu nor ComposerModelTrigger listens for an outside click or Escape; open only controls the animation and pointer-events. Close the menu from wherever you decide it should close, typically a click-outside handler or by closing it on selection, as above.
Anatomy
<div>
<button aria-expanded={/* open */}>{/* model name */}<svg /* chevron */ /></button>
<div data-slot="composer-menu" data-open={/* open */}>
<button data-slot="composer-menu-item" data-active={/* selected */}>
<span>{/* name */}</span>
<span>{/* meta, tabular-nums */}</span>
{/* a check mark fades and zooms in only on the selected row */}
</button>
</div>
</div>The trigger never disables itself and never manages its own open state; both are the caller's, in either lane.
Examples
Reading and writing are two different calls
s.modelContext.modelName only reflects what has been registered; setting it is always a register call, never a direct assignment. A component that only displays the name (say, in a header) needs just the selector; only the piece that owns the picker UI needs to call register.
const modelName = useAuiState((s) => s.modelContext.modelName); // read
aui.modelContext.register({ getModelContext: () => ({ config: { modelName: "Frontier" } }) }); // writeRestyle the trigger and menu
Both lanes share ComposerMenu and ComposerModelItem with Slash commands and Mentions; a restyle there restyles this menu too. The trigger itself is a standalone pill and takes its own className.
<ComposerModelTrigger model={selected.name} open={open} className="text-foreground/80" />API reference
Model context
| Selector | Type | Description |
|---|---|---|
s.modelContext.modelName | string | undefined | The config.modelName of the highest-priority registered provider. |
s.modelContext.toolNames | readonly string[] | Names of every tool merged from registered providers. |
aui.modelContext.register(provider) | (provider: ModelContextProvider) => Unsubscribe | Registers { getModelContext: () => ({ config: { modelName } }) }; call the returned function to unregister. |
Registration is additive and priority-ordered: several providers can be registered at once (from different components), and the one with the highest priority wins when they disagree on modelName.
ComposerModel
| Field | Type | Description |
|---|---|---|
name | string | Shown as the row's label. |
meta | string | Shown right-aligned, e.g. a price. |
ComposerModelTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
model | string | required | The name shown before the chevron. |
open | boolean | required | Sets aria-expanded; does not control any menu by itself. |
className | string | Merged onto the root. |
ComposerModelItem
| Prop | Type | Default | Description |
|---|---|---|---|
entry | ComposerModel | required | The row to render. |
selected | boolean | required | Shows the animated check mark. |