diff --git a/crates/tui/src/client/chat.rs b/crates/tui/src/client/chat.rs index 83d3cbee1c..6f2d6a3b68 100644 --- a/crates/tui/src/client/chat.rs +++ b/crates/tui/src/client/chat.rs @@ -1034,8 +1034,31 @@ fn mirror_minimax_reasoning_details_for_messages(messages: &mut [Value]) { } } -fn mirror_minimax_reasoning_details_for_body(body: &mut Value, provider: ApiProvider) { - if provider != ApiProvider::Minimax { +/// Whether this MiniMax route still speaks the `reasoning_details` +/// continuity dialect. +/// +/// MiniMax's own API reference (checked 2026-11-04) carries no +/// `reasoning_details` in the Chat Completions request schema — the +/// documented multi-turn contract is echoing `reasoning_content` (or +/// preserving `` content) alongside role:tool pairing — so mirroring +/// on a first-party endpoint would only add an undocumented field. +/// `MINIMAX_BASE_URL` stays user-configurable, so gateway dialects behind a +/// custom `base_url` (e.g. an OpenRouter-compatible proxy for MiniMax M2, +/// which still documents the field) keep the mirror. +fn minimax_route_mirrors_reasoning_details(provider: ApiProvider, base_url: &str) -> bool { + provider == ApiProvider::Minimax + && !codewhale_config::provider::is_exact_minimax_chat_route( + codewhale_config::ProviderKind::Minimax, + base_url, + ) +} + +fn mirror_minimax_reasoning_details_for_body( + body: &mut Value, + provider: ApiProvider, + base_url: &str, +) { + if !minimax_route_mirrors_reasoning_details(provider, base_url) { return; } let Some(messages) = body.get_mut("messages").and_then(Value::as_array_mut) else { @@ -1265,7 +1288,7 @@ pub(crate) fn build_chat_wire_body( } else { None }; - mirror_minimax_reasoning_details_for_body(&mut body, provider); + mirror_minimax_reasoning_details_for_body(&mut body, provider, base_url); Ok(ChatWireBody { body, @@ -1838,7 +1861,7 @@ impl<'a> PromptBuilder<'a> { if provider == ApiProvider::Arcee { apply_arcee_waf_safe_message_encoding(&mut messages); } - if provider == ApiProvider::Minimax { + if minimax_route_mirrors_reasoning_details(provider, base_url) { mirror_minimax_reasoning_details_for_messages(&mut messages); } if is_exact_mistral_chat_route(provider, base_url) { @@ -5061,12 +5084,9 @@ mod arcee_waf_message_encoding_tests { #[cfg(test)] mod minimax_reasoning_replay_tests { - use super::{ - build_chat_messages_for_request_and_provider, - build_chat_messages_for_request_and_provider_and_route, - }; + use super::{build_chat_messages_for_request_and_provider_and_route, build_chat_wire_body}; use crate::config::{ - ApiProvider, DEFAULT_KIMI_CODE_BASE_URL, DEFAULT_MINIMAX_MODEL, + ApiProvider, DEFAULT_KIMI_CODE_BASE_URL, DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL, DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL, DEFAULT_MOONSHOT_BASE_URL, KIMI_CODE_K3_MODEL, }; use crate::models::Role; @@ -5105,10 +5125,47 @@ mod minimax_reasoning_replay_tests { } #[test] - fn minimax_history_replays_thinking_as_reasoning_details() { + fn minimax_official_route_replays_thinking_as_reasoning_content_only() { + // MiniMax's own API reference (checked 2026-11-04) no longer + // documents `reasoning_details` anywhere in the Chat Completions + // request schema; their multi-turn contract is to echo + // `reasoning_content` (or preserve `` content) alongside + // role:tool pairing. First-party endpoints must not receive the + // synthesized field. + let request = request_with_assistant_thinking(); + + let messages = build_chat_messages_for_request_and_provider_and_route( + &request, + ApiProvider::Minimax, + DEFAULT_MINIMAX_BASE_URL, + ); + let assistant = &messages[0]; + + assert_eq!( + assistant + .get("reasoning_content") + .and_then(|value| value.as_str()), + Some("Inspect tool state") + ); + assert!( + assistant.get("reasoning_details").is_none(), + "the official endpoint dropped reasoning_details from its documented schema: {assistant}" + ); + } + + #[test] + fn minimax_gateway_route_still_mirrors_thinking_as_reasoning_details() { + // MINIMAX_BASE_URL is user-configurable, so gateway dialects stay + // reachable through the minimax provider slot; a gateway serving + // OpenRouter's MiniMax M2 page still documents the `reasoning_details` + // round-trip, so non-official endpoints keep the mirror. let request = request_with_assistant_thinking(); - let messages = build_chat_messages_for_request_and_provider(&request, ApiProvider::Minimax); + let messages = build_chat_messages_for_request_and_provider_and_route( + &request, + ApiProvider::Minimax, + "https://gateway.example/v1", + ); let assistant = &messages[0]; assert_eq!( @@ -5131,6 +5188,29 @@ mod minimax_reasoning_replay_tests { ); } + #[test] + fn minimax_wire_body_on_official_route_carries_no_reasoning_details() { + // The wire-body pass mirrors after the prompt builder; it must + // respect the same first-party-vs-gateway split so a cached or + // rebuilt body cannot reintroduce the field. + let request = request_with_assistant_thinking(); + + let body = build_chat_wire_body( + &request, + ApiProvider::Minimax, + DEFAULT_MINIMAX_BASE_URL, + false, + ) + .expect("MiniMax wire body"); + + for message in body.body["messages"].as_array().expect("messages array") { + assert!( + message.get("reasoning_details").is_none(), + "official endpoint must not receive synthesized reasoning_details: {message}" + ); + } + } + #[test] fn kimi_code_k3_replays_thinking_only_on_the_exact_membership_route() { let mut request = request_with_assistant_thinking();