Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 91 additions & 11 deletions crates/tui/src/client/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<think>` 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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This guard sees the configured base_url before transport normalization. For a valid MiniMax configuration such as https://api.minimax.io (or https://api.minimaxi.com), is_exact_minimax_chat_route returns false because there is no /v1, so this treats the route as a gateway and mirrors reasoning_details. The TUI transport later calls versioned_base_url, which appends /v1, and sends to the first-party /v1/chat/completions endpoint. Thus the documented field still reaches the official endpoint for this base URL form. Please classify against the effective normalized endpoint (or normalize the bare official hosts consistently) and add a regression case.

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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 `<think>` 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!(
Expand All @@ -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();
Expand Down
Loading