Skip to content
Ari Mayer edited this page Mar 23, 2026 · 1 revision

Architecture

This page describes the internal architecture of models for contributors and developers. It covers the module structure, data flow, async patterns, and key design decisions.

Tech stack

  • Language: Rust (stable)
  • TUI framework: ratatui with crossterm backend
  • Async runtime: tokio
  • HTTP client: reqwest with rustls-tls-native-roots (loads certificates from the OS trust store)
  • Serialization: serde + serde_json + toml
  • CLI framework: clap (derive)
  • Markdown parsing: comrak (CLI only), custom regex-based converter (TUI)

High-level data flow

                    ┌──────────────┐
                    │  models.dev  │
                    │     API      │
                    └──────┬───────┘
                           │
    ┌──────────────┐       │       ┌──────────────┐
    │  Artificial  │       │       │   GitHub      │
    │  Analysis    │       │       │   REST API    │
    │  (CDN)       │       │       │               │
    └──────┬───────┘       │       └──────┬────────┘
           │               │              │
           ▼               ▼              ▼
    ┌─────────────────────────────────────────────┐
    │              tokio::spawn tasks              │
    │         (background fetch workers)           │
    └──────────────────┬──────────────────────────┘
                       │ mpsc channels
                       ▼
    ┌─────────────────────────────────────────────┐
    │              Message enum                    │
    │         (ModelsLoaded, BenchmarksLoaded,     │
    │          AgentsLoaded, StatusLoaded, ...)    │
    └──────────────────┬──────────────────────────┘
                       │
                       ▼
    ┌─────────────────────────────────────────────┐
    │              App::update()                   │
    │         (state mutation)                     │
    └──────────────────┬──────────────────────────┘
                       │
                       ▼
    ┌─────────────────────────────────────────────┐
    │              draw()                          │
    │         (render current state)               │
    └─────────────────────────────────────────────┘

Elm architecture

The TUI follows an Elm-architecture pattern:

  1. Events arrive as crossterm KeyEvents or async Messages via mpsc channels
  2. event.rs maps key events to Message variants using the NavAction dedup pattern
  3. App::update() processes messages and mutates state
  4. draw() renders the current state to the terminal

There are no callbacks. All state changes flow through the Message enum.

Module structure

TUI modules

src/tui/
├── mod.rs              # Startup, event loop, async channel handling
├── app.rs              # App struct, Tab enum, Message enum, update() logic
├── event.rs            # Keybinding -> Message mapping, NavAction dedup
├── ui.rs               # draw(), shared helpers (focus_border, caret, selection_style)
├── markdown.rs         # Custom regex-based markdown converter
├── models/
│   ├── app.rs          # ModelsApp state, Focus, Filters, SortOrder
│   └── render.rs       # Models tab rendering
├── agents/
│   ├── app.rs          # AgentsApp state, AgentFocus, AgentSortOrder
│   └── render.rs       # Agents tab rendering, picker modal
├── benchmarks/
│   ├── app.rs          # BenchmarksApp state, BenchmarkFocus, BottomView
│   ├── render.rs       # Benchmarks tab rendering
│   ├── compare.rs      # H2H table, scatter plot
│   └── radar.rs        # Radar chart rendering
├── status/
│   ├── app.rs          # StatusApp state, StatusFocus, panel focus enums
│   ├── render.rs       # Status tab dispatch + shared helpers
│   ├── overall.rs      # Overall dashboard rendering
│   └── detail.rs       # Provider detail rendering
└── widgets/
    ├── scrollable_panel.rs  # ScrollablePanel (bordered scroll with scrollbar)
    ├── scroll_offset.rs     # ScrollOffset (Cell<u16> for render-time writeback)
    ├── soft_card.rs         # SoftCard (health-colored accent stripe cards)
    └── comparison_legend.rs # ComparisonLegend (benchmarks compare views)

Each tab follows the same pattern: app.rs for state and types, render.rs for drawing. The tab's mod.rs re-exports via pub use app::*.

Data modules

