~/writing/mistral-vibe-devex-audit

Mistral · DevEx audit

What Mistral's Free tier really offers.

From a failed Vibe login to an empirical map of models, capabilities, and access restrictions.

30 min read

~14 min listen · ▶ Listen in transcript →

I started this investigation with a broad 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 led to a narrower question:

What can I actually use on the Free tier?

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.

From platform survey to DevEx audit

The investigation eventually looked like this:

The investigation path
  1. 01 Mistral platform
  2. 02 Free tier
  3. 03 Vibe entry point
  4. 04 Browser login
  5. 05 Credential failure
  6. 06 HTTP 429
  7. 07 0 RPM
  8. 08 46 model IDs
  9. 09 Capability probes
  10. 10 Labs state
  11. 11 Empirical map
  12. 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 diagnosis

A developer using a CLI agent can reasonably be expected to understand environment variables, HTTP errors and API keys. None of that was the task I wanted to perform: I wanted to find out what Mistral’s Free tier could do, but first had to reconstruct the platform state.

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.

Credential provisioning → rate-limit diagnosis
  1. 01 Install Vibe
  2. 02 Browser login succeeds
  3. 03 Medium 3.5 request fails
  4. 04 API key exists online
  5. 05 ~/.vibe/.env missing
  6. 06 Manual key provisioning
  7. 07 Authentication works
  8. 08 Request still fails
  9. 09 Debug console: 429
  10. 10 Direct API probe
  11. 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.

“Authentication succeeded in the browser” and “the local CLI has usable credentials” are two different states, but the setup flow 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. The request still failed.

The second failure was independent

The same default model still could not complete a request.

Vibe now kept retrying instead of returning Invalid API key. Its debug output showed:

Retrying request category=rate_limited detail=HTTP 429

The credential problem had been real. Fixing it 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:

Raw API response · measured artifact
{
	"object": "error",
	"message": "Rate limit exceeded",
	"type": "rate_limited",
	"code": "1300",
	"raw_status_code": 429
}

The headers showed:

x-ratelimit-limit-req-minute: 0
x-ratelimit-remaining-req-minute: 0

For this account, the effective request allowance for Medium 3.5 was exactly:

0 RPM

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

Vibe’s default hosted model had zero allowance. I needed to find which models the Free account could 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:

46 model IDs returned by /v1/models catalog discovery

Catalog discovery

46 model IDs Raw provider inventory

Operational result

Empirical capability map Usability requires entitlement, limits, and the correct endpoint

The model catalog is structured data, not a picker

The /v1/models response 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

For developer tooling, this makes a practical distinction:

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.

I wanted empirical data before designing anything.

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 $HEADERS
The complete 74-line probe remains available, but no longer dominates the article's reading flow.

The probe preserves 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 returned:

Model familyObserved stateRPMTPM
Ministral 3BAvailable7501,300,000
Ministral 8BAvailable188625,000
Ministral 14BAvailable30937,500
Codestral / Mistral CodeAvailable125625,000
Voxtral SmallAvailable6050,000
Medium 3.5 aliasesEffective 0 RPM0—
Small 4 aliasesEffective 0 RPM0—
Vibe CLI aliasesEffective 0 RPM0—
750 RPM Ministral 3B 1.3M TPM
125 RPM Codestral 625k TPM
0 RPM Medium 3.5 aliases effective allowance

I also used two documented models that were absent from the account’s returned 46-ID catalog as controls:

zai-glm-5-2
→ 403 tier_not_allowed

mistral-large-latest
→ 403 tier_not_allowed
known model + direct 403 tier_not_allowed
        ≠
model returned as available by authenticated /v1/models
        ≠
model with non-zero operational allowance

The catalog linked the Vibe-specific IDs:

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

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.

The response means:

400 invalid_model on /chat/completions

means:

this model cannot be used through this endpoint

not:

this account cannot use this model

I changed the probe strategy.

Initial interpretation

400 invalid_model Free cannot use this model

Correct interpretation

Wrong capability endpoint The model may be usable through another API

From model probing to capability probing

Subsequent probes followed this capability map:

Capability to endpoint map
GET /v1/models
  • 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

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

I used a valid test document. 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.

Labs was another state dimension

Two Leanstral IDs initially returned:

403 labs_not_enabled

The error identifies an account setting to change.

Labs activation path
  1. 01 403 labs_not_enabled
  2. 02 Error points to Privacy
  3. 03 Vibe Privacy: no toggle
  4. 04 API Privacy: toggle exists
  5. 05 Enable Labs
  6. 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 setting

Labs after

Leanstral · 5M TPM 38 RPM after enabling Labs in API → Privacy

The measured states were:

Vibe default:
Mistral Medium 3.5
→ 0 RPM

coding-focused Labs model:
Leanstral 1.5
→ 38 RPM
→ 5,000,000 TPM

The capability existed, but 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
end

Both TTS IDs returned 200.

Observed limit:

12,000 input characters/minute

The original 400 invalid_model was again caused by using the wrong endpoint, not by a subscription restriction.

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())
The complete streaming probe remains available for reproducibility without interrupting the surrounding result narrative.

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

Every specialized model family that had produced 400 invalid_model in the generic chat probe worked through the correct API or protocol.

The resulting empirical capability map

For this specific Mistral Free EU organization on 2026-09-10, I observed:

Chat and coding

Model familyObserved stateRPMTPM
Leanstral 1.5Available after Labs opt-in385,000,000
Ministral 3BAvailable7501,300,000
Ministral 14BAvailable30937,500
Ministral 8BAvailable188625,000
Codestral / Mistral CodeAvailable125625,000
Voxtral SmallAvailable6050,000
Medium 3.5 aliasesEffective 0 RPM0—
Small 4 aliasesEffective 0 RPM0—
Vibe CLI aliasesEffective 0 RPM0—
GLM 5.2Tier denied——
Large 3Tier denied——

Specialized APIs

CapabilityObserved stateObserved limit
EmbeddingsAvailable60 RPM
ModerationAvailable100 RPM
OCRAvailableNo equivalent RPM/TPM header observed
Offline transcriptionAvailable60 RPM + 3,600 audio seconds/min
TTSAvailable12,000 input characters/min
Realtime transcriptionAvailableSuccessful streaming sessions
Preset voice discoveryAvailable30 voices observed

New finding: Mistral Large 4 is recognized but blocked on Free

Mistral’s Mistral Large 4 announcement introduces the model; the probe below separately checks whether it is usable on the tested Free tier.

A later probe for mistral-large-4 returned HTTP 403 with type: tier_not_allowed and code 1910: “This model is not available in your subscription tier”. The model slug and endpoint were recognized, and the API key was valid. This points to an entitlement block on the tested subscription tier, not failed authentication, an invalid model name, or rate limiting. The 403 response included no rate-limit headers; the request never reached a usable model allowance.

mistral-large-4
→ HTTP 403 · tier_not_allowed · code 1910
→ model and endpoint recognized, API key valid
→ unavailable on the tested subscription tier

A custom Mistral Large 4 entry in Vibe through the standard API provider hits the same entitlement boundary. This describes the tested account and tier, not other accounts or tiers.

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 understated what the platform exposed.

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.

The gap was capability legibility, which made this a DevEx audit.

The missing abstraction

The recurring failure pattern was that raw platform state existed, but the developer had to translate it manually.

Availability is a chain of distinct states
  1. Discovered
  2. Entitled
  3. Non-zero limit
  4. Correct capability
  5. 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

This requires more than making /model prettier. It changes the state the client exposes to developers.

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

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.

Do not turn discovery into more API traffic

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.

Flow:

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:

A normalized model-availability pipeline
  1. 01 Provider
  2. 02 Discovery
  3. 03 Capability normalization
  4. 04 Entitlement
  5. 05 Rate-limit state
  6. 06 ModelAvailability
  7. 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 components consume normalized state instead of implementing provider failure rules independently.

Why correcting my own probe matters

My first test asked 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.

For developer tooling:

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

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.

Another developer independently described spending too long debugging Vibe authentication and agreed that the picker should communicate model availability directly.

One reader summarized the desired outcome simply:

“Let’s hope Mistral incorporates these findings.”

Since Vibe is open source, the next step is to 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 account exposed considerably more than the first-run Vibe experience suggested. 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.

A developer tool does not need to hide the underlying platform. It should make its state legible enough for developers to choose the next action without reverse-engineering it first.

A developer tool should not require developers to reverse-engineer information that its own platform can already provide programmatically.