Skip to content

feat(routing): record durable route decision traces - #1003

Merged
Wibias merged 7 commits into
lidge-jun:devfrom
Wibias:feat/ri-01-route-decision-traces
Aug 4, 2026
Merged

feat(routing): record durable route decision traces#1003
Wibias merged 7 commits into
lidge-jun:devfrom
Wibias:feat/ri-01-route-decision-traces

Conversation

@Wibias

@Wibias Wibias commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

RI-01 of the Router Intelligence / Routing Control Plane programme
(devlog/_plan/260804_router_intelligence/000_master_plan.md). Adds a
versioned, bounded, privacy-safe route decision trace for every existing
deterministic routing path, persisted through the canonical usage.jsonl
ledger and hydrated into request-log DTOs.

The trace answers: which route kind resolved the request, which candidates
were considered, why each was excluded or skipped, and what was selected -
without changing any routing behavior.

Scope

  • New src/routing/trace.ts: RouteDecisionTraceV1 types, bounded builder,
    defensive normalizer, explicit truncation metadata.
  • src/router.ts: RouteResult now carries routeKind / routeReason /
    routeDecision. routeModel() records a trace for all five existing route
    kinds: explicit-account, explicit-provider, native, combo,
    default-provider. Combo routes trace every configured target with
    eligibility and exclusion reasons (unconfigured, disabled, cooldown,
    already-attempted, not-selected).
  • Persistence: PersistedUsageEntry.routeDecision (additive field through the
    existing whitelist normalizer in src/usage/log.ts).
  • Hydration: RequestLogContext/RequestLogEntry carry the trace;
    addFinalRequestLog, addRequestLog (-> usage.jsonl), and
    requestLogEntryFromPersistedUsage (/api/logs) round-trip it.
  • Capture points: /v1/responses (core.ts, incl. subagent fallback
    re-routes), /v1/responses/compact, /v1/chat/completions,
    /v1/messages routed path, and the Codex-account-qualified web-search path.

Selection vs execution

The trace records the selection decision before dispatch. Fallback
execution attempts remain the existing attempts[] array on the usage
entry; the two are never merged.

Bounds (deterministic)

  • Candidates per trace: 8 (selected candidate is never dropped by truncation)
  • Exclusions per candidate: 16
  • Requirements per trace: 16
  • Strings: 128 chars
  • Serialized trace: 16 KiB budget with a deterministic detail-drop fallback
  • Truncation always sets truncated.{candidates,exclusions,strings}

Privacy / security

  • Never persisted: prompts, message bodies, tool payloads, API keys, OAuth
    tokens, raw account emails, raw quota responses, authorization headers,
    hidden reasoning, raw upstream bodies.
  • The builder only receives provider/model name strings and opaque account
    references; account-namespace routes record the user-chosen namespace handle,
    never the underlying account id or credential.
  • bun run privacy:scan passes.

Compatibility

  • Old usage.jsonl rows (no routeDecision) parse unchanged.
  • New rows are additive and valid for every existing reader (/api/logs,
    /api/usage, per-key rollups).
  • No routing behavior change: routeModel() provider/model resolution is
    byte-identical; the trace is a pure addition.

Dependency

  • Base: dev at e44d234f08e03dd4dbf0c4aa13af43046d86b0a6
    (upstream head, 2026-08-04). This PR is the first in a 10-PR stacked
    programme; later PRs build on it. Nothing is merged by this programme.

Non-goals

  • No policy/routing-profile execution yet (RI-04/05).
  • No health/quota/cost scoring (RI-06/07/08).
  • No analytics or explainability APIs (RI-03/09).
  • No GUI changes (RI-10).

Local verification (exact)

  • bun x tsc --noEmit -> PASSED (0 errors)
  • bun run test tests/route-decision-trace.test.ts -> 14/14 pass
    (75 assertions)
  • Focused regression suites (combos, codex-routing, usage-log, request-log,
    combo-management-api, codex-account-namespaces) -> 253/253 pass
  • bun run test tests/server-combo-failover-e2e.test.ts -> 44/44 pass
  • bun run privacy:scan -> passed
  • Full-suite run on the clean base is in progress out-of-band; results are
    recorded in the stack ledger
    (devlog/_plan/260804_router_intelligence/001_pr_stack_status.md).

Tests added

tests/route-decision-trace.test.ts covers: all five route kinds,
combo-candidate eligibility/exclusions, candidate-count truncation with the
selected candidate preserved, exclusion/string caps, credential absence,
JSONL round-trip + hydration, legacy rows, corrupt rows, hand-edited
oversized rows, and determinism.

Stack_Overview

Summary by CodeRabbit

New Features

  • Added privacy-safe route-decision details to request and usage records.
  • Records now show selected provider/model, decision reason, candidate options, and eligibility information across supported routing paths.
  • Combo routing preserves one decision history across successful and failed requests.
  • Decision details are bounded while protecting sensitive data.

Bug Fixes

  • Improved handling of legacy, malformed, or oversized routing data during log loading.

Documentation

  • Added planning and status documentation for future routing intelligence and explainability features.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e1b3f685-d4f7-4042-b720-1a7fac6cb4f6

📥 Commits

Reviewing files that changed from the base of the PR and between 414861d and f6b4bce.

📒 Files selected for processing (3)
  • src/routing/trace.ts
  • tests/route-decision-trace.test.ts
  • tests/server-combo-failover-e2e.test.ts

📝 Walkthrough

Walkthrough

The PR adds a versioned route-decision trace contract, classifies routing outcomes, captures combo evidence, propagates traces through request and usage logs, normalizes persisted data, and adds coverage for routing, privacy, truncation, persistence, and determinism.

Changes

Router Intelligence trace capture

