8.0k New

Model Selector

Two menus. One picks the model the agent runs, the other picks how hard it thinks.

Epicenter

Local-first, open source apps

Special Sponsor
<script lang="ts">
  import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
  import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
  import PlusIcon from "@lucide/svelte/icons/plus";
  import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
 
  const providers = [
    {
      name: "OpenAI",
      models: [
        { id: "gpt-5", name: "GPT-5" },
        { id: "gpt-5-mini", name: "GPT-5 mini" }
      ]
    },
    {
      name: "Anthropic",
      models: [
        { id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" },
        { id: "claude-haiku-4-5", name: "Claude Haiku 4.5" }
      ]
    }
  ];
 
  const models = providers.flatMap((provider) => provider.models);
 
  const efforts = [
    { id: "low", name: "Low" },
    { id: "medium", name: "Medium" },
    { id: "high", name: "High" }
  ];
 
  let text = $state("");
  let model = $state(models[0].id);
  let effort = $state(efforts[1].id);
  let form = $state<HTMLFormElement | null>(null);
  const currentModel = $derived(
    models.find((item) => item.id === model) ?? models[0]
  );
  const currentEffort = $derived(
    efforts.find((item) => item.id === effort) ?? efforts[1]
  );
  const canSend = $derived(text.trim().length > 0);
 
  function onsubmit(event: SubmitEvent) {
    event.preventDefault();
    if (!canSend) {
      return;
    }
 
    text = "";
  }
</script>
 
<div class="mx-auto w-full max-w-lg py-6">
  <form bind:this={form} {onsubmit}>
    <InputGroup.Root>
      <InputGroup.Textarea
        bind:value={text}
        placeholder="Ask the agent to change your code..."
        onkeydown={(event) => {
          if (event.key === "Enter" && !event.shiftKey) {
            event.preventDefault();
            form?.requestSubmit();
          }
        }}
        class="p-3"
      />
      <InputGroup.Addon align="block-end">
        <InputGroup.Button
          type="submit"
          size="icon-sm"
          variant="outline"
          class="rounded-full"
          disabled={canSend}
          aria-label="Attach"
        >
          <PlusIcon />
        </InputGroup.Button>
        <DropdownMenu.Root>
          <DropdownMenu.Trigger>
            {#snippet child({ props })}
              <InputGroup.Button
                {...props}
                size="sm"
                variant="outline"
                aria-label="Model: {currentModel.name}"
                class="ml-auto"
              >
                {currentModel.name}
                <ChevronDownIcon />
              </InputGroup.Button>
            {/snippet}
          </DropdownMenu.Trigger>
          <DropdownMenu.Content align="start" class="w-48">
            <DropdownMenu.RadioGroup bind:value={model}>
              {#each providers as provider, index (provider.name)}
                {#if index > 0}
                  <DropdownMenu.Separator />
                {/if}
                <DropdownMenu.Group>
                  <DropdownMenu.Label>{provider.name}</DropdownMenu.Label>
                  {#each provider.models as item (item.id)}
                    <DropdownMenu.RadioItem value={item.id} closeOnSelect>
                      {item.name}
                    </DropdownMenu.RadioItem>
                  {/each}
                </DropdownMenu.Group>
              {/each}
            </DropdownMenu.RadioGroup>
          </DropdownMenu.Content>
        </DropdownMenu.Root>
        <DropdownMenu.Root>
          <DropdownMenu.Trigger>
            {#snippet child({ props })}
              <InputGroup.Button
                {...props}
                size="sm"
                variant="outline"
                aria-label="Effort: {currentEffort.name}"
              >
                <span class="text-muted-foreground">Effort</span>
                {currentEffort.name}
                <ChevronDownIcon />
              </InputGroup.Button>
            {/snippet}
          </DropdownMenu.Trigger>
          <DropdownMenu.Content align="start" class="w-36">
            <DropdownMenu.Group>
              <DropdownMenu.Label>Effort</DropdownMenu.Label>
              <DropdownMenu.RadioGroup bind:value={effort}>
                {#each efforts as item (item.id)}
                  <DropdownMenu.RadioItem value={item.id} closeOnSelect>
                    {item.name}
                  </DropdownMenu.RadioItem>
                {/each}
              </DropdownMenu.RadioGroup>
            </DropdownMenu.Group>
          </DropdownMenu.Content>
        </DropdownMenu.Root>
        <InputGroup.Button
          type="submit"
          size="icon-sm"
          variant="default"
          class="rounded-full"
          disabled={!canSend}
          aria-label="Send"
        >
          <ArrowUpIcon />
        </InputGroup.Button>
      </InputGroup.Addon>
    </InputGroup.Root>
  </form>
</div>

Installation

Model Selector is built with the Dropdown Menu and Input Group components.

pnpm dlx shadcn-svelte@latest add dropdown-menu input-group

Usage

<script lang="ts">
  import ArrowUpIcon from "@lucide/svelte/icons/arrow-up";
  import ChevronDownIcon from "@lucide/svelte/icons/chevron-down";
  import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
  import * as InputGroup from "$lib/components/ui/input-group/index.js";
</script>
<InputGroup.Root>
  <InputGroup.Textarea placeholder="Ask the agent to change your code..." />
  <InputGroup.Addon align="block-end">
    <DropdownMenu.Root>
      <DropdownMenu.Trigger>
        {#snippet child({ props })}
          <InputGroup.Button {...props} size="sm">
            GPT-5
            <ChevronDownIcon />
          </InputGroup.Button>
        {/snippet}
      </DropdownMenu.Trigger>
      <DropdownMenu.Content align="start">
        <DropdownMenu.RadioGroup bind:value={model}>
          <DropdownMenu.Group>
            <DropdownMenu.Label>OpenAI</DropdownMenu.Label>
            <DropdownMenu.RadioItem value="gpt-5" closeOnSelect>
              GPT-5
            </DropdownMenu.RadioItem>
            <DropdownMenu.RadioItem value="gpt-5-mini" closeOnSelect>
              GPT-5 mini
            </DropdownMenu.RadioItem>
          </DropdownMenu.Group>
        </DropdownMenu.RadioGroup>
      </DropdownMenu.Content>
    </DropdownMenu.Root>
    <DropdownMenu.Root>
      <DropdownMenu.Trigger>
        {#snippet child({ props })}
          <InputGroup.Button {...props} size="sm">
            Medium
            <ChevronDownIcon />
          </InputGroup.Button>
        {/snippet}
      </DropdownMenu.Trigger>
      <DropdownMenu.Content align="start">
        <DropdownMenu.RadioGroup bind:value={effort}>
          <DropdownMenu.RadioItem value="low" closeOnSelect>
            Low
          </DropdownMenu.RadioItem>
          <DropdownMenu.RadioItem value="medium" closeOnSelect>
            Medium
          </DropdownMenu.RadioItem>
          <DropdownMenu.RadioItem value="high" closeOnSelect>
            High
          </DropdownMenu.RadioItem>
        </DropdownMenu.RadioGroup>
      </DropdownMenu.Content>
    </DropdownMenu.Root>
    <InputGroup.Button type="submit" variant="default" class="ml-auto">
      <ArrowUpIcon />
    </InputGroup.Button>
  </InputGroup.Addon>
</InputGroup.Root>

AI SDK

Send the model and the effort with the message. Map them to a model and provider options in the route.

chat.sendMessage({ text }, { body: { model, effort } });
src/routes/api/chat/+server.ts
import { anthropic } from "@ai-sdk/anthropic";
import { openai } from "@ai-sdk/openai";
import { convertToModelMessages, streamText, type UIMessage } from "ai";
import type { RequestHandler } from "./$types";
 
const models = {
  "gpt-5": openai("gpt-5"),
  "gpt-5-mini": openai("gpt-5-mini"),
  "claude-sonnet-4-5": anthropic("claude-sonnet-4-5"),
  "claude-haiku-4-5": anthropic("claude-haiku-4-5"),
};
 
export const POST: RequestHandler = async ({ request }) => {
  const { messages, model, effort } = (await request.json()) as {
    messages: UIMessage[];
    model: keyof typeof models;
    effort: "low" | "medium" | "high";
  };
 
  const result = streamText({
    model: models[model] ?? models["gpt-5"],
    messages: await convertToModelMessages(messages),
    providerOptions: {
      openai: { reasoningEffort: effort },
      anthropic: { effort },
    },
  });
 
  return result.toUIMessageStreamResponse();
};

Each provider reads its own key in providerOptions and ignores the rest.