Skip to content

feat(latency): pass route-selection spend metadata to LiteLLM#54

Open
eric-liu-nvidia wants to merge 2 commits into
mainfrom
eric-liu/switch-914-pass-route-selection-metadata-from-switchyard-to-litellm
Open

feat(latency): pass route-selection spend metadata to LiteLLM#54
eric-liu-nvidia wants to merge 2 commits into
mainfrom
eric-liu/switch-914-pass-route-selection-metadata-from-switchyard-to-litellm

Conversation

@eric-liu-nvidia

@eric-liu-nvidia eric-liu-nvidia commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

For tokenomics/spend reporting (SWITCH-914), a LiteLLM front proxy needs to tie its provider spend-log rows back to the Switchyard logical route that selected them. This adds both halves of that contract to the latency-service backend:

  • Outbound: every upstream attempt is stamped with an x-litellm-spend-logs-metadata request header whose JSON payload records the route selection (router_model, router_strategy: "latency", router_selected_endpoint, router_selected_model, router_selected_provider) plus a per-request uuid4 router_correlation_id. One correlation id is shared across failover attempts; the selection fields are re-stamped per attempt so each provider spend-log row records the endpoint it actually hit.
  • Inbound: Switchyard responses return the successful attempt's selection as x-switchyard-router-model, x-switchyard-selected-model, x-switchyard-selected-provider, and x-switchyard-router-correlation-id (streaming included). Error responses also carry them when the failure happened after a billed upstream success (e.g. a response-translation error following an upstream 200), so the provider row's correlation id stays joinable.

Design notes

  • Always-on, no config flag — consistent with the existing default X-Switchyard-Version outbound telemetry header.
  • Cross-component contract: the backend writes ctx.metadata[CTX_ROUTE_SELECTION]; the endpoint layer maps it to response headers via the new switchyard/lib/endpoints/route_selection.py, shared by the success serializer (serialize_chain_result) and the error path (handle_chain_exception).
  • Header values are re-validated as header material: the client-controlled router_model is skipped when not latin-1-encodable or containing control characters, so a hostile model string can neither fail a billed response nor inject response headers. The outbound JSON header is immune (json.dumps escapes it) and still records the raw value.
  • A passthrough body carrying its own SDK-style extra_headers field keeps working: it is merged under the spend-logs header, which wins name conflicts so it cannot be spoofed.
  • serialize_chain_result now requires ctx, so a future endpoint cannot silently opt out of spend attribution.
  • Docs: new "Route-selection spend attribution" section in docs/internal/latency_service_routing.md; switchyard-lib-core skill Quick Reference row added.

Testing

  • Wire-level e2e (tests/test_route_selection_headers.py): the real OpenAILLMClient/OpenAI SDK against a loopback HTTP stub, asserting the outbound header on the wire and the response headers back — chat (non-streaming + streaming), Anthropic, and Responses ingress; hostile-model and body-extra_headers regressions; error-path semantics in both directions.
  • Backend unit tests (failover re-stamping under one correlation id, per-request id freshness, provider derivation, route_model fallback) in tests/test_latency_service_llm_backend.py.
  • uv run ruff check . clean, uv run mypy switchyard (strict) clean, full hermetic suite: 2000 passed.
  • Live round-trip through switchyard --routing-profiles … -- serve (real uvicorn, loopback stub upstream) verified both header surfaces, streaming included.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added route-selection metadata to support spend and token usage attribution across upstream provider attempts.
    • Added x-switchyard-* response headers identifying the selected model, provider, route, and correlation ID.
    • Added support for these headers in standard, streaming, and applicable error responses.
    • Improved failover tracking by recording each attempt while preserving a shared request correlation ID.
  • Documentation

    • Added guidance for configuring and correlating route-selection spend attribution.

@eric-liu-nvidia eric-liu-nvidia requested a review from a team as a code owner July 11, 2026 08:47
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

🚀 View preview at
https://NVIDIA-NeMo.github.io/Switchyard/pr-preview/pr-54/

Built to branch gh-pages at 2026-07-13 02:15 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds correlated LiteLLM spend-log metadata for each latency-service attempt, records successful route selections in request context, and exposes sanitized x-switchyard-* headers across JSON, streaming, and error responses. Tests and documentation cover failover, endpoint variants, wire behavior, and unsafe header values.

Changes

Route Selection Attribution

Layer / File(s) Summary
Upstream route recording
switchyard/lib/proxy_context.py, switchyard/lib/backends/latency_service_llm_backend.py, tests/test_latency_service_llm_backend.py, tests/_chain_test_helpers.py
Defines CTX_ROUTE_SELECTION, stamps each upstream attempt with correlated spend-log metadata, records the successful selection, and validates failover, UUID, route-model, failure, and Responses behavior.
Response header propagation
switchyard/lib/endpoints/route_selection.py, switchyard/lib/endpoints/dispatch.py, switchyard/lib/endpoints/*_endpoint.py, switchyard/lib/endpoints/upstream_error.py, tests/test_route_selection_headers.py
Sanitizes recorded selection fields and attaches x-switchyard-* headers to JSON, SSE, and error responses across endpoint paths.
Attribution contract documentation
.agents/skills/switchyard-lib-core/SKILL.md, docs/internal/latency_service_routing.md
Documents request metadata, response headers, failover semantics, sanitization, and implementation references.
Wire-level lifecycle validation
tests/test_route_selection_headers.py, tests/_chain_test_helpers.py
Exercises real loopback requests for chat, messages, responses, streaming, failures, hostile model values, and client-supplied header conflicts.

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

Poem

I’m a rabbit with headers tucked neat,
Correlation hops from request to receipt.
Failover trails leave a clear little sign,
Safe fields stream in a telemetry line.
Binky—the routes now all rhyme!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% 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 is concise and accurately summarizes the main change: propagating route-selection spend metadata to LiteLLM.
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.

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

🧹 Nitpick comments (1)
tests/test_route_selection_headers.py (1)

26-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use pytest-mock's mocker fixture instead of unittest.mock.patch.

HealthPoller.start/stop are patched directly via unittest.mock.patch.object. As per coding guidelines, tests should "use ... pytest-mock for general mocking." Swapping to the mocker fixture would align with the repo convention (no functional difference here since patches are unconditional for the fixture's lifetime).

♻️ Suggested refactor using pytest-mock
-from unittest.mock import patch
+
-        with patch.object(HealthPoller, "start"), patch.object(HealthPoller, "stop"):
+        mocker.patch.object(HealthPoller, "start")
+        mocker.patch.object(HealthPoller, "stop")
+        if True:

(requires adding a mocker: MockerFixture parameter to the latency_app fixture)

As per coding guidelines, tests/**/*.py should "use respx for HTTP mocking and pytest-mock for general mocking." The custom _OpenAICompatStub loopback server (line 226) deviates from respx too, but that's explicitly justified in the module docstring for wire-level header assertions, so it's not flagged as a separate issue.

Also applies to: 239-239

🤖 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_route_selection_headers.py` at line 26, Replace
unittest.mock.patch usage in the tests, including the HealthPoller.start/stop
patches, with pytest-mock's mocker fixture. Add the appropriate mocker fixture
parameter to latency_app and use it for the unconditional patches, removing the
patch import while preserving the existing test behavior.

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 `@switchyard/lib/backends/latency_service_llm_backend.py`:
- Around line 669-703: Update _call_endpoint’s extra_headers merge to remove or
overwrite every case variant of the protected spend-logs metadata header before
applying extra_headers, ensuring client-provided casing cannot spoof or
duplicate it. Preserve the protected header value from extra_headers and add a
test covering a differently-cased spoof key.

In `@switchyard/lib/endpoints/dispatch.py`:
- Around line 111-113: Update the response handling around
serialize_chain_result so route_selection_headers(ctx) is applied before
returning an existing Response. Merge the generated x-switchyard-* attribution
headers onto that response, preserving its existing headers and body, while
retaining the current serialization path for non-Response results.

---

Nitpick comments:
In `@tests/test_route_selection_headers.py`:
- Line 26: Replace unittest.mock.patch usage in the tests, including the
HealthPoller.start/stop patches, with pytest-mock's mocker fixture. Add the
appropriate mocker fixture parameter to latency_app and use it for the
unconditional patches, removing the patch import while preserving the existing
test behavior.
🪄 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: 95878eb8-f38a-45b0-96eb-5e01868fcada

📥 Commits

Reviewing files that changed from the base of the PR and between a2a939d and ccb0331.

📒 Files selected for processing (13)
  • .agents/skills/switchyard-lib-core/SKILL.md
  • docs/internal/latency_service_routing.md
  • switchyard/lib/backends/latency_service_llm_backend.py
  • switchyard/lib/endpoints/anthropic_messages_endpoint.py
  • switchyard/lib/endpoints/dispatch.py
  • switchyard/lib/endpoints/openai_chat_endpoint.py
  • switchyard/lib/endpoints/responses_endpoint.py
  • switchyard/lib/endpoints/route_selection.py
  • switchyard/lib/endpoints/upstream_error.py
  • switchyard/lib/proxy_context.py
  • tests/_chain_test_helpers.py
  • tests/test_latency_service_llm_backend.py
  • tests/test_route_selection_headers.py

Comment thread switchyard/lib/backends/latency_service_llm_backend.py
Comment thread switchyard/lib/endpoints/dispatch.py Outdated
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