Skip to content
KONIGI

AI Assistants / Input and invocation / Model picker

6 of 7

Model picker

Several models sit behind one box, and the difference between them stays invisible until the answer arrives.

Updated September 12, 2026

Problem

A dropdown offers four names that differ by a version number and a size word. The viewer has no way to know which one answers their question better, what each costs, or whether the choice even matters for what they’re about to type.

Solution

The picker exists because the differences are real: speed, depth, price, context length, what modalities are accepted, how recent the training data is. The design failure is describing those differences in the vocabulary of the people who built the models rather than the people choosing between them. A parameter count, a codename and a point release communicate nothing to someone deciding whether to ask about a spreadsheet.

Describe each option by the job it’s good at and the price of using it. Fast and cheap for everyday questions. Slower and more careful for analysis and code. Largest context for long documents. That framing is HAX guidelines 1 and 2 in one control: make clear what the system can do, and make clear how well it can do it. A list of names does the first badly and the second not at all.

Three properties deserve to be visible in the list rather than buried in a docs page:

  1. Cost, on any metered plan. It is the main decision variable and hiding it makes the picker decorative.
  2. Context length, because it is the one hard limit that changes what tasks are possible rather than how well they go.
  3. Modality, because a viewer about to attach an image needs to know the fast option cannot see it before they attach it.

Switching mid-conversation is the behaviour worth designing deliberately. The new model inherits the existing transcript, which is usually what people want and occasionally surprising, because the voice changes and the earlier turns were produced by something else. A quiet marker on the turn where the switch happened costs nothing and explains a discontinuity that otherwise reads as the model losing the thread.

The default matters more than the picker does. Most people never open it, so the routing behind the default determines the experience for the majority. Automatic routing by question complexity is increasingly the answer, and it moves the design problem from choosing to explaining: when the system picked, the viewer should be able to see what it picked and override it.

Use when

The options genuinely differ in a way a viewer can act on, and the product can explain the difference in a sentence each.

Don’t use when

Two options differ by a benchmark point and nothing a person would notice. Surfacing a choice that can’t be made well is a cost with no benefit, and it invites people to believe they’re getting it wrong.

Trade-offs

Every model in the list is a decision demanded before the viewer has typed anything, and decision cost lands hardest on the newcomers who understand it least. Plain-language labels are kinder and drift from the technical truth, which frustrates the expert users most likely to open the menu. Automatic routing removes the burden and removes the agency, and it fails loudly the first time someone notices their hard question went to the cheap model. Long lists age badly: every model ever shipped wants to stay for the people who depend on it, and the menu becomes a changelog.

Checklist

  • Can a non-expert tell which option to pick from the labels alone?
  • Is the cost difference visible at the point of choosing?
  • Is the context limit stated where it matters, or only in documentation?
  • Does the list say which options accept images, audio, or files?
  • What happens to the conversation when the model changes mid-thread?
  • Is the switch recorded anywhere the viewer can see later?
  • If routing is automatic, can the viewer see what was chosen and override it?
  • How many options are in the list, and what’s the retirement rule?
  • Does the default change without warning, and are people told when it does?
  • Is the current selection visible without opening the menu?

Compare

ChatGPT names its tiers and puts a short capability line under each, so the menu is doing the explaining rather than deferring to documentation. Claude ties the choice to the retry control as well as the composer. Switching becomes a way to get a second opinion rather than a setting you change once. Perplexity lets the underlying model come from several vendors. The picker becomes a genuine market choice, and the product a layer over models rather than a face for one. Microsoft Copilot mostly hides the question inside the host application, betting that someone writing a document does not want to make an inference-tier decision first, which is the strongest argument against the pattern existing at all.

Mode switch is the adjacent control and the two are constantly confused. Usage meter is where the cost of the expensive option shows up. Regenerate is the most useful place to offer a switch, since a second attempt with a different model is a real correction. Composer holds the control. Knowledge cutoff notice is the model property most likely to matter and least likely to be in the menu.

Model picker anatomy An open model menu where each row carries a job description, a relative cost, a context length and the modalities it accepts, rather than a version number alone. A marker on the turn records where the model was switched mid-conversation. Describe the job, not the architecture Fast everyday questions text only Careful analysis and code text, images Long context whole documents 1 million tokens auto-routed picked Careful switched here earlier turns came from another model 1 2 3 4 1 A JOB, NOT A VERSION A parameter count and a point release tell a chooser nothing they can act on. 2 COST IN THE LIST On a metered plan this is the main variable. Hiding it makes the menu decor. 3 WINDOW AND MODALITY These change which tasks are possible, not just how well they go. 4 MARK THE SWITCH The new model inherits the transcript, and the change of voice reads as losing track. Most people never open this menu, so the default and its routing decide the experience.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Rows that describe the job rather than the architecture: what it is for, what it costs relative to the cheap one, how much context it holds and what it accepts. Switching mid-conversation leaves a marker, because the turns above came from something else.

shadcn
npx shadcn@latest add dropdown-menu button
npm
lucide-react
Tokens
--card--card-foreground--popover--popover-foreground--accent--accent-foreground--muted--muted-foreground--border--chart-1

