Repository navigation
Description
Upstream issue report (draft for mistralai/client-python)
Title: [BUG MODEL] mistral-large-4 chat completions loses tool-call blocks server-side (finish_reason=tool_calls with none present); reasoning responses use a choices[].messages[] shape the SDK parses as message=None
Environment
mistralaiSDK 3.5.0 (Python 3.12),client.chat.complete_async- Model:
mistral-large-4, chat completions endpoint, 2026-11-01 - Request: system + user turn, 31 function tools (a real coding agent's toolset),
parallel_tool_calls=True,prompt_cache_keyset - Also reproduced through two independent agent harnesses (Mistral Vibe and a Rust client), so it is not client-specific
Bug 1 — tool calls are lost between generation and serialization
With a large tool set and thinking enabled, most turns answer with only a thinking block and a text block. Two flavours:
finish_reason: "stop"right after a plan, while the thinking text shows the model intended to call tools next.- Worse:
finish_reason: "tool_calls"and no tool-call block anywhere in the response — the raw choice contains exactly one message with[thinking, text]content blocks and nothing else:
{
"index": 0,
"finish_reason": "tool_calls",
"messages": [{
"role": "assistant",
"content": [
{"thinking": [{"text": "The user wants me to demonstrate tool usage: 3 background tasks each with 3 ..."}], "type": "...", "closed": ...},
{"text": "Here's what I'd use, and why:\n\n1. **`todo_write`** — track ...", "type": "..."}
]
}]
}The tool calls appear to be dropped between generation and serialization. Nothing client-side can recover them. Across ~20 replayed attempts the failure rate was ~60–100% for this prompt shape; it varies over the day (possibly backend routing — x-affinity/prompt_cache_key pin a session to a backend).
The same model + prompt over the Responses API (public preview) serializes function_call output items reliably (4/4 in our clean runs), which strongly suggests the loss is in the chat-completions serialization path for reasoning models.
This also matches the defensive machinery Vibe itself carries (adapters/_tool_call_finish.py: responses "can finish with tool calls without sending any", retried as UnusableToolCalls).
Bug 2 — the reasoning response shape parses as message=None
For a thinking model, the choice carries messages (plural) — a list of assistant messages with thinking/text content blocks — instead of the documented message. SDK 3.5.0 deserializes the choice to message=None, so even a successful text answer reaches SDK users as an empty completion:
answer.choices[0].message # -> None
answer.choices[0].finish_reason # -> "stop"Either the SDK needs to parse the new shape, or the API should keep emitting message alongside messages.
Reproducer
# pip install mistralai==3.5.0 ; set MISTRAL_API_KEY
import asyncio, uuid
from mistralai.client import Mistral
client = Mistral(api_key=__import__("os").environ["MISTRAL_API_KEY"])
TOOLS = [...] # a large toolset matters; we used 31 real agent tools
PROMPT = """I want you to run 3 background tasks each with 3 background monitors,
each of them counting from 1 to N with N 4, 10, 30, taking 1 second per increment."""
async def main():
for i in range(5):
a = await client.chat.complete_async(
model="mistral-large-4",
messages=[{"role": "user", "content": PROMPT}],
tools=TOOLS, parallel_tool_calls=True,
prompt_cache_key=str(uuid.uuid4()),
)
c = a.choices[0]
calls = c.message.tool_calls if c.message else None
print(i, c.finish_reason, "tool_calls:", len(calls) if calls else 0)
asyncio.run(main())(Scripts used for the full evidence: system-prompt + 31-tool replay, and a raw block dumper that prints every block of every message in choices[0].messages.)
Expected
finish_reason: "tool_calls"implies at least one tool-call block in the response.- The SDK parses the API's own response (
messagepresent, thinking content either in a typed block or surfaced undercontent).
Side note
The Responses API endpoint returned 500 {"message": "Service unavailable.", "code": "3000"} intermittently during the same session — expected for a preview, just noting it.
Activity