Repository navigation
translator: preserve reasoning in both directions for Anthropic-to-OpenAI with vLLM - #2956
anencore94 wants to merge 3 commits into
Conversation
…enAI with vLLM Signed-off-by: Jaeyeon Kim <anencore94@gmail.com>
✅ Deploy Preview for theagentrouter canceled.
|
|
Hi @aabchoo @nacx @yuzisun — could you please approve the CI workflow run and take a look at this PR when you have a moment? It addresses both the response ( |
|
We're hitting the same problem described in #2937 and would like to see this land. Our setup: Envoy AI Gateway runs in front of vLLM, which serves reasoning models. Clients call the Anthropic Messages API ( Symptom:
What we found while debugging:
This PR's response-side change addresses exactly that cause, and the precedence ( Related PR: #2945 by @hustxiayang fixes the same response-side problem in nearly the same way: it reads @aabchoo @nacx @yuzisun, can we consider this one to be merged to fix the reasoning functionality for vLLM? Thanks @anencore94! |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
Will take a look sometime this week or next. Could you fix the codecov for now? |
| if len(rc.ReasoningContent.RedactedContent) > 0 { | ||
|
|
||
| // Fall back to Reasoning (used by vLLM >= v0.30.0, which renamed reasoning_content to reasoning). | ||
| if msg.Reasoning != nil && *msg.Reasoning != "" { |
There was a problem hiding this comment.
fall back to reasoning in both non-streaming and streaming response translation when reasoning_content is unset or empty
This function doesn't actually do that? This only checks if reasoning is valid
There was a problem hiding this comment.
Fixed in 9501f60. The nonempty string branch already returned early, but an empty structured reasoning_content object could also return early and suppress the reasoning fallback. The structured branch now returns only when it appends meaningful text/signature or redacted content. Added wire-format response regressions for absent/null/empty content and structured objects, plus precedence checks for signed and redacted content.
| if text != "" { | ||
| assistantMsg.Content = openai.StringOrAssistantRoleContentUnion{Value: text} | ||
| } | ||
| text := anthropicContentToText(msg.Content) |
There was a problem hiding this comment.
side note, anthropicContentToText joins without a separator which is very different from the above.
There was a problem hiding this comment.
Changed in 9501f60 to concatenate without inserting a newline, matching anthropicContentToText. The regression preserves whitespace supplied by the original blocks exactly, while retaining the individual signed thinking_blocks unchanged.
| } | ||
|
|
||
| // Redact reasoning from backends that name the field "reasoning" (vLLM) | ||
| if msg.Reasoning != nil { |
There was a problem hiding this comment.
should this be treated as a fall back as well?
There was a problem hiding this comment.
I kept the two redactions independent. This function sanitizes the object for logging rather than selecting the response field: both fields can coexist, so redacting only the preferred field could leave the other field’s private reasoning in the logged object. Added a regression in 9501f60 that supplies both fields, verifies both are redacted, and verifies the original object is unchanged.
| if block.Thinking.Thinking != "" { | ||
| reasoningTexts = append(reasoningTexts, block.Thinking.Thinking) | ||
| } | ||
| thinkingBlocks = append(thinkingBlocks, openai.ThinkingBlock{ |
There was a problem hiding this comment.
Worth checking if thinking and signature is empty?
There was a problem hiding this comment.
Fixed in 9501f60: skip a thinking block when both thinking and signature are empty. Nonempty unsigned text and signature-only blocks remain preserved, as do redacted-thinking blocks. Added regression cases for each boundary.
Signed-off-by: Jaeyeon Kim <anencore94@gmail.com>
|
@aabchoo pushed 9501f60 with the review fixes and coverage additions; I replied to each inline thread. Added 17 boundary/regression cases. Four cases fail before the fixes and pass afterward. No live-provider or GPU E2E pass is claimed. This follow-up used OpenAI Codex for implementation, review and testing. |
Description
This commit fixes reasoning loss in both directions when translating between the Anthropic Messages API and OpenAI Chat Completions with vLLM (>= v0.30.0) backends:
reasoning_contenttoreasoning(choices[].message.reasoningin non-streaming andchoices[].delta.reasoningin streaming) and no longer emitsreasoning_content. AddReasoning(json:"reasoning,omitempty") toChatCompletionResponseChoiceMessageandChatCompletionResponseChunkChoiceDelta, redact it inredactResponseMessage, and fall back toreasoningin both non-streaming and streaming response translation whenreasoning_contentis unset or empty (ThinkingBlocks>ReasoningContent>Reasoning).appendAnthropicAssistantMessagepreviously encoded assistantthinkingblocks as structured content parts ({"type":"thinking","text":...}), which vLLM ignores when rendering assistant reasoning into the chat template (<think>...</think>). Place assistant thinking text on the message asreasoning_contentand preserve structuredthinking(with signatures) andredacted_thinkingblocks inthinking_blocks, matching the convention established in translator: stream reasoning_content as plain string, signatures via thinking_blocks #2319 and translator: serialize non-streaming Bedrock reasoning_content as plain string #2524.Related Issues/PRs (if applicable)
Fixes #2937