Skip to content

Architecture

Document status

This document separates verified architecture from remaining hypotheses. Diagrams and flows marked Verified are confirmed by local investigation (Phase 1B, 2026-08-02; Phase 2 read-only verification, 2026-08-02). The Approved architecture (Phase 2) section records the synthesized statusline integration design and its data sources. Unverified elements are retained as planning artifacts and clearly labeled.

System objective

Determine whether a reliable, provider-aware statusline can obtain the actual routed provider/model used by Claude Code through OmniRoute, without hardcoding assumptions or changing the upstream systems.

Logical statusline flow

Status: Verified.

Claude Code
    ↓ invokes (on session start, resume, new message, /compact, permission change)
Statusline command (node <path-to>/statusline.js)
    ↓ provides runtime input via stdin
JSON payload: model.id, model.display_name, workspace.current_dir,
              context_window.*, cost.*, session_id, transcript_path, ...
    ↓ script reads fields and renders
Statusline display
    ↓ viewed by
User

Verified (Phase 1B): The statusline is configured in ~/.claude/settings.json. The script reads model.display_name from the stdin payload. In this installation, model.display_name equals the configured ANTHROPIC_MODEL (auto/cheap) — the requested model, not the resolved route. The full documented schema is confirmed present locally. No resolved-provider field exists in the payload.

Current hypothesized routing flow

Status: Verified.

Claude Code (configured model: auto/cheap)
    ↓ request (ANTHROPIC_BASE_URL: <gateway-url>)
OmniRoute (diegosouzapw/omniroute:latest in Docker)
    ↓ automatic routing engine (combo-based)
Resolved provider/model (e.g., model: big-pickle, provider: opencode)
    ↓ upstream request
LLM provider
    ↓ response (resolved model echoed in response.metadata.model)
OmniRoute
    ↓ response (resolved model returned to client)
Claude Code
    ↓ stores per-message model in transcript JSONL

Verified (Phase 1B): - The gateway resolves the auto/cheap combo to a concrete provider/model. - The resolved route is stored per-request in the Docker volume SQLite call_logs table (columns model + provider). - The resolved model is echoed in the API response, which Claude Code records in the session transcript (per-assistant-message model field). - The statusline payload (model.display_name) reflects the requested model, not the resolved route.

Investigation boundaries

Claude Code boundary

Questions to answer:

  • How is the statusline command invoked? (Verified: configured in the Claude Code settings file; script at the configured path.)
  • What is written to stdin? (Verified: documented JSON schema; model.display_name = requested model.)
  • Which fields identify the model, workspace, context, and session? (Verified: model.id, model.display_name, workspace.current_dir, session_id, transcript_path.)
  • Does the payload contain the resolved provider/model or only the requested label? (Verified: only the requested label.)

Statusline boundary

Questions to answer:

  • What execution time and output constraints apply? (Verified: 300ms debounce; slow scripts cancelled; no token cost.)
  • Can it perform a read-only local lookup? (Verified: yes — transcript JSONL or SQLite via Docker.)
  • How should it behave when metadata is missing or unavailable? (Open: fallback behavior design is a Phase 2/3 concern.)
  • What information must never be displayed or logged? (Verified: tokens, prompts, secrets, sensitive request content.)

OmniRoute boundary

Questions to answer:

  • Where does automatic routing occur? (Verified: combo engine inside the Docker container.)
  • Which component or lifecycle stage selects the concrete provider/model? (Verified: combo step selection; route stored in call_logs.)
  • What logs, APIs, events, or request metadata expose that selection? (Verified: call_logs SQLite table; transcript per-message model field; management APIs blocked without separate token.)
  • Can the route be correlated with the Claude Code request safely? (Partially verified: per-request correlation possible via timestamp; per-session correlation blocked by NULL session_tag.)

Resolved-route data flow (verified)

Two read-only paths exist to obtain the resolved provider/model:

Path 1: SQLite (Docker volume)

OmniRoute combo engine
    ↓ writes per-request
SQLite call_logs table (omniroute-data volume)
    ↓ read-only query (docker exec / docker run -v)
Statusline script
    ↓ renders resolved provider/model
Statusline display

Verified fact: call_logs.model and call_logs.provider capture the post-routing values. Query is low-latency, read-only, and requires no auth — only Docker permissions.

Path 2: Transcript JSONL (Claude Code)

OmniRoute response (model echoed in response.metadata.model)
    ↓ API response
Claude Code (stores per-message model in transcript)
    ↓ read-only JSONL parse (tail last assistant message)
Statusline script
    ↓ renders resolved model string
Statusline display

Verified fact: Every assistant message in the transcript carries a model field with the resolved model name. No separate provider field is available in this path.

Trade-off: Path 1 provides model + provider separately. Path 2 provides only the model string but has no Docker dependency.


Approved architecture (Phase 2)

Status: Approved (2026-08-02). Records the architecture synthesized from Phase 1B verified facts and Phase 2 read-only verification. The design covers only the data sources identified below; anything not identified here is not relied upon.

Design principle

The statusline is a reader, not an inferrer. Every displayed value maps to a documented source with a defined fallback. The integration is read-only, adds no live traffic, and never surfaces secrets, prompts, or request content.

Display values and their data sources

Displayed value Primary source Fallback Verification status
Resolved model Transcript: last assistant message message.model Statusline payload model.display_name (the requested model) until the first assistant message exists Verified — present on 78/78 messages (Phase 2); values match call_logs.model
Resolved provider SQLite call_logs.provider, correlated by timestamp to the last assistant response Model-only display (provider omitted) or unknown — not a crash Verified source; correlation is heuristic — one row confirmed; shared-gateway noise limits precision
Requested model (session config) Statusline payload model.display_name / model.id Verified (Phase 1B)
Route staleness Transcript timestamp of the last assistant message vs wall clock stale marker appended to the last known value Design threshold set in Phase 3

Proposed statusline integration — logical flow

Claude Code
    ↓ invokes statusline on each event (300 ms debounce)
Statusline script
    ↓ reads stdin payload (model.display_name, session_id, transcript_path, ...)
    ↓ reads transcript tail (transcript_path)          [local, no privilege]
    ↓ parses last assistant message → message.model     [sub-ms, session-accurate]
    ↓ [optional: provider enrichment]
    ↓   docker run --rm -v omniroute-data:/data:ro <sqlite3 image>
    ↓     SELECT provider, status, timestamp FROM call_logs ...  [~0.25 s]
    ↓ renders bounded, single-line status
Statusline display

Observed: transcript message.model is nested under message (current Claude Code version); the top-level model field seen in Phase 1B has been relocated. The parser must check both.

Physical architecture

