Merge PR #3046 from Hmbown: add Moonshot/Kimi to reasoning-content provider and model gates

fix(reasoning): add Moonshot/Kimi to reasoning-content provider and model support
This commit is contained in:
Hunter Bown
2026-06-10 22:29:44 -07:00
committed by GitHub
2 changed files with 102 additions and 0 deletions
+87
View File
@@ -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
)
}
@@ -2745,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(
@@ -3649,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]
+15
View File
@@ -312,6 +312,12 @@ 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.
// `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!(
lower.as_str(),
"arcee-ai/trinity-large-thinking"
@@ -542,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!(