fix: treat deepseek-chat / deepseek-reasoner aliases as reasoning models
`requires_reasoning_content()` only matched literal `deepseek-v4*` model IDs, but `deepseek-chat` and `deepseek-reasoner` are DeepSeek's public API aliases that resolve server-side to `deepseek-v4-flash` and `deepseek-v4-pro` respectively. Both have thinking mode enabled by default, so when a user sets `default_text_model = "deepseek-chat"` (the value `deepseek auth` / onboarding writes), the thinking-mode sanitizer is skipped and tool-call assistant messages are sent without `reasoning_content`. DeepSeek then rejects the second turn with: HTTP 400: The `reasoning_content` in the thinking mode must be passed back to the API. Extend `requires_reasoning_content()` to recognise the `deepseek-chat` and `deepseek-reasoner` alias prefixes (covering suffixed variants like `deepseek-chat:free` used by proxied deployments). The explicit `reasoning_effort = "off"` escape hatch still disables replay via the unchanged `should_replay_reasoning_content()` check. Adds `alias_thinking_detection_tests` covering the aliases, explicit V4 IDs (regression guard), excluded non-thinking models, suffixed variants, and the reasoning-off override. Refs: https://api-docs.deepseek.com/guides/thinking_mode (cherry picked from commit 46941142123827fa16fc9a1fb41b78c293e935ce)
This commit is contained in:
committed by
Hunter Bown
parent
9279d92f4e
commit
ca284d1fc0
@@ -1506,7 +1506,16 @@ fn log_thinking_mode_violations(body: &Value) {
|
||||
|
||||
fn requires_reasoning_content(model: &str) -> bool {
|
||||
let lower = model.to_lowercase();
|
||||
// V4-family direct model IDs.
|
||||
lower.contains("deepseek-v4")
|
||||
// Public DeepSeek API aliases routed server-side to the V4 family.
|
||||
// `deepseek-chat` resolves to `deepseek-v4-flash` and `deepseek-reasoner`
|
||||
// resolves to `deepseek-v4-pro`; both have thinking mode enabled by
|
||||
// default, so any assistant message carrying tool_calls must replay
|
||||
// `reasoning_content` on subsequent turns or the API returns 400.
|
||||
|| lower.starts_with("deepseek-chat")
|
||||
|| lower.starts_with("deepseek-reasoner")
|
||||
// Generic reasoning markers used by custom/proxied deployments.
|
||||
|| lower.contains("reasoner")
|
||||
|| lower.contains("-reasoning")
|
||||
|| lower.contains("-thinking")
|
||||
@@ -2615,3 +2624,78 @@ mod stream_decoder_tests {
|
||||
assert!(tool_layers[1].deduplicated);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod alias_thinking_detection_tests {
|
||||
//! Regression coverage for the DeepSeek public model aliases.
|
||||
//!
|
||||
//! `deepseek-chat` and `deepseek-reasoner` are the canonical alias names
|
||||
//! published in DeepSeek's API docs. Server-side they resolve to V4-flash
|
||||
//! and V4-pro respectively, both of which have thinking mode enabled by
|
||||
//! default. If the TUI does not classify those aliases as reasoning
|
||||
//! models, the sanitizer skips replaying `reasoning_content` on tool-call
|
||||
//! assistant messages and DeepSeek returns a 400 ("the `reasoning_content`
|
||||
//! in the thinking mode must be passed back to the API") on the second
|
||||
//! turn. See upstream API docs:
|
||||
//! https://api-docs.deepseek.com/guides/thinking_mode
|
||||
use super::{requires_reasoning_content, should_replay_reasoning_content};
|
||||
|
||||
#[test]
|
||||
fn aliases_routed_to_v4_require_reasoning_content() {
|
||||
// Documented public aliases.
|
||||
assert!(requires_reasoning_content("deepseek-chat"));
|
||||
assert!(requires_reasoning_content("deepseek-reasoner"));
|
||||
// Case-insensitive: users sometimes copy/paste with capitalisation.
|
||||
assert!(requires_reasoning_content("DeepSeek-Chat"));
|
||||
assert!(requires_reasoning_content("DEEPSEEK-REASONER"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_v4_ids_still_require_reasoning_content() {
|
||||
// Direct V4 IDs continue to match (regression guard for the existing
|
||||
// `lower.contains("deepseek-v4")` branch).
|
||||
assert!(requires_reasoning_content("deepseek-v4-flash"));
|
||||
assert!(requires_reasoning_content("deepseek-v4-pro"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_thinking_aliases_remain_excluded() {
|
||||
// Legacy non-thinking IDs and unrelated provider models must not be
|
||||
// misclassified, otherwise we would force a placeholder
|
||||
// `reasoning_content` on providers that reject the field.
|
||||
assert!(!requires_reasoning_content("deepseek-v3"));
|
||||
assert!(!requires_reasoning_content("deepseek-coder"));
|
||||
assert!(!requires_reasoning_content("gpt-4o"));
|
||||
assert!(!requires_reasoning_content("claude-sonnet-4-6"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_prefix_handles_suffixed_variants() {
|
||||
// OpenRouter / proxy deployments occasionally suffix the canonical
|
||||
// alias (e.g. `deepseek-chat:free`). Those routes still hit V4
|
||||
// server-side, so they must continue to require reasoning_content.
|
||||
assert!(requires_reasoning_content("deepseek-chat:free"));
|
||||
assert!(requires_reasoning_content("deepseek-reasoner-2025-05"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_reasoning_off_overrides_alias_detection() {
|
||||
// `reasoning_effort = "off"` is the documented escape hatch: even when
|
||||
// the model is in the thinking family, the user can opt out and the
|
||||
// sanitizer must respect that choice.
|
||||
assert!(!should_replay_reasoning_content(
|
||||
"deepseek-chat",
|
||||
Some("off")
|
||||
));
|
||||
assert!(!should_replay_reasoning_content(
|
||||
"deepseek-reasoner",
|
||||
Some("disabled")
|
||||
));
|
||||
// Without an explicit override, alias models still trigger replay.
|
||||
assert!(should_replay_reasoning_content("deepseek-chat", None));
|
||||
assert!(should_replay_reasoning_content(
|
||||
"deepseek-reasoner",
|
||||
Some("medium")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user