diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index 50998854..b6538458 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -1265,8 +1265,14 @@ mod tests { #[test] fn generic_openai_provider_drops_deepseek_reasoning_content() { + // #1542 intent (narrowed by #1739/#1694): a *genuine non-DeepSeek* + // model on the generic openai provider must not carry DeepSeek-only + // `reasoning_content`. A DeepSeek reasoning model on the openai + // provider (DeepSeek-compatible endpoint) is now covered separately + // and DOES replay reasoning_content — see + // `deepseek_model_on_openai_provider_still_replays_reasoning_content`. let request = MessageRequest { - model: "deepseek-v4-pro".to_string(), + model: "gpt-4o".to_string(), messages: vec![Message { role: "assistant".to_string(), content: vec![ @@ -1291,19 +1297,6 @@ mod tests { top_p: None, }; - let deepseek = - build_chat_messages_for_request_and_provider(&request, ApiProvider::Deepseek); - let native_assistant = deepseek - .iter() - .find(|value| value.get("role").and_then(Value::as_str) == Some("assistant")) - .expect("assistant message"); - assert_eq!( - native_assistant - .get("reasoning_content") - .and_then(Value::as_str), - Some("plan") - ); - let openai = build_chat_messages_for_request_and_provider(&request, ApiProvider::Openai); let generic_assistant = openai .iter() @@ -2707,8 +2700,12 @@ mod tests { #[test] fn sanitize_thinking_mode_skips_generic_openai_provider() { + // #1542 intent (narrowed by #1739/#1694): the sanitizer only skips for + // a *genuine non-DeepSeek* model on the generic openai provider. A + // DeepSeek reasoning model on the openai provider still gets sanitized + // (see chat.rs `deepseek_model_on_openai_provider_still_replays_*`). let mut body = json!({ - "model": "deepseek-v4-pro", + "model": "gpt-4o", "messages": [ { "role": "user", "content": "hi" }, { @@ -2721,7 +2718,7 @@ mod tests { let result = sanitize_thinking_mode_messages( &mut body, - "deepseek-v4-pro", + "gpt-4o", Some("max"), ApiProvider::Openai, ); diff --git a/crates/tui/src/client/chat.rs b/crates/tui/src/client/chat.rs index 8e9af335..15dc3927 100644 --- a/crates/tui/src/client/chat.rs +++ b/crates/tui/src/client/chat.rs @@ -1628,7 +1628,12 @@ fn should_replay_reasoning_content_for_provider( model: &str, effort: Option<&str>, ) -> bool { - if !provider_accepts_reasoning_content(provider) { + if !provider_accepts_reasoning_content(provider) && !requires_reasoning_content(model) { + // Generic non-DeepSeek model on a provider that rejects the field: + // keep stripping it (preserves the #1542 fix). But a known DeepSeek + // reasoning model pointed at a DeepSeek-compatible endpoint via the + // generic `openai` provider still requires reasoning_content replay, + // or the thinking-mode API returns 400 (#1739 / #1694). return false; } should_replay_reasoning_content(model, effort) @@ -2828,7 +2833,7 @@ mod alias_thinking_detection_tests { //! https://api-docs.deepseek.com/guides/thinking_mode use super::{ provider_accepts_reasoning_content, requires_reasoning_content, - should_replay_reasoning_content, + should_replay_reasoning_content, should_replay_reasoning_content_for_provider, }; use crate::config::ApiProvider; @@ -2897,4 +2902,49 @@ mod alias_thinking_detection_tests { assert!(provider_accepts_reasoning_content(ApiProvider::Deepseek)); assert!(provider_accepts_reasoning_content(ApiProvider::NvidiaNim)); } + + #[test] + fn deepseek_model_on_openai_provider_still_replays_reasoning_content() { + // #1739 / #1694: a DeepSeek thinking model pointed at a + // DeepSeek-compatible endpoint via the generic `openai` provider must + // still replay reasoning_content, even though the provider itself does + // not accept the field. Otherwise the thinking-mode API returns 400. + assert!(should_replay_reasoning_content_for_provider( + ApiProvider::Openai, + "deepseek-v4-flash", + None, + )); + assert!(should_replay_reasoning_content_for_provider( + ApiProvider::Openai, + "deepseek-v4-pro", + None, + )); + assert!(should_replay_reasoning_content_for_provider( + ApiProvider::Openai, + "deepseek-reasoner", + Some("medium"), + )); + // The documented escape hatch still wins over model detection. + assert!(!should_replay_reasoning_content_for_provider( + ApiProvider::Openai, + "deepseek-v4-flash", + Some("off"), + )); + } + + #[test] + fn generic_model_on_openai_provider_still_strips_reasoning_content() { + // #1542 no-regression guard: a genuine non-DeepSeek model on the + // openai provider must continue to have reasoning_content stripped. + assert!(!should_replay_reasoning_content_for_provider( + ApiProvider::Openai, + "gpt-4o", + None, + )); + assert!(!should_replay_reasoning_content_for_provider( + ApiProvider::Openai, + "claude-sonnet-4-6", + None, + )); + } }