agent: Skip empty assistant messages for Mistral provider (#47579)

Ian Chamberlain created

This small change *may* help with #39211, #35025 or #39031 although I'm
not positive it will fix it entirely. At a minimum, it should prevent
sending invalid requests to the Mistral API, which return a 400 error
and can prevent the thread from progressing if it gets stuck in a retry
loop.

This should prevent sending a message like `{ "role": "assistant",
"content": "" }` if for some reason the thread contains a message with
an empty text content and no tool calls.

Some users (including myself) have encountered this with the Devstral 2
models, although it doesn't seem trivial to reproduce:
https://github.com/zed-industries/zed/issues/37021#issuecomment-3728902562

When it occurs, error logs look something like this:
```
2026-01-24T19:53:35-08:00 ERROR [crates/agent/src/thread.rs:984] invalid type: string "Failed to connect to Mistral API: 400 Bad Request {\"object\":\"error\",\"message\":\"Assistant message must have either content or tool_calls, but not none.\",\"type\":\"invalid_request_assistant_message\",\"param\":null,\"code\":\"3240\"}", expected struct EditFileToolOutput
2026-01-24T19:53:35-08:00 ERROR [crates/agent/src/thread.rs:984] invalid type: string "Failed to connect to Mistral API: 400 Bad Request {\"object\":\"error\",\"message\":\"Assistant message must have either content or tool_calls, but not none.\",\"type\":\"invalid_request_assistant_message\",\"param\":null,\"code\":\"3240\"}", expected struct EditFileToolOutput
2026-01-24T19:53:35-08:00 ERROR [crates/agent/src/thread.rs:984] invalid type: string "Failed to connect to Mistral API: 400 Bad Request {\"object\":\"error\",\"message\":\"Assistant message must have either content or tool_calls, but not none.\",\"type\":\"invalid_request_assistant_message\",\"param\":null,\"code\":\"3240\"}", expected struct EditFileToolOutput
```

Release Notes:

- Fixed agent sometimes sending invalid messages to Mistral API

Change summary

crates/language_models/src/provider/mistral.rs | 10 ++++++++++
1 file changed, 10 insertions(+)

Detailed changes

crates/language_models/src/provider/mistral.rs 🔗

@@ -405,6 +405,9 @@ pub fn into_mistral(
             Role::Assistant => {
                 for content in &message.content {
                     match content {
+                        MessageContent::Text(text) if text.is_empty() => {
+                            // Mistral API returns a 400 if there's neither content nor tool_calls
+                        }
                         MessageContent::Text(text) => {
                             messages.push(mistral::RequestMessage::Assistant {
                                 content: Some(mistral::MessageContent::Plain {
@@ -853,6 +856,13 @@ mod tests {
                     cache: false,
                     reasoning_details: None,
                 },
+                // should skip empty assistant messages
+                LanguageModelRequestMessage {
+                    role: Role::Assistant,
+                    content: vec![MessageContent::Text("".into())],
+                    cache: false,
+                    reasoning_details: None,
+                },
             ],
             temperature: Some(0.5),
             tools: vec![],