~/writing/mistral-vibe-devex-audit
September 10, 2026 · 31 min read · DevEx · AI tooling · API design · Debugging
What does Mistral's Free tier actually give a developer?
A survey of Mistral's Free tier turned into API instrumentation, a model-capability map, and eventually a DevEx audit of Mistral Vibe.
I did not start this investigation because I wanted to debug Mistral Vibe.
I started with a much broader question:
What does Mistral actually offer a developer today?
Mistral’s platform had grown well beyond a single family of chat models: coding models, smaller local-friendly models, audio, OCR, moderation, embeddings, Labs models, and a CLI coding agent. Then I noticed there was a Free tier.
That immediately led to the more useful question:
What can I actually use on the Free tier?
Not what exists in the catalog. Not what appears in documentation. Not what has a pricing row. What can an authenticated Free account actually call, through which interface, and with what limits?
Vibe looked like the obvious developer-facing place to start. It is a local CLI coding agent, it supports Mistral-hosted models, and the Free account exposed Vibe-specific usage. So instead of beginning with synthetic API tests, I tried the product the way a developer reasonably would.
That is where the rabbit hole started.
From platform survey to DevEx audit
The investigation eventually looked like this:
- 01 Mistral platform
- 02 Free tier
- 03 Vibe entry point
- 04 Browser login
- 05 Credential failure
- 06 HTTP 429
- 07 0 RPM
- 08 46 model IDs
- 09 Capability probes
- 10 Labs state
- 11 Empirical map
- 12 DevEx audit
investigation-path.txt text Show source
What does Mistral offer today?
↓
there is a Free tier
↓
what is actually included?
↓
Vibe is a developer-facing entry point
↓
install Vibe
↓
browser login
↓
login succeeds
↓
try default Medium 3.5
↓
API / authentication error
↓
check Mistral account online
↓
API / Vibe key exists
↓
check local credential state
↓
~/.vibe/.env DOES NOT EXIST
↓
create a new Vibe API key manually
↓
manually create ~/.vibe/.env
↓
authentication works
↓
request STILL doesn't work ಠ_ಠ
↓
debug console reveals HTTP 429
↓
direct API request + response headers
↓
Medium 3.5 = 0 RPM
↓
"okay, what CAN I use?"
↓
/model does not answer that
↓
GET /v1/models
↓
46 IDs ಠ_ಠ
↓
build instrumentation
↓
map aliases, capabilities and limits
↓
discover first generic probe is incomplete
↓
build capability-specific probes
↓
discover Labs activation state
↓
discover two different Privacy pages
↓
Leanstral = 38 RPM / 5M TPM
↓
test embeddings / moderation / OCR
↓
test transcription / TTS / realtime
↓
build an empirical Free-tier capability map
↓
compare what the platform knows
with what Vibe communicates
↓
DevEx diagnosisThe interesting part is not that I was able to debug this.
A developer using a CLI agent can reasonably be expected to understand environment variables, HTTP errors and API keys. The problem is that none of this investigation was the task I wanted to perform.
I wanted to find out what Mistral’s Free tier could do.
Instead, I first had to reconstruct the state of the platform.
The first failure was real: credential provisioning
Vibe v2.25.1 started on my Mistral Free EU account with Medium 3.5 selected.
I used Vibe’s browser login. The browser flow completed successfully. Then I sent a trivial prompt and received an authentication/API error.
At that point authentication was the obvious hypothesis.
- 01 Install Vibe
- 02 Browser login succeeds
- 03 Medium 3.5 request fails
- 04 API key exists online
- 05 ~/.vibe/.env missing
- 06 Manual key provisioning
- 07 Authentication works
- 08 Request still fails
- 09 Debug console: 429
- 10 Direct API probe
- 11 Medium 3.5: 0 RPM
I ran setup again:
vibe --setup The browser/setup flow reported success.
I checked the Mistral account online. An API/Vibe credential existed there. From the service side, the account looked provisioned.
Then I checked what Vibe actually had locally:
test -f ~/.vibe/.env; and echo exists; or echo missing Result:
missing The browser login had succeeded, but ~/.vibe/.env had not been
created.
That distinction matters. “Authentication succeeded in the browser” and “the local CLI has usable credentials” are two different states, but the setup flow had presented them as one successful operation.
I created a new Vibe API key manually and provisioned the local state myself:
~/.vibe/.env
MISTRAL_API_KEY=... For shell-level API debugging I could then load it without printing the secret:
set -lx MISTRAL_API_KEY
(string replace 'MISTRAL_API_KEY=' '' < ~/.vibe/.env)
echo "MISTRAL_API_KEY loaded, length:"
(string length -- $MISTRAL_API_KEY) Authentication now worked.
That should have ended the debugging.
It did not.
The second failure was independent
The same default model still could not complete a request.
Instead of Invalid API key, Vibe now kept retrying. The useful clue
appeared in debug output:
Retrying request category=rate_limited detail=HTTP 429 So the credential problem had been real, but fixing it had merely exposed a second problem.
I removed Vibe from the equation and called the API directly.
A minimal diagnostic request looked like this:
set MODEL mistral-medium-latest
curl -sS -D /tmp/mistral-headers
https://api.mistral.ai/v1/chat/completions
-H "Authorization: Bearer $MISTRAL_API_KEY"
-H "Content-Type: application/json"
-d (jq -nc --arg model "$MODEL" '{
model: $model,
messages: [{role:"user", content:"hi"}],
max_tokens: 1
}')
-o /tmp/mistral-body
cat /tmp/mistral-headers
jq . /tmp/mistral-body The response body said:
{
"object": "error",
"message": "Rate limit exceeded",
"type": "rate_limited",
"code": "1300",
"raw_status_code": 429
}The interesting evidence was in the headers:
x-ratelimit-limit-req-minute: 0
x-ratelimit-remaining-req-minute: 0 This was not a temporarily exhausted minute.
For this account, the effective request allowance for Medium 3.5 was exactly:
0 RPM That is a materially different state from “you are being rate limited because you sent too many requests”.
Establishing a control
Before concluding that the Free account or API was generally broken, I needed a control.
I sent the same class of request with the same API key to:
ministral-3b-latest That returned HTTP 200.
Its response headers included:
x-ratelimit-limit-tokens-minute: 1300000
x-ratelimit-remaining-tokens-minute: 1299995
x-ratelimit-tokens-query-cost: 5
x-ratelimit-limit-req-minute: 750
x-ratelimit-remaining-req-minute: 749 That isolated the state cleanly:
credential valid yes
API reachable yes
chat completions working yes
Free account usable yes
Medium 3.5 usable no: effective 0 RPM This was the point where the original platform-survey question became much more interesting.
If Vibe’s default hosted model had zero allowance, which models could the Free account actually use?
/model could not answer the question
The obvious next action was to switch models.
Vibe’s /model picker did not give me an operational view of account
availability. It showed a small set of choices, but it did not answer:
Which hosted models are available to this account?
Which are chat/coding models?
Which are aliases?
Which have non-zero limits?
Which require another account setting? So I went underneath Vibe again.
curl -sS
https://api.mistral.ai/v1/models
-H "Authorization: Bearer $MISTRAL_API_KEY"
| jq . The authenticated endpoint returned:
Catalog discovery
46 model IDs Raw provider inventoryOperational result
Empirical capability map Usability requires entitlement, limits, and the correct endpointThat was more information, but not yet an answer.
The model catalog is structured data, not a picker
The /v1/models response was useful because it contained more than
names.
It exposed machine-readable capability state such as:
{
"completion_chat": true,
"function_calling": true,
"reasoning": true,
"completion_fim": false,
"vision": true,
"ocr": false,
"classification": false,
"moderation": false,
"audio": false,
"audio_transcription": false,
"audio_transcription_realtime": false,
"audio_speech": false
} It also exposed relationships between IDs.
Several of the 46 entries were aliases rather than independent model families:
Mistral Medium 3.5
├─ mistral-medium-latest
├─ mistral-medium
├─ mistral-medium-3
├─ mistral-medium-3-5
├─ mistral-medium-3.5
├─ mistral-medium-2604
├─ mistral-vibe-cli-latest
├─ mistral-vibe-cli-with-tools
└─ magistral-medium-latest
Mistral Small 4
├─ mistral-small-latest
├─ mistral-vibe-cli-fast
└─ magistral-small-latest
Codestral 2508
├─ codestral-latest
├─ mistral-code-latest
└─ mistral-code-fim-latest That immediately suggests an important distinction for developer tooling:
A provider catalog is not the same abstraction as a model-selection UI.
The catalog contains raw provider state. A useful picker needs to normalize that state into decisions.
But before designing anything, I wanted empirical data.
Instrument the account instead of guessing
Trying 46 IDs manually would be tedious and difficult to reproduce.
So I wrote a small Fish probe that:
- fetches the authenticated model catalog;
- discovers IDs dynamically;
- sends the smallest useful chat request;
- records HTTP status;
- captures rate-limit headers;
- preserves structured API error types;
- sleeps between requests to avoid turning the probe itself into the rate-limit problem.
The core probe:
probe-free-models.fish fish Show source
#!/usr/bin/env fish
if not set -q MISTRAL_API_KEY
if test -f ~/.vibe/.env
set -lx MISTRAL_API_KEY
(string replace 'MISTRAL_API_KEY=' '' < ~/.vibe/.env)
end
end
if not set -q MISTRAL_API_KEY
echo "MISTRAL_API_KEY is not set."
exit 1
end
set MODEL_BODY /tmp/mistral-models.json
set BODY /tmp/mistral-probe-body
set HEADERS /tmp/mistral-probe-headers
curl -sS
https://api.mistral.ai/v1/models
-H "Authorization: Bearer $MISTRAL_API_KEY"
> $MODEL_BODY
set MODELS (jq -r '.data[].id' $MODEL_BODY | sort)
echo "Found "(count $MODELS)" models"
for MODEL in $MODELS
echo
echo "=== $MODEL ==="
set HTTP (curl -sS
-o $BODY
-D $HEADERS
-w '%{http_code}'
https://api.mistral.ai/v1/chat/completions
-H "Authorization: Bearer $MISTRAL_API_KEY"
-H "Content-Type: application/json"
-d (jq -nc --arg model "$MODEL" '{
model: $model,
messages: [{role:"user", content:"hi"}],
max_tokens: 1
}'))
echo "HTTP: $HTTP"
set RPM (grep -i '^x-ratelimit-limit-req-minute:' $HEADERS
| string replace -ri '^.*: *' '')
set REM_RPM (grep -i '^x-ratelimit-remaining-req-minute:' $HEADERS
| string replace -ri '^.*: *' '')
set TPM (grep -i '^x-ratelimit-limit-tokens-minute:' $HEADERS
| string replace -ri '^.*: *' '')
set REM_TPM (grep -i '^x-ratelimit-remaining-tokens-minute:' $HEADERS
| string replace -ri '^.*: *' '')
test -n "$RPM"; and echo "RPM: $RPM"
test -n "$REM_RPM"; and echo "Remaining RPM: $REM_RPM"
test -n "$TPM"; and echo "TPM: $TPM"
test -n "$REM_TPM"; and echo "Remaining TPM: $REM_TPM"
jq '{
error_type: .type,
error_code: .code,
message: .message,
model: .model,
usage: .usage
}' $BODY 2>/dev/null; or cat $BODY
sleep 1.1
end
rm -f $MODEL_BODY $BODY $HEADERSThe important design choice here is preserving the distinction between:
200
400 invalid_model
403 tier_not_allowed
403 labs_not_enabled
429 rate_limited Flattening all of those into available = false would throw away
exactly the state I was trying to understand.
First empirical map
The first chat-oriented pass produced this useful subset:
| Model family | Observed state | RPM | TPM |
|---|---|---|---|
| Ministral 3B | Available | 750 | 1,300,000 |
| Ministral 8B | Available | 188 | 625,000 |
| Ministral 14B | Available | 30 | 937,500 |
| Codestral / Mistral Code | Available | 125 | 625,000 |
| Voxtral Small | Available | 60 | 50,000 |
| Medium 3.5 aliases | Effective 0 RPM | 0 | — |
| Small 4 aliases | Effective 0 RPM | 0 | — |
| Vibe CLI aliases | Effective 0 RPM | 0 | — |
Two other documented models were useful controls even though they were not part of the account’s returned 46-ID catalog:
zai-glm-5-2
→ 403 tier_not_allowed
mistral-large-latest
→ 403 tier_not_allowed That distinction is useful:
known model + direct 403 tier_not_allowed
≠
model returned as available by authenticated /v1/models
≠
model with non-zero operational allowance The Vibe-specific IDs were particularly interesting.
The catalog linked:
mistral-vibe-cli-fast
→ Mistral Small 4
mistral-vibe-cli-latest
→ Mistral Medium 3.5
mistral-vibe-cli-with-tools
→ Mistral Medium 3.5 So these were not three mysterious hidden Vibe models. They were aliases into the same families that were already returning zero RPM.
Then my probe was wrong
This is one of the more important parts of the investigation.
The generic probe sent every ID through:
POST /v1/chat/completions A number of models returned:
400 invalid_model It would have been easy to turn that into a clean-looking table:
400 = unavailable on Free It would also have been wrong.
The /v1/models capability metadata already contained the clue. Some of
these IDs were embeddings, OCR, moderation, transcription,
text-to-speech or realtime-transcription models.
The request was invalid because I was asking the wrong API question.
That changes the interpretation:
400 invalid_model on /chat/completions means:
this model cannot be used through this endpoint not:
this account cannot use this model So instead of defending the first probe, I changed the probe strategy.
Initial interpretation
400 invalid_model Free cannot use this modelCorrect interpretation
Wrong capability endpoint The model may be usable through another APIFrom model probing to capability probing
The better abstraction is:
- completion_chat /v1/chat/completions chat and coding
- embeddings /v1/embeddings vector representations
- moderation /v1/moderations classification
- ocr /v1/ocr documents and images
- audio_transcription /v1/audio/transcriptions speech to text
- audio_speech /v1/audio/speech text to speech
That is a much more useful model for both testing and product design.
Embeddings
The embedding probe used /v1/embeddings:
for MODEL in
codestral-embed
codestral-embed-2505
mistral-embed
mistral-embed-2312
echo
echo "=== $MODEL ==="
set HTTP (curl -sS
-o /tmp/mistral-body
-D /tmp/mistral-headers
-w '%{http_code}'
https://api.mistral.ai/v1/embeddings
-H "Authorization: Bearer $MISTRAL_API_KEY"
-H "Content-Type: application/json"
-d (jq -nc --arg model "$MODEL" '{
model: $model,
input: ["hello"]
}'))
echo "HTTP: $HTTP"
grep -i '^x-ratelimit-' /tmp/mistral-headers; or true
jq '{model, usage, error: .message}' /tmp/mistral-body
end All four worked:
codestral-embed → 200 · 60 RPM
codestral-embed-2505 → 200 · 60 RPM
mistral-embed → 200 · 60 RPM
mistral-embed-2312 → 200 · 60 RPM Moderation
curl -sS
-o /tmp/mistral-body
-D /tmp/mistral-headers
https://api.mistral.ai/v1/moderations
-H "Authorization: Bearer $MISTRAL_API_KEY"
-H "Content-Type: application/json"
-d '{
"model": "mistral-moderation-2603",
"input": ["hello"]
}'
grep -i '^x-ratelimit-' /tmp/mistral-headers
jq . /tmp/mistral-body Result:
mistral-moderation-2603
→ 200
→ 100 RPM OCR
The OCR family was tested against /v1/ocr.
A representative request:
set MODEL mistral-ocr-latest
curl -sS
-o /tmp/mistral-body
-D /tmp/mistral-headers
https://api.mistral.ai/v1/ocr
-H "Authorization: Bearer $MISTRAL_API_KEY"
-H "Content-Type: application/json"
-d (jq -nc --arg model "$MODEL" '{
model: $model,
document: {
type: "document_url",
document_url: "https://example.com/test.pdf"
}
}')
jq . /tmp/mistral-body For the actual investigation I used a valid test document. The important
result was that all tested OCR IDs returned HTTP 200:
mistral-ocr-2512
mistral-ocr-3
mistral-ocr-3-0
mistral-ocr-4
mistral-ocr-4-0
mistral-ocr-4-1
mistral-ocr-latest I did not observe the same RPM/TPM headers for OCR, so I did not invent a quota value.
That sounds obvious, but it is an important testing principle:
Absence of a measured value is not permission to infer one.
Labs was another state dimension
Two Leanstral IDs initially returned:
403 labs_not_enabled That is a better error than a generic failure because it encodes an actionable state.
- 01 403 labs_not_enabled
- 02 Error points to Privacy
- 03 Vibe Privacy: no toggle
- 04 API Privacy: toggle exists
- 05 Enable Labs
- 06 Leanstral works
The remaining problem was finding the setting.
There were two relevant Privacy pages:
Vibe → Privacy
API → Privacy The Labs activation toggle was under:
API → Privacy After enabling Labs I reran a minimal probe:
for MODEL in labs-leanstral-1-5 labs-leanstral-1-5-1
echo
echo "=== $MODEL ==="
curl -sS -D /tmp/mistral-labs-headers
https://api.mistral.ai/v1/chat/completions
-H "Authorization: Bearer $MISTRAL_API_KEY"
-H "Content-Type: application/json"
-d (jq -nc --arg model "$MODEL" '{
model: $model,
messages: [{role:"user", content:"hi"}],
max_tokens: 1
}')
| jq
echo "--- limits ---"
grep -i '^x-ratelimit-' /tmp/mistral-labs-headers
end Both worked immediately:
labs-leanstral-1-5
38 RPM
5,000,000 TPM
labs-leanstral-1-5-1
38 RPM
5,000,000 TPM At the time of this test, Leanstral had the highest TPM allowance of any chat model I measured on this Free account.
Labs before
403 labs_not_enabled Capability gated behind an account settingLabs after
Leanstral · 5M TPM 38 RPM after enabling Labs in API → PrivacyWhich produced a fairly absurd contrast:
Vibe default:
Mistral Medium 3.5
→ 0 RPM
coding-focused Labs model:
Leanstral 1.5
→ 38 RPM
→ 5,000,000 TPM The capability existed.
The developer-facing discovery path did not communicate it.
Audio made the Free tier look even better
The remaining 400 invalid_model results included Voxtral models.
Again, the catalog’s capability metadata told me which APIs to use.
For offline transcription and TTS I generated a one-second local WAV so the probe did not depend on an external audio source:
ffmpeg -loglevel error -y
-f lavfi -i "sine=frequency=440:duration=1"
-ar 16000 -ac 1
/tmp/mistral-probe.wav Offline transcription
for MODEL in voxtral-mini-2602 voxtral-mini-latest
echo
echo "=== $MODEL ==="
set HTTP (curl -sS
-o /tmp/mistral-audio-body
-D /tmp/mistral-audio-headers
-w '%{http_code}'
https://api.mistral.ai/v1/audio/transcriptions
-H "Authorization: Bearer $MISTRAL_API_KEY"
-F "model=$MODEL"
-F "file=@/tmp/mistral-probe.wav")
echo "HTTP: $HTTP"
grep -i '^x-ratelimit-' /tmp/mistral-audio-headers; or true
jq . /tmp/mistral-audio-body
end Both aliases returned 200.
The headers exposed a different quota dimension:
x-ratelimit-limit-audio-seconds-minute: 3600
x-ratelimit-limit-req-minute: 60 So the observed Free allowance was:
60 RPM
3,600 audio seconds/minute The one-second sine wave was genuinely processed:
prompt_audio_seconds: 1
audio_tokens: 375 The empty transcription was expected; the input contained no speech.
TTS and voice discovery
Instead of hardcoding a voice ID, I queried the voice API first:
curl -sS
"https://api.mistral.ai/v1/audio/voices?type=preset&limit=5"
-H "Authorization: Bearer $MISTRAL_API_KEY"
| jq . The account exposed:
30 preset voices Then I used one returned voice ID to probe TTS:
probe-tts.fish fish Show source
set VOICE_ID (curl -sS
"https://api.mistral.ai/v1/audio/voices?type=preset&limit=1"
-H "Authorization: Bearer $MISTRAL_API_KEY"
| jq -r '.items[0].id')
for MODEL in voxtral-mini-tts-2603 voxtral-mini-tts-latest
echo
echo "=== $MODEL ==="
set HTTP (curl -sS
-o /tmp/mistral-tts-body
-D /tmp/mistral-tts-headers
-w '%{http_code}'
https://api.mistral.ai/v1/audio/speech
-H "Authorization: Bearer $MISTRAL_API_KEY"
-H "Content-Type: application/json"
-d (jq -nc
--arg model "$MODEL"
--arg voice "$VOICE_ID"
'{
model: $model,
input: "Hello.",
voice_id: $voice,
response_format: "wav",
stream: false
}'))
echo "HTTP: $HTTP"
grep -i '^x-ratelimit-' /tmp/mistral-tts-headers; or true
jq 'if .audio_data then {
success: true,
audio_bytes_approx:
((.audio_data | length) * 3 / 4 | floor)
} else . end' /tmp/mistral-tts-body
endBoth TTS IDs returned 200.
Observed limit:
12,000 input characters/minute Again, the original 400 invalid_model had not been a subscription
restriction. It had been a bad endpoint choice in my first probe.
Realtime needed a different protocol entirely
The final specialized family was realtime transcription.
This could not be tested honestly by sending another REST request. I used Mistral’s realtime Python client instead.
Temporary environment:
python -m venv /tmp/mistral-realtime-probe
source /tmp/mistral-realtime-probe/bin/activate.fish
pip install 'mistralai[realtime]' Generate raw PCM:
ffmpeg -loglevel error -y
-f lavfi -i "sine=frequency=440:duration=1"
-f s16le -acodec pcm_s16le
-ar 16000 -ac 1
/tmp/mistral-realtime.pcm The probe:
probe-realtime.py python Show source
import asyncio
import os
from pathlib import Path
from mistralai.client import Mistral
from mistralai.client.models import AudioFormat
MODELS = [
"voxtral-mini-transcribe-realtime-2602",
"voxtral-mini-realtime-2602",
"voxtral-mini-realtime-latest",
]
PCM = Path("/tmp/mistral-realtime.pcm")
CHUNK_BYTES = 7680 # ~240 ms @ 16 kHz mono s16le
async def audio_stream():
data = PCM.read_bytes()
for pos in range(0, len(data), CHUNK_BYTES):
yield data[pos : pos + CHUNK_BYTES]
await asyncio.sleep(0.24)
async def probe(model):
print()
print(f"=== {model} ===")
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
audio_format = AudioFormat(
encoding="pcm_s16le",
sample_rate=16000,
)
try:
got_event = False
async for event in client.audio.realtime.transcribe_stream(
audio_stream=audio_stream(),
model=model,
audio_format=audio_format,
):
got_event = True
event_type = getattr(event, "type", type(event).__name__)
print(f"EVENT: {event_type}")
for field in (
"text",
"error",
"message",
"code",
"usage",
):
value = getattr(event, field, None)
if value not in (None, "", [], {}):
print(f" {field}: {value}")
if got_event:
print("RESULT: stream completed")
else:
print("RESULT: stream completed without events")
except Exception as exc:
print("RESULT: ERROR")
print(f"TYPE: {type(exc).__name__}")
print(f"ERROR: {exc}")
async def main():
for model in MODELS:
await probe(model)
asyncio.run(main())All three IDs completed a real streaming session:
session.created
session.updated
transcription.done
RESULT: stream completed Usage was reported as well:
prompt_tokens=23
completion_tokens=23
total_tokens=46
prompt_audio_seconds=2 At this point every specialized model family that had produced 400 invalid_model in the generic chat probe had worked when tested
through the correct API or protocol.
That is a much more useful result than the original 400s.
The resulting empirical capability map
For this specific Mistral Free EU organization on 2026-09-10, the useful observed state was:
Chat and coding
| Model family | Observed state | RPM | TPM |
|---|---|---|---|
| Leanstral 1.5 | Available after Labs opt-in | 38 | 5,000,000 |
| Ministral 3B | Available | 750 | 1,300,000 |
| Ministral 14B | Available | 30 | 937,500 |
| Ministral 8B | Available | 188 | 625,000 |
| Codestral / Mistral Code | Available | 125 | 625,000 |
| Voxtral Small | Available | 60 | 50,000 |
| Medium 3.5 aliases | Effective 0 RPM | 0 | — |
| Small 4 aliases | Effective 0 RPM | 0 | — |
| Vibe CLI aliases | Effective 0 RPM | 0 | — |
| GLM 5.2 | Tier denied | — | — |
| Large 3 | Tier denied | — | — |
Specialized APIs
| Capability | Observed state | Observed limit |
|---|---|---|
| Embeddings | Available | 60 RPM |
| Moderation | Available | 100 RPM |
| OCR | Available | No equivalent RPM/TPM header observed |
| Offline transcription | Available | 60 RPM + 3,600 audio seconds/min |
| TTS | Available | 12,000 input characters/min |
| Realtime transcription | Available | Successful streaming sessions |
| Preset voice discovery | Available | 30 voices observed |
These numbers are observations, not promises.
They describe one account, one organization, one region and one point in time.
That distinction matters for any empirical platform investigation.
What the experiment actually says about Mistral Free
The initial Vibe experience made the platform look worse than it was.
The deeper probe showed almost the opposite.
The Free account had access to:
- several usable hosted chat/coding models;
- a coding-focused Labs model with a 5M TPM allowance;
- embeddings;
- moderation;
- multiple OCR aliases;
- offline audio transcription;
- realtime transcription;
- TTS;
- global preset voices.
The problem was not primarily capability scarcity.
It was capability legibility.
That is where this stopped being a Free-tier benchmark and became a DevEx audit.
The missing abstraction
The recurring failure pattern was that raw platform state existed, but the developer had to translate it manually.
- Discovered ≠
- Entitled ≠
- Non-zero limit ≠
- Correct capability ≠
- Usable in Vibe
During one model-selection task I had to determine:
Did browser authentication actually provision the CLI?
Is the API key valid?
Is this a global API failure or model-specific?
Does 429 mean exhausted quota or a configured limit of zero?
Is this model returned for my account?
Is this ID canonical or an alias?
Is this a chat model?
Which capability does it expose?
Which endpoint implements that capability?
Does it require Labs?
Does 403 mean Labs disabled or subscription tier denied?
Which alternative model can I use right now? Most of the ingredients already existed somewhere:
/v1/models
→ IDs
→ aliases
→ capabilities
API errors
→ labs_not_enabled
→ tier_not_allowed
→ rate_limited
response headers
→ RPM
→ TPM
→ audio-seconds/min
→ input-characters/min
Vibe
→ provider
→ selected model
→ authentication/setup state What was missing was a normalized representation between provider state and developer-facing decisions.
Conceptually:
raw provider state
↓
discovery
↓
normalization
├── canonical model
├── aliases
├── capabilities
└── provider
↓
availability enrichment
├── tier state
├── Labs state
├── observed limits
└── last relevant error
↓
cached ModelAvailability
↓
consumers
├── /model
├── setup
├── retry/error handling
└── fallback suggestions That is a broader change than making /model prettier.
It is a UX and DX fix.
What /model could become
Once the client has normalized provider state, the picker becomes relatively simple.
For example:
Select model
Hosted · Available
● Codestral 2508 125 RPM · 625K TPM
● Ministral 3B 750 RPM · 1.3M TPM
● Ministral 8B 188 RPM · 625K TPM
● Ministral 14B 30 RPM · 937K TPM
● Leanstral 1.5 38 RPM · 5M TPM Labs
Hosted · Unavailable
○ Mistral Medium 3.5 0 RPM Rate limit
○ Mistral Small 4 0 RPM Rate limit
Local
● Devstral The exact UI is less important than the state model behind it.
A Labs error can become:
Leanstral 1.5 is disabled for this organization.
Enable Labs under:
API → Privacy A zero-RPM request can become:
Mistral Medium 3.5 is currently unavailable for this account.
Observed request limit:
0 requests/minute
Available alternatives:
Codestral 2508
Ministral 3B
Ministral 8B
Ministral 14B
Use /model to switch. That is much more useful than repeatedly retrying a request whose configured request allowance is zero.
Do not turn discovery into more API traffic
There is an important implementation constraint.
A better picker should not probe every model every time /model opens.
The generic 46-ID probe was appropriate for an investigation. It would be a terrible normal interaction pattern.
Instead:
setup
→ fetch catalog
→ normalize metadata
→ cache
successful API calls
→ update observed limits
relevant API errors
→ update availability state
explicit refresh / cache expiry
→ refresh provider state
/model
→ render cached normalized state This also separates concerns cleanly.
/model should consume a model-availability abstraction. It should not
become a pile of Mistral-specific API rules.
A possible representation
The exact implementation should follow Vibe’s existing architecture, but conceptually I would want something close to:
- 01 Provider
- 02 Discovery
- 03 Capability normalization
- 04 Entitlement
- 05 Rate-limit state
- 06 ModelAvailability
- 07 Picker · setup · fallback · diagnostics
ModelAvailability(
model_id="codestral-2508",
canonical_id="codestral-2508",
aliases=[
"codestral-latest",
"mistral-code-latest",
"mistral-code-fim-latest",
],
capabilities={
"completion_chat": True,
"completion_fim": True,
"function_calling": True,
},
status="available",
limits=RateLimits(
rpm=125,
tpm=625_000,
),
) versus:
ModelAvailability(
model_id="mistral-medium-latest",
canonical_id="mistral-medium-latest",
aliases=[
"mistral-medium",
"mistral-medium-3.5",
"mistral-vibe-cli-latest",
"mistral-vibe-cli-with-tools",
],
status="zero_rate_limit",
limits=RateLimits(
rpm=0,
),
) And potentially:
ModelAvailability(
model_id="labs-leanstral-1-5",
status="labs_disabled",
action=EnableLabs(
location="API → Privacy",
),
) The developer-facing components then do not need to understand every provider failure independently.
They consume normalized state.
Why correcting my own probe matters
The most valuable technical moment in this investigation was not discovering 0 RPM.
It was discovering that my first test was asking the wrong question.
A generic chat probe produced a set of clean 400 invalid_model responses. Those results looked easy to classify. The model metadata
contradicted that interpretation.
So I changed the test.
Every specialized family I subsequently tested correctly worked on the Free account.
That is a useful reminder for developer tooling in general:
An error is evidence about the request that produced it, not automatically evidence for the broader conclusion you want to draw.
The same principle applied repeatedly here:
browser login succeeded
≠ local credential provisioned
429 rate_limited
≠ quota temporarily exhausted
model returned by /v1/models
≠ non-zero operational allowance
400 invalid_model on chat endpoint
≠ unavailable model
403
≠ one universal access failure Preserving those distinctions is what turned debugging into a useful platform model.
External validation
I published the initial findings to r/MistralAI while the
investigation was still fresh, then updated the thread after testing
Labs and after correcting the specialized-model results.
Reddit’s own insights showed the post briefly reaching roughly:
#3 post of all time in r/MistralAI
~1.4k views
10 shares within the first ~40 minutes.
More useful than the ranking itself was a comment from another developer who independently described spending too long debugging Vibe authentication and agreed that model availability should be communicated directly by the picker.
That is qualitative validation.
The public write-up also produced a small but useful signal that the findings were actionable rather than merely interesting. One reader summarized it simply:
“Let’s hope Mistral incorporates these findings.”
Since Vibe is open source, that leaves an obvious next step: turn the audit into a contribution.
The stronger evidence remains the reproducible technical state:
HTTP responses
rate-limit headers
/v1/models metadata
capability-specific API calls
realtime protocol events What started as a survey became the case study
The original question was:
What does Mistral’s Free tier actually give a developer?
The empirical answer was: considerably more than the first-run Vibe experience suggested.
But getting that answer required:
- repairing a failed local credential-provisioning state;
- separating authentication from model availability;
- inspecting raw rate-limit headers;
- discovering the authenticated model catalog;
- normalizing aliases;
- instrumenting 46 returned IDs;
- recognizing a flaw in my own generic probe;
- switching from model-based to capability-based testing;
- finding organization-level Labs activation;
- testing REST and realtime audio interfaces;
- and finally reconstructing a usable account capability map.
That is the DevEx problem.
A developer tool does not need to hide the underlying platform.
It should make the platform’s state legible enough that developers can make the next correct decision without reverse-engineering it first.
A developer tool should not require developers to reverse-engineer information that its own platform can already provide programmatically.