Host (macOS, Docker Desktop)
├─ Claude Code CLI
│   ├─ writes/reads transcript JSONL (~/.claude/projects/*/*.jsonl)
│   ├─ invokes statusline (node <path-to>/statusline.js)
│   └─ → HTTP → OmniRoute gateway (127.0.0.1:<gateway-port>)
│
├─ Docker Desktop VM
│   └─ container omniroute (image diegosouzapw/omniroute:latest, tag unpinned)
│       ├─ listens 127.0.0.1:<gateway-port>
│       └─ named volume <omniroute-data> → /app/data
│           ├─ storage.sqlite (call_logs, usage_history)
│           ├─ logs/application/
│           └─ call_logs/YYYY-MM-DD/ artifact files
│
└─ Statusline script (node, runs per statusline event)
    ├─ stdin payload (no privilege)
    ├─ transcript tail read (user-owned local file; no privilege)
    └─ optional SQLite read via docker run --rm -v omniroute-data:/data:ro
       (Docker permission required; read-only mount)

Component responsibilities and trust boundaries

Component Responsibility Trust boundary
Claude Code Invokes statusline; provides payload; writes transcript Owner of the contract; its schema can change between versions
Statusline script Renders a bounded, deterministic, single-line display Read-only; must never write OmniRoute data; must not read request bodies, server.env, or key tables
OmniRoute gateway Resolves auto/* combos; records routes in call_logs Untrusted label source — model/provider strings are displayed as data, never executed or used as instructions
SQLite volume (omniroute-data) Persistent store of routes, usage, artifacts Read-only via :ro mount; must never be modified
Transcript JSONL Session record including resolved model User-owned file; read-only
Docker daemon Runs container; exposes volume to throwaway readers Docker group permission required for provider-enrichment path

Secrets: the statusline never reads .claude/settings.local.json, OmniRoute server.env, api_keys, or artifact bodies. Only metadata columns are in scope: model, provider, combo_name, status, timestamp, requested_model, combo_step_id.

Candidate integration options and tradeoffs

Option A — Transcript-only (model display).

  • Resolved model from message.model; no provider.
  • Pros: sub-millisecond; session-accurate; no Docker; 100% field presence observed.
  • Cons: no provider field; the project's core objective (provider-aware) is not met.
  • Verified dependencies: a readable transcript.

Option B — Transcript + SQLite provider enrichment (recommended).

  • Model from transcript (fast, authoritative); provider from call_logs.provider correlated by timestamp.
  • Pros: meets the project objective (provider-aware display); model source remains fast.
  • Cons: ~0.25–1.1 s (Docker startup; a pre-baked sqlite3 image is required, not apk add at read time); shared-gateway noise (connection-test rows, other clients) requires filtering; session_tag is NULL so session-level filtering is impossible; requires Docker and sqlite3 in a throwaway image.
  • Verified dependencies: Docker running; volume accessible :ro; timestamp correlation confirmed (Phase 2: one row matched within 68 ms).

Option C — SQLite-only.

  • Provider and model both from call_logs.
  • Cons: all of Option B's latency and ambiguity, plus it cannot distinguish this session's requests from other gateway clients (no session correlation) — a raw "last row" would show connection-test health checks or other clients' traffic.
  • Verified: not viable as a primary path when a gateway is shared.

Option B with a defined fallback chain. The resolved model is the reliable core (transcript, fast, authoritative). Provider display is best-effort, architecturally subordinate, and has a defined degradation path:

  1. Resolved model: always from transcript message.model — sub-ms, session-accurate.
  2. Provider: optional enrichment, one of:
  3. SQLite timestamp correlation (authoritative; slow; requires Docker);
  4. empirical model→provider mapping (fast; local; heuristic — Phase 2 finding 7 shows the mapping is currently unambiguous across observed providers, but may drift as providers add/remove models);
  5. omitted (model-only display).
  6. Fallback: when provider enrichment is unavailable or fails → model-only display. A missing provider never causes a wrong display or a crash.

The design must not depend on the provider being correct. Provider display is a nice-to-have built atop the reliable model core, not the reverse.

Failure and fallback behavior

Condition Behavior
No assistant message yet in transcript Display requested model (model.display_name) from the payload
Transcript missing or unreadable Display requested model from the payload; no crash
Transcript has a malformed line Skip the malformed line; use the last parseable assistant message
message.model absent on the last assistant message Use the previous assistant message; then the payload model
SQLite read unavailable (Docker down or image missing) Omit provider; display model-only
SQLite read fails or no row matches the timestamp Omit provider; display model-only
Last resolved model older than the staleness threshold Append a stale marker to the last known value
Statusline script exceeds the cancellation timeout Claude Code cancels it; nothing displayed (documented Claude Code behavior)

No failure mode exposes tokens, prompts, or request content.

Version-change behavior

  • Claude Code transcript structure. Phase 2 observed model relocated from the assistant message top level to message.model. The parser checks message.model, then top-level model, and treats an absent field as "no resolved model yet" rather than failing.
  • OmniRoute image tag is latest (unpinned). The call_logs schema, combo_name semantics, or column names could change on update. The SQLite query selects named columns and treats a missing column or empty combo_name as "no route info."
  • Statusline payload schema. Only model.display_name and model.id are consumed, with the documented || fallback between them.

Non-functional requirements (updated)

  • Resolved-model latency: sub-millisecond (transcript tail read; verified).
  • Provider enrichment: optional; must tolerate cancellation; must use a pre-baked sqlite3 image (never apk add at read time — latency budget is ~300 ms per documented debounce).
  • Read-only: transcript is a user-owned file (no privilege); SQLite path uses :ro volume mount.
  • Statically-allowlisted read path: statusline commands must not depend on the auto/cheap classifier evaluation (Phase 1B gating finding; circular dependency risk).
  • Output: bounded, deterministic, single-line; never expose tokens, prompts, or request content.
  • Tolerance: each statusline event re-reads; route changes between invocations are expected and displayed without error.
  • Failure modes: see table above; no crash in any case.

Design decisions (resolved in Phase 3)

The four decisions deferred from this architecture were resolved in the Phase 3 design (Statusline/Design/):

  • Staleness threshold: 5 minutes by default (configurable constant).
  • Empirical mapping: versioned JSON at ~/.claude/statusline-model-provider.json, refreshed out-of-band by a Scripts/ maintenance script (never at read time); treated as a heuristic per ADR-0006. Since V2 it also carries an optional sibling contextWindows block (resolved model's real window from model_capabilities.limit_context, payload as fallback); provider resolution is unchanged.
  • Provider display: defaults to the empirical mapping; SQLite enrichment is a documented optional path (off by default, gated on a pre-baked sqlite3 image); missing provider degrades to model-only.
  • Output format: bounded single line provider/model (model-only when the provider is unknown) with a [stale] marker past the threshold.

Details, evidence, and acceptance criteria are in the Phase 3 design documents. The design was approved on 2026-08-02 (ADR-0009), closing the Phase 3 gate; Phase 4 (Implementation) is approved to begin in a new session.


Proposed evidence chain

External claim
    ↓ preserve verbatim
Research/
    ↓ test locally
Findings/
    ↓ review and promote
Knowledge-Base.md
    ↓ update when material
Architecture.md / Decisions.md

Candidate source-code areas

External research referenced possible routing components such as chat.ts, virtualFactory.ts, engine.ts, and scoring.ts. These names are research leads only; their relevance, paths, and behavior must be confirmed against the actual local or authoritative source tree before being recorded as architecture.

Non-functional requirements to establish

  • Low enough latency for interactive statusline use.
  • Read-only behavior during discovery and, preferably, in the final integration.
  • Graceful degradation when OmniRoute or a metadata source is unavailable.
  • No secrets, prompts, tokens, or sensitive request data in output or logs.
  • Deterministic formatting and bounded output size.
  • Clear behavior when the routed provider/model changes.

Architecture approval criteria

Architecture can move from hypothesis to approved design only when:

  1. The source of every displayed value is identified.
  2. The automatic-routing resolution path is demonstrated locally or explicitly marked unavailable.
  3. The integration does not depend on undocumented behavior without a fallback.
  4. Failure, timeout, privacy, and version-change behavior are documented.
  5. The decision is recorded in Decisions.md.