Mistral AI Function Calling and Tool Use
The request succeeded. The body parsed. And the model decided, on its own, to take an action โ the wrong one, or the right one twice, or none at all while explaining what it would have done.
๐ก Monitor your APIs โ know when they go down before your users do
Better Stack checks uptime every 30 seconds with instant Slack, email & SMS alerts. Free tier available.
Affiliate link โ we may earn a commission at no extra cost to you
Every other class of Mistral AI API problem is about a request: it failed, it was slow, it cost too much, it came back in a shape you could not parse. Tool calling is about a decision. You hand the model a set of functions and it chooses whether to act, which function, with which arguments, and when it is finished. Nothing in that sentence is covered by a status code, which is why the failure modes in this guide almost all return HTTP 200 with a perfectly valid body attached to a completely wrong outcome.
The short version
Write tool descriptions as if they were the prompt, because they are. Validate model-authored arguments against a strict schema before the function body runs, and return validation failures to the model as tool messages instead of throwing. Bound the loop with a hard step ceiling and a repeated-call guard. And treat tool support on Mistral AI as a property of the specific model you pinned, verified by a smoke test, not as a guarantee of the endpoint.
A note on scope, because these two get conflated: this guide is not about structured output. Structured output constrains the shape of an answer you already know the model will produce. Tool calling hands over control flow. They compose โ most real agents want validated arguments going in and a validated answer coming out โ but a JSON schema on the response body does nothing to stop an agent from calling your refund endpoint twice.
The Wire Shape on Mistral AI
Mistral AI uses near-OpenAI: tools array, tool_calls on the assistant message, results as role "tool" messages carrying tool_call_id. The mechanical rule that breaks more multi-turn agents than any other: the assistant message containing the tool calls must go back into the conversation verbatim, and every call in it must be answered by exactly one tool-role message carrying the original id. Summarise that assistant turn, drop one of several parallel calls, or regenerate the ids while rebuilding history from your own database, and the next request fails โ after your tool already ran and its side effect already happened.
Store transcripts as the provider's own message objects, not as your prettier internal representation. Every team that invents a cleaner schema for agent history rediscovers this on the second turn.
The Description Is the Prompt
A tool that is never called and a tool that is called at the wrong moment are the same bug, and it is nearly always in the description rather than in the model. The model cannot see your implementation. It sees a name, a sentence, and a parameter list, and from those it decides. Most teams write descriptions for the human reading the code and then wonder why the model chose badly.
Three rules carry most of the improvement. Name the trigger conditions in the user's vocabulary, not yours โ the description should contain the words a customer would actually type. State the negative case explicitly, because tools get confused with their neighbours far more often than they get invented from nothing. And replace every free-text parameter that has a closed set of valid values with an enum, which converts a whole class of silent wrong-argument bugs into an impossibility.
// Bad: the model has to guess, and it will guess plausibly and wrongly.
{
name: 'search',
description: 'Search',
parameters: { type: 'object', properties: { q: { type: 'string' } } }
}
// Good: the description is the prompt. Enums remove whole failure classes.
{
name: 'search_orders',
description:
'Find orders belonging to the CURRENT customer. Use when the user asks about ' +
'an order, a delivery, or a refund. Do NOT use for product questions -- use ' +
'search_catalog for those. Returns at most 20 orders, newest first.',
parameters: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['pending', 'shipped', 'delivered', 'cancelled'],
description: 'Omit to search all statuses.'
},
since: {
type: 'string',
format: 'date',
description: 'ISO date, YYYY-MM-DD. Omit for no lower bound.'
}
},
required: [],
additionalProperties: false
}
}Keep the set small. Beyond roughly a dozen tools in one call, selection accuracy degrades on every model, and the fix is not a better description โ it is a router turn that picks a small tool group first, then a second turn with only that group registered.
The Loop, With Every Guard It Needs
The loop is where agents actually fail, and the naive version โ call, execute, call again until the model stops โ has no ceiling, no argument validation, no repeat detection, and turns any tool exception into a dead conversation. Each guard below exists because its absence is a known production incident.
// The loop is the product. Everything below is a guard you will wish you had.
const MAX_STEPS = 6; // hard ceiling: a bug must terminate
const messages = [systemMsg, userMsg];
for (let step = 0; step < MAX_STEPS; step++) {
const res = await client.chat.completions.create({
model: PINNED_MODEL, // tool support is per-model; pin it
messages,
tools: TOOLS,
tool_choice: step === 0 ? 'auto' : 'auto',
});
const msg = res.choices[0].message;
messages.push(msg); // push the assistant turn VERBATIM
const calls = msg.tool_calls ?? [];
if (calls.length === 0) return msg.content; // model answered: done
for (const call of calls) {
const tool = REGISTRY[call.function.name];
// 1. Unknown tool name -> tell the model, do not throw.
if (!tool) {
messages.push(toolMsg(call.id, { error: 'unknown_tool', name: call.function.name }));
continue;
}
// 2. Arguments are model output, i.e. untrusted input. Validate BEFORE executing.
let args;
try {
args = tool.schema.parse(JSON.parse(call.function.arguments));
} catch (e) {
messages.push(toolMsg(call.id, { error: 'invalid_arguments', detail: String(e) }));
continue; // the model gets to correct itself; the tool never ran
}
// 3. Repeat detection: identical call twice in a row is a loop, not progress.
const fingerprint = call.function.name + ':' + call.function.arguments;
if (seen.has(fingerprint)) {
messages.push(toolMsg(call.id, { error: 'repeated_call', hint: 'You already called this with these arguments. Use the previous result.' }));
continue;
}
seen.add(fingerprint);
// 4. Side effects get an idempotency key derived from the call id.
try {
const result = await tool.run(args, { idempotencyKey: call.id });
messages.push(toolMsg(call.id, result));
} catch (err) {
// 5. Tool failures are DATA, not exceptions. The model can route around them.
messages.push(toolMsg(call.id, { error: 'tool_failed', message: err.message }));
}
}
}
// 6. Ceiling hit. This is an incident signal, not a silent fallback.
metrics.increment('agent.step_ceiling_hit', { model: PINNED_MODEL });
throw new AgentStepLimitError(MAX_STEPS, messages);
// Every tool result must be a tool-role message carrying the ORIGINAL call id.
// Dropping or rewriting that id is the single most common multi-turn break.
function toolMsg(id, payload) {
return { role: 'tool', tool_call_id: id, content: JSON.stringify(payload) };
}Two of those deserve emphasis. Returning tool failures as data rather than exceptions is what separates an agent that degrades gracefully from one that dies mid-task: a model told that the lookup failed will apologise or try another route, exactly as a competent human would. And the step ceiling should be an alert, not a silent fallback โ a task that legitimately needs more steps than you budgeted is a design change, while a task that loops is a defect, and you want to know which one you are looking at.
Forcing, Preventing, and Parallel Calls
tool_choice is the cheapest correctness lever in the whole surface and it is routinely left on auto for turns where the answer was never in doubt. An extraction turn should force a specific named tool: prose is not an acceptable output there, so do not allow it and then write retry logic to catch it. A final summarisation turn should set none, so a model that has already gathered everything cannot decide on one more lookup. Auto is for genuinely open turns, which is fewer of them than most agents assume.
Parallel calls are a real latency win for independent lookups and a real hazard for a handler written on the assumption of one call per turn. The failure is quiet: you execute the first element of the array, ignore the rest, and the next request fails or the model proceeds believing it received answers it never got. Iterate over every call, emit one tool message per id, and for anything with a side effect prefer serialisation plus an idempotency key over concurrency.
Arguments Are Untrusted Input
A tool argument is a value written by a language model that read whatever your user typed. That makes it exactly as trustworthy as a form field, and the fact that it arrived via an API response rather than a POST body changes nothing. Parse it against a strict schema, reject unknown properties instead of quietly ignoring them, and enforce authorisation in your own code โ scope every query to the current session's tenant rather than to an identifier the model supplied.
The dangerous shape is a tool that accepts a record id and looks it up without a tenant check, because a persuasive user message can convince a model to ask for someone else's record and your API will return it with a clean 200. This is where tool calling meets prompt injection: the injected instruction does not need to break your parser, it only needs to persuade the model to call a tool you were willing to expose.
Tool-Calling Failure Modes, and How They Present
Note the status-code column. Five of these six return 200, which is why uptime dashboards and error-rate alerts see nothing while the agent misbehaves.
| Symptom | HTTP | Usual cause | Fix |
|---|---|---|---|
| Model answers in prose, tool_calls is empty | 200 | Model lacks tool support, or the description does not match the user's wording | Verify capability on the pinned model; rewrite the description around user phrasing; force with tool_choice |
| Right tool, wrong arguments | 200 | Free-text parameter where a closed set exists; ambiguous description | Enums instead of strings, explicit units and formats, strict schema validation before execution |
| Same call repeated until the ceiling | 200 | Tool result was empty or unreadable, so the model did not register progress | Return non-empty structured results; fingerprint calls and reject repeats with a hint |
| Second turn 400s after the tool ran | 400 | tool_call_id dropped, rewritten, or regenerated when rebuilding history | Persist the provider's id verbatim and replay it unchanged; one tool message per call |
| Tool ran twice, customer charged twice | 200 | Retry or parallel calls on a side-effecting tool with no idempotency | Idempotency key derived from the call id; serialise side-effecting tools |
| Agent returns data the user should not see | 200 | Authorisation delegated to the model instead of enforced in the tool | Scope every query to the session's tenant in code; never trust an id the model supplied |
The Mistral Trap: tool_call_id Is Format-Checked, and tool_choice Has an Extra Setting
Mistral validates the tool_call_id you echo back. It expects a short alphanumeric identifier -- nine characters -- and it rejects the request if you invent your own longer or punctuated id, which is exactly what happens when you rebuild the conversation from your own database rather than replaying the id the API handed you. The failure is a 400 on the follow-up turn, after the tool already ran, so the side effect has happened and the conversation cannot be continued. If you persist agent transcripts, store the provider's id verbatim as an opaque string and replay it unchanged; do not regenerate ids, do not normalise them, and do not reuse an id from another provider's transcript when replaying a conversation across backends.
The second difference is in tool_choice. Alongside auto and none, Mistral offers a setting that forces the model to call some tool without naming which one -- the model still chooses, it simply is not allowed to answer in prose. That is the right setting for a router turn where any of several tools is acceptable but a chat reply is not, and it removes a whole class of retry logic people write to handle the model answering when it was supposed to act. Forcing a specific named tool remains available and remains the right choice for an extraction turn.
Both traps share a moral: Mistral is OpenAI-shaped enough that a port compiles and different enough that it fails on the second turn. Test the multi-turn path, not the single call, when you move an agent here.
What to Log
Agent observability is not request observability. Log, per turn: the number of steps the task consumed, every tool name chosen with the outcome of its argument validation, the count of repeated-call rejections, and whether the step ceiling was hit. Those four numbers turn "the agent feels flaky" into a specific broken tool description. Step-count distribution in particular is the single best health metric an agent has โ when its tail starts creeping toward your ceiling, something has stopped working before anyone complains.
What your own logs cannot tell you is whether Mistral AI itself is the reason a step failed, because they only contain your traffic. That is the gap external monitoring fills.
Frequently Asked Questions
Why does Mistral AI ignore my tool and answer in prose instead?
Three causes, in the order you should check them. First, the model you named may not support tool calling at all -- capability is a property of the model, not of the endpoint, so an unsupported model happily accepts your tools array and returns an ordinary completion. Second, your tool description may not connect to the user's phrasing: the description is the only thing the model has to decide with, and "Search" tells it nothing while "Find orders belonging to the current customer; use when the user asks about a delivery or a refund" tells it everything. Third, you may simply be leaving the choice open when you did not mean to -- if a plain answer is never acceptable on this turn, force a call with tool_choice rather than writing retry logic to catch the times it answered.
How do I stop an agent from calling the same Mistral AI tool forever?
With two independent guards, because they catch different bugs. A hard step ceiling -- five or six iterations for most tasks -- terminates the loop no matter what, and hitting it should page someone rather than fall back silently, since a task that legitimately needs more steps than you budgeted is a design change and a task that loops is a defect. Separately, fingerprint each call as name plus serialised arguments and refuse to execute a repeat: return a tool message telling the model it already made that exact call and should use the earlier result. The repeat guard fixes the common case, where the model did not notice its own previous result; the ceiling catches everything else, including the cases you have not thought of.
Should tool errors be thrown or returned to the model?
Returned, in almost every case. If your database times out and you throw, the agent dies mid-task and the user gets nothing; if you return a tool message saying the lookup failed, the model can apologise, try a different tool, or ask a clarifying question, which is what a competent human assistant would do. Return the error as structured data with a short machine-readable code and a human-readable message, and keep it terse -- an entire stack trace fed back into the context window is tokens spent teaching the model nothing. The exceptions worth throwing on are authentication failures and anything indicating your own misconfiguration, because those are not conditions the model can route around and they should surface as incidents rather than being smoothed over.
Is it safe to execute the arguments Mistral AI generates?
Not without validating them first, because tool arguments are model output and model output is untrusted input in exactly the way a form field is. Parse them against a strict schema before the function body runs, reject unknown properties instead of ignoring them, and enforce authorisation on the resulting call in your own code rather than trusting that the model only asked for data belonging to the current user. The most dangerous shape is a tool that takes a free-text identifier and looks it up without a tenant check, because a well-phrased user message can persuade a model to ask for someone else's record and the API will faithfully return it. Validate, authorise, then execute -- and when validation fails, hand the failure back as a tool message so the model can correct itself while the side effect never happened.
What is the difference between tool calling and structured output?
Structured output is about the shape of an answer; tool calling is about control flow. When you ask for JSON you already know what the model will do -- it will answer -- and you are constraining how the answer is formatted. When you register tools you are handing the model a decision: whether to act, which action, with what arguments, and how many times before it is finished. That is why the failure modes are so different. Structured output fails by returning a body you cannot parse or cannot trust; tool calling fails by taking a wrong action, taking a right action twice, or taking no action while explaining what it would have done. They compose -- most agents want validated arguments going in and a validated final answer coming out -- but they need separate defences.
Should I let Mistral AI call several tools in one turn?
Yes when the calls are genuinely independent, and carefully. Parallel calls are a real latency win when a task needs three lookups that do not depend on each other, and they are a real hazard when your handler assumes one call per turn: the classic bug is executing the first element of the array and dropping the rest, which leaves tool results missing for ids the model is waiting on and breaks the next turn. Iterate over every call, return one tool message per call id, and keep them in the conversation in order. Where a tool has side effects, be more conservative -- serialise those, give each an idempotency key derived from the call id, and consider forcing a single call per turn so the model sees each outcome before deciding the next action.
Related Guides
A Stalled Agent and a Degraded API Look Identical From Inside
When tool loops start hitting the ceiling, the first question is whether your schema changed or api.mistral.ai is having a bad hour โ and your own logs cannot answer it, because they only contain your traffic. API Status Check probes api.mistral.ai and the rest of your stack independently and alerts on latency and errors, so that question is already settled before you start reading agent transcripts.
Start Your Free Trial โ๐ Tools We Use & Recommend
Tested across our own infrastructure monitoring 200+ APIs daily
Uptime Monitoring & Incident Management
Used by 100,000+ websites
Monitors your APIs every 30 seconds. Instant alerts via Slack, email, SMS, and phone calls when something goes down.
โWe use Better Stack to monitor every API on this site. It caught 23 outages last month before users reported them.โ
Secrets Management & Developer Security
Trusted by 150,000+ businesses
Manage API keys, database passwords, and service tokens with CLI integration and automatic rotation.
โAfter covering dozens of outages caused by leaked credentials, we recommend every team use a secrets manager.โ
SEO & Site Performance Monitoring
Used by 10M+ marketers
Track your site health, uptime, search rankings, and competitor movements from one dashboard.
โWe use SEMrush to track how our API status pages rank and catch site health issues early.โ