Elements

Elements · Reasoning

Reasoning effort

How hard to think, and how much of that budget the run actually spent.

Thinking3,360 / 24,000
fig. 01 · plays once, replay from the corner

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 init

Then 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.

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

components/assistant-ui/elements/reasoning-effort.tsx
"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.

Anatomy

<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 / callTypeDescription
aui.modelContext.register(provider)(provider: ModelContextProvider) => UnsubscribeRegisters config.reasoningEffort for the next run; the returned function unregisters it.
useThreadTokenUsage() (@assistant-ui/ai-sdk)ThreadTokenUsage | undefinedreasoningTokens is tokens spent on reasoning for the latest message with usage data.