fix(client): replay reasoning_content for DeepSeek models on openai provider (#1739, #1694)#1743
Conversation
…rovider (Hmbown#1739) should_replay_reasoning_content_for_provider() returned false whenever provider_accepts_reasoning_content(provider) was false (true for ApiProvider::Openai) without checking the model. This single gate feeds both build_for_provider (include_reasoning) and sanitize_thinking_mode_messages, so a DeepSeek reasoning model on the generic openai provider (DeepSeek-compatible endpoint) had all reasoning_content stripped -> the DeepSeek thinking-mode API 400s ('reasoning_content in the thinking mode must be passed back'). This is the over-aggressive half of ac01b22 (fix Hmbown#1542). Gate the early return on the model too: !provider_accepts_reasoning_content(provider) && !requires_reasoning_content(model). Known DeepSeek reasoning models replay regardless of provider; genuine non-DeepSeek models on openai still strip (effort=off still wins). Hmbown#1542 not regressed (provider_accepts_reasoning_content untouched). Two pre-existing client.rs tests asserted the buggy case (deepseek-v4-pro on Openai -> dropped); retargeted to gpt-4o to preserve their Hmbown#1542 intent without encoding the bug. New positive/negative coverage in chat.rs. Refs Hmbown#1739, Hmbown#1694, Hmbown#1542, Hmbown#1736.
There was a problem hiding this comment.
Code Review
This pull request adjusts the reasoning content replay logic to support DeepSeek models used through generic OpenAI providers. It updates tests to distinguish between genuine non-DeepSeek models and DeepSeek-compatible ones. Feedback identifies a missing logic update in the streaming handler that could cause reasoning tokens to be misclassified and suggests renaming a test for clarity.
| effort: Option<&str>, | ||
| ) -> bool { | ||
| if !provider_accepts_reasoning_content(provider) { | ||
| if !provider_accepts_reasoning_content(provider) && !requires_reasoning_content(model) { |
There was a problem hiding this comment.
While this change correctly updates the predicate for replaying reasoning content, a similar check is missing in handle_chat_completion_stream at line 256:
let is_reasoning_model =
requires_reasoning_content(&model) && provider_accepts_reasoning_content(api_provider);Currently, when using the openai provider with a DeepSeek model, is_reasoning_model will be false during stream processing. This causes parse_sse_chunk to treat incoming reasoning tokens as regular text instead of thinking blocks (see line 1941). Consequently, the reasoning content will be stored in the message history as part of the content field rather than reasoning_content. This will cause the subsequent replay to fail or be rejected by the DeepSeek API with a 400 error because the expected reasoning_content field will be missing or incorrect in the next request.
Please update line 256 to align with the logic introduced here.
| // `deepseek_model_on_openai_provider_still_replays_reasoning_content`. | ||
| let request = MessageRequest { | ||
| model: "deepseek-v4-pro".to_string(), | ||
| model: "gpt-4o".to_string(), |
There was a problem hiding this comment.
Since the model has been changed from deepseek-v4-pro to gpt-4o, the test name generic_openai_provider_drops_deepseek_reasoning_content (line 1267) is now misleading. It no longer tests DeepSeek-specific reasoning content, but rather verifies that reasoning content is stripped for generic non-DeepSeek models on the OpenAI provider. Consider renaming the test to something like generic_openai_provider_drops_reasoning_content_for_non_deepseek_models to maintain code clarity.
…Hmbown#1743) Address gemini-code-assist review on PR Hmbown#1743: - HIGH: should_replay_reasoning_content_for_provider was made model-aware in the previous commit, but handle_chat_completion_stream still computed is_reasoning_model = requires_reasoning_content(model) && provider_accepts_reasoning_content(provider). On the openai provider + a DeepSeek model that was false during SSE parsing, so reasoning tokens were stored as content (not reasoning_content) and the next request still 400'd -- the fix was incomplete. Extract is_reasoning_model_for_stream() and route the stream call site through it; add an equivalence test locking it to the replay predicate so the two paths can't drift. - MEDIUM: rename generic_openai_provider_drops_deepseek_reasoning_content -> generic_openai_provider_drops_reasoning_content_for_non_deepseek_models (now uses gpt-4o; old name was misleading). Non-DeepSeek models on any provider are unaffected (Hmbown#1542 not regressed). Refs Hmbown#1739, Hmbown#1694, Hmbown#1542.
|
Thanks for the review — both points addressed in the latest push:
Non-DeepSeek models on any provider are unchanged (#1542 not regressed). 13 reasoning/sanitizer tests green. |
Summary / 概述
EN: Fixes #1739 and #1694. With provider
openaipointed at a DeepSeek‑compatible endpoint and a DeepSeek reasoning model (deepseek-v4-flash/-pro/deepseek-chat/deepseek-reasoner), multi‑turn or tool‑calling conversations 400 with: "Thereasoning_contentin the thinking mode must be passed back to the API." This PR makesreasoning_contentreplay model‑aware, not provider‑only.中文: 修复 #1739 与 #1694。当 provider 为
openai且指向 DeepSeek 兼容端点、模型为 DeepSeek 推理模型(deepseek-v4-flash/-pro/deepseek-chat/deepseek-reasoner)时,多轮或带工具调用的会话会 400:“thinking 模式下的reasoning_content必须回传给 API。” 本 PR 让reasoning_content的回放按模型判断,而非仅按 provider。Root cause / 根因
EN:
should_replay_reasoning_content_for_provider()incrates/tui/src/client/chat.rsreturnedfalsewheneverprovider_accepts_reasoning_content(provider)was false (true forApiProvider::Openai), without checking the model. This single function gates bothPromptBuilder::build_for_provider(include_reasoning) andsanitize_thinking_mode_messages, so a DeepSeek reasoning model on the genericopenaiprovider had allreasoning_contentstripped → the DeepSeek thinking‑mode API rejects the next request. Introduced byac01b225(fix #1542), which correctly stopped sending DeepSeek‑only fields to generic OpenAI backends but was too aggressive — it also stripped them when the OpenAI‑compatible endpoint actually routes to DeepSeek. Arequires_reasoning_content(model)predicate already exists in the file.中文:
crates/tui/src/client/chat.rs的should_replay_reasoning_content_for_provider():只要provider_accepts_reasoning_content(provider)为假(Openai即为假)就返回false,完全不看模型。该函数同时把守PromptBuilder::build_for_provider(include_reasoning)与sanitize_thinking_mode_messages两条路径,因此openaiprovider 下的 DeepSeek 推理模型会被剥掉全部reasoning_content→ DeepSeek thinking 模式 API 拒绝下一次请求。该问题由ac01b225(修 #1542)引入:它正确地阻止向通用 OpenAI 后端发送 DeepSeek 专属字段,但过于激进——当 OpenAI 兼容端点实际转发到 DeepSeek 时也一并剥离。文件中已存在requires_reasoning_content(model)判定。Fix / 修复
One‑line predicate change at the single shared gate:
A known DeepSeek reasoning model now replays
reasoning_contentregardless of provider; a genuine non‑DeepSeek model onopenaistill has it stripped (theeffort="off"escape hatch still wins downstream).provider_accepts_reasoning_contentis untouched, so #1542 is not regressed.中文: 在唯一的共享门处做一行判定变更(见上)。已知 DeepSeek 推理模型从此无视 provider 都会回放
reasoning_content;openai上真正的非 DeepSeek 模型仍被剥离(下游effort="off"逃生口依旧优先)。provider_accepts_reasoning_content不动,不回退 #1542。Note on two pre‑existing tests / 关于两处既有测试
EN — please read: Two existing tests in
crates/tui/src/client.rs(generic_openai_provider_drops_deepseek_reasoning_content,sanitize_thinking_mode_skips_generic_openai_provider) useddeepseek-v4-proonApiProvider::Openaiand asserted reasoning was dropped — i.e. they encoded the exact behavior #1739/#1694 report as a bug. They were retargeted togpt-4o(a genuine non‑DeepSeek model), which preserves their original #1542 intent (generic model on generic provider → stripped) while no longer asserting the buggy case. One now‑meaningless sub‑assertion (a non‑reasoning model on the native Deepseek provider) was removed. New positive/negative coverage lives inchat.rs.中文(请注意):
crates/tui/src/client.rs两处既有测试此前用deepseek-v4-pro+ApiProvider::Openai断言 reasoning 被丢弃——它们恰好把 #1739/#1694 报告的 bug 当成了预期。现改为gpt-4o(真正的非 DeepSeek 模型),保留 #1542 原意而不再断言这个 bug;并删除一处因换模型而失去意义的子断言。新增的正/反向覆盖放在chat.rs。Testing / 测试
New tests:
deepseek_model_on_openai_provider_still_replays_reasoning_content(deepseek‑v4‑flash/‑pro/‑reasoner replay;offstill suppresses) andgeneric_model_on_openai_provider_still_strips_reasoning_content(gpt‑4o / claude still stripped — #1542 guard).Refs #1739, #1694, #1542, #1736.