Skip to content
KONIGI Patterns

AI Assistants / Starting a request / Model picker

‹ 6 of 7 ›

Model picker

People need to understand which model fits their task and what using it will cost.

Updated September 12, 2026

Problem

Model names and version numbers do not explain which option suits a task or what it will cost.

Solution

Describe each model by the tasks it suits and the trade-offs people can act on. Show differences in speed and cost, supported file types, and how much material it can handle.

Choose a useful default so people can start without studying the list. If the system selects a model automatically, show what it selected and provide an override where available.

When people switch models during a conversation, explain whether the new model can use the existing messages and attachments. Mark the switch in the conversation so changes in behavior have a visible explanation.

Use when

Available models differ in ways people can understand and use to make a choice.

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

A model menu adds decisions that newcomers may not be ready to make. Simple descriptions can leave out details experts need. Long lists become harder to maintain as models change.

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 selects how the assistant works. Usage meter shows the allowance consumed. Regenerate can offer another attempt with a different model.

Composer holds the picker. Knowledge cutoff notice explains limits on the model’s knowledge.

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 1× text only Careful analysis and code 8× text, images Long context whole documents 6× 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 screenshots yet for this pattern. Browse Products to see other examples.