HextaUI

Models

The Models page of an AI product's settings. A default model with its context, speed and cost at a glance, a default effort that knows what each model supports, a searchable model list grouped by provider with filters, pins and bulk switches, OpenAI-compatible servers with a real connection test, and a refresh that tells you what's new.

Every AI product grows a Models page: which models show up in the picker, which one new chats start with, how hard it thinks, and which small model runs the background jobs. Models is that page as one component, ModelSettings, that drops into any Settings section and saves through the same island as the rest of your settings.

Default model opens a menu of the models that are on, grouped by provider, each with its context, cost and abilities. Under the picker, chips show the chosen model's context window, speed and cost, so the tradeoff is visible before anyone opens a chat. Default effort uses the effort slider from Prompt Input and knows what the default model supports. Drag past what it can do and the slider settles on its highest level, the line under it says why, and screen readers hear it too. Models that don't reason grey the slider out and explain that effort doesn't apply.

The model list searches by name, provider and ability, and filters to Enabled, Reasoning, Fast or Vision. Models are grouped by provider with a count of what's on and one switch to turn a whole provider on or off. Each row shows context, a four-dot cost scale and badges for New and Preview. Pin the models you use most from a row's menu and they move to a Pinned group at the top, in the order the picker shows them. Move them with Move up and Move down, or Alt and the arrow keys, without dragging anything.

Add model connects any OpenAI-compatible server, including local ones. It checks the URL, flags a model you already added and tests the connection before adding, showing how long the server took to answer or the reason it failed. The key field hides the key until you show it. Refresh asks your server for the latest catalog and shows a quiet banner such as "2 new models: Nova 3.5 and Atlas 2 Vision", with Show to filter to them. New models arrive off, so nothing changes in the picker until you choose.

Task models give the explore subagent, chat titles and summaries their own model, or Auto. Nothing saves until you save: every change raises the Settings save bar, Discard puts everything back, including removed custom models, and the default model can't be turned off by accident.

  1. Add the Pro registry to components.json

    components.json
    {
      "registries": {
        "@hextaui-pro": {
          "url": "https://hextaui.com/r/pro/{name}.json",
          "headers": {
            "Authorization": "Bearer ${HEXTAUI_PRO_TOKEN}"
          }
        }
      }
    }
  2. Add your token

    Create a token on your account page and put it in .env.local as HEXTAUI_PRO_TOKEN.

  3. Add the block

    pnpm dlx shadcn@latest add @hextaui-pro/models

Wire it to your API

Put ModelSettings inside a SettingsSection. Pass the catalog and what's saved, save in onSave, and return the latest catalog from onRefresh. Throw from either to show the message, and the draft stays.

"use client"

import { IconCube } from "@tabler/icons-react"

import { SettingsSection, SettingsShell, type SettingsSectionItem } from "../settings/settings"
import { ModelSettings, type ModelInfo, type ModelSettingsValues } from "@/components/blocks/models/model-settings"

const sections: SettingsSectionItem[] = [{ id: "models", label: "Models", icon: <IconCube /> }]

export function ModelsPage({
  models,
  values,
}: {
  models: ModelInfo[]
  values: ModelSettingsValues
}) {
  return (
    <SettingsShell sections={sections} className="h-svh">
      <SettingsSection id="models">
        <ModelSettings
          models={models}
          values={values}
          onSave={async (next) => {
            const response = await fetch("/api/settings/models", {
              method: "PUT",
              body: JSON.stringify(next),
            })
            if (!response.ok) throw new Error("Check your connection and try again.")
          }}
          onRefresh={async () => {
            const response = await fetch("/api/models")
            if (!response.ok) throw new Error("The model list is unavailable.")
            return (await response.json()) as ModelInfo[]
          }}
        />
      </SettingsSection>
    </SettingsShell>
  )
}

Test custom servers

onTestConnection gets the base URL, model ID and key. Resolve when the server answers, or throw with a message people can act on. Add model runs the same test first, so a broken server never lands in the list.

"use client"

import { ModelSettings, type ModelConnection, type ModelInfo, type ModelSettingsValues } from "@/components/blocks/models/model-settings"

async function testConnection({ baseUrl, model, apiKey }: ModelConnection) {
  const response = await fetch(`${baseUrl}/chat/completions`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
    },
    body: JSON.stringify({ model, max_tokens: 1, messages: [{ role: "user", content: "ping" }] }),
  }).catch(() => {
    throw new Error(`Couldn’t reach ${new URL(baseUrl).host}. Check the URL and that the server is running.`)
  })
  if (response.status === 401) throw new Error("The server said 401. Check the API key.")
  if (response.status === 404) throw new Error(`The server doesn’t know “${model}”.`)
  if (!response.ok) throw new Error(`The server said ${response.status}.`)
}

