Skip to content

fix: normalize raw CoT reasoning content from provider_details - #176

Closed
Million-mo wants to merge 3 commits into
develop/agenticfrom
fix/normalize-raw-cot-reasoning
Closed

fix: normalize raw CoT reasoning content from provider_details#176
Million-mo wants to merge 3 commits into
develop/agenticfrom
fix/normalize-raw-cot-reasoning

Conversation

@Million-mo

Copy link
Copy Markdown
Owner

Problem

When using OpenAIResponsesModel with raw CoT providers (vLLM, LM Studio, litellm bridge, gpt-oss via OpenRouter), reasoning content is not displayed to users. pydantic-ai intentionally keeps ThinkingPart.content empty for these providers — raw reasoning is stored only in provider_details['raw_content'] (by design). AgentPool's protocol converters only read content/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 intercepts ThinkingPart/ThinkingPartDelta events with empty content/content_delta and populates them from provider_details['raw_content'].

Key technique

For PartDeltaEvent, provider_details may be a callable closure (_make_raw_content_updater). Calling it with None returns {'raw_content': [delta]} — the current delta text can be extracted from raw_content[-1] without tracking accumulated state.

For PartStartEvent, provider_details is already a resolved dict (parts manager resolves callable at creation time), so raw_content is directly accessible.

Official OpenAI reasoning summaries (where content is already populated) are not modified — normalization only triggers when content/content_delta is empty/None.

Changes

  • event_mapper.py: Add _normalize_thinking_event() and _extract_raw_content_text() functions. Call normalization in map_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_start and _process_thinking_delta (preserve empty strings, only skip None)
  • builtin_handlers.py: if delta:if delta is not None: for thinking delta printing

Test Results

  • 22/22 unit tests passed (tests/orchestrator/test_event_mapper_thinking_normalization.py)
  • ruff check — clean
  • ruff format --check — clean
  • mypy — clean

Acceptance Criteria

  • Reasoning content from raw CoT providers is visible to all protocol converters via content/content_delta
  • Official OpenAI reasoning summaries still work (content path unchanged)
  • builtin_handlers.py shows reasoning text
  • ACP converter no longer substitutes "\n" for empty deltas
  • OpenCode converter preserves empty string deltas (only skips None)
  • No regressions in existing tests

备份迁移自 SRC-PR#119 · 作者 @Leoyzen · 创建于 2026-07-07T06:45:56Z · head=fix/normalize-raw-cot-reasoning base=develop/agentic
源状态: merged · merge_commit_sha=1a444a30e5b3cf33d1623194e5d8f6585f24fa70

Leoyzen added 3 commits July 7, 2026 15:52
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants