Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(client): replay reasoning_content for DeepSeek models on openai provider (#1739, #1694)#1743

Open
LeoLin990405 wants to merge 2 commits into
Hmbown:mainfrom
LeoLin990405:fix/1739-reasoning-content-deepseek-model
Open

fix(client): replay reasoning_content for DeepSeek models on openai provider (#1739, #1694)#1743
LeoLin990405 wants to merge 2 commits into
Hmbown:mainfrom
LeoLin990405:fix/1739-reasoning-content-deepseek-model

Conversation

@LeoLin990405
Copy link
Copy Markdown

Summary / 概述

EN: Fixes #1739 and #1694. With provider openai pointed 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: "The reasoning_content in the thinking mode must be passed back to the API." This PR makes reasoning_content replay 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() in crates/tui/src/client/chat.rs returned false whenever provider_accepts_reasoning_content(provider) was false (true for ApiProvider::Openai), without checking the model. This single function gates both PromptBuilder::build_for_provider (include_reasoning) and sanitize_thinking_mode_messages, so a DeepSeek reasoning model on the generic openai provider had all reasoning_content stripped → the DeepSeek thinking‑mode API rejects the next request. Introduced by ac01b225 (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. A requires_reasoning_content(model) predicate already exists in the file.

中文: crates/tui/src/client/chat.rsshould_replay_reasoning_content_for_provider():只要 provider_accepts_reasoning_content(provider) 为假(Openai 即为假)就返回 false完全不看模型。该函数同时把守 PromptBuilder::build_for_providerinclude_reasoning)与 sanitize_thinking_mode_messages 两条路径,因此 openai provider 下的 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:

-    if !provider_accepts_reasoning_content(provider) {
+    if !provider_accepts_reasoning_content(provider) && !requires_reasoning_content(model) {
         return false;
     }

A known DeepSeek reasoning model now replays reasoning_content regardless of provider; a genuine non‑DeepSeek model on openai still has it stripped (the effort="off" escape hatch still wins downstream). provider_accepts_reasoning_content is untouched, so #1542 is not regressed.

中文: 在唯一的共享门处做一行判定变更(见上)。已知 DeepSeek 推理模型从此无视 provider 都会回放 reasoning_contentopenai 上真正的非 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) used deepseek-v4-pro on ApiProvider::Openai and asserted reasoning was dropped — i.e. they encoded the exact behavior #1739/#1694 report as a bug. They were retargeted to gpt-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 in chat.rs.

中文(请注意): crates/tui/src/client.rs 两处既有测试此前用 deepseek-v4-pro + ApiProvider::Openai 断言 reasoning 被丢弃——它们恰好把 #1739/#1694 报告的 bug 当成了预期。现改为 gpt-4o(真正的非 DeepSeek 模型),保留 #1542 原意而不再断言这个 bug;并删除一处因换模型而失去意义的子断言。新增的正/反向覆盖放在 chat.rs

Testing / 测试

cargo test -p deepseek-tui --bin deepseek-tui -- \
  client::chat::alias_thinking_detection_tests \
  client::tests::sanitize_thinking_mode_skips_generic_openai_provider \
  client::tests::generic_openai_provider_drops_deepseek_reasoning_content
# 10 passed; 0 failed  (before this fix the suite was 82 passed / 2 failed —
#  the 2 "failures" were the old tests asserting the bug)

New tests: deepseek_model_on_openai_provider_still_replays_reasoning_content (deepseek‑v4‑flash/‑pro/‑reasoner replay; off still suppresses) and generic_model_on_openai_provider_still_strips_reasoning_content (gpt‑4o / claude still stripped — #1542 guard).

Refs #1739, #1694, #1542, #1736.

…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.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Comment thread crates/tui/src/client.rs
// `deepseek_model_on_openai_provider_still_replays_reasoning_content`.
let request = MessageRequest {
model: "deepseek-v4-pro".to_string(),
model: "gpt-4o".to_string(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.
@LeoLin990405
Copy link
Copy Markdown
Author

Thanks for the review — both points addressed in the latest push:

  • HIGH (incomplete fix): the stream path was indeed missing the same model-aware gate. handle_chat_completion_stream now routes through a new is_reasoning_model_for_stream(provider, model) helper instead of requires_reasoning_content(model) && provider_accepts_reasoning_content(provider), so a DeepSeek reasoning model on the openai provider is classified as a reasoning model during SSE parsing too (tokens land in reasoning_content, not content). Added stream_classification_matches_replay_predicate to lock the stream classifier and the replay predicate together so they can't drift again.
  • MEDIUM (test name): renamed generic_openai_provider_drops_deepseek_reasoning_contentgeneric_openai_provider_drops_reasoning_content_for_non_deepseek_models.

Non-DeepSeek models on any provider are unchanged (#1542 not regressed). 13 reasoning/sanitizer tests green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant