diff --git a/crates/forge_app/src/dto/openai/transformers/pipeline.rs b/crates/forge_app/src/dto/openai/transformers/pipeline.rs index 36df972c73..59c92aa6ef 100644 --- a/crates/forge_app/src/dto/openai/transformers/pipeline.rs +++ b/crates/forge_app/src/dto/openai/transformers/pipeline.rs @@ -82,10 +82,12 @@ impl Transformer for ProviderPipeline<'_> { || provider.id == ProviderId::FIREWORKS_AI_FIREPASS || is_deepseek_compatible(provider, request) || when_model("kimi")(request) + || is_xiaomi_mimo_provider(provider) }); - let default_reasoning_content = DefaultReasoningContent - .when(move |request: &Request| is_deepseek_compatible(provider, request)); + let default_reasoning_content = DefaultReasoningContent.when(move |request: &Request| { + is_deepseek_compatible(provider, request) || is_xiaomi_mimo_provider(provider) + }); let cerebras_compat = MakeCerebrasCompat.when(move |_| provider.id == ProviderId::CEREBRAS); @@ -158,6 +160,12 @@ fn is_deepseek_compatible(provider: &Provider, request: &Request) -> bool { false } +/// Checks if provider is Xiaomi MiMo, which requires reasoning to be replayed +/// as a flat reasoning_content field in follow-up requests. +fn is_xiaomi_mimo_provider(provider: &Provider) -> bool { + provider.id == ProviderId::XIAOMI_MIMO +} + /// Checks if the request model is a gemini-3 model (which supports thought /// signatures) fn is_gemini3_model(req: &Request) -> bool { @@ -384,6 +392,22 @@ mod tests { } } + fn xiaomi_mimo(key: &str) -> Provider { + Provider { + id: ProviderId::XIAOMI_MIMO, + provider_type: Default::default(), + response: Some(ProviderResponse::OpenAI), + url: Url::parse("https://token-plan-sgp.xiaomimimo.com/v1/chat/completions").unwrap(), + auth_methods: vec![forge_domain::AuthMethod::ApiKey], + url_params: vec![], + credential: make_credential(ProviderId::XIAOMI_MIMO, key), + custom_headers: None, + models: Some(ModelSource::Url( + Url::parse("https://token-plan-sgp.xiaomimimo.com/v1/models").unwrap(), + )), + } + } + fn opencode_go(key: &str) -> Provider { Provider { id: ProviderId::OPENCODE_GO, @@ -1214,6 +1238,61 @@ mod tests { assert!(message.reasoning_details.is_some()); } + #[test] + fn test_xiaomi_mimo_provider_converts_reasoning_details_to_reasoning_content() { + let provider = xiaomi_mimo("xiaomi-mimo"); + let fixture = Request::default().messages(vec![crate::dto::openai::Message { + role: crate::dto::openai::Role::Assistant, + content: Some(crate::dto::openai::MessageContent::Text("test".to_string())), + name: None, + tool_call_id: None, + tool_calls: None, + reasoning_details: Some(vec![crate::dto::openai::ReasoningDetail { + r#type: "reasoning.text".to_string(), + text: Some("thinking...".to_string()), + signature: None, + data: None, + id: None, + format: None, + index: None, + }]), + reasoning_text: None, + reasoning_opaque: None, + reasoning_content: None, + extra_content: None, + }]); + + let mut pipeline = ProviderPipeline::new(&provider, false); + let actual = pipeline.transform(fixture); + + let message = actual.messages.unwrap().into_iter().next().unwrap(); + assert_eq!(message.reasoning_content, Some("thinking...".to_string())); + assert!(message.reasoning_details.is_none()); + } + + #[test] + fn test_xiaomi_mimo_provider_falls_back_to_empty_reasoning_content_when_none() { + let provider = xiaomi_mimo("xiaomi-mimo"); + let fixture = Request::default().messages(vec![crate::dto::openai::Message { + role: crate::dto::openai::Role::Assistant, + content: Some(crate::dto::openai::MessageContent::Text("test".to_string())), + name: None, + tool_call_id: None, + tool_calls: None, + reasoning_details: None, + reasoning_text: None, + reasoning_opaque: None, + reasoning_content: None, + extra_content: None, + }]); + + let mut pipeline = ProviderPipeline::new(&provider, false); + let actual = pipeline.transform(fixture); + + let message = actual.messages.unwrap().into_iter().next().unwrap(); + assert_eq!(message.reasoning_content, Some(String::new())); + } + #[test] fn test_openai_provider_does_not_enforce_strict_tool_schema() { let provider = openai("openai");