Skip to content

feat(serve): route-log per-request routing stats for profile-config serve#118

Merged
ryan-lempka merged 4 commits into
NVIDIA-NeMo:mainfrom
BrianNewsom:feat/routing-log-serve-config
Jul 23, 2026
Merged

feat(serve): route-log per-request routing stats for profile-config serve#118
ryan-lempka merged 4 commits into
NVIDIA-NeMo:mainfrom
BrianNewsom:feat/routing-log-serve-config

Conversation

@BrianNewsom

@BrianNewsom BrianNewsom commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Why

When a switchyard server is run to serve a proxy, it can keep ongoing cost statistics and write them to a file. If run in the context of an evaluation, this proxy then only has aggregate evaluation level statistics, and loses fidelity of any individual request.

This diff allows segmentation of the routing stats so that a consumer can have task level cost attribution.

Summary

Extends the --routing-log-file routing log (added in #116) in three ways:

  1. Support --routing-log-file on serve --config. Previously this path raised SystemExit and rejected the flag outright; it now wires a RoutingLogResponseProcessor into every served profile and target.
  2. Per-request cache-token breakdown. Log records (and the run_manifest task rollup) now carry cached_tokens and cache_creation_tokens alongside prompt/completion/total, extracted from OpenAI Chat, Responses, and Anthropic usage shapes.
  3. GET /v1/routing/session-stats?session_id=.... A new endpoint returns session-scoped model/token aggregates from the durable log, contributed via get_endpoint() on the processor (available on both serve paths).

⚠️ Reversed boundary (please review deliberately)

#116 intentionally rejected --routing-log-file on serve --config with a SystemExit. This PR removes that guard and implements the feature there instead. This is a deliberate scope expansion, not an oversight — calling it out explicitly so it's a reviewed decision.

Why the processor attaches at the adapter (ProfileSwitchyard)

serve --config serves a heterogeneous mix — Rust-defined profiles (passthrough, random-routing, ...), Python-defined profiles, and passthrough targets. The Rust and passthrough profiles have no Python-visible response chain to inject into, so the only universal seam for a Python cross-cutting response processor is ProfileSwitchyard.call, which every profile funnels through before translation. The processor loop there mirrors the existing chain.py / noop.py pattern (SwitchyardProcessorError wrapping + ChatResponse type guard). Processors run after profile.run() returns, i.e. after any in-profile evict/retry, so the log records the final response.

Behavior with no session_id

Logging is unchanged and never gated on session identity: every request still appends one line, with session_id: null when absent. Such records are simply invisible to the new session-scoped endpoint (they can't match a queried session_id) and still roll up under their task in run_manifest.

Notes

  • snapshot_session re-reads and re-parses the full log per call (documented in its docstring) — sized for benchmark runs, not a production server with millions of sessions. The read is lock-free so snapshots don't contend with request logging.
  • RoutingLogStatsEndpoint added to switchyard/lib/endpoints/__init__.__all__ for consistency with sibling contributed endpoints.

Validation

  • ruff check clean, mypy clean (167 files)
  • Full suite green except one pre-existing, unrelated failure (test_classifier_planner_chainProxyContext missing record_submodel_call, a stale-Rust-build issue that fails identically on main)

Summary by CodeRabbit

  • New Features

    • Added routing-log support for serve --config.
    • Added session statistics at GET /v1/routing/session-stats.
    • Session statistics now include per-model call counts and token usage.
    • Routing logs now capture cached and cache-creation token counts.
    • Improved model reporting in routing logs.
  • Documentation

    • Updated CLI reference documentation for routing logs and session statistics.

@BrianNewsom
BrianNewsom requested a review from a team as a code owner July 22, 2026 22:12
…erve

Support --routing-log-file on `serve --config` (previously rejected), add
per-request cache-token breakdown, and expose GET /v1/routing/session-stats
for session-scoped aggregates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Brian Newsom <brnewsom@nvidia.com>
@BrianNewsom
BrianNewsom force-pushed the feat/routing-log-serve-config branch from 5f56008 to 31aed1d Compare July 22, 2026 22:16
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Routing logs now capture cache-related token metrics and actual models, support session aggregation through a new HTTP endpoint, and integrate routing-log processing with v2 serve --config. Profile response processing now runs before response translation.

Changes

Routing Log Statistics

Layer / File(s) Summary
Routing log processing and aggregation
benchmark/run_manifest.py, switchyard/lib/processors/routing_log_response_processor.py, tests/test_routing_log_response_processor.py, tests/test_run_manifest.py
Token extraction and manifest aggregation now include cached and cache-creation tokens; session snapshots aggregate matching JSONL records by model and ignore invalid records.
Session statistics endpoint
switchyard/lib/endpoints/*, tests/test_routing_log_response_processor.py
GET /v1/routing/session-stats returns a session snapshot or HTTP 404 when no matching calls exist.
Response processor pipeline
switchyard/lib/profiles/switchyard_adapter.py
ProfileSwitchyard accepts ordered response processors, applies them before translation, and wraps failures or invalid result types.
v2 serve integration and validation
switchyard/cli/switchyard_cli.py, tests/test_serve_profile_config.py, docs/cli_reference.md
serve --config accepts --routing-log-file, injects the processor into generated switchyards, and documents and tests session statistics.

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

Poem

I’m a rabbit with logs in my den,
Counting cache tokens again and again.
Sessions now bloom with stats bright,
Endpoints serve them just right.
Hop, hop—clean records take flight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and matches the main change: routing-log support and session stats for profile-config serve.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
switchyard/lib/endpoints/__init__.py (1)

26-59: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

RoutingLogStatsEndpoint is listed in __all__ but missing from __getattr__ — package-level import will raise AttributeError.

__all__ now advertises RoutingLogStatsEndpoint, but the lazy-loader __getattr__ has no matching branch, so from switchyard.lib.endpoints import RoutingLogStatsEndpoint fails at runtime with AttributeError: module 'switchyard.lib.endpoints' has no attribute 'RoutingLogStatsEndpoint'. This contradicts the stated goal of exposing the endpoint through the package.

🐛 Proposed fix
     elif name == "ResponsesEndpoint":
         from switchyard.lib.endpoints.responses_endpoint import (
             ResponsesEndpoint,
         )
         return ResponsesEndpoint
+    elif name == "RoutingLogStatsEndpoint":
+        from switchyard.lib.endpoints.routing_log_stats_endpoint import (
+            RoutingLogStatsEndpoint,
+        )
+        return RoutingLogStatsEndpoint
     raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
🤖 Prompt for 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.

In `@switchyard/lib/endpoints/__init__.py` around lines 26 - 59, Update the
lazy-loader __getattr__ to add a RoutingLogStatsEndpoint branch that imports and
returns RoutingLogStatsEndpoint from its endpoint module, matching the existing
lazy-loading branches and making the symbol advertised by __all__ available
through package-level imports.
🧹 Nitpick comments (2)
tests/test_serve_profile_config.py (1)

140-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert CLI-to-route-table processor propagation.

This now passes routing_log_file, but the test only checks registered models and never verifies that _cmd_serve_profile_config attached RoutingLogResponseProcessor. The later test injects the processor directly, so a regression in this forwarding path would go undetected.

🤖 Prompt for 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.

In `@tests/test_serve_profile_config.py` at line 140, Update the test using
_serve_args and _cmd_serve_profile_config to assert that the generated route
table includes RoutingLogResponseProcessor when routing_log_file is provided.
Keep the existing registered-model assertions and ensure the assertion verifies
CLI argument propagation rather than relying on the later test’s direct
processor injection.
switchyard/lib/profiles/switchyard_adapter.py (1)

59-84: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docstring doesn't mention the new failure mode. call()'s docstring says only "Run the wrapped Profile and translate its response for the caller." but the method can now raise SwitchyardProcessorError if a processor throws or returns a non-ChatResponse. As per coding guidelines, switchyard/**/*.py docstrings should "document behavior, important invariants, and relevant errors."

📝 Proposed docstring update
-        """Run the wrapped Profile and translate its response for the caller."""
+        """Run the wrapped Profile and translate its response for the caller.
+
+        Raises:
+            SwitchyardProcessorError: If a response processor raises or returns
+                a value that is not a ``ChatResponse``.
+        """
🤖 Prompt for 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.

In `@switchyard/lib/profiles/switchyard_adapter.py` around lines 59 - 84, Update
the `call()` method docstring to document that response processors may cause
`SwitchyardProcessorError` when they raise an exception or return a value that
is not a `ChatResponse`; retain the existing description of running the profile
and translating its response.

Source: Coding guidelines

🤖 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 `@docs/cli_reference.md`:
- Around line 229-231: Add --routing-log-file PATH to the v2 command synopsis
near the existing flags, keeping the documented routing-log-file option
consistent between the synopsis and flags table.

In `@tests/test_serve_profile_config.py`:
- Around line 95-103: Update the HTTP mocking in the affected test in
tests/test_serve_profile_config.py to use respx instead of the hand-written
loopback server. Replace the socket-binding server setup and route responses
through respx while preserving the test’s existing provider response payload and
assertions.

---

Outside diff comments:
In `@switchyard/lib/endpoints/__init__.py`:
- Around line 26-59: Update the lazy-loader __getattr__ to add a
RoutingLogStatsEndpoint branch that imports and returns RoutingLogStatsEndpoint
from its endpoint module, matching the existing lazy-loading branches and making
the symbol advertised by __all__ available through package-level imports.

---

Nitpick comments:
In `@switchyard/lib/profiles/switchyard_adapter.py`:
- Around line 59-84: Update the `call()` method docstring to document that
response processors may cause `SwitchyardProcessorError` when they raise an
exception or return a value that is not a `ChatResponse`; retain the existing
description of running the profile and translating its response.

In `@tests/test_serve_profile_config.py`:
- Line 140: Update the test using _serve_args and _cmd_serve_profile_config to
assert that the generated route table includes RoutingLogResponseProcessor when
routing_log_file is provided. Keep the existing registered-model assertions and
ensure the assertion verifies CLI argument propagation rather than relying on
the later test’s direct processor injection.
🪄 Autofix (Beta)

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: CHILL

Plan: Enterprise

Run ID: 5c9a1de8-6083-44c5-b750-66676dbe8180

📥 Commits

Reviewing files that changed from the base of the PR and between 097d0e4 and 5f56008.

📒 Files selected for processing (10)
  • benchmark/run_manifest.py
  • docs/cli_reference.md
  • switchyard/cli/switchyard_cli.py
  • switchyard/lib/endpoints/__init__.py
  • switchyard/lib/endpoints/routing_log_stats_endpoint.py
  • switchyard/lib/processors/routing_log_response_processor.py
  • switchyard/lib/profiles/switchyard_adapter.py
  • tests/test_routing_log_response_processor.py
  • tests/test_run_manifest.py
  • tests/test_serve_profile_config.py

Comment thread docs/cli_reference.md Outdated
Comment thread tests/test_serve_profile_config.py Outdated
@BrianNewsom

BrianNewsom commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the remaining review feedback in 5c371f72:

  • Added the missing RoutingLogStatsEndpoint lazy-loader branch and package-import coverage.
  • Added CLI-to-route-table coverage proving --routing-log-file creates exactly one processor with the requested path.
  • Documented ProfileSwitchyard.call()'s SwitchyardProcessorError contract.
  • Added --routing-log-file PATH to the serve synopsis.

I kept the loopback provider mock because this path uses Rust reqwest, which respx cannot intercept; details are in the inline reply.

Validation: Ruff passed; mypy passed (167 files); strict MkDocs passed; focused tests passed (13); full hermetic suite passed (2,020 passed, 12 skipped, 43 integration tests deselected). No live provider calls were made.

Signed-off-by: Brian Newsom <brnewsom@nvidia.com>
@BrianNewsom
BrianNewsom force-pushed the feat/routing-log-serve-config branch from a576369 to 5c371f7 Compare July 22, 2026 22:40
ryan-lempka
ryan-lempka previously approved these changes Jul 23, 2026
@ryan-lempka

ryan-lempka commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

@BrianNewsom can you share a bit more about the use-case for the endpoint? I can think of some ideas of how this would be useful - but making sure I understand your perspective and end goal for the endpoint.

Signed-off-by: Brian Newsom <brnewsom@nvidia.com>
@BrianNewsom

Copy link
Copy Markdown
Contributor Author

@BrianNewsom can you share a bit more about the use-case for the endpoint? I can think of some ideas of how this would be useful - but making sure I understand your perspective and end goal for the endpoint.

Updated the diff with a "why" section and rebased!

@ryan-lempka ryan-lempka left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

Signed-off-by: Brian Newsom <brnewsom@nvidia.com>
@ryan-lempka
ryan-lempka merged commit d0a924c into NVIDIA-NeMo:main Jul 23, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants