fix(translation): keep foreign reasoning controls out of Responses requests - #792
harshitwandhare wants to merge 1 commit into
Conversation
…quests Signed-off-by: Harshit Wandhare <harshitwandhare45@gmail.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: NVIDIA-NeMo/Switchyard/.coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review. WalkthroughThe Responses encoder now emits only supported reasoning fields, reports omitted fields through diagnostics, and applies strict-policy failures when required. Tests cover Anthropic control loss and preservation of supported Responses reasoning fields. ChangesResponses reasoning encoding
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~12 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
A rabbit guards the reasoning gate Comment |
|
Heads up for whoever picks this up: #799, opened today, fixes the same leak by a more general route. It replaces I measured both heads on the same input rather than reading the diff. At So the useful part of this PR is now the reporting, not the allowlist. If #799 goes in, I will either fold the two diagnostics tests into it or reduce this PR to the |
|
@harshitwandhare please feel free to copy any useful bits from #799 otherwise, I have closed for now. |
|
Thanks, and understood on the alignment. I am leaving this PR as it stands rather than copying Withdrawing the note I left above: with #799 closed there is nothing to settle first, and this is ready to review as it stands. Head is If the per-format direction comes back later, ping me and I will rebase onto it or close this, whichever is less work for you. |
What is broken
Translating an Anthropic Messages request into OpenAI Responses copies the inbound
thinkingobject straight into the Responsesreasoningfield.Input:
{"model": "claude-opus-5", "max_tokens": 32000, "messages": [{"role": "user", "content": "hi"}], "thinking": {"type": "enabled", "budget_tokens": 10000}}Output on
mainatbfcd023c, reproduced before writing anything:{"model": "claude-opus-5", "input": "hi", "max_output_tokens": 32000, "reasoning": {"type": "enabled", "budget_tokens": 10000}}diagnostics: [].typeandbudget_tokensare Anthropic's spelling. The Responsesreasoningobject defineseffort,summaryandgenerate_summary. So the request names a Responses field and fills it with another provider's controls, and nothing is reported.I have not sent this to a live Responses endpoint, so I am not claiming what any particular server does with it. The emitted shape is not the one Responses defines, and this repo already holds the position that Anthropic thinking has no Responses representation:
anthropic_thinking_is_dropped_from_responses_inputasserts that for thinking content blocks. The request-level control was skipping the same rule.Why
ReasoningParams.rawholds the source format's reasoning object verbatim. The Anthropic decoder fills it fromthinking(codecs/anthropic/buffered.rs:80) and the Responses decoder fromreasoning(codecs/responses/buffered.rs:69). The Responses encoder then mergedrawinto its output with no check on where it came from:That is correct when the request arrived as Responses and wrong for every other source, and nothing on
ReasoningParamsrecords which format filled it.That merge arrived in #774 two days ago, and the carry-forward is where the leak starts. Checked by running the same input at both commits:
The Responses-to-Responses behaviour #774 was after is the part worth keeping, and this change keeps it.
The fix
The Responses encoder copies only the keys Responses defines and sends the rest through
push_lossy. Under the defaultAllowWithDiagnosticsthe request goes out without the foreign keys and carries alossy_conversionwarning naming them; underRejectit errors, which is what a caller sets that policy to get. A Responses source is unaffected:effortandsummarystill carry through.The key set allowed through is
effort,summaryandgenerate_summary. If Responses takes others that I have missed, say so and I will widen it.Scope I did not take
budget_tokenscould arguably map ontoeffortinstead of being dropped. How a token budget corresponds to an effort level is a semantic call, so I left it alone.reasoning.rawwith no diagnostic, for example Anthropicthinkinginto Chat, or Responsessummaryinto Chat. The same gap in different encoders. I can send that separately if you want it.How it was verified
Three tests added to
crates/switchyard-translation/tests/request_translation.rs. Run against pristinemainwith only the tests applied:The third is the no-regression guard and passes on both trees. All three pass with the fix.
Gates run locally on the branch, in WSL Ubuntu 24.04 on the pinned 1.96.1 toolchain:
cargo fmt --all --checkcargo clippy --workspace --all-targets --locked -- -D warningscargo clippy -p switchyard-server --all-targets --features prefill-router --locked -- -D warningscargo test --workspace --lockedmainis 852 / 0, so the delta is exactly the 3 new testscargo test -p switchyard-runner --features prefill-router --lockeduv run ruff check .All checks passed!uv run mypy switchyardSuccess: no issues found in 3 source filesscanned=225 missing=0uv run pytest tests/ -q -m "not integration"The one pytest failure is
test_escalation_falls_back_on_first_stream_context_overflow, and it fails the same way onmainin the same checkout and environment, so it is not from this change. I have not chased it further.Summary by CodeRabbit
Bug Fixes
Tests