Skip to content

feat(tui): show pricing & capabilities in /model picker#2511

Merged
dgageot merged 1 commit intodocker:mainfrom
dgageot:board/richer-model-picker-dialog-with-pricing-0f8bccd7
Apr 26, 2026
Merged

feat(tui): show pricing & capabilities in /model picker#2511
dgageot merged 1 commit intodocker:mainfrom
dgageot:board/richer-model-picker-dialog-with-pricing-0f8bccd7

Conversation

@dgageot
Copy link
Copy Markdown
Member

@dgageot dgageot commented Apr 25, 2026

The /model dialog now reads as a proper table with a details panel,
making it much easier to compare models and pick one. Catalog metadata
(pricing, context window, modalities, family) is pulled best-effort
from the models.dev store and surfaced both inline and in a stable
panel below the list.

What changes

Runtime

  • runtime.ModelChoice gains optional fields populated from models.dev
    (Family, InputCost, OutputCost, CacheReadCost,
    CacheWriteCost, ContextLimit, OutputLimit, InputModalities,
    OutputModalities).
  • LocalRuntime populates them for both configured models and catalog
    entries via small populateCatalogMetadata / applyCatalogMetadata
    helpers. Slice fields are cloned with slices.Clone so the
    models.dev cache stays immutable through ModelChoice.

TUI

  • The model list is now a 3-column table: name + badges on the left,
    then right-aligned Input/1M, Output/1M, and Context
    columns, with a static column header above. Missing values render
    as muted em-dashes.
  • The inline provider/model description has moved out of the row
    into a 4-line details panel below the list:
    • Referenceprovider/model (and family if not redundant)
    • Pricing — input, output, cache read/write, per 1M tokens
    • Limits — context window + max output
    • Modalitiestext, image → text
  • The panel keeps a stable height; missing data is rendered as
    "unavailable" so the dialog never resizes.
  • Styling lives in pickerRowPalette / detailsStyles instead of
    being threaded through renderer arguments. renderModel is now a
    three-line composition over renderRowName + renderRowStats, and
    the row + header share pickerNameColWidth for the table layout.
  • formatCostPerMillion keeps four decimals for sub-cent prices so
    values in (0, 0.005) don't collapse to a misleading "$0.00".

Tests

  • New tests cover the formatting helpers
    (formatCostPerMillion, formatContextCell, rightAlign,
    modelReference) and dialog rendering: column headers and per-row
    stats are present, the provider/model reference is gone from rows,
    the details panel exposes the full breakdown, and missing-info
    models fall back to "unavailable".

Preview

╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│                                         Select Model                                         │
│                                                                                              │
│  > Type to search or enter custom model (provider/model)…                                    │
│  ──────────────────────────────────────────────────────────────────────────────────────────  │
│                                                                Input/1M Output/1M Context    │
│  default_model (current)                                           $2.5       $10  128.0K    │
│  ── Other models ──────────────────────────────────────────────────────────────────          │
│  Claude Sonnet 4.5                                                   $3       $15  200.0K    │
│  Gemini 2.5 Flash                                                 $0.30      $2.5    1.0M    │
│  ──────────────────────────────────────────────────────────────────────────────────────────  │
│  Reference   anthropic/claude-sonnet-4-5 · claude family                                     │
│  Pricing     $3 in · $15 out · $0.30 cache read · $3.75 cache write · per 1M tokens          │
│  Limits      200.0K context window · 8.2K max output                                         │
│  Modalities  text, image → text                                                              │
│                                                                                              │
│                            ↑/↓ navigate  enter select  esc cancel                            │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯

Validation

mise lint, full mise test, and mise build all pass.

Assisted-By: docker-agent

The `/model` dialog now reads as a proper table with a details panel,
making it much easier to compare models and pick one. Catalog metadata
(pricing, context window, modalities, family) is pulled best-effort
from the models.dev store and surfaced both inline and in a stable
panel below the list.

Runtime
- Extend `runtime.ModelChoice` with optional `Family`, `InputCost`,
  `OutputCost`, `CacheReadCost`, `CacheWriteCost`, `ContextLimit`,
  `OutputLimit`, `InputModalities`, `OutputModalities` fields.
- Populate them from models.dev for both configured models
  (`AvailableModels`) and catalog entries (`buildCatalogChoices`) via
  small `populateCatalogMetadata` / `applyCatalogMetadata` helpers
  (slice fields cloned with `slices.Clone` so cached models stay
  immutable through `ModelChoice`).

TUI
- Render the model list as a 3-column table: name + badges on the
  left, then right-aligned `Input/1M`, `Output/1M`, and `Context`
  columns, with a static column header above. Missing values render
  as muted em-dashes.
- Drop the inline `provider/model` description from each row and move
  it into a 4-line details panel under the list, alongside the full
  pricing breakdown (input, output, cache read/write, per-1M tokens),
  context/output limits, and modality flow (`text, image → text`).
  The panel keeps a stable height; "unavailable" replaces missing
  data so the dialog never resizes.
- Pack row styling into a `pickerRowPalette` (built by
  `pickerRowStyles(selected)`) and details styling into
  `detailsStyles`, so renderers don't pass a swarm of styles around.
- Compose `renderModel` from `renderRowName` + `renderRowStats` and
  share `pickerNameColWidth` between row and header for one source of
  truth on table layout.
- `formatCostPerMillion` keeps four decimals for sub-cent prices so
  values in (0, 0.005) don't collapse to a misleading "$0.00".

Tests
- Cover formatting helpers (`formatCostPerMillion`, `formatContextCell`,
  `rightAlign`, `modelReference`) and dialog rendering: column
  headers and per-row stats are present, the provider/model reference
  is gone from rows, the details panel exposes the full breakdown,
  and missing-info models fall back to "unavailable".

Assisted-By: docker-agent
@dgageot dgageot requested a review from a team as a code owner April 25, 2026 18:00
@dgageot dgageot merged commit fc66835 into docker:main Apr 26, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants