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_logsSQLite table; transcript per-messagemodelfield; 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.providercorrelated 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 addat read time); shared-gateway noise (connection-test rows, other clients) requires filtering;session_tagis 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-testhealth checks or other clients' traffic. - Verified: not viable as a primary path when a gateway is shared.
Recommended architecture¶
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:
- Resolved model: always from transcript
message.model— sub-ms, session-accurate. - Provider: optional enrichment, one of:
- SQLite timestamp correlation (authoritative; slow; requires Docker);
- 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);
- omitted (model-only display).
- 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
modelrelocated from the assistant message top level tomessage.model. The parser checksmessage.model, then top-levelmodel, and treats an absent field as "no resolved model yet" rather than failing. - OmniRoute image tag is
latest(unpinned). Thecall_logsschema,combo_namesemantics, or column names could change on update. The SQLite query selects named columns and treats a missing column or emptycombo_nameas "no route info." - Statusline payload schema. Only
model.display_nameandmodel.idare 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 addat read time — latency budget is ~300 ms per documented debounce). - Read-only: transcript is a user-owned file (no privilege); SQLite path uses
:rovolume mount. - Statically-allowlisted read path: statusline commands must not depend on the
auto/cheapclassifier 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 aScripts/maintenance script (never at read time); treated as a heuristic per ADR-0006. Since V2 it also carries an optional siblingcontextWindowsblock (resolved model's real window frommodel_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:
- The source of every displayed value is identified.
- The automatic-routing resolution path is demonstrated locally or explicitly marked unavailable.
- The integration does not depend on undocumented behavior without a fallback.
- Failure, timeout, privacy, and version-change behavior are documented.
- The decision is recorded in
Decisions.md.