Elements · Reasoning
Reasoning effort
How hard to think, and how much of that budget the run actually spent.
Installation
npx shadcn@latest add "@assistant-ui/elements-reasoning-effort"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-reasoning-effort"Props-driven: no runtime or provider required.
Getting started
reasoningEffort is a real field on assistant-ui's model context, the same mechanism the composer's own model picker uses to pass an effort setting through to the model. Selecting a level and reading how much it cost are two separate concerns: the first is a registration, the second is usage data from the adapter.
Register the selected effort
"use client";
import { useEffect, useState } from "react";
import { useAui } from "@assistant-ui/react";
import { ReasoningEffort, type EffortLevel } from "@/components/assistant-ui/elements/reasoning-effort";
const LEVELS: readonly EffortLevel[] = [
{ key: "low", label: "Low", budget: 2_000 },
{ key: "medium", label: "Medium", budget: 8_000 },
{ key: "high", label: "High", budget: 24_000 },
];
function useRegisteredEffort() {
const [selectedKey, setSelectedKey] = useState("medium");
const aui = useAui();
useEffect(() => {
return aui.modelContext.register({
getModelContext: () => ({ config: { reasoningEffort: selectedKey } }),
});
}, [aui, selectedKey]);
return { selectedKey, setSelectedKey };
}budget has no runtime counterpart: it is a fact about your own effort tiers, so it stays app defined either way. Registering through modelContext only affects the next run; it does not retroactively change one already in flight.
Show what the run spent
import { useThreadTokenUsage } from "@assistant-ui/ai-sdk";
function AssistantEffort() {
const { selectedKey, setSelectedKey } = useRegisteredEffort();
const usage = useThreadTokenUsage();
return (
<ReasoningEffort
levels={LEVELS}
selectedKey={selectedKey}
spent={usage?.reasoningTokens ?? 0}
onSelect={setSelectedKey}
/>
);
}useThreadTokenUsage is exported by @assistant-ui/ai-sdk; with a different adapter, reasoningTokens comes from whatever shape that provider's usage data takes.
Standalone, levels, selectedKey, and spent are all plain values you own; the element only renders them and reports clicks.
Hold the selected level
"use client";
import { useState } from "react";
import { ReasoningEffort, type EffortLevel } from "@/components/assistant-ui/elements/reasoning-effort";
const LEVELS: readonly EffortLevel[] = [
{ key: "low", label: "Low", budget: 2_000 },
{ key: "high", label: "High", budget: 24_000 },
];
export function Effort() {
const [selectedKey, setSelectedKey] = useState("low");
return (
<ReasoningEffort levels={LEVELS} selectedKey={selectedKey} spent={0} onSelect={setSelectedKey} />
);
}Track spend yourself
const [spent, setSpent] = useState(0);
// bump `spent` from whatever event marks progress in your own flowAnatomy
<div data-slot="reasoning-effort">
<div>{/* "Thinking" label + "spent / budget" */}</div>
<div>{/* one button per level, aria-pressed on the active one */}</div>
<span role="progressbar">{/* progress track, width = spent / budget as a percentage */}</span>
</div>Levels render in the order given; there is no reordering or grouping. If selectedKey does not match any level.key, the budget resolves to 0 and the progress fill stays collapsed at 0 percent rather than erroring. The fill's width is spent as a share of the matched level's budget, clamped between 0 and 100 percent, so an over-large spent value does not overflow the bar. The track is a named progressbar, not a meter, because a spent budget climbs over the course of a run rather than resting at a reading; its 0…100 value matches that width and its value text reads the same spent of budget the header prints.
Examples
Reading the model's own effort field
The composer's model picker (/elements/model-selector) writes to the same config.reasoningEffort field through its own effort control. Mounting both at once registers two providers for the same field, so keep only one of them wired to the model context in a given app; assistant-ui merges registered model contexts by priority, not by which component happens to render.
Restyle the control
Both lanes take className on the root, which starts as flex w-full max-w-sm flex-col gap-2.5. The segmented control's track uses the shared field surface and the counts use the mono token, both from surfaces.tsx.
<ReasoningEffort className="max-w-none" /* ... */ />API reference
Model context and usage
| Selector / call | Type | Description |
|---|---|---|
aui.modelContext.register(provider) | (provider: ModelContextProvider) => Unsubscribe | Registers config.reasoningEffort for the next run; the returned function unregisters it. |
useThreadTokenUsage() (@assistant-ui/ai-sdk) | ThreadTokenUsage | undefined | reasoningTokens is tokens spent on reasoning for the latest message with usage data. |
ReasoningEffort
| Prop | Type | Default | Description |
|---|---|---|---|
levels | readonly EffortLevel[] | required | { key: string; label: string; budget: number }[], rendered in order. |
selectedKey | string | required | The active level's key. An unmatched key resolves the budget to 0. |
spent | number | required | Progress fill value, read against the selected level's budget. |
onSelect | (key: string) => void | required | Called with a level's key when its button is pressed. |
className | string | Merged onto the root. |
All other div props except children, levels, selectedKey, spent, and onSelect are forwarded to the root.