export function ModelsWithCustomServers({
  models,
  values,
  onSave,
}: {
  models: ModelInfo[]
  values: ModelSettingsValues
  onSave: (values: ModelSettingsValues) => Promise<void>
}) {
  return (
    <ModelSettings
      models={models}
      values={values}
      onSave={onSave}
      onTestConnection={testConnection}
    />
  )
}

Your own effort levels and tasks

efforts sets the slider's levels, and each model's efforts lists the ones it supports. tasks sets the background jobs that get their own model.

"use client"

import { ModelSettings, type ModelInfo, type ModelSettingsValues } from "@/components/blocks/models/model-settings"

const models: ModelInfo[] = [
  {
    id: "nova-3",
    name: "Nova 3",
    provider: "Hexta",
    context: 200_000,
    speed: "balanced",
    price: 2,
    efforts: ["none", "light", "deep"],
    recommendedEffort: "light",
  },
  {
    id: "nova-3-mini",
    name: "Nova 3 Mini",
    provider: "Hexta",
    context: 128_000,
    speed: "fast",
    price: 1,
    efforts: ["none", "light"],
  },
]

const efforts = [
  { value: "none", label: "None" },
  { value: "light", label: "Light" },
  { value: "deep", label: "Deep" },
]

const tasks = [
  { id: "titles", label: "Chat titles", description: "Names new chats from the first message." },
]

export function ModelsWithOwnLevels({
  values,
  onSave,
}: {
  values: ModelSettingsValues
  onSave: (values: ModelSettingsValues) => Promise<void>
}) {
  return (
    <ModelSettings models={models} values={values} onSave={onSave} efforts={efforts} tasks={tasks} />
  )
}

Anatomy

The parts you compose, from the outside in.

PartDescription
ModelSettingsThe whole Models section: defaults, the model picker list and task models. Place it inside a SettingsSection.
Default modelA menu of the enabled models with their stats, and chips for the chosen one's context, speed and cost.
Default effortThe Prompt Input effort slider, limited to the levels the default model supports.
Model pickerSearch, filters, a refresh banner, a Pinned group and one group per provider, each row with a switch and a menu.
Add model dialogBase URL, model ID, display name and API key for an OpenAI-compatible server, with a connection test.
Task modelsA model per background job, or Auto.

ModelSettings

Must be rendered inside SettingsShell, usually in a SettingsSection, because it saves through the shell's save bar.

PropTypeDefault
modelsThe catalog. { id, name, provider, context?, speed?, price?, vision?, efforts?, recommendedEffort?, status?, description? }. speed is "fast" | "balanced" | "thorough", price is 1 to 4, efforts lists the effort values the model supports (leave it out for models that don't reason), and status is "new" | "preview".
ModelInfo[]–
valuesWhat's saved: { defaultModel, effort, enabled, pinned, tasks, custom }. enabled and pinned are model ids, pinned in picker order. tasks maps a task id to a model id or "auto". custom holds the added servers.
ModelSettingsValues–
onSaveSave the draft. Throw to show the message in the save bar and keep the draft.
(values) => void | Promise<void>–
onRefreshFetch the latest catalog. Models with ids it hasn't seen get a New badge and a banner. Throw to show the reason with Try again. Leave it out to hide the refresh button.
() => Promise<ModelInfo[]>–
onTestConnectionCheck a custom server. Resolve if it answers and throw with a message if not. Leave it out to add custom models without a test.
(connection: { baseUrl, model, apiKey }) => Promise<void>–
effortsThe effort levels, from fastest to smartest.
{ value, label }[]Low, Medium, High, Max
tasksBackground jobs that get their own model. Pass [] to hide the group.
{ id, label, description? }[]Explore subagent, Chat titles, Summaries
KeyAction
EscapeIn search, clears the query.
EnterIn search with no matches, opens Add model with the query as the model ID.
←→Moves between filters. In the effort slider, changes the level, stopping at what the default model supports.
SpaceTurns the focused model on or off.
Alt↑On a pinned model's switch or menu, moves it up the picker. Alt+↓ moves it down. Focus stays on the same control.
⌘SSaves, like the rest of Settings. Ctrl+S on Windows and Linux.
  • Each provider and the Pinned group is a list named by its heading, and every row is named by its model.
  • Switches are labelled with the model's name and described by its context, cost and abilities. The default model's switch is disabled and says why.
  • A polite live region announces how many models match a search or filter, refresh results, bulk changes, pins, moves and when effort is limited by the model.
  • When a pinned model moves or a row changes group, focus follows it to the same control.
  • The cost scale has a text name, such as Low cost, so the dots are never the only signal.
  • With reduced motion, the banner, the row highlight and the slider don't animate.

Code

4 files, added to components/blocks/models.