From 7c4d359ed7ea3ca85430a6a266df81bc61d7125c Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Wed, 10 Jun 2026 16:26:20 -0700 Subject: [PATCH 1/2] fix(reasoning): add Moonshot/Kimi to reasoning-content provider and model support (#3016) Two targeted fixes to close the Kimi/Moonshot reasoning gap: 1. chat.rs: add ApiProvider::Moonshot to provider_accepts_reasoning_content so Kimi thinking traces stream as Thinking blocks instead of leaking as plain answer text on Moonshot's native endpoint. 2. models.rs: add kimi-* prefix match to model_supports_reasoning so bare Moonshot-native IDs (kimi-k2.6, kimi-*-thinking, etc.) are recognized as reasoning-capable without requiring the OpenRouter-style moonshotai/ prefix. These two changes together ensure is_reasoning_model_for_stream returns true for Kimi models on Moonshot, fixing the RC dialet gap. --- crates/tui/src/client/chat.rs | 1 + crates/tui/src/models.rs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/crates/tui/src/client/chat.rs b/crates/tui/src/client/chat.rs index 93b08714..47a08be6 100644 --- a/crates/tui/src/client/chat.rs +++ b/crates/tui/src/client/chat.rs @@ -1996,6 +1996,7 @@ fn provider_accepts_reasoning_content(provider: ApiProvider) -> bool { | ApiProvider::Volcengine | ApiProvider::Arcee | ApiProvider::Sglang + | ApiProvider::Moonshot // #3016: Kimi thinking traces use reasoning_content ) } diff --git a/crates/tui/src/models.rs b/crates/tui/src/models.rs index 80849c94..bda1d2e5 100644 --- a/crates/tui/src/models.rs +++ b/crates/tui/src/models.rs @@ -312,6 +312,10 @@ pub fn model_supports_reasoning(model: &str) -> bool { if lower.contains("deepseek") && lower.contains("v4") { return true; } + // #3016: Moonshot-native Kimi IDs also emit reasoning_content + if lower.starts_with("kimi-") { + return true; + } matches!( lower.as_str(), "arcee-ai/trinity-large-thinking" From 7a64119635c3be1d4c970116eef0bbf43452e8b4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 02:35:36 +0000 Subject: [PATCH 2/2] =?UTF-8?q?test(reasoning):=20#3016=20coverage=20?= =?UTF-8?q?=E2=80=94=20Moonshot=20in=20provider=5Faccepts=5Freasoning=5Fco?= =?UTF-8?q?ntent,=20kimi-k2.6=20stream=20classification,=20multi-chunk=20M?= =?UTF-8?q?oonshot=20reasoning=5Fcontent=20decoder=20fixture;=20exclude=20?= =?UTF-8?q?non-thinking=20kimi-for-coding=20from=20the=20kimi-=20prefix=20?= =?UTF-8?q?rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude https://claude.ai/code/session_018zaP8vUfTAsrE38L6h6fw5 --- crates/tui/src/client/chat.rs | 86 +++++++++++++++++++++++++++++++++++ crates/tui/src/models.rs | 15 +++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/crates/tui/src/client/chat.rs b/crates/tui/src/client/chat.rs index 47a08be6..438bc94e 100644 --- a/crates/tui/src/client/chat.rs +++ b/crates/tui/src/client/chat.rs @@ -2746,6 +2746,76 @@ mod stream_decoder_tests { ); } + #[test] + fn decoder_streams_moonshot_multi_chunk_reasoning_as_thinking() { + // #3016: recorded shape from Moonshot's native endpoint — kimi-k2.6 + // streams `reasoning_content` deltas before the answer text. The + // thinking deltas must accumulate into ONE thinking block and the + // answer must arrive as text, not be glued into the trace. + let chunks = [ + r#"{"id":"cmpl-kimi","model":"kimi-k2.6","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"Let me check"}}]}"#, + r#"{"id":"cmpl-kimi","model":"kimi-k2.6","choices":[{"index":0,"delta":{"reasoning_content":" the config."}}]}"#, + r#"{"id":"cmpl-kimi","model":"kimi-k2.6","choices":[{"index":0,"delta":{"content":"The answer is 42."}}]}"#, + ]; + + let is_reasoning = + is_reasoning_model_for_stream(crate::config::ApiProvider::Moonshot, "kimi-k2.6"); + let mut content_index = 0u32; + let mut text_started = false; + let mut thinking_started = false; + let mut tool_indices = std::collections::HashMap::new(); + let mut events = Vec::new(); + for chunk in chunks { + let value: Value = serde_json::from_str(chunk).expect("valid SSE JSON"); + events.extend(parse_sse_chunk( + &value, + &mut content_index, + &mut text_started, + &mut thinking_started, + &mut tool_indices, + is_reasoning, + )); + } + + let thinking: String = events + .iter() + .filter_map(|event| match event { + StreamEvent::ContentBlockDelta { + delta: Delta::ThinkingDelta { thinking }, + .. + } => Some(thinking.as_str()), + _ => None, + }) + .collect(); + assert_eq!(thinking, "Let me check the config."); + + let thinking_starts = events + .iter() + .filter(|event| { + matches!( + event, + StreamEvent::ContentBlockStart { + content_block: ContentBlockStart::Thinking { .. }, + .. + } + ) + }) + .count(); + assert_eq!(thinking_starts, 1, "one thinking block: {events:?}"); + + let text: String = events + .iter() + .filter_map(|event| match event { + StreamEvent::ContentBlockDelta { + delta: Delta::TextDelta { text }, + .. + } => Some(text.as_str()), + _ => None, + }) + .collect(); + assert_eq!(text, "The answer is 42."); + } + #[test] fn decoder_accepts_openrouter_reasoning_delta_with_extra_fields() { let events = decode_chunk( @@ -3650,6 +3720,22 @@ mod alias_thinking_detection_tests { assert!(provider_accepts_reasoning_content(ApiProvider::NvidiaNim)); assert!(provider_accepts_reasoning_content(ApiProvider::XiaomiMimo)); assert!(provider_accepts_reasoning_content(ApiProvider::Arcee)); + // #3016: Moonshot's native endpoint streams Kimi thinking as + // reasoning_content. + assert!(provider_accepts_reasoning_content(ApiProvider::Moonshot)); + } + + #[test] + fn stream_classifies_moonshot_kimi_as_reasoning() { + // #3016: without this, kimi-k2.6 thinking leaked into answer text. + assert!(is_reasoning_model_for_stream( + ApiProvider::Moonshot, + "kimi-k2.6" + )); + assert!( + !is_reasoning_model_for_stream(ApiProvider::Moonshot, "kimi-for-coding"), + "kimi-for-coding is Moonshot's documented non-thinking model" + ); } #[test] diff --git a/crates/tui/src/models.rs b/crates/tui/src/models.rs index bda1d2e5..ed0b505a 100644 --- a/crates/tui/src/models.rs +++ b/crates/tui/src/models.rs @@ -312,8 +312,10 @@ pub fn model_supports_reasoning(model: &str) -> bool { if lower.contains("deepseek") && lower.contains("v4") { return true; } - // #3016: Moonshot-native Kimi IDs also emit reasoning_content - if lower.starts_with("kimi-") { + // #3016: Moonshot-native Kimi IDs also emit reasoning_content. + // `kimi-for-coding` is Moonshot's documented non-thinking model — it + // must not be classified as reasoning-capable by the prefix rule. + if lower.starts_with("kimi-") && lower != "kimi-for-coding" { return true; } matches!( @@ -546,6 +548,15 @@ mod tests { } } + #[test] + fn moonshot_native_kimi_ids_support_reasoning_except_for_coding() { + // #3016: bare Moonshot ids (no moonshotai/ prefix) emit + // reasoning_content; kimi-for-coding is the non-thinking exception. + assert!(model_supports_reasoning("kimi-k2.6")); + assert!(model_supports_reasoning("kimi-k2.5")); + assert!(!model_supports_reasoning("kimi-for-coding")); + } + #[test] fn arcee_direct_models_have_static_windows_without_reasoning_flag() { assert_eq!(