diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 5fef40b2b1badbc77133ebe67fbe0f1fe5521259..fdb0fb7b3a7f510c8e55deefcea8e3b7f4d1eb86 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -652,6 +652,7 @@ impl MistralEventMapper { if let Some(tool_id) = tool_call.id.clone() && !tool_id.is_empty() + && tool_id != "null" { entry.id = tool_id; } @@ -905,6 +906,69 @@ mod tests { use super::*; use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent}; + fn tool_call_chunk( + id: Option<&str>, + name: Option<&str>, + arguments: Option<&str>, + finish_reason: Option<&str>, + ) -> mistral::StreamResponse { + mistral::StreamResponse { + id: "resp".into(), + object: "chat.completion.chunk".into(), + created: 0, + model: "test".into(), + choices: vec![mistral::StreamChoice { + index: 0, + delta: mistral::StreamDelta { + role: None, + content: None, + tool_calls: if finish_reason.is_some() { + None + } else { + Some(vec![mistral::ToolCallChunk { + index: 0, + id: id.map(Into::into), + function: Some(mistral::FunctionChunk { + name: name.map(Into::into), + arguments: arguments.map(Into::into), + }), + }]) + }, + }, + finish_reason: finish_reason.map(Into::into), + }], + usage: None, + } + } + + #[test] + fn test_streaming_tool_call_ignores_null_id() { + // Mistral's streaming API sometimes sends `"id": "null"` in continuation chunks. + let mut mapper = MistralEventMapper::new(); + + mapper.map_event(tool_call_chunk( + Some("real_id_123"), + Some("read_file"), + Some("{\"path\":"), + None, + )); + mapper.map_event(tool_call_chunk( + Some("null"), + None, + Some("\"a.txt\"}"), + None, + )); + let events = mapper.map_event(tool_call_chunk(None, None, None, Some("tool_calls"))); + + let Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) = &events[0] else { + panic!("Expected first event to be ToolUse, got: {:?}", events[0]); + }; + + assert_eq!(tool_use.id.to_string(), "real_id_123"); + assert_eq!(tool_use.name.as_ref(), "read_file"); + assert_eq!(tool_use.input, serde_json::json!({"path": "a.txt"})); + } + #[test] fn test_into_mistral_basic_conversion() { let request = LanguageModelRequest {