fix(sdk): stop mirroring reasoning_content in provider_specific_fields (#30110)

The dict-to-response conversion path mirrored reasoning_content into
provider_specific_fields, while live provider transforms (Anthropic's
_build_provider_specific_fields) only set it top-level on the Message.
Cache-replayed messages therefore serialized differently from live
ones, breaking disk cache key stability for multi-turn conversations
with extended thinking.

The mirror was added for DeepSeek before Message.reasoning_content
existed as a top-level attribute. The top-level field is still set by
the converter, so DeepSeek's request-side promotion is unaffected.

Fixes #27337.
This commit is contained in:
Filippo Menghi 2026-06-10 12:42:44 +02:00 • committed by GitHub
parent 993650fdf8
commit ea29b28f40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 36 additions and 5 deletions

View file

@ -633,11 +633,6 @@ def convert_to_model_response_object( # noqa: PLR0915
thinking_blocks = choice["message"]["thinking_blocks"]
provider_specific_fields["thinking_blocks"] = thinking_blocks
if reasoning_content:
provider_specific_fields["reasoning_content"] = (
reasoning_content
)
message = Message(
content=content,
role=choice["message"]["role"] or "assistant",

View file

@ -2425,6 +2425,42 @@ class TestConvertToModelResponseObjectCompletion:
assert result.choices[0].message.content == "The answer is 4."
assert result.choices[0].message.reasoning_content == "2+2=4"
def test_reasoning_content_not_mirrored_into_provider_specific_fields(self):
"""Mirroring reasoning_content into provider_specific_fields made
cache-replayed messages diverge from live Anthropic messages, which
only set it top-level, breaking cache key stability (issue #27337)."""
response_object = {
"id": "chatcmpl-5",
"model": "claude-sonnet-4-5",
"choices": [
{
"finish_reason": "stop",
"index": 0,
"message": {
"content": "The answer is 4.",
"role": "assistant",
"reasoning_content": "2+2=4",
"thinking_blocks": [
{
"type": "thinking",
"thinking": "2+2=4",
"signature": "sig",
}
],
},
}
],
"usage": {"prompt_tokens": 5, "completion_tokens": 10, "total_tokens": 15},
}
result = convert_to_model_response_object(
response_object=response_object,
model_response_object=ModelResponse(),
)
message = result.choices[0].message
assert message.reasoning_content == "2+2=4"
assert "reasoning_content" not in (message.provider_specific_fields or {})
def test_response_none_raises(self):
with pytest.raises(Exception):
convert_to_model_response_object(