~/writing/mistral-vibe-devex-audit

Speech transcript

What Mistral's Free tier really offers.

~14 min listen

A version prepared for listening. Code, tables, and visual flows have been turned into continuous explanations.

← Read the full article

From Free-tier question to DevEx audit

I started with a broad question: what does Mistral actually offer a developer today? The platform included coding and chat models, local-friendly smaller models, audio, OCR, moderation, embeddings, Labs models, and a CLI coding agent called Vibe. The Free tier made the narrower question more useful: what can an authenticated Free account actually call, through which interface, and with which limits?

Vibe was the reasonable place to start. It is a local CLI coding agent, it supports Mistral-hosted models, and the Free account offered Vibe-specific usage. I used it as a developer would, rather than beginning with synthetic API tests. That decision turned a platform survey into a DevEx audit.

The sequence matters. Browser login succeeded. A request to the selected Medium 3.5 model failed. The Mistral account had an API or Vibe key, but the local file ~/.vibe/.env did not exist. I manually created a new Vibe API key and put it in that file as MISTRAL_API_KEY. Authentication then worked, but requests still failed. Vibe’s debug output showed HTTP 429. A direct API probe exposed an effective Medium 3.5 limit of zero RPM. At that point the question became: what can this account use?

Two independent failures

Vibe v2.25.1 started on my Mistral Free EU account with Medium 3.5 selected. Its browser setup completed successfully, then a trivial prompt returned an authentication or API error. Re-running vibe --setup also reported success. Online, the account contained a Vibe credential. Locally, Vibe had no ~/.vibe/.env file.

That was a genuine provisioning failure. A successful browser authentication and a CLI with usable local credentials are separate states. The setup flow treated them as one success. Creating the key and local environment file fixed authentication.

It did not fix the request. Vibe now retried it as rate-limited. I sent the smallest useful chat-completion request directly to /v1/chat/completions, using mistral-medium-latest, the same key, and one token of output. The API returned a rate_limited error, code 1300, with HTTP 429. Its response headers reported both the request limit and remaining requests as zero RPM.

I needed a control before declaring the API or Free account broken. The same request with the same key against ministral-3b-latest returned HTTP 200. It reported 750 RPM, 1,300,000 TPM, 1,299,995 tokens remaining, and a five-token query cost. The credential was valid, the API was reachable, and chat completions worked. Medium 3.5 was the model with a zero operational allowance.

Discovery was not enough

Vibe’s /model picker showed choices but did not answer whether the account could use them, whether they were chat or coding models, whether IDs were aliases, whether a limit was above zero, or whether another account setting was needed. I queried the authenticated /v1/models endpoint instead. It returned 46 IDs.

That catalog contained more than names. It exposed capability metadata such as completion_chat, function calling, reasoning, vision, OCR, moderation, audio transcription, realtime transcription, and speech. It also exposed alias relationships. Medium 3.5 included mistral-medium-latest, several versioned names, mistral-vibe-cli-latest, mistral-vibe-cli-with-tools, and magistral-medium-latest. Small 4 included mistral-small-latest, mistral-vibe-cli-fast, and magistral-small-latest. Codestral 2508 included codestral-latest, mistral-code-latest, and mistral-code-fim-latest.

The catalog is raw provider state, not a model-selection UI. I wrote a small Fish probe which fetched that catalog, discovered IDs dynamically, issued the smallest chat request, recorded the HTTP status and rate-limit headers, preserved structured error types, and paused between requests. It distinguished HTTP 200, 400 invalid_model, 403 tier_not_allowed, 403 labs_not_enabled, and 429 rate_limited instead of collapsing them into one unavailable flag.

The first chat-oriented run found Ministral 3B at 750 RPM and 1.3 million TPM, Ministral 8B at 188 RPM and 625,000 TPM, Ministral 14B at 30 RPM and 937,500 TPM, Codestral or Mistral Code at 125 RPM and 625,000 TPM, and Voxtral Small at 60 RPM and 50,000 TPM. Medium 3.5, Small 4, and the Vibe CLI aliases had effective zero RPM. Two documented models outside the 46-ID catalog, zai-glm-5-2 and mistral-large-latest, returned 403 tier_not_allowed.

A later probe for mistral-large-4 returned HTTP 403, tier_not_allowed, code 1910. The model slug, endpoint, and API key were valid, but the tested subscription tier could not use the model. There were no rate-limit headers because the request never reached a usable model allowance. A custom Vibe entry through the standard API provider hits the same tier restriction.

Correcting the probe

My generic probe sent every ID to POST /v1/chat/completions. Several returned 400 invalid_model. Treating that as Free-tier denial would have produced a convincing, wrong table.

The capability metadata already explained the problem. Some of those IDs were for embeddings, OCR, moderation, transcription, text to speech, or realtime transcription. The request was invalid because I had asked the wrong API question. 400 invalid_model from the chat endpoint meant that model could not be used through that endpoint. It did not mean the account could not use the model.

I changed from model probes to capability probes. All four tested embedding IDs, codestral-embed, codestral-embed-2505, mistral-embed, and mistral-embed-2312, returned 200 from /v1/embeddings at 60 RPM. mistral-moderation-2603 returned 200 from /v1/moderations at 100 RPM. Seven tested OCR IDs, from mistral-ocr-2512 through mistral-ocr-latest, returned 200 from /v1/ocr with a valid document. OCR did not expose equivalent RPM or TPM headers, so I did not assign it an invented quota.

Labs and audio

Two Leanstral IDs initially returned 403 labs_not_enabled. The error pointed to Privacy, but there were two relevant pages: Vibe Privacy and API Privacy. The switch was under API Privacy. After enabling Labs, both labs-leanstral-1-5 and labs-leanstral-1-5-1 worked at 38 RPM and 5,000,000 TPM. At the time, that was the largest chat-model TPM allowance I measured on this Free account. Vibe had started with Medium 3.5 at zero RPM while the coding-focused Labs model was available after one hidden organization setting.

For audio, I generated a one-second local sine-wave WAV to avoid depending on external content. voxtral-mini-2602 and voxtral-mini-latest both returned 200 from /v1/audio/transcriptions. The headers showed 60 RPM and 3,600 audio seconds per minute. The one-second audio was processed as one prompt-audio second and 375 audio tokens; an empty transcript was expected because the file contained no speech.

I queried the voice API rather than hardcoding an ID and found 30 preset voices. Both voxtral-mini-tts-2603 and voxtral-mini-tts-latest returned 200 from /v1/audio/speech, with an observed limit of 12,000 input characters per minute.

Realtime transcription needed a different protocol again. I used Mistral’s realtime Python client, generated raw 16-kilohertz mono PCM, and streamed it in approximately 240-millisecond chunks. voxtral-mini-transcribe-realtime-2602, voxtral-mini-realtime-2602, and voxtral-mini-realtime-latest each completed a real session: session.created, session.updated, and transcription.done. The usage report included 23 prompt tokens, 23 completion tokens, 46 total tokens, and two prompt-audio seconds.

The observed capability map

For this Mistral Free EU organization on 10 September 2026, Leanstral 1.5 was available after Labs opt-in at 38 RPM and 5 million TPM. Ministral 3B, 14B, and 8B were available at 750 RPM and 1.3 million TPM, 30 RPM and 937,500 TPM, and 188 RPM and 625,000 TPM respectively. Codestral or Mistral Code was available at 125 RPM and 625,000 TPM. Voxtral Small was available at 60 RPM and 50,000 TPM. Medium 3.5, Small 4, and Vibe CLI aliases had zero RPM. GLM 5.2 and Large 3 were tier-denied.

Embeddings, moderation, OCR, offline transcription, TTS, realtime transcription, and preset voice discovery were all available. The observed limits were 60 RPM for embeddings, 100 RPM for moderation, 60 RPM plus 3,600 audio seconds per minute for offline transcription, 12,000 input characters per minute for TTS, and 30 preset voices. These are observations for one account, organization, region, and point in time, not promises.

The missing availability abstraction

The Free account exposed considerably more than the initial Vibe run suggested. The friction was capability legibility. During one model-selection task I had to determine whether browser authentication had provisioned the CLI, whether the key was valid, whether 429 meant exhausted quota or zero allowance, whether an ID was canonical or an alias, which capability it exposed, which endpoint implemented it, whether Labs was required, whether 403 meant Labs disabled or tier denied, and which alternative could work immediately.

Mistral already exposed the pieces: catalog IDs, aliases, and capabilities; structured errors such as labs_not_enabled, tier_not_allowed, and rate_limited; response headers for RPM, TPM, audio seconds, and input characters; and Vibe’s provider, selected model, and setup state. What was missing was a normalized ModelAvailability representation between provider state and developer decisions.

That representation would retain a canonical model ID, aliases, capabilities, status, observed limits, Labs state, and the last relevant error. It would let /model, setup, retry handling, diagnostics, and fallback suggestions consume one state model. A picker could show Codestral, the Ministral models, and Leanstral as available with their observed limits; show Medium 3.5 and Small 4 as zero-RPM choices; explain that Leanstral needs Labs under API Privacy; and suggest alternatives after a zero-RPM request.

Discovery should not mean probing all 46 IDs whenever /model opens. Setup can fetch and normalize the catalog, successful calls can update observed limits, relevant errors can update availability, and explicit refresh or cache expiry can re-query provider state. /model should render that cached normalized state rather than carry a pile of provider-specific rules.

What a useful client would say

The model picker does not need to expose every raw detail, but it should expose the state needed for a decision. On this account it could list Codestral 2508 at 125 RPM and 625,000 TPM; Ministral 3B at 750 RPM and 1.3 million TPM; Ministral 8B at 188 RPM and 625,000 TPM; Ministral 14B at 30 RPM and 937,500 TPM; and Leanstral 1.5 at 38 RPM and 5 million TPM, marked as Labs. Medium 3.5 and Small 4 belong in an unavailable group with their observed zero RPM, rather than looking like normal defaults.

The same state would improve errors. For Leanstral, the client can say that the organization has Labs disabled and point directly to API Privacy. For Medium 3.5, it can say that the observed request limit is zero requests per minute, then offer Codestral and the available Ministral models as alternatives and send the user to /model. This does not hide Mistral’s state. It makes the state actionable.

The shape can remain small. A ModelAvailability record for Codestral needs its canonical ID, aliases such as codestral-latest, mistral-code-latest, and mistral-code-fim-latest, its chat, fill-in-the-middle, and function-calling capabilities, an available status, and 125 RPM with 625,000 TPM. The Medium 3.5 record needs its aliases, a zero-rate-limit status, and zero RPM. A Leanstral record can carry labs_disabled and an action that points to API Privacy. Components consume this normalized data instead of each translating provider errors independently.

The same separation applies to specialized APIs. A model catalog entry advertises a capability; the client chooses the endpoint or protocol that implements it. Chat and coding go through /v1/chat/completions. Embeddings go through /v1/embeddings; moderation through /v1/moderations; OCR through /v1/ocr; offline transcription through /v1/audio/transcriptions; and speech through /v1/audio/speech. Realtime transcription is a streaming client session. Putting all of those behind one chat probe was the original testing error in miniature.

The audit does not argue that a Free-tier catalog is a permanent entitlement. Limits and availability are account-, organization-, region-, and time-dependent. It argues that a client can distinguish catalog discovery, entitlement, a non-zero operational limit, the correct capability and endpoint, and usability in the local tool. Those are separate pieces of state. Treating them as one green or red model label is where the information disappeared.

Conclusion and external validation

Correcting the probe matters because an error describes the request that produced it. Browser login did not prove local credential provisioning. A 429 did not prove temporarily exhausted quota. A model returned by /v1/models did not prove a non-zero allowance. 400 invalid_model on a chat endpoint did not prove an unavailable model. A 403 did not name one universal access failure.

I published the initial findings in r/MistralAI and updated them after Labs and specialized-model tests. Reddit’s own insights briefly showed roughly the third post of all time in that subreddit, about 1.4 thousand views, and ten shares within the first 40 minutes. Another developer independently described spending too long debugging Vibe authentication and agreed that the picker should communicate availability directly. The reproducible evidence remains the HTTP responses, rate-limit headers, /v1/models metadata, capability-specific calls, and realtime protocol events.

A developer tool does not need to hide the platform. It should make platform state clear enough that a developer can take the next action without reverse-engineering information the platform already provides programmatically.

← Read the full article