src/
├── data.rs                # Provider/Model data structures (models.dev API)
├── api.rs                 # Model data fetching
├── config.rs              # User config file (TOML)
├── formatting.rs          # Shared utilities (truncate, parse_date, format_tokens, etc.)
├── provider_category.rs   # Provider categorization logic
├── agents/
│   ├── data.rs            # Agent, GitHubData, AgentEntry types
│   ├── loader.rs          # Load embedded agents.json
│   ├── detect.rs          # Version detection (runs CLI commands)
│   ├── github.rs          # GitHub API fetching (releases, repo metadata)
│   ├── cache.rs           # Disk cache with ETag conditional fetching
│   ├── changelog_parser.rs # Comrak AST -> normalized IR (ChangelogBlock)
│   └── health.rs          # Agent-to-status-provider mapping
├── benchmarks/
│   ├── store.rs           # BenchmarkStore/BenchmarkEntry types
│   ├── fetch.rs           # CDN fetcher
│   └── traits.rs          # AA <-> models.dev matching
└── status/
    ├── types.rs           # Status IR types (ProviderStatus, Incident, etc.)
    ├── registry.rs        # Provider registry and strategy mapping
    ├── assessment.rs      # Health assessment logic
    ├── fetch.rs           # Multi-source status fetching
    └── adapters/          # Per-platform parsers (Statuspage, BetterStack, etc.)

CLI modules

src/cli/
├── mod.rs           # Module index
├── picker.rs        # PickerTerminal, nav helpers, shared styles
├── models.rs        # Models picker (filter, sort, preview)
├── benchmarks.rs    # Benchmarks picker
├── agents.rs        # Clap schema, command dispatch
├── agents_ui.rs     # Release browser, changelog search, source picker
├── list.rs          # `models list` subcommand
├── search.rs        # `models search` subcommand
├── show.rs          # `models show` subcommand
└── styles.rs        # Shared CLI colors

All three inline pickers (models, benchmarks, agents) use ratatui Viewport::Inline with the shared PickerTerminal wrapper for raw mode lifecycle.

Async pattern

Background fetches use tokio::spawn + mpsc channels:

  1. At startup, background tasks are spawned for model, benchmark, agent, and status data fetching
  2. Each task sends results through an mpsc channel as Message variants
  3. The main event loop polls both the terminal (key events) and the mpsc receiver
  4. When a message arrives, App::update() processes it and the next draw() call reflects the new state

The app never blocks on network calls. Loading states are shown until data arrives.

NavAction dedup

event.rs defines a NavAction enum to avoid duplicating keybinding logic:

NavAction: Down | Up | First | Last | PageDown | PageUp | FocusLeft | FocusRight | Search | ClearEsc

parse_nav_key() maps crossterm KeyCode to NavAction (handling vim keys, arrow keys, Page Up/Down, etc. in one place). Each tab-specific handler then converts NavAction to tab-specific Message variants.

Key design decisions

  • No disk cache for benchmarks -- data is fetched fresh from CDN on every launch. The store is empty until the CDN responds.
  • Embedded agent catalog -- data/agents.json is compiled into the binary via include_str!. No runtime file dependency.
  • ETag conditional fetching for GitHub -- minimizes API calls. Cache stores ETags alongside data; 304 responses reuse cached data.
  • ScrollOffset(Cell<u16>) -- interior mutability enables render-time scroll clamping and writeback without requiring &mut self in render functions.
  • rustls-tls-native-roots -- uses OS certificate store instead of bundled certs, supporting corporate TLS-inspecting proxies.
  • Separate markdown renderers -- CLI uses comrak (CommonMark AST) for plain-text changelog output; TUI uses a custom regex-based converter that preserves inline formatting for styled ratatui rendering.

GitHub Actions

Workflow Trigger Purpose
ci.yml PR/push Format check, clippy, tests
release.yml v* tags Build 5 targets, package .deb/.rpm, publish to crates.io, update AUR
update-benchmarks.yml Every 30 min Fetch AA API, commit if data changed

Pre-release tags (containing -) skip crates.io publish and AUR update, and mark the GitHub release as prerelease.

Adding a new tab

  1. Create src/tui/{tab}/ with mod.rs, app.rs, render.rs
  2. In mod.rs: pub mod app; pub(in crate::tui) mod render; pub use app::*;
  3. Add pub mod {tab}; to tui/mod.rs
  4. Add {Tab} variant to Tab enum in tui/app.rs
  5. Add tab-specific Message variants
  6. Implement update() handlers in tui/app.rs
  7. Add render call in ui.rs
  8. Add keybinding handlers to event.rs using NavAction pattern
  9. Add footer hints and help text to ui.rs

Clone this wiki locally