Claude Code & coding agents

Building software with Mistral AI: which tools, which model and which IDE (November 2026)

Devstral is gone from the API and Vibe runs on a model that is being retired by default. What Mistral offers for building software in November 2026: Vibe in VS Code and PhpStorm, which model for which task, prices, the EU endpoint and a tested PHP call.

Erik van de Blaak
Erik van de Blaak
27 min read • 116 views
Building software with Mistral AI: which tools, which model and which IDE (November 2026)

If you follow a tutorial on programming with Mistral AI today, there is a good chance you install something that no longer exists. Devstral 2, the coding model Mistral launched its coding agent Vibe with in December 2025, has been gone from the API since 31 July 2026. The JetBrains plugin Mistral Code is deprecated. And Mistral Medium 3.5, the model Vibe uses by default, has been on the list of models being retired since 31 October. That was three days ago.

That says nothing about the quality of Mistral. It does say that you need to build differently with this vendor than you are used to. This article covers what Mistral offers in November 2026, which tools and which IDE to pick, which model fits which task, what it costs and where your data goes. I also wrote a piece of PHP for it and ran it against a Mistral model on my own laptop. That taught me something no manual mentions.

Short answer. Yes, you can build software well with Mistral, in two roles. As a coding assistant you use Vibe: in the terminal, in VS Code, or in PhpStorm and other JetBrains IDEs through the AI Assistant. Set Mistral Large 4 yourself, because the default model is being retired. Inside your own application you call the API: Mistral Small 4 for bulk work, Large 4 for hard tasks, and api.eu.mistral.ai if processing has to stay in Europe. Mistral does not have the best coding model. Claude, GPT and Gemini score higher. It does have the strongest model you are also allowed to run yourself.

Facts checked on 3 November 2026 against Mistral's documentation, changelog and price list. I ran the test in this article locally the same day with Ministral 3 8B. I did not run anything against Mistral's paid API; where I have not seen something myself, the text says so.

Two different things that are both called "building software with Mistral"

The question "can I build software with Mistral?" is in practice about two different things, and most of the confusion comes from mixing them up.

Mistral as a coding assistant. An agent reads your code, edits files and runs commands while you watch. The product is called Vibe. You usually pay with a subscription, and the result is code that ends up in your repository. After that, Mistral is out of the picture: your application runs without Mistral.

Mistral as part of your software. Your application sends requests to a Mistral model itself: turn a job ad into fields, answer a customer question, read a PDF. That goes through the API of Mistral AI Studio, and you pay per token. Here your users' data goes to Mistral, not just your source code.

Diagram with two columns. Left: Mistral as a coding assistant, with Vibe in the terminal, in VS Code, in JetBrains IDEs and Zed through ACP, and Vibe Code Web; paid by subscription from 14.99 dollars a month. Right: Mistral inside your own software, with the API and the models Large 4, Small 4, Ministral 3, Codestral, Embed and OCR; paid per million tokens. At the bottom three routes for the data: api.mistral.ai without a fixed location, api.eu.mistral.ai in the EU at a 10 percent surcharge, and your own server with open weights.
Same vendor, two bills and two privacy questions. With the coding assistant your source code goes to Mistral, with the API your users' data does.

You can choose the two independently. You can build an application that calls Mistral with Claude Code, or an application that uses OpenAI with Vibe. Anyone who says "we are going to work with Mistral" first has to say which of the two they mean.

What Mistral offers in November 2026, and what has already disappeared

On 28 May 2026 Mistral renamed its chat service Le Chat to Vibe. Since then Vibe has three modes. Work is the assistant for research, summaries and documents. Chat is the old Le Chat. Code is the coding agent, and it comes in three forms: the command-line tool, an extension for VS Code, and Vibe Code Web, which works on a GitHub repository in a cloud sandbox.

Next to that there is Mistral AI Studio, the platform with the API, the API keys and the settings for your organisation. That is where the models live. Worth noting: Mistral now also sells other vendors' models through that API. Z.ai GLM 5.3, GLM 5.3 Flash and DeepSeek V4.1 Flash are simply in the model list, next to Mistral's own.

The list of what disappeared is at least as instructive:

ModelReleasedRemoved from the APIReplacement according to Mistral
Devstral Small 2December 202531 March 2026Medium 3.5
Devstral 2December 202531 July 2026Medium 3.5
Magistral Medium 1.2September 202531 July 2026Medium 3.5
Mistral Medium 3.528 April 202630 April 2027 (deprecated as of 31 October)Large 4
Mistral Large 3December 202530 April 2027 (deprecated as of 31 October)Large 4

Devstral 2 was available for about eight months. Medium 3.5 was deprecated after six. The replacement of the replacement is Large 4. Anyone who built an application on Devstral 2 in May had to move it to Medium 3.5 over the summer, and now has to move it again.

The weights of open models do not disappear: you can still download Devstral Small 2 and run it yourself. Only the API version stops. For your planning that makes little difference. Expect a new model from Mistral every six months, and put the model name in your configuration, not scattered through your code. Further down you will see why a fixed test set is the most useful thing you can build.

How well does Mistral write code? The independent numbers

Mistral Large 4 came out as a preview on 6 October and with open weights on 29 October. It has 1.05 trillion parameters, of which 52 billion take part per token, and a context window of one million tokens. I wrote a separate article about the model itself. Here there is one question: how good is its code?

Artificial Analysis, an independent benchmarking firm, tests coding agents on real programming tasks. In its Coding Agent Index, Large 4 running in Mistral Vibe scores 48 points. GLM-5.3 gets 49, DeepSeek V4.1 Flash 46. Among open models, that puts Large 4 at the top. The difference is in consumption: Large 4 needed 8.5 million tokens per task, GLM-5.3 15.9 million. For an agent that pays per token, that is almost half the bill.

The broad comparison is less flattering. In the general Intelligence Index of the same firm, Large 4 scores 26 points. Gemini 4 Carbon is at 59, Claude Opus 5.5 at 58 and GPT-6 Astra at 57. Seven open models score higher than Large 4. In a blind preference test that Mistral itself publishes, reviewers preferred Large 4's code over Claude Opus 5.5's in 22 percent of the comparisons.

Also note how far Mistral's numbers and independent testers' numbers can drift apart. On Terminal-Bench 4.0 Mistral reports 41 percent; Artificial Analysis measured 25 percent. The gap usually comes from the setup: different settings, a different agent around the model, more attempts. For your choice, the independent measurements count, and after that mostly your own test.

My conclusion: Large 4 is a solid coding model for ordinary work. Think refactoring, writing tests, CRUD code and tracking down a bug with a clear error message. For a change that runs through twenty files and three layers of architecture, the closed top models are still clearly better. So do not pick Mistral because a chart says it is the best. Pick it because it is European, because you are allowed to run the model yourself, or because it is cheap per task. Those are real reasons.

Which IDE? Vibe in VS Code, PhpStorm, Zed or the terminal

Mistral's coding agent is Vibe. There is no separate Mistral product per editor any more: it is the same agent everywhere, with the same settings in ~/.vibe/. What you choose depends on the editor you already work in.

VS Code: the only official extension

For VS Code, Mistral has its own extension: Mistral Vibe for VS Code, id mistralai.mistral-vibe-code. You need VS Code 1.94 or newer. The agent is bundled in the extension, so you do not have to install the command-line tool separately. On first use you paste an API key; it goes into your operating system's keychain, or into ~/.vibe/.env if there is no keychain. Signing in through the browser, so you use your subscription instead of a key, is on its way to the extension according to the documentation. The extension shares settings and sessions with the command-line version.

PhpStorm and other JetBrains IDEs: through ACP

There is no standalone Mistral plugin for JetBrains. The old Mistral Code plugin is deprecated, and Mistral advises against using it. The route is the Agent Client Protocol (ACP), an open standard that lets an editor talk to an external agent. JetBrains built it into the AI Assistant. This is how it works in PhpStorm, IntelliJ IDEA, WebStorm or PyCharm:

  1. Make sure you have version 2025.3.2 or newer, with the AI Assistant plugin.
  2. Open Settings (Ctrl+Alt+S, on a Mac Cmd+,) and go to Tools, AI Assistant, Agents.
  3. Search for "Mistral Vibe" and click Install.
  4. Open the AI Chat window and choose Mistral Vibe in the dropdown below the input field.

You do not need a JetBrains AI subscription for this; JetBrains writes that ACP agents work without one. You only pay Mistral. If you have already set up Vibe in the terminal or in VS Code, the JetBrains version uses the same credentials. If you work in PhpStorm every day, this is the route. I use Claude Code in PhpStorm myself, and the principle is the same: the agent works in your project, you review the changes in the IDE's diff view.

Zed, and any other editor with ACP

Zed came up with ACP. There you first install the command-line tool, which ships the vibe-acp command, and point to that command under agent_servers in Zed's settings. The same works in any other editor that supports ACP.

The terminal

The command-line tool is on PyPI as mistral-vibe; the latest version is 3.0.4, from 30 October 2026. You need Python 3.12 or newer. On Linux and macOS:

curl -LsSf https://mistral.ai/vibe/install.sh | bash

On Windows, install uv first and then:

uv tool install mistral-vibe

Vibe works on Windows, but according to its own README Mistral officially targets Linux and macOS. On first start you choose between signing in through the browser, which uses your subscription, and an API key. My earlier article about Vibe explains how the agent works and how to run it fully locally. Note: the model names in that article (Devstral 2) have since been removed from the API.

And autocomplete while you type?

Vibe is an agent you give a task. It is not autocomplete that suggests the rest of your line as you type. For that, Mistral has a separate model, Codestral, which fills in the code between your cursor and the rest of the file ("fill-in-the-middle"). Mistral's own plugin for that no longer exists for JetBrains. If you want Codestral as autocomplete, you need a third-party plugin that lets you enter your own Mistral key.

Vibe runs on a model that is being retired by default: set Large 4 yourself

This is the detail the introduction articles leave out. Vibe's configuration documentation says: Vibe ships with one built-in model, Mistral Medium 3.5. That same model has been on the list of deprecated models since 31 October, with 30 April 2027 as end date and Large 4 as replacement. The /model picker only shows the models you configured yourself.

You add Large 4 with a few lines in ~/.vibe/config.toml:

[[models]]
name = "mistral-large-4"
provider = "mistral"
alias = "large-4"

active_model = "large-4"

# Telemetry is on by default
enable_telemetry = false

# By default Vibe edits files without asking ("accept-edits")
default_agent = "ask"

# No access to files outside the project folder
outside_workdir = "deny"

The fields come from Vibe's configuration documentation, the model name from the Large 4 model card. I have not tested this configuration with a paid key. After saving, type /model in Vibe and check that large-4 is listed.

The three lines at the bottom are a choice, not a requirement. By default Vibe collects usage data, and the default mode accept-edits writes changes straight into your files; Vibe only asks permission for shell commands. If you are starting with a new agent, set it to ask until you know how it behaves. And always work from a clean Git working tree, so git diff shows what changed.

Subscription or API key: what does coding with Vibe cost?

You can pay for Vibe in two ways. With a subscription:

PlanPrice (excl. VAT)For coding
Freefreea limited number of coding sessions
Pro$14.99 per month"all-day coding" in the terminal, the IDE and on the web, plus $15 a month in API credit
Team$24.99 per user per monthhigher limits, domain verification, data export
Students$5.99 per monthPro, for verified students, twelve months

Or with an API key, paying per token. A worked example shows why that quickly gets more expensive for daily coding. Artificial Analysis measured 8.5 million tokens per task for Large 4 in Vibe. Suppose all those tokens are cached input, the cheapest case: 8.5 million times $0.07 per million is $0.60 per task. Without the cache it is $5.95. Twenty tasks per working day, twenty working days a month, makes 400 tasks: in the best case about $240 a month, and output tokens are not even included yet.

A benchmark task is not your task, so read this as an order of magnitude. It does show why a $14.99 subscription is the logical choice for daily work, as long as the plan's limits do not stop you. An API key is useful for automation, for example an agent running in your CI pipeline, and for anyone who wants to see costs per project.

Mistral inside your own software: which model for which task

For the API the choice is different. There you pay for every call, so the cheapest model that does the task well wins. Prices per million tokens, according to Mistral's price list on 3 November 2026:

TaskModelInput / outputWhy
Agents, long documents, hard trade-offsMistral Large 4 (mistral-large-4)$0.70 / $2.10 (cached input $0.07)strongest Mistral model, 1 million token window
Classifying, extracting fields, summarising in bulkMistral Small 4 (mistral-small-2603)$0.15 / $0.60almost five times cheaper than Large 4 on input; open weights under Apache 2.0
Simple tasks, or running it yourselfMinistral 3 (3B, 8B, 14B)$0.10 to $0.20, same for input and outputApache 2.0; the 8B runs on a laptop
Code completion in your own toolCodestral$0.30 / $0.90built for fill-in-the-middle
Searching by meaningMistral Embed / Codestral Embed$0.10 / $0.15 (input only)turn text or code into vectors
Reading PDFs and scansOCR 4.1$4 per 1,000 pageswith blocks and confidence scores
Speech to textVoxtral Mini Transcribe 3$0.0035 per minutewith speaker diarisation
Blocking unwanted inputMistral Moderation 2freealso detects jailbreak attempts

Batch processing, where you do not need the answers right away, costs half. A worked example: sorting 10,000 customer questions into categories, each with 800 input tokens and 100 output tokens. With Small 4 that is 8 million times $0.15 plus 1 million times $0.60: $1.80. With Large 4 it is $5.60 plus $2.10: $7.70. Through the European endpoint, add 10 percent. As a batch, it is half.

My advice: start with Small 4 and only move to Large 4 if your test set shows the difference is worth the price. Searching by meaning works just like in my article about an intelligent database with MySQL and PHP, only with a Mistral embedding model instead of OpenAI's.

PHP and Mistral: no official SDK, and you do not need one

Mistral maintains official SDKs for Python (mistralai, version 2, in which the imports moved to mistralai.client) and TypeScript (@mistralai/mistralai). There is no official PHP SDK. For PHP you have four routes:

  • Laravel: the official Laravel AI SDK (laravel/ai, version 1.3.0) supports Mistral for text, speech to text and embeddings. You put MISTRAL_API_KEY in your .env.
  • Symfony: Symfony AI has a Mistral bridge, symfony/ai-mistral-platform. It is at 1.0.0-BETA1, from 26 October 2026: still a beta.
  • Community clients: for example softcreatr/php-mistral-ai-sdk or, for Laravel, helgesverre/mistral. Before using one, check how actively it is maintained.
  • Plain HTTP: the API is a JSON API. One function with cURL is enough, and then you know exactly what goes over the wire.

For this article I took the last route. The function below talks to api.mistral.ai, to api.eu.mistral.ai and to a local model in Ollama, because all three use the same path /v1/chat/completions and the same message format.

mistral.php

<?php
declare(strict_types=1);

/**
 * One function for the Mistral chat API.
 * Works against api.mistral.ai, api.eu.mistral.ai and a local model in Ollama:
 * all three offer /v1/chat/completions with the same message format.
 */
function mistral_chat(string $baseUrl, ?string $apiKey, array $payload): array
{
    $headers = ['Content-Type: application/json'];
    if ($apiKey) {
        $headers[] = 'Authorization: Bearer ' . $apiKey;
    }

    $ch = curl_init(rtrim($baseUrl, '/') . '/v1/chat/completions');
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => $headers,
        CURLOPT_POSTFIELDS     => json_encode($payload, JSON_THROW_ON_ERROR),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 120,
    ]);
    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error  = curl_error($ch);
    curl_close($ch);

    if ($body === false) {
        throw new RuntimeException("No connection: $error");
    }
    if ($status !== 200) {
        throw new RuntimeException("HTTP $status: $body");
    }

    return json_decode($body, true, 512, JSON_THROW_ON_ERROR);
}

If you later want an SDK or a Laravel package after all, only this function changes. The rest of your code will not notice.

Tested: turning job ads into JSON with Ministral 3 8B

A task that shows up in almost every application: turning messy text into fields you can put in a database. I took five made-up job ads, each written differently, and had Ministral 3 8B extract eight fields: job title, city, workplace, minimum and maximum hours, minimum and maximum salary, and whether that salary is per hour, month or year.

The test ran in Dutch, so the code below is shown exactly as it ran: Dutch job ads and Dutch field names. A small glossary: functietitel is job title, plaats city, werkplek workplace (kantoor office, hybride hybrid), uren hours, salaris salary, and uur, maand, jaar are hour, month and year.

The setup: Ollama 0.40.13 on my laptop (RTX 4070 Laptop with 8 GB of video memory), the model ministral-3:8b at 4 bits (6.0 GB, 92 percent on the GPU), PHP 8.2.35. The model is open, under Apache 2.0, and running it cost nothing. The job ads:

vacatures.php

<?php
// Five made-up Dutch job ads, the way they arrive in practice: messy and each one different.
return [
    'V-101' => "Wij zoeken een Medior PHP Developer (Laravel) voor ons team in Utrecht. 32-40 uur. Salaris tussen €3.800 en €5.200 bruto per maand. Twee dagen per week thuiswerken is mogelijk.",
    'V-102' => "Backend engineer gezocht! Je werkt volledig remote vanuit Nederland of België. Fulltime (40u). We bieden 55k tot 68k per jaar, afhankelijk van ervaring. Stack: Symfony, PostgreSQL.",
    'V-103' => "Voor onze vestiging in Eindhoven zoeken we een junior webontwikkelaar, 24 uur per week, start per direct. Salarisindicatie: schaal 8 (cao), max. €3.400. Werken op kantoor.",
    'V-104' => "Lead developer — Amsterdam (hybride). Je stuurt een team van zes aan. Salaris: marktconform. 36 uur.",
    'V-105' => "Freelance opdracht: WordPress-specialist, 16 tot 20 uur per week, 6 maanden, volledig op afstand. Tarief €85 per uur.",
];

The JSON schema the model has to fill in. Mistral calls this structured outputs: the model can only answer in this shape.

schema.php

<?php
// The JSON schema the model has to fill in (field names in Dutch, as tested).
return [
    'type' => 'object',
    'properties' => [
        'functietitel'   => ['type' => 'string'],
        'plaats'         => ['type' => ['string', 'null']],
        'werkplek'       => ['type' => 'string', 'enum' => ['kantoor', 'hybride', 'remote']],
        'uren_min'       => ['type' => ['integer', 'null']],
        'uren_max'       => ['type' => ['integer', 'null']],
        'salaris_min'    => ['type' => ['integer', 'null']],
        'salaris_max'    => ['type' => ['integer', 'null']],
        'salaris_per'    => ['type' => ['string', 'null'], 'enum' => ['uur', 'maand', 'jaar', null]],
    ],
    'required' => ['functietitel', 'plaats', 'werkplek', 'uren_min', 'uren_max', 'salaris_min', 'salaris_max', 'salaris_per'],
    'additionalProperties' => false,
];

Round 1: the JSON is always valid, the content is not

With a short system prompt (in Dutch: "Extract the requested fields from the job ad. If something is not stated explicitly, fill in null. Do not convert anything and do not guess.") the model returned valid JSON five times. Of the 35 fact fields (job title not counted), 7 were wrong. Run three times with temperature at 0: three times exactly the same answers, so also three times the same mistakes.

  • V-101 mentions "two days a week working from home". The model filled in kantoor (office) instead of hybride.
  • V-103 says "max. €3,400". The model turned that into a salary of exactly 3,400 to 3,400, a lower bound that appears nowhere.
  • V-104 mentions 36 hours. The model filled in 36 as the minimum and null as the maximum, and with "market rate" still gave jaar (year) as the unit.
  • V-102 and V-105 name no city. The model wrote text there anyway: "Remote (Nederland of België)" and "Onbekend (volledig op afstand)", meaning "unknown (fully remote)".
  • The hourly rate of €85 in V-105 was missing.

Structured outputs guarantee the shape, not the truth. A schema stops text in a number field. It does not stop a wrong number in a number field.

Round 2: explanations in the schema change nothing

The obvious fix is adding explanations to the schema. A description on every field: "hybrid = partly at home, partly at the office", "'max. X' has no lower bound: null". The result: the same seven mistakes. Not roughly the same, but the same answers down to the character, three times in a row.

My explanation, and that is an explanation, not a measurement: Ollama uses the schema to constrain the output, so the model can only pick tokens that fit the schema. The descriptions do not end up in the prompt. The model never reads them. Whether Mistral's API does pass the descriptions to the model, I have not tested. Do not assume it without checking yourself.

Round 3: rules in the prompt help, and create new mistakes

The same rules, now in the system prompt:

regels.txt

Haal de gevraagde velden uit de vacaturetekst. Staat iets er niet expliciet in, vul dan null in. Gok niet.
Regels per veld:
- plaats: alleen een plaatsnaam. Geen plaats genoemd: null.
- werkplek: kantoor, hybride (deels thuis, deels op kantoor) of remote (volledig op afstand).
- uren_min en uren_max: wordt één getal genoemd, vul dan bij allebei dat getal in.
- salaris_min: alleen als de tekst een ondergrens noemt. "max. X" heeft geen ondergrens: null.
- salaris_max: in hele euro's, "55k" is 55000. Een uurtarief telt ook als salaris.
- salaris_per: uur, maand of jaar. Geen bedrag genoemd: null.

That helped. V-101 became hybride, V-103 no longer got an invented lower bound, V-104 was completely right and the hourly rate of V-105 came through. Of the 35 fields, 4 were still wrong. But all four were new mistakes, of a kind that was not there in round 1:

  • For V-102 and V-105 the model wrote the text "null" as the city, in quotes. That is a four-letter string, not an empty value. An if ($city === null) in your code walks right past it.
  • For V-103, "max. €3,400" per month suddenly became 34,000 per year. The model converted, and converted wrongly too: twelve times 3,400 is 40,800. Look closely at the rules above: the sentence "do not convert anything" from round 1 is no longer in them. I lost it while writing out the per-field rules. One dropped sentence, and the model does exactly what that sentence prevented.

A second lesson from this round, for anyone who thinks temperature 0 locks everything down: with the short prompt from round 1, but loaded from a file with a line break at the end, the answers for V-105 changed, and not for the better: "fully remote" became kantoor. One invisible character. So keep your prompts in files under version control, and run your test set again after every change.

Round 4: a check in plain PHP catches the rest

The model does not become more reliable by talking to it more sternly. What does work: checking what comes back, with code you understand yourself. Three rules, each with a concrete reason: "null" as text becomes a real null, every number must literally appear in the source text, and a lower bound may not be above the upper bound.

controle.php

<?php
declare(strict_types=1);

/**
 * Checks what the model returned, in plain PHP.
 * Returns the cleaned fields plus a list of warnings (waarschuwingen).
 */
function controleer(array $velden, string $tekst): array
{
    $waarschuwingen = [];

    // 1. The word "null" as text is not null.
    foreach ($velden as $veld => $waarde) {
        if (is_string($waarde) && strtolower(trim($waarde)) === 'null') {
            $velden[$veld] = null;
        }
    }

    // 2. Every amount and every number of hours must appear in the source text.
    $getallen = getallen_in($tekst);
    foreach (['uren_min', 'uren_max', 'salaris_min', 'salaris_max'] as $veld) {
        if ($velden[$veld] !== null && !in_array($velden[$veld], $getallen, true)) {
            $waarschuwingen[] = "$veld = {$velden[$veld]} staat niet in de tekst";
        }
    }

    // 3. Lower bound not above upper bound.
    foreach (['uren', 'salaris'] as $soort) {
        [$min, $max] = [$velden["{$soort}_min"], $velden["{$soort}_max"]];
        if ($min !== null && $max !== null && $min > $max) {
            $waarschuwingen[] = "{$soort}_min is hoger dan {$soort}_max";
        }
    }

    return [$velden, $waarschuwingen];
}

/** All numbers in a text: "€3.800" becomes 3800, "55k" becomes 55000. */
function getallen_in(string $tekst): array
{
    preg_match_all('/(\d{1,3}(?:\.\d{3})+|\d+)(k\b)?/i', $tekst, $m, PREG_SET_ORDER);

    return array_map(
        fn (array $g) => (int) str_replace('.', '', $g[1]) * (empty($g[2]) ? 1 : 1000),
        $m
    );
}

The script that brings it all together. With the environment variables SCHEMA and PROMPT you can repeat the rounds above; with MISTRAL_BASE_URL, MISTRAL_API_KEY and MISTRAL_MODEL the same script runs against Mistral's API.

extract.php

<?php
declare(strict_types=1);
require __DIR__ . '/mistral.php';
require __DIR__ . '/controle.php';

// Local (Ollama) or the Mistral API: only these three lines differ.
$baseUrl = getenv('MISTRAL_BASE_URL') ?: 'http://localhost:11434';
$apiKey  = getenv('MISTRAL_API_KEY') ?: null;
$model   = getenv('MISTRAL_MODEL') ?: 'ministral-3:8b';

$schema    = require __DIR__ . '/' . (getenv('SCHEMA') ?: 'schema.php');
$regels    = file_get_contents(__DIR__ . '/' . (getenv('PROMPT') ?: 'regels.txt'));
$vacatures = require __DIR__ . '/vacatures.php';

foreach ($vacatures as $ref => $tekst) {
    $start = microtime(true);

    $response = mistral_chat($baseUrl, $apiKey, [
        'model'       => $model,
        'temperature' => 0,
        'messages'    => [
            ['role' => 'system', 'content' => $regels],
            ['role' => 'user', 'content' => $tekst],
        ],
        'response_format' => [
            'type'        => 'json_schema',
            'json_schema' => ['name' => 'vacature', 'schema' => $schema, 'strict' => true],
        ],
    ]);

    $seconden = microtime(true) - $start;
    $velden   = json_decode($response['choices'][0]['message']['content'], true, 512, JSON_THROW_ON_ERROR);
    $ruw      = json_encode($velden, JSON_UNESCAPED_UNICODE);

    [$velden, $waarschuwingen] = controleer($velden, $tekst);

    printf("%s (%.1f s): %s\n", $ref, $seconden, $ruw);
    foreach ($waarschuwingen as $w) {
        echo "  LET OP: $w\n";
    }
    if (json_encode($velden, JSON_UNESCAPED_UNICODE) !== $ruw) {
        echo "  na controle: " . json_encode($velden, JSON_UNESCAPED_UNICODE) . "\n";
    }
}

The output, with the rules in the prompt and the check behind it (shortened where the dots are; "LET OP" means "warning", "na controle" means "after the check"):

V-101 (2.5 s): {"functietitel":"Medior PHP Developer (Laravel)","plaats":"Utrecht","werkplek":"hybride","uren_min":32,"uren_max":40,"salaris_min":3800,"salaris_max":5200,"salaris_per":"maand"}
V-102 (2.3 s): {..."plaats":"null"...}
  na controle: {..."plaats":null...}
V-103 (2.4 s): {..."salaris_min":null,"salaris_max":34000,"salaris_per":"jaar"}
  LET OP: salaris_max = 34000 staat niet in de tekst
V-104 (2.1 s): {"functietitel":"Lead developer","plaats":"Amsterdam","werkplek":"hybride","uren_min":36,"uren_max":36,"salaris_min":null,"salaris_max":null,"salaris_per":null}
V-105 (2.2 s): {..."plaats":"null",..."salaris_max":85,"salaris_per":"uur"}
  na controle: {..."plaats":null...}

Two mistakes were repaired automatically. V-103 is flagged: a person needs to look at that one. No mistake goes into the database unnoticed any more. That is the yardstick that counts: not how much the model gets right, but how many mistakes you do not see.

Bar chart of wrong fields out of 35 with Ministral 3 8B. Round 1, short prompt: 7 wrong. Round 2, descriptions in the schema: 7 wrong, identical answers. Round 3, rules in the prompt: 4 wrong, all of a new kind. Round 4, rules plus PHP check: 0 unnoticed mistakes; 2 repaired automatically, 1 job ad flagged for a human to check.
Measured on 3 November 2026 with the code from this article and five made-up job ads. Rounds 1 to 3 each ran three times, with the same result every time. Round 4 is the output of round 3 with the PHP check behind it.

After loading, each job ad took 2.1 to 3.2 seconds. The very first call took 45.6 seconds, because Ollama still had to load the model into memory.

Two caveats. These are five job ads, not a study; the numbers show what kind of mistakes to expect, not how often. And Ministral 3 8B is the smallest serious model in the list. Small 4 and Large 4 probably make fewer mistakes, but I have not measured that. The point stands: every model makes mistakes a schema does not stop, and the check in PHP costs nothing per call.

Where does your data go? A European company is not yet European processing

Mistral is a French company, and for many organisations that is the reason to choose it. But the default API api.mistral.ai does not promise a fixed processing location in its documentation. If you want your requests processed in Europe, use the regional endpoint api.eu.mistral.ai, with data centres in EU and EFTA countries. Three conditions apply:

  • It costs 1.1 times the normal price, for input, output and cache.
  • The Agents API, batch processing and the Files API do not work there. Function calling is the only tool you can use regionally.
  • Account data, such as your keys, billing and usage statistics, may be processed outside the region.

Where your request is processed and whether it is stored afterwards are two separate settings. For the second, Mistral has zero data retention, separate from the regional endpoint. Putting eu in the URL only covers half of it.

Then training. On the free API plan (Experiment), Mistral may use your requests to train models. You switch that off in the Admin Console, under Privacy. On the paid Scale plan it is off by default. Vibe has its own switch; the Vibe article explains why you need to check both.

The strongest form of control is running the model yourself. Ministral 3 and Small 4 are under Apache 2.0: do what you like with them, including commercially. Large 4 falls under the Mistral Ecosystem Open-Weight License. Internal use is allowed, including commercial use and regardless of company size. If you offer the model as a service to others ("model as a service"), you need a separate commercial licence. The catch: at 4 bits Large 4 is already 610 GB, and that is only the memory for the weights. That is a server rack, not a workstation. Which model does fit on a laptop is in my measurement with 16 GB.

How to start this week

For the coding assistant: create a free account, switch off training in the Admin Console, and install Vibe in the editor you already use: the extension in VS Code, the agent through the AI Assistant in PhpStorm, or the command-line tool. Put Large 4 in your config.toml and start in ask mode. Only move to Pro when the free plan holds you back.

For your application: write the test set first, then the code. Twenty real examples from your own practice, with the right answer per field. Run them against Small 4 and against Large 4, count the mistakes per field, and add a PHP check for every kind of mistake you run into. Choose the endpoint based on where the data is allowed to go, not on the flag at headquarters.

That test set is also your answer to Mistral's pace. Medium 3.5 stops in April, and in six months there will surely be a successor to Large 4. With a test set, switching models is an afternoon's work: run the script, count the mistakes, decide. Without one, it is a gamble you try out on your users.

In November 2026 Mistral is not the choice for anyone looking for the best coding model. It is the choice for anyone who wants to be able to leave: to the European endpoint, to their own server, to another model behind the same API. That only works if your test set is already there.

Further reading on this site

Sources

Share this article

Erik van de Blaak

Written by

Erik van de Blaak

AI Solutions Engineer & Full-Stack Developer

Erik van de Blaak is an AI Solutions Engineer and full-stack developer at CareerValue BV. Here he writes about AI coding tools and agents, such as Claude Code.

Comments (0)

Comment on this article

Never published

I read every comment before it goes online.

No comments on this article yet.