fix(dwctl): refuse native thinking fields on Responses (DW-843)
Why
/v1/responses accepted provider-native thinking fields and silently ignored them, while /v1/chat/completions refuses the same fields with a 400. A request with top-level reasoning_effort, thinking: {type: disabled} or chat_template_kwargs: {enable_thinking: false} returned 200, and the model still reasoned.
onwards already has the Responses refusal list (parse_reasoning_request for /responses), but it never saw these fields. The edge translator rebuilds the request as Chat Completions and keeps only reasoning.effort (as reasoning_effort) and nvext, so every other unknown field was dropped before onwards checked the body.
What changes
- Translator:
OpenResponses::translate_requestrunsonwards::reasoning::validate_canonical_reasoning("/responses", …)on the request's unknown fields plusreasoningbefore converting it. Every Responses request goes through this in both strict and non-strict mode, including flex, background and batch rows when the daemon dispatches them. - Error envelope: a new
TranslationError::Reasoningkeeps the onwards error's status,paramandcode. A newProtocolTranslator::error_for_codehook (which defaults toerror_for_param) lets the Responses translator putcodein its envelope. The refusal therefore matches Chat Completions:type: invalid_request_error,code: unsupported_parameter,paramnaming the field, and the messageUnsupported parameter '<field>'; use 'reasoning.effort'. - Before acceptance: flex, background and realtime
background: truerequests are accepted (or queued) before translation runs. The inference middleware runs the same check on every Responses request first, so the caller gets the 400 instead of a 202 or a failed response object later. - Side effect: an invalid
reasoning.effort(for example"ultra") is now refused asreasoning.effort. Before, it was namedreasoning_effortafter translation.
Batch uploads already refused these fields: files.rs and batches.rs validate each line against its own /v1/responses URL.
Testing
Ran locally:
cargo test -p dwctl --lib inference::translation: 166 passed, including the newnative_thinking_fields_are_refused_before_translation,an_invalid_reasoning_effort_is_named_by_its_responses_pathandcanonical_reasoning_and_nvext_still_translate.cargo test -p dwctl --lib responses_refuse_native_thinking_fields_before_acceptance: passed (flex, flex with background, background, andbackground: truewith omitted,autoandprioritytiers).- With the translator check removed, the new translator test fails because the request reaches the upstream handler.
cargo fmt -p dwctlandcargo clippy --package dwctl --all-features --no-deps. The only warnings are two that were already inprofiling/mod.rs. The ZDR payload-logging guard passed.
The full suite is left to CI.