fix: normalize raw CoT reasoning content from provider_details - #176
Closed
Million-mo wants to merge 3 commits into
Closed
fix: normalize raw CoT reasoning content from provider_details#176Million-mo wants to merge 3 commits into
Million-mo wants to merge 3 commits into
Conversation
pydantic-ai intentionally keeps ThinkingPart.content empty for raw CoT providers (vLLM, LM Studio, litellm bridge, gpt-oss via OpenRouter) — raw reasoning is stored only in provider_details['raw_content']. This is by design per pydantic-ai docs. AgentPool's protocol converters only read content/content_delta, so reasoning was silently dropped across all protocols (ACP, OpenCode, AG-UI, builtin handlers). Fix: Add centralized normalization in EventMapper.map_event() that intercepts ThinkingPart/ThinkingPartDelta events with empty content/content_delta and populates them from provider_details['raw_content']. For callable provider_details (used by _make_raw_content_updater), call with None to extract the current delta text without needing accumulated state. Also fixes: - ACP converter: delta or "\n" → delta (remove lossy fallback) - OpenCode converter: if not delta → if delta is None (preserve empty strings, only skip None) - builtin_handlers: if delta → if delta is not None Closes #118
… handler - ACP converter: add `if delta is not None:` guard before AgentThoughtChunk.text() — delta can still be None when normalization is skipped (no raw_content in provider_details) - builtin_handlers: revert to `if delta:` — empty string from standard providers should not print empty 💭 bubble Addresses gemini-code-assist review on PR #119.
This was referenced Aug 22, 2026
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.
Problem
When using
OpenAIResponsesModelwith raw CoT providers (vLLM, LM Studio, litellm bridge, gpt-oss via OpenRouter), reasoning content is not displayed to users. pydantic-ai intentionally keepsThinkingPart.contentempty for these providers — raw reasoning is stored only inprovider_details['raw_content'](by design). AgentPool's protocol converters only readcontent/content_delta, so reasoning was silently dropped across all protocols (ACP, OpenCode, AG-UI, builtin handlers).Closes #118
Solution
Add a centralized event normalization step in
EventMapper.map_event()that interceptsThinkingPart/ThinkingPartDeltaevents with emptycontent/content_deltaand populates them fromprovider_details['raw_content'].Key technique
For
PartDeltaEvent,provider_detailsmay be a callable closure (_make_raw_content_updater). Calling it withNonereturns{'raw_content': [delta]}— the current delta text can be extracted fromraw_content[-1]without tracking accumulated state.For
PartStartEvent,provider_detailsis already a resolved dict (parts manager resolves callable at creation time), soraw_contentis directly accessible.Official OpenAI reasoning summaries (where
contentis already populated) are not modified — normalization only triggers whencontent/content_deltais empty/None.Changes
event_mapper.py: Add_normalize_thinking_event()and_extract_raw_content_text()functions. Call normalization inmap_event()before returning.event_converter.py(ACP):delta or "\n"→delta(remove lossy fallback)event_processor.py(OpenCode):if not delta:→if delta is None:in_process_thinking_startand_process_thinking_delta(preserve empty strings, only skip None)builtin_handlers.py:if delta:→if delta is not None:for thinking delta printingTest Results
tests/orchestrator/test_event_mapper_thinking_normalization.py)ruff check— cleanruff format --check— cleanmypy— cleanAcceptance Criteria
content/content_deltabuiltin_handlers.pyshows reasoning text"\n"for empty deltasNone)