What function calling with Mistral gets you
Function calling — sometimes called tool use — lets a Mistral model return structured JSON that matches a tool definition you supplied, instead of free text. That JSON is what your code executes. The pattern turns a conversational LLM into a router for real actions: database reads, third-party API calls, code execution, retrieval. Every serious agent framework on Mistral is built on this primitive.
The tool schema
You pass a `tools` array where each tool has a name, description, and JSON-schema parameters block. Mistral requires strict Draft-07 with `additionalProperties: false` on every object — loose schemas are the number-one cause of `invalid_tool_schema` 400s that the docs describe but the error surface makes hard to debug. Write your schemas with a validator in your test suite before you ever call the API.
The two-step dance
- First call: the model returns `tool_calls` on the assistant message. There is no `content` text yet.
- You execute the tool locally and append a `role: tool` message with the result, keyed to the `tool_call_id`.
- Second call: the model reads the tool result and writes the final assistant reply.
Parallel tool calls
Mistral can emit multiple tool_calls in a single turn. Execute them in parallel — Promise.all in Node, asyncio.gather in Python — then append all the `role: tool` messages before the next model call. Do not serialize unless the tools genuinely depend on each other. Latency compounds across sequential tool round-trips; the parallel case is often what makes an agent feel fast.
tool_choice: forcing and forbidding calls
Pass `tool_choice: 'auto'` to let the model pick; `'any'` to force some tool call; `'none'` to disable tools for this turn. Pass an object like `{ type: 'function', function: { name: 'my_tool' } }` to force a specific tool. Note that Mistral uses `'any'` where OpenAI uses `'required'` — the semantics are the same but the string differs.
Common failure modes
- Schema uses `type: 'any'` or omits `additionalProperties: false` → 400 invalid_tool_schema.
- You send the model back without a `role: tool` message → it hallucinates a plausible result.
- You put natural-language errors in the tool response → the model treats them as data. Return structured `{ error: '...' }` instead so it can plan a retry.
- You forget to include `tool_call_id` on the response message → the model cannot match the reply to the call.
- You mutate the conversation history between calls → the second call sees a different tool_calls block than the first emitted, and refuses to continue.
Which Mistral models support tools
Commercial Mistral Large, Mistral Small, and Ministral all support tool calling with the same schema. Codestral is a code-specialist model and does not — reach for Large if you need tools in a code workflow. Open-weight releases (Mistral 7B, Mixtral) support tools only when you run them through a serving layer that adds the harness.
Testing your tool schema
Write a Jest or pytest suite that validates each tool schema against a Draft-07 JSON-schema validator. Then run a smoke test that hits Mistral with an obviously tool-triggering prompt for each tool and asserts the returned tool_calls parses. This catches 90% of the surface errors before they reach production and turns invalid_tool_schema from a mystery 400 into a green light in CI.
Streaming and tool_calls
Mistral supports SSE streaming for chat completions, including turns that emit tool_calls. The `tool_calls` field arrives incrementally — you'll see the function name first, then the arguments string built up chunk by chunk. Do not try to JSON.parse the arguments until the stream signals finish_reason. Buffer the argument string across chunks, parse once at the end, and dispatch the tool call. Naive parsers that run on every chunk will throw and be swallowed silently in production.
Debugging invalid_tool_schema quickly
When a request 400s with invalid_tool_schema, do not guess. Copy the tool JSON out of your request, drop it into an online Draft-07 validator, and read the error path. Nine times out of ten the fix is one of: missing `additionalProperties: false` on a nested object, a `type: 'any'` that should be `type: 'string'` with an enum, or a `required` array that names a property the schema does not actually declare. Fix in isolation, then re-run the request.
Agents built on Mistral tool use
An agent is a loop: model call → tool_calls → execute tools → append role: tool messages → model call again → repeat until the model returns a plain assistant message with no tool_calls. Mistral's `agents` endpoint wraps this loop for you, but rolling your own is a hundred lines of code and gives you the observability hooks production actually needs. Log every step with timing so you can spot the tool call that dominates end-to-end latency.
End-to-end example: weather tool in ~30 lines
Define the tool schema: `{ type: 'function', function: { name: 'get_weather', description: 'Get current weather for a city', parameters: { type: 'object', additionalProperties: false, required: ['city'], properties: { city: { type: 'string', description: 'City name, e.g. Paris' } } } } }`. First call: `client.chat.complete({ model: 'mistral-large-latest', messages: [{ role: 'user', content: 'Weather in Paris?' }], tools: [weatherTool], tool_choice: 'auto' })`. The response comes back with `choices[0].message.tool_calls = [{ id: 'call_abc', function: { name: 'get_weather', arguments: '{"city":"Paris"}' } }]`. Parse the arguments string with `JSON.parse`, execute your real function, then append two messages to history: the assistant message with `tool_calls`, and a `{ role: 'tool', tool_call_id: 'call_abc', content: '{"temp_c":18,"conditions":"cloudy"}' }` message. Second call with the extended history returns the plain assistant reply — 'It's 18°C and cloudy in Paris right now.' That is the entire pattern; every agent framework is variations on it.
Mistral vs OpenAI function calling: what actually differs
The shape is identical — same `tools` array, same `tool_calls` on the assistant message, same `role: 'tool'` for results. The differences are small but bite in production. Mistral uses `tool_choice: 'any'` where OpenAI uses `'required'`; both mean 'force some tool call'. Mistral validates JSON schema more strictly — `additionalProperties: false` is effectively mandatory on every object, and `type: 'any'` will 400 where OpenAI silently tolerates it. Mistral's parallel-tool-call behavior is the same, but the specific triggering prompts differ slightly because model-side tool routing is trained differently. Streaming payload shapes match. Migration from OpenAI to Mistral is usually a base URL swap plus tightening any loose schemas your OpenAI code got away with.
Common pitfalls that waste half a day
The five failures we see repeatedly: shipping without a JSON-schema validator in CI (invalid_tool_schema in prod is embarrassing and completely preventable); parsing tool_call arguments incrementally during streaming instead of buffering (throws swallowed silently); forgetting the `tool_call_id` on the `role: tool` message so the model cannot match the reply; putting free-text errors in tool responses instead of structured `{ error: '...' }` (model treats them as data and hallucinates a plan around them); and stuffing 40+ tools into every request because selection is 'the model's problem' (past ~20 tools, selection quality collapses — narrow with a classifier first). Fixing all five is under a day of work and saves a week of debugging over the next quarter.