Layer / File(s) Summary
Programme contract and stack ledger
devlog/_plan/260804_router_intelligence/*
The planning files define the trace schema, routing architecture, privacy rules, compatibility behavior, acceptance criteria, rollback rules, and stack status.
Trace contract, bounds, and normalization
src/routing/trace.ts
Adds RouteDecisionTraceV1, evidence types, bounded construction, deterministic truncation, decision IDs, and defensive normalization.
Route classification and trace assembly
src/router.ts
Adds route kinds and reasons, combo candidate evidence, and trace metadata for explicit, native, account, combo, configured, and fallback routes.
Request and usage log propagation
src/server/*.ts, src/server/responses/*.ts, src/server/request-log.ts, src/usage/log.ts
Handlers store route decisions in logging context. Request and usage logs serialize, hydrate, and normalize traces. Combo flows preserve the parent trace.
Trace behavior and persistence tests
tests/route-decision-trace.test.ts, tests/server-combo-failover-e2e.test.ts
Tests route selection, evidence limits, privacy filtering, persistence, malformed rows, oversized fields, deterministic content, and combo trace retention.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RequestHandler
  participant routeModel
  participant RequestLog
  participant UsageLog
  Client->>RequestHandler: submit model request
  RequestHandler->>routeModel: resolve model route
  routeModel-->>RequestHandler: route and RouteDecisionTraceV1
  RequestHandler->>RequestLog: store routeDecision in log context
  RequestLog->>UsageLog: serialize normalized routeDecision
  UsageLog-->>RequestLog: hydrate valid trace or omit invalid trace
Loading

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: recording durable route decision traces for routing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@devlog/_plan/260804_router_intelligence/000_master_plan.md`:
- Around line 163-183: Align the RouteDecisionTraceV1.requestedModel schema
documentation with the enforced 128-character MAX_TRACE_STRING limit. Update the
requestedModel comment and related contract references near the trace string
limits so values up to 128 characters are the documented maximum; keep the
existing normalizer behavior unchanged.
- Line 60: Make the locale acceptance requirement consistent across the master
plan: update the references around the six listed locale files and the
acceptance criteria at lines 107-108 and 393-398 to require en, de, ja, ko, ru,
and zh, or explicitly document the intentional exclusion of zh. Ensure
documentation and GUI acceptance checks use the same locale scope.

In `@devlog/_plan/260804_router_intelligence/001_pr_stack_status.md`:
- Line 42: Update the RI-01 status ledger to record the exact SHA of the commit
used for the passing verification results instead of leaving the head or final
commit as pending; keep the reviewed commit distinct from the eventual final
commit, and replace the final SHA placeholder only after that commit is created.
Apply the same correction to the related entries in the RI-01 status section.

In `@src/router.ts`:
- Around line 538-556: Update routeModel to resolve route.combo’s configuration
once, omit tieBreak when that lookup is undefined, and derive it only from the
resolved strategy. Pass the resolved combo into comboRouteCandidates so
candidate generation reuses it instead of calling getCombo again.

In `@src/routing/trace.ts`:
- Around line 322-338: Update enforceByteBudget to measure serialized JSON in
UTF-8 bytes rather than UTF-16 code units: compute each JSON.stringify result
once and compare its encoded byte length against MAX_TRACE_BYTES. Apply this to
both the initial trace and slimmed trace checks while preserving the existing
truncation behavior and candidate slicing.
- Around line 474-478: Update the evidence-block construction around
parseCapability, parseHealth, parseQuota, parseCost, and parseScore to compute
each parser result once per candidate, store or bind the result locally, and
conditionally include that single value. Preserve the current behavior of
omitting falsy parsed values.
- Around line 276-279: Update the trace type and normalization flow so
requirement truncation is represented explicitly: add an optional requirements
flag to the truncated shape, change the truncation branch after slicing
requirements to set truncated.requirements instead of truncated.candidates, and
whitelist requirements in normalizeRouteDecisionTrace so the flag survives
hydration.

In `@src/server/request-log.ts`:
- Line 349: The usage row now persists routeDecision for every entry, increasing
snapshot row size and reducing the dashboard window under
OcxConfig.managementUsageMaxReadBytes. Update the request-log serialization
around entry.routeDecision to persist traces only for combo and fallback routes,
while preserving diagnostics for those routes and omitting low-value
single-candidate traces; verify representative row sizes and the resulting
snapshot row count.
- Around line 255-267: Update normalizeRouteDecisionTraceForLog and its
routeDecision caller to omit the field when normalizeRouteDecisionTrace returns
null instead of falling back to the unvalidated entry. Store the normalized
result in a local const, spread routeDecision only when it is valid, and
preserve the existing optional-field behavior for invalid persisted traces.

In `@tests/route-decision-trace.test.ts`:
- Around line 220-237: Add a regression test for
requestLogEntryFromPersistedUsage using a persisted entry with an invalid
routeDecision, such as an empty candidates array, and assert
hydrated.routeDecision is undefined. Then update
normalizeRouteDecisionTraceForLog so failed validation does not fall back to the
unvalidated entry; return the normalized valid trace or omit the trace instead.
- Around line 125-141: Add a new test case that directly exercises the
preserved-selection truncation branch at trace.ts lines 258-259 by calling
buildRouteDecisionTrace with a candidates array where the selected candidate has
a candidateIndex positioned beyond the MAX_TRACE_CANDIDATES cap. Set up the test
so only the candidate at the high index (e.g., index 11) is eligible while
others are excluded, then verify that the selected candidate survives truncation
and that selected.candidateIndex is correctly adjusted to point to its final
position in the truncated candidates array, confirming the off-by-one guard is
working as documented in the trace.ts comment at lines 254-255.
- Around line 175-182: Update the credential-trace test around the “trace never
contains credentials or prompt content” case to construct the secret sentinel at
runtime from non-token-shaped pieces, reuse that single value for both provider
configuration and the serialized-trace assertion, and replace the broad “prompt”
assertion with checks against the actual routing inputs used by this test. Run
the privacy scan afterward and adjust the sentinel construction if it still
matches the scanner.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c1f3b4ce-7ee8-4211-a1ad-12fd0baa1c9a

📥 Commits

Reviewing files that changed from the base of the PR and between e44d234 and b5a8e7c.

📒 Files selected for processing (12)
  • devlog/_plan/260804_router_intelligence/000_master_plan.md
  • devlog/_plan/260804_router_intelligence/001_pr_stack_status.md
  • src/router.ts
  • src/routing/trace.ts
  • src/server/chat-completions.ts
  • src/server/claude-messages.ts
  • src/server/request-log.ts
  • src/server/responses/compact.ts
  • src/server/responses/core.ts
  • src/server/search.ts
  • src/usage/log.ts
  • tests/route-decision-trace.test.ts

Comment thread devlog/_plan/260804_router_intelligence/000_master_plan.md
Comment thread devlog/_plan/260804_router_intelligence/000_master_plan.md
Comment thread devlog/_plan/260804_router_intelligence/001_pr_stack_status.md
Comment thread src/router.ts
Comment thread src/routing/trace.ts
Comment thread src/server/request-log.ts Outdated
Comment thread src/server/request-log.ts
Comment thread tests/route-decision-trace.test.ts
Comment thread tests/route-decision-trace.test.ts
Comment thread tests/route-decision-trace.test.ts
@Wibias
Wibias marked this pull request as ready for review August 4, 2026 17:50

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e0522b22b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/server/responses/core.ts
Comment thread src/routing/trace.ts Outdated
Comment thread src/routing/trace.ts Outdated
Comment thread src/server/responses/core.ts
Comment thread src/routing/trace.ts Outdated
Comment thread src/usage/log.ts
Wibias added 2 commits August 4, 2026 20:05
- byte-accurate trace budget (UTF-8, not code units)
- explicit truncated.requirements flag (type + normalizer whitelist)
- parse evidence blocks once per candidate (dead code removed)
- combo resolved once; tieBreak only when the combo lookup succeeds
- hydration drops invalid traces instead of forwarding them unvalidated
- regression tests: preserved-selection truncation, corrupt hydration,
  credential assertions against real routing inputs
- plan docs: requestedModel bound 128, locale scope made explicit
- ledger: record reviewed commit SHAs
@Wibias

Wibias commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

[GD] Full review — feat(routing): record durable route decision traces (#1003)

Reviewed heads: b5a8e7c4c (implementation) → 2e0522b2 (privacy fix) →
1f3ace9f (CodeRabbit round) → 3d7b086e (byte-budget fix). Final reviewed
head: 3d7b086e.

Verdict

Approve for merge pending the maintainer's final call. No Critical or High
findings remain; Medium/Low findings are resolved or explicitly kept with
rationale. CI is green on the final head (ci, gates, macos, npm-global,
react-doctor, enforce-target, changes, label).

CodeRabbit findings (12) — triage

Resolved (10):

  • Master plan: requestedModel documented bound corrected from 256 to the
    enforced 128; locale scope made explicit (GUI six locales
    en/de/ja/ko/ru/zh; docs-site five locales en/ja/ko/ru/zh-cn — no German
    docs edition by design).
  • Ledger: RI-01 section records the actual reviewed commit SHAs.
  • router.ts: the combo config is resolved once; tieBreak is only emitted
    when the combo lookup succeeds (no more fabricated "failover"); the
    resolved combo is passed into comboRouteCandidates (one normalization
    instead of three).
  • trace.ts: requirement truncation now sets an explicit
    truncated.requirements flag (type + builder + normalizer whitelist).
  • trace.ts: the 16 KiB budget is measured in UTF-8 bytes, not UTF-16 code
    units.
  • trace.ts: each evidence block is parsed once per candidate (five helpers
    x two calls removed); unused capOptionalString deleted.
  • request-log.ts: the hydration guard drops invalid persisted traces
    instead of forwarding them unvalidated (?? entry removed); the value is
    computed once into a local const.
  • Tests: regression for the selected-past-the-cap truncation branch;
    regression for corrupt-trace hydration; credential test now asserts
    against the real routing inputs (apiKey, baseUrl, upstream URL) instead
    of a broad "prompt" substring.

Kept with rationale (2):

  • Persist the trace on every row (not only combo/fallback): single-candidate
    traces are ~200 bytes bounded, the programme contract mandates one trace
    per routing decision, and explainability for ordinary routes depends on it.
    The snapshot-read-cap concern is noted for measurement in the stack PRs.
  • Docstring coverage: docstrings added to the trace helpers; the remaining
    threshold gap is a bot preference, not a correctness gate.

Additional finding from the author review pass (not in CodeRabbit)

  • trace.ts byte-budget fallback (second stage) sliced candidates to 4
    without re-pointing selected.candidateIndex — the selected candidate
    could fall out of the trace or the index could point past the kept list.
    Fixed: the winner is preserved at the last kept slot and the index is
    adjusted; regression test added (byte-budget fallback keeps the selected candidate and re-points the index).

Simplify candidates applied (behavior-preserving)

  • Parse-once evidence blocks (also a CodeRabbit finding).
  • Dead capOptionalString removed.
  • Combo resolved once in routeModel; candidate builder takes the resolved
    config (no repeated normalizeComboConfig).
  • Hydration guard inlined to a single local const with null-omit semantics.

Verification (exact)

  • bun x tsc --noEmit — PASSED
  • bun run privacy:scan — PASSED
  • bun run test tests/route-decision-trace.test.ts — 17/17 (added two
    regression tests)
  • Focused suites (combos, request-log, usage-log, combo e2e) — 166/166
  • CI on head 3d7b086e — green (ci, gates, macos, npm-global all platforms)

Stack note

This is the stack bottom (#1004..1018 build on it). The fixes here are
foundation-level; the child branches will absorb them when they are synced
onto dev after this PR merges. No child PR needs changes for the privacy
fix (it was already propagated by branch ancestry).

…I-01)

- combo requests persist one immutable combo trace; child adoption can no
  longer overwrite it with a concrete child route trace (P1)
- attach the initial route trace immediately after routeModel so
  pre-dispatch rejections still record the decision
- normalizer now marks every cap it applies (candidates/exclusions/
  requirements/strings) and unions it with incoming flags
- startup hydration keeps expanding the read window to the file start and
  budgets for trace-sized rows
- byte-budget fallback re-measures after shrinking and strips exclusions
  deterministically as a last resort
- tests: combo server-path trace, normalizer flags, trace-sized hydration

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/routing/trace.ts`:
- Around line 325-361: Extend enforceByteBudget after the exclusion reduction so
oversized traces continue deterministic reduction of requirements and optional
evidence, while preserving the selected candidate and its index. Repeatedly
reduce or remove these fields as needed, and perform a final
serializedByteLength(result) <= MAX_TRACE_BYTES check before returning to
guarantee the hard byte limit.
- Around line 426-431: Apply collection bounds to persisted arrays before any
validation, parsing, or traversal in the trace normalizer, including
reasoningEfforts and the candidates, exclusions, and requirements paths around
the referenced sections. Perform caps checks and mappings only on the bounded
inputs so corrupt rows cannot trigger unbounded CPU or memory use. When
truncating candidates, preserve the selected candidate and remap
selected.candidateIndex to its new position.
- Around line 548-553: Update the decisionId validation in the trace normalizer
to accept only the documented wire format: exactly 12 lowercase hexadecimal
characters. Replace the current non-empty-string check while preserving the
existing null return for invalid values and length-cap handling for valid
identifiers.

In `@tests/server-combo-failover-e2e.test.ts`:
- Around line 427-453: The existing combo test covers only successful child
adoption; add a focused exhausted-combo test near it that configures every
target to return a retryable failure and verifies both attempt receipts retain
the parent combo routeDecision (routeKind "combo", requestedModel "combo/free",
and both candidates) while recording all physical attempts. Exercise the
terminal failure path through adoptFailedChildLog without changing production
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6ae08174-dae9-4315-942d-1a7e9b04540b

📥 Commits

Reviewing files that changed from the base of the PR and between 2e0522b and 414861d.

📒 Files selected for processing (9)
  • devlog/_plan/260804_router_intelligence/000_master_plan.md
  • devlog/_plan/260804_router_intelligence/001_pr_stack_status.md
  • src/router.ts
  • src/routing/trace.ts
  • src/server/request-log.ts
  • src/server/responses/core.ts
  • src/usage/log.ts
  • tests/route-decision-trace.test.ts
  • tests/server-combo-failover-e2e.test.ts

Comment thread src/routing/trace.ts
Comment thread src/routing/trace.ts
Comment thread src/routing/trace.ts
Comment thread tests/server-combo-failover-e2e.test.ts
…mits (RI-01)

- byte budget: deterministic shrink loop (exclusions -> drop exclusions ->
  halve candidates, selected preserved) until the 16 KiB bound holds
- normalizer: slice candidates/exclusions/requirements before parsing so a
  corrupt oversized row cannot force unbounded parse work
- decisionId: enforce the documented 12-hex format on hydration
- tests: terminal combo failure keeps the combo trace through child adoption
@Wibias

Wibias commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Merging this foundation PR: every request now records a bounded, privacy-safe route-decision trace through the canonical usage.jsonl, with no routing behavior change. The remaining PRs in the stack (#1004..#1018) build on this contract; after this merge, they will be synced onto the updated dev one by one.

@Wibias
Wibias merged commit 34d21b1 into lidge-jun:dev Aug 4, 2026
22 checks passed
@Wibias
Wibias deleted the feat/ri-01-route-decision-traces branch August 4, 2026 20:09
Wibias added a commit to Wibias/opencodex that referenced this pull request Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant