Insight
Insight records the outcome of every upstream call to a local, append-only JSONL log: which provider, account, and model actually served (or was tried for) each requested model, and how it went — success with latency and token counts, or failure with a coarse status. The history is yours and stays on your machine; routeplane insight reads it back as a per-route summary.
Insight is pure observation. It never changes routing, never blocks a request, and is off by default. The subsystem that acts on these outcomes is the routing-influence circuit breaker — a separate feature with its own switch.
Configure
Section titled “Configure”The top-level insight: block in routeplane.yaml:
insight: enabled: true # default false — records nothing, writes no file path: insight.jsonl # relative paths resolve against the Routeplane home (~/.routeplane/) max_bytes: 10000000 # rotate at this size; default 10 MB, 0 disables rotation| Key | Default | What it does |
|---|---|---|
enabled |
false |
Record each upstream call’s outcome to the log. Off is a pure no-op: nothing is observed, no file is written. |
path |
insight.jsonl |
The append-only JSONL log. A relative path resolves against the Routeplane home (~/.routeplane/); an absolute path is used as-is. A leading ~ is not expanded — use an absolute path or a plain relative one. |
max_bytes |
10000000 (10 MB) |
Size cap before the log rotates (see Rotation). 0 disables rotation — an unbounded log, as a deliberate opt-out. |
What one line contains
Section titled “What one line contains”Each per-attempt upstream call appends one JSON line: a wall-clock stamp, the route’s identity, and the result.
{"recorded_at_ms":1789000000000,"requested_model":"claude-opus-4-1","provider":"anthropic","served_model":"claude-opus-4-1","account_label":"max-1","result":{"ok":{"latency_ms":2841,"prompt_tokens":1200,"completion_tokens":350}}}- Identity —
requested_model(what the caller asked for),provider,served_model(the concrete upstream model), andaccount_label(the subscription seat, when the target is account-scoped). - Result — one of three:
ok— the call succeeded: end-to-endlatency_msplus theprompt_tokens/completion_tokensthe upstream reported (0when it reported none).err— the call failed: the HTTP-ishstatusplus a coarseclass(credit_exhausted,upstream_server_error,timeout,rate_limitedwith the provider’sretry_after_secswhen present, orignorefor everything that isn’t routing-health evidence)."abandoned"— the stream opened and ran but the client dropped it before it completed, without an error. Recorded distinctly rather than mislabelled a success.
recorded_at_ms— Unix-epoch milliseconds when the outcome was recorded.
A streaming call records its terminal outcome exactly once: the first error the stream yielded, a clean natural end (with whole-stream duration and the last usage the stream reported), or abandonment.
Privacy
Section titled “Privacy”[!NOTE] The log never contains prompt content or upstream error messages — ever. An outcome carries model ids, provider, account label, latency, token counts, and — on failure — a numeric status and a coarse class only. Upstream error text is deliberately excluded because it can echo the prompt or a secret. This is a design guarantee, not a redaction pass.
Rotation and bounds
Section titled “Rotation and bounds”The log cannot grow without bound and cannot slow your requests down:
- Size rotation. Before a write that would push a non-empty log over
max_bytes, the log is renamed to<path>.1(replacing any previous.1) and a fresh log starts — disk use stays bounded to roughly twice the cap. One generation is kept;max_bytes: 0disables rotation. - Off the hot path. Outcomes are handed to a dedicated background writer thread through a bounded channel (depth 1024). A request never blocks on disk IO; recording failures never fail the request being observed.
- Best-effort by construction. A burst deeper than the buffer, or a broken log directory, drops records rather than stalling anything. The insight log is a routing-history convenience, not an audit trail — don’t build compliance on it.
- On shutdown the writer drains its backlog before exiting, so a graceful stop doesn’t lose the tail.
Reading it back: routeplane insight
Section titled “Reading it back: routeplane insight”routeplane insight [--config PATH] [--path PATH] [--since WINDOW] [--top N]Reads the JSONL file directly (no running daemon needed) and aggregates it per route — requested model → the provider/account/model that served it:
● what worked — 128 record(s) (lifetime) scope lifetime log /Users/you/.routeplane/insight.jsonl anthropic/max-1/claude-opus-4-1 → claude-opus-4-1 93 ok / 96 attempts, ~2841ms (2×5xx, 1×429), recovered at 2026-07-29T16:42:08.125Z openai/-/gpt-5.2 → gpt-5.2 28 ok / 32 attempts, ~1904ms (1×402, 2×timeout, 1×abandoned), last failure at 2026-07-29T16:44:31.007ZPer route you get attempts, successes, mean latency over successes, total tokens, and a failure breakdown bucketed as 402 (credit exhausted), 5xx, timeout, 429 (rate limited), other, and abandoned. Timestamped successes and routing-health failures also produce last_ok_at_ms and last_failure_at_ms in JSON. The human view says recovered at … only when the latest known success is later than the latest known routing-health failure; otherwise it keeps the latest failure visible. Ignored errors, local rejects, abandoned streams, and records whose age is unknown never fabricate recovery recency. Routes sort by requested model, then busiest first.
--config PATH— locate the log via that config’sinsight.path.--path PATH— read a specific log file, overriding the config.--top N— show only the N busiest routes by attempt count. Use it for a daily glance when the lifetime log contains many routes; omit it when a script needs the full set. It conflicts with--import, which writes rather than summarises.- The global
--jsonflag emits the summary as JSON for tooling. - A missing log reports zero records; malformed or older-schema lines are skipped, not fatal.
Scope: --since — and why the default can mislead
Section titled “Scope: --since — and why the default can mislead”Without --since, every figure above is a LIFETIME aggregate, and a lifetime rate can be arithmetically correct while being the wrong answer to the question you asked. A 137-record log once reported a 23% timeout rate for one provider; the provider was healthy, and every one of those timeouts came from a single twelve-hour upstream outage two days earlier. Read as “this provider is unreliable”, it pointed at the wrong lane.
routeplane insight --since 24h # the last dayrouteplane insight --since 24h --top 5 # five busiest routes todayrouteplane insight --since 30m # what is happening right nowrouteplane insight --since 7d # the last week- The unit suffix — one of
s,m,h,d— is required. A bare--since 24is refused rather than guessed at, because seconds, hours and days are all plausible readings. - Both renderings state their own scope: the human view prints a
scopefield oflifetimeorwindow, and the JSON emitssince_ms/until_mswhen windowed and omits them when not. A consumer can always tell which claim it received. - An empty window is reported differently from an empty log — “no records in the requested window” tells you nothing happened lately; “no insight records yet” tells you nothing ever did.
- A record whose timestamp is missing (written before the field existed) or unset has an unknown age and is excluded by any window, though a lifetime read still counts it. A record stamped in the future — written while the clock ran fast — is excluded too, rather than being allowed to haunt every later window.
--sinceconflicts with--import: import writes rather than summarises, and a window it silently ignored would be worse than one it refuses.
Boundaries
Section titled “Boundaries”- Abandoned is not failure. A client cancelling a stream says nothing about upstream health, so it’s counted in its own bucket — and the circuit breaker neither trips nor resets on it.
- The line schema evolves. Historical lines may lack newer fields; the reader skips what it can’t parse rather than failing. Don’t assume every line in an old log has every field.
- Best-effort means gaps are possible. Under extreme burst or disk trouble, records are dropped in favor of your requests. Counts are representative, not exact.