ModelPicker.tsxA DropdownMenu of jobs, costs and windows, with an Auto row that says what it picked. The switch marker is the second export.

import { useState } from "react";
import { Check, ChevronDown } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";

/**
 * Each row describes the job, not the architecture. A parameter count and a
 * point release tell a chooser nothing they can act on; "analysis and code"
 * and "8×" do. Cost, window and modality sit in the list because they change
 * which tasks are possible, and hiding them makes the menu decor.
 */
export const MODELS = [
  { id: "fast",    name: "Fast",         job: "everyday questions", cost: "1×", meta: "text only" },
  { id: "careful", name: "Careful",      job: "analysis and code",  cost: "8×", meta: "text, images" },
  { id: "long",    name: "Long context", job: "whole documents",    cost: "6×", meta: "1 million tokens" },
] as const;
export type ModelId = (typeof MODELS)[number]["id"];

type Props = {
  /** "auto" lets the system route by the question. Most people never open
   *  this menu, so auto and its routing decide the experience. */
  value: ModelId | "auto";
  /** What auto picked for the current turn, so the choice is visible and can
   *  be overridden rather than silent. */
  routed?: ModelId;
  onChange: (id: ModelId | "auto") => void;
  /** Start open. Used when the picker is the thing on screen, as in a demo. */
  defaultOpen?: boolean;
};

export function ModelPicker({ value, routed, onChange, defaultOpen = false }: Props) {
  const [open, setOpen] = useState(defaultOpen);
  const current = MODELS.find((m) => m.id === value);
  const picked = MODELS.find((m) => m.id === routed);

  return (
    // Non-modal: a picker beside a composer should not lock the page behind it.
    <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="sm" aria-label="Choose a model">
          {current ? current.name : "Auto"}
          {!current && picked && <span className="text-muted-foreground">picked {picked.name}</span>}
          <ChevronDown className="size-4" />
        </Button>
      </DropdownMenuTrigger>

      <DropdownMenuContent align="end" className="w-80 p-1.5">
        {MODELS.map((m) => (
          <DropdownMenuItem
            key={m.id}
            role="menuitemradio"
            aria-checked={value === m.id}
            onSelect={() => onChange(m.id)}
            className="flex items-start gap-3 px-3 py-2"
          >
            <div className="min-w-0 flex-1">
              <div className="text-sm">{m.name}</div>
              <div className="text-xs text-muted-foreground">{m.job}</div>
            </div>
            <div className="text-right text-xs text-muted-foreground">
              <div className="tabular-nums">{m.cost}</div>
              <div>{m.meta}</div>
            </div>
            <Check className={`mt-0.5 size-4 shrink-0 ${value === m.id ? "" : "invisible"}`} />
          </DropdownMenuItem>
        ))}

        <DropdownMenuSeparator />

        <DropdownMenuItem
          role="menuitemradio"
          aria-checked={value === "auto"}
          onSelect={() => onChange("auto")}
          className="flex items-start gap-3 px-3 py-2"
        >
          <div className="min-w-0 flex-1">
            <div className="text-sm">Auto</div>
            <div className="text-xs text-muted-foreground">auto-routed by the question{picked ? `, picked ${picked.name}` : ""}</div>
          </div>
          <Check className={`mt-0.5 size-4 shrink-0 ${value === "auto" ? "" : "invisible"}`} />
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

/**
 * Sits on the turn where the model changed. The new model inherits the
 * transcript, and without this the change of voice reads as losing track.
 */
export function ModelSwitchMarker({ to }: { to: ModelId }) {
  const m = MODELS.find((x) => x.id === to)!;
  return (
    <div role="separator" aria-label={`switched to ${m.name}`} className="my-3 text-xs text-muted-foreground">
      <div className="flex items-center gap-3">
        <span className="h-px flex-1 bg-chart-1" />
        <span className="whitespace-nowrap text-chart-1">switched here to {m.name}</span>
        <span className="h-px flex-1 bg-chart-1" />
      </div>
      <p className="mt-1 text-center">earlier turns came from another model</p>
    </div>
  );
}

demo.tsxHow it is called: open on arrival, Auto routed to Careful, and a switch marker once a model is chosen.

import { useState } from "react";
import { ModelPicker, ModelSwitchMarker, type ModelId } from "./ModelPicker";

/**
 * Opens on arrival, because the menu is the thing being shown. Auto is the
 * default and it picked Careful for this turn. Choosing a model records a
 * switch marker below.
 */
export default function Demo() {
  const [value, setValue] = useState<ModelId | "auto">("auto");
  const [switched, setSwitched] = useState<ModelId | null>(null);

  return (
    <div className="min-h-[340px] rounded-lg border bg-card p-4">
      <div className="flex justify-end">
        <ModelPicker
          value={value}
          routed="careful"
          defaultOpen
          onChange={(id) => {
            setValue(id);
            if (id !== "auto") setSwitched(id);
          }}
        />
      </div>
      {switched && <ModelSwitchMarker to={switched} />}
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No captures reference this pattern yet. Captures arrive product by product; see Products for what's in the gallery so far.