Skip to content

fix(translation): report the served model on every response path - #169

Merged
eric-liu-nvidia merged 1 commit into
mainfrom
eric-liu/switch-922-switchyard-ui-sometimes-stamps-switchyard-as-the-model-name
Jul 30, 2026
Merged

fix(translation): report the served model on every response path#169
eric-liu-nvidia merged 1 commit into
mainfrom
eric-liu/switch-922-switchyard-ui-sometimes-stamps-switchyard-as-the-model-name

Conversation

@eric-liu-nvidia

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

Copy link
Copy Markdown
Contributor

What

Switchyard sometimes reported the route id the client asked for as the response model, instead of the model that actually served the request. This makes every serving path and wire format report the served model.

Why

Closes SWITCH-922.

Benchmark route bundles register their route under the literal key switchyard, so the Bench runs UI labelled trajectory steps Switchyard — not a model. It showed up as a mix within one run because only some paths were affected: streamed Anthropic/Responses replies on the staged chain, and everything libsy-served. Buffered and same-format-passthrough turns already reported the real model.

The routed model was already correct in x-model-router-selected-model, /v1/routing/stats, and Intake's served_model — only the response body disagreed. The fix feeds the existing target_model plumbing from ProxyContext.selected_model (staged chain) and Decision::selected_model() (libsy) instead of from a request body.

How tested

  • uv run ruff check . / uv run mypy switchyard clean
  • uv run pytest tests/ green (1728 passed)
  • cargo fmt --check, clippy --workspace --all-targets -- -D warnings, cargo test --workspace green (611 tests)
  • Manual smoke — served a bundle whose route key is an alias against a mock upstream:
    message_start model went from my-route-aliasupstream/real-model, same for /v1/responses, /v1/chat/completions, and both buffered formats.

Each new regression test was confirmed to fail without the fix, not just pass with it.

One deselected test, tests/test_verify.py::...::test_secrets_file_wins_when_no_cli_or_env, fails identically on a clean main on this machine (local secrets.json leaking a base URL) — not a regression.

Checklist

  • One class per file; filename = snake_case of the primary class.
  • New public symbols exported from switchyard/__init__.py.__all__ — none added.
  • Unit tests added for new components / bug fixes.
  • README / --help updated if customer-facing surface changed — CHANGELOG entry added; no CLI change.
  • Commits signed off per the DCO.

Notes for reviewers

One deliberate behavior change beyond the bug fix: streamed OpenAI-chat replies now report Switchyard's configured target id (anthropic/claude-opus-4.7) rather than the provider's own string, since I converged that codec on the accessor the other two already used. It also stops emitting the literal "unknown" when a provider omits model on delta chunks. Worth a deliberate yes/no.

Knock-on: a front proxy attributing spend by response.model (e.g. LiteLLM) will now group by routed model rather than route alias.

🤖 Generated with Claude Code

@eric-liu-nvidia
eric-liu-nvidia requested a review from a team as a code owner July 28, 2026 21:27
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

Served model identity

Layer / File(s) Summary
Translation model propagation
crates/switchyard-translation/src/codecs/*, crates/switchyard-translation/src/helpers.rs, crates/switchyard-translation/tests/stream_translation.rs
Streaming codecs and response encoders now prefer the served model, with upstream model fallback when no override is present.
Translation engine routing wiring
switchyard_rust/translation.py, switchyard/lib/processors/format_translate.py, tests/test_response_translation_engine.py, tests/test_format_translate_processor.py
Routing-selected models are passed directly through buffered and streaming translation APIs and validated across wire formats.
Server and client response wiring
crates/libsy-llm-client/src/client.rs, crates/switchyard-server/src/{lib.rs,response.rs}, crates/switchyard-server/tests/server.rs, CHANGELOG.md
Server decisions and raw client encoding now stamp responses with the served model for buffered and streamed paths.
Routing regression coverage
tests/test_deterministic_routing_profile.py
Deterministic routing coverage verifies streamed responses report the selected tier model rather than the route or request model.

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

Poem

I’m a rabbit routing through the hay,
Stamping served models on display.
Streams and chunks now tell the truth,
Aliases fade, upstreams sleuth.
Hop, hop—each response knows its way!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.47% 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
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 clearly summarizes the main change: response paths now report the served model instead of the requested or route model.

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 (2)
switchyard_rust/translation.py (1)

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

Document that served_model only affects streaming translations.

served_model is threaded through response_to, response_for_request, and stream_for_request, but only the streaming branch of each actually uses it — the buffered branch always falls through to translate_response/_response_body untouched. This is intentional (the buffered body already carries the real served model from the upstream call), but none of the docstrings say so, which could mislead a future caller into thinking served_model also stamps buffered responses.

📝 Suggested docstring clarification
     def response_to(
         self,
         target: str | ChatRequestType,
         response: ChatResponse,
         *,
         served_model: str | None = None,
     ) -> ChatResponse:
-        """Return a ChatResponse wrapper in the target wire format."""
+        """Return a ChatResponse wrapper in the target wire format.
+
+        ``served_model`` only affects streaming responses (stamped into the
+        translated stream state); buffered responses keep whatever model the
+        source body already carries.
+        """

As per coding guidelines, "Add concise triple-quoted docstrings for public functions, classes, methods, and API entry points, documenting behavior, important invariants, and relevant errors."

Also applies to: 165-199

🤖 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_rust/translation.py` around lines 143 - 163, Update the docstrings
for response_to, response_for_request, and stream_for_request to state that
served_model is used only for streaming translations; buffered responses
preserve the served-model information already present in their upstream body and
are not stamped or modified by this parameter.

Source: Coding guidelines

tests/test_response_translation_engine.py (1)

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

Remove unnecessary @pytest.mark.asyncio decorators.

All five new tests in TestTranslateStampsTheServedModel add @pytest.mark.asyncio, but the repo relies on automatic asyncio mode.

♻️ Suggested fix
-    `@pytest.mark.asyncio`
     async def test_anthropic_stream_reports_the_served_model(self):
...
-    `@pytest.mark.asyncio`
     async def test_responses_stream_reports_the_served_model(self):
...
-    `@pytest.mark.asyncio`
     async def test_openai_chat_stream_reports_the_served_model(self):
...
-    `@pytest.mark.asyncio`
     async def test_buffered_response_reports_the_served_model(self):
...
-    `@pytest.mark.asyncio`
     async def test_stream_falls_back_to_the_upstream_model_without_a_selection(self):

As per coding guidelines, "tests/**/*.py: ... rely on the configured automatic asyncio mode without adding @pytest.mark.asyncio unnecessarily."

🤖 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_response_translation_engine.py` around lines 277 - 387, Remove the
unnecessary `@pytest.mark.asyncio` decorators from all five async test methods in
TestTranslateStampsTheServedModel, relying on the repository’s configured
automatic asyncio mode while leaving the test bodies unchanged.

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 `@crates/switchyard-server/tests/server.rs`:
- Around line 487-489: Remove the “SWITCH-922:” tracker reference from the
comment near the streaming codec behavior, while preserving its explanation that
codecs must report the routed target rather than the caller-addressed route ID
and why that matters to trajectory consumers.

In `@tests/test_deterministic_routing_profile.py`:
- Around line 644-652: Remove the SWITCH-922 tracker reference from the
docstring in tests/test_deterministic_routing_profile.py lines 644-652 while
preserving the clone-semantics and stats explanation; also remove “(SWITCH-922)”
from the section-header comment in tests/test_response_translation_engine.py
lines 261-264, with no other content changes.

---

Nitpick comments:
In `@switchyard_rust/translation.py`:
- Around line 143-163: Update the docstrings for response_to,
response_for_request, and stream_for_request to state that served_model is used
only for streaming translations; buffered responses preserve the served-model
information already present in their upstream body and are not stamped or
modified by this parameter.

In `@tests/test_response_translation_engine.py`:
- Around line 277-387: Remove the unnecessary `@pytest.mark.asyncio` decorators
from all five async test methods in TestTranslateStampsTheServedModel, relying
on the repository’s configured automatic asyncio mode while leaving the test
bodies unchanged.
🪄 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: 1c9787fb-ed36-46fe-aeb8-b4a2355f94a5

📥 Commits

Reviewing files that changed from the base of the PR and between 4a39c5f and 0f3b71b.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • crates/libsy-llm-client/src/client.rs
  • crates/switchyard-server/src/lib.rs
  • crates/switchyard-server/src/response.rs
  • crates/switchyard-server/tests/server.rs
  • crates/switchyard-translation/src/codecs/openai_chat/stream.rs
  • crates/switchyard-translation/src/codecs/stream.rs
  • crates/switchyard-translation/src/helpers.rs
  • crates/switchyard-translation/tests/stream_translation.rs
  • switchyard/lib/processors/format_translate.py
  • switchyard_rust/translation.py
  • tests/test_deterministic_routing_profile.py
  • tests/test_format_translate_processor.py
  • tests/test_response_translation_engine.py

Comment thread crates/switchyard-server/tests/server.rs
Comment thread tests/test_deterministic_routing_profile.py
Signed-off-by: Eric Liu <zengyuanl@nvidia.com>
@eric-liu-nvidia
eric-liu-nvidia force-pushed the eric-liu/switch-922-switchyard-ui-sometimes-stamps-switchyard-as-the-model-name branch from 0f3b71b to 5a886c2 Compare July 30, 2026 16:45
@eric-liu-nvidia
eric-liu-nvidia merged commit a3a3d31 into main Jul 30, 2026
18 checks passed
@eric-liu-nvidia
eric-liu-nvidia deleted the eric-liu/switch-922-switchyard-ui-sometimes-stamps-switchyard-as-the-model-name branch July 30, 2026 18:21
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