Python: Fix structured output parsing when text contents are not coalesced#4897
Open
t-anjan wants to merge 1 commit intomicrosoft:mainfrom
Open
Python: Fix structured output parsing when text contents are not coalesced#4897t-anjan wants to merge 1 commit intomicrosoft:mainfrom
t-anjan wants to merge 1 commit intomicrosoft:mainfrom
Conversation
…esced
When `Message.text` joins multiple `TextContent` objects, it uses
`" ".join()` which is correct for natural language but corrupts JSON
when used for structured output parsing. The `value` property on both
`ChatResponse` and `AgentResponse` feeds `self.text` directly into
`model_validate_json()`, causing Pydantic validation failures when
text chunks happen to not be fully coalesced into a single content.
This fix makes the `value` property concatenate text contents directly
(without spaces) instead of going through `Message.text`, preserving
the integrity of structured JSON output.
## Real-world impact
This bug was observed in production with the OpenAI-compatible chat
client (OpenRouter → Gemini) where streaming responses intermittently
produced multiple text Content objects that survived coalescing. Two
distinct failure modes were observed:
**Failure 1 — Space injected into JSON key:**
The LLM returned valid JSON with `"action": "request_evidence"`, but
`Message.text` produced `"action ": "request_evidence"` (trailing
space in key). Pydantic rejected this with:
`Field required [type=missing, input_value={'action ': ...}]`
**Failure 2 — Space injected into JSON value:**
The LLM returned `"readiness": "not_started"`, but `Message.text`
produced `"readiness": "not_started "` (trailing space in value).
Pydantic rejected this with:
`Input should be 'not_started', ... [input_value='not_started ']`
Both failures were intermittent (retrying the same request succeeded)
and the raw LLM response was valid JSON — the corruption was
introduced by the `" ".join()` in `Message.text`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ChatResponse.valueandAgentResponse.valueuseself.textto feed intomodel_validate_json()for structured output parsing.self.textdelegates toMessage.text, which joins multipleTextContentobjects with" ".join().This is correct for natural language responses, but corrupts JSON when text contents are not fully coalesced into a single content — spaces get injected into JSON keys and values, causing Pydantic
validation failures.
The fix is scoped to the
valueproperty, which only executes whenresponse_formatis set (i.e., structured output).Message.textand its" ".joinbehavior are unchanged — natural language responsesare unaffected.
Real-world failures
Observed in production with OpenAI-compatible chat client (OpenRouter → Gemini) using
response_formatfor structured output. Failures were intermittent — retrying the same request succeeded.Failure 1 — Space in JSON key:
LLM returned valid JSON with
"action": "request_evidence", but afterMessage.textprocessing, Pydantic received"action ": "request_evidence"(trailing space in key). Error:Field required [type=missing, input_value={'action ': 'request_evid...}]
Failure 2 — Space in JSON value:
LLM returned
"readiness": "not_started", but Pydantic received"readiness": "not_started "(trailing space in value). Error:Input should be 'not_started', ... [input_value='not_started ']
In both cases, the raw LLM response was valid JSON — the corruption was introduced by
" ".join()inMessage.text.Changes
ChatResponse.value: when parsing structured output viaresponse_format, concatenate text contents directly instead of usingself.text(which delegates toMessage.text's" ".join)AgentResponse.value: same fixMessage.textis not modified — natural language joining behavior is preservedTest plan
test_chat_response_value_multi_chunk_jsonandtest_agent_response_value_multi_chunk_jsonTextContentobjects (simulating uncoalesced streaming chunks), then asserts:message.textcontains injected spaces — confirming" ".joinbehavior exists:'{"resp onse": "He llo"}'.valuestill parses correctly despite that — confirming the fix worksresponse.valuethrows:ValidationError: Field required [type=missing, input_value={'resp onse': 'He llo'}]— the same class of error observed in productiontest_types.pytests continue to pass