fix(agent): re-pad reasoning_content on cross-provider fallback to require-side providers - #33784
Conversation
…quire-side providers
api_messages is built once before the retry loop while the primary provider
is active. When a mid-conversation fallback switches to a require-side thinking
provider (DeepSeek/Kimi/MiMo), assistant turns built under a non-require primary
(e.g. Codex) go out without reasoning_content and the new provider rejects the
request with HTTP 400 ("reasoning_content must be passed back").
Re-apply the echo-back pad against the current provider immediately before
building the request kwargs. Idempotent and a no-op unless the active provider
enforces echo-back, so it covers all fallback paths without affecting normal or
reject-side operation.
Drafted by Claude (Opus 4.7) under human review while fixing a personal deployment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Related: #21033 takes the broader approach of extracting |
|
Salvaged and merged via PR #33795. Your commit was cherry-picked verbatim onto current Thanks for the tight diagnosis, the live DeepSeek validation, and the upfront AI-assistance disclosure — everything we needed to verify and ship. Scope as described in your PR: closes the require-side variant. The reject-side siblings (#29205 Codex→Anthropic, #32617 Codex→xAI) and the broader rebuild-on-fallback structural fix (#21033, #13235) stay open as separate work — they need live repros we don't have yet, plus a different shape (strip vs. pad). |
Closes #33783
Problem
When the primary is
openai-codex/gpt-5.5and a mid-conversation fallback switches to a require-side thinking provider (deepseek/deepseek-v4-pro, also Kimi / MiMo), the first request on the new provider 400s:api_messagesis built once, before the retry loop (agent/conversation_loop.py:919), while the primary (Codex) is active — so_needs_thinking_reasoning_pad()isFalseand bare assistant tool-call turns get noreasoning_content._try_activate_fallback()(L1159/1395/1465) switches the provider butcontinues without rebuilding, and_build_api_kwargs(api_messages)(L1186) re-sends the stale, primary-shaped history to DeepSeek, which requires the field on every assistant turn.The pad logic in
_copy_reasoning_content_for_apiis correct — it just never re-runs after the switch.Fix
Add
reapply_reasoning_echo_for_provider(agent, api_messages)and call it once, immediately before_build_api_kwargsin the retry loop. It re-applies the echo-back pad against the current provider. It is:reasoning_content; a second call is a no-op._needs_thinking_reasoning_pad()is True for the active provider, so non-failover and reject-side paths are unaffected.continues and any added later.+43 lines across 3 files.
Validation
End-to-end against the live DeepSeek API, replaying a captured real failing history (33 messages, 14 assistant tool-call turns, 5 bare):
finish=tool_calls, real continuation, full context preserved (42,182 prompt tokens, 42,112 cached)Tests
Adds
TestReapplyReasoningEchoForProviderSwitchtotests/run_agent/test_deepseek_reasoning_content_echo.py:All 40 tests in the file pass.
Scope
This fixes the require-side failover (DeepSeek / Kimi / MiMo). The reject-side failovers (Codex→Anthropic #29205, Codex→xAI #32617) need stripping/sanitizing instead and are out of scope for this PR.