~/writing/mistral-vibe-devex-audit

10. September 2026 · 31 Min. Lesezeit · DevEx · AI tooling · API design · Debugging

Was bietet Mistrals Free Tier Entwicklerinnen und Entwicklern tatsächlich?

Eine Untersuchung von Mistrals Free Tier führte zu API-Instrumentierung, einer Karte von Modellen und Fähigkeiten und schließlich zu einem DevEx-Audit von Mistral Vibe.

Ich habe diese Untersuchung nicht begonnen, weil ich Mistral Vibe debuggen wollte.

Ich begann mit einer viel breiteren Frage:

Was bietet Mistral Entwicklerinnen und Entwicklern heute tatsächlich?

Mistrals Plattform war längst über eine einzelne Familie von Chat-Modellen hinausgewachsen: Coding-Modelle, kleinere lokal gut nutzbare Modelle, Audio, OCR, Moderation, Embeddings, Labs-Modelle und ein CLI-Coding-Agent. Dann fiel mir auf, dass es ein Free Tier gab.

Das führte direkt zur nützlicheren Frage:

Was kann ich im Free Tier tatsächlich nutzen?

Nicht, was im Katalog existiert. Nicht, was in der Dokumentation steht. Nicht, was eine Preiszeile hat. Was kann ein authentifizierter Free-Account tatsächlich aufrufen, über welche Schnittstelle und mit welchen Limits?

Vibe schien der naheliegende Einstieg aus Entwicklersicht zu sein. Es ist ein lokaler CLI-Coding-Agent, unterstützt von Mistral gehostete Modelle, und der Free-Account bot Vibe-spezifische Nutzung an. Statt mit synthetischen API-Tests zu beginnen, probierte ich das Produkt so aus, wie es eine Entwicklerin oder ein Entwickler vernünftigerweise tun würde.

An dieser Stelle begann die eigentliche Recherche.

Von der Plattformübersicht zum DevEx-Audit

Die Untersuchung sah schließlich so aus:

Der Untersuchungsweg
  1. 01 Mistral-Plattform
  2. 02 Free Tier
  3. 03 Vibe als Einstiegspunkt
  4. 04 Browser-Login
  5. 05 Fehler bei Zugangsdaten
  6. 06 HTTP 429
  7. 07 0 RPM
  8. 08 46 model IDs
  9. 09 Fähigkeitsprobes
  10. 10 Labs-Status
  11. 11 Empirische Karte
  12. 12 DevEx-Audit
investigation-path.txt text Quelltext anzeigen
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

Der interessante Teil ist nicht, dass ich das Problem debuggen konnte.

Von einer Person, die einen CLI-Agenten verwendet, kann man vernünftigerweise erwarten, dass sie Umgebungsvariablen, HTTP-Fehler und API-Keys versteht. Das Problem ist: Nichts davon war die Untersuchung, die ich durchführen wollte.

Ich wollte herausfinden, was Mistrals Free Tier kann.

Stattdessen musste ich zunächst den Zustand der Plattform rekonstruieren.

Der erste Fehler war real: Zugangsdaten wurden nicht eingerichtet

Vibe v2.25.1 startete auf meinem Mistral-Free-EU-Konto mit ausgewähltem Medium 3.5.

Ich nutzte Vibes Browser-Login. Der Browser-Ablauf wurde erfolgreich abgeschlossen. Dann schickte ich einen trivialen Prompt und erhielt einen Authentifizierungs-/API-Fehler.

Zu diesem Zeitpunkt lag ein Authentifizierungsproblem als naheliegende Hypothese nahe.

Zugangsdaten → Rate-Limit-Diagnose
  1. 01 Vibe installieren
  2. 02 Browser-Login erfolgreich
  3. 03 Anfrage an Medium 3.5 scheitert
  4. 04 API-Key online vorhanden
  5. 05 ~/.vibe/.env fehlt
  6. 06 Key manuell einrichten
  7. 07 Authentifizierung funktioniert
  8. 08 Anfrage scheitert weiterhin
  9. 09 Debug-Konsole: 429
  10. 10 Direkter API-Test
  11. 11 Medium 3.5: 0 RPM

Ich führte das Setup erneut aus:

vibe --setup

Der Browser-/Setup-Ablauf meldete Erfolg.

Ich prüfte das Mistral-Konto online. Dort existierte ein API-/Vibe-Credential. Von der Service-Seite aus sah das Konto eingerichtet aus.

Dann prüfte ich, was Vibe lokal tatsächlich vorfand:

test -f ~/.vibe/.env; and echo exists; or echo missing

Ergebnis:

missing

Der Browser-Login war erfolgreich, aber ~/.vibe/.env war nicht angelegt worden.

Dieser Unterschied ist entscheidend. „Authentifizierung im Browser erfolgreich“ und „die lokale CLI hat verwendbare Zugangsdaten“ sind zwei verschiedene Zustände. Der Setup-Ablauf hatte sie jedoch als einen erfolgreichen Vorgang dargestellt.

Ich legte manuell einen neuen Vibe-API-Key an und richtete den lokalen Zustand selbst ein:

~/.vibe/.env

MISTRAL_API_KEY=...

Für das API-Debugging auf Shell-Ebene konnte ich ihn laden, ohne das Secret auszugeben:

set -lx MISTRAL_API_KEY 
    (string replace 'MISTRAL_API_KEY=' '' < ~/.vibe/.env)

echo "MISTRAL_API_KEY loaded, length:" 
    (string length -- $MISTRAL_API_KEY)

Die Authentifizierung funktionierte nun.

Damit hätte das Debugging enden können.

Tat es aber nicht.

Der zweite Fehler war unabhängig davon

Dasselbe Default-Modell konnte weiterhin keine Anfrage abschließen.

Statt Invalid API key wiederholte Vibe die Anfrage nun ständig. Der entscheidende Hinweis tauchte in der Debug-Ausgabe auf:

Retrying request category=rate_limited detail=HTTP 429

Das Problem mit den Zugangsdaten war also real gewesen. Seine Behebung hatte jedoch nur ein zweites Problem sichtbar gemacht.

Ich nahm Vibe aus der Gleichung und rief die API direkt auf.

Eine minimale Diagnose-Anfrage sah so aus:

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

Der Response-Body sagte:

Rohe API-Antwort · Messartefakt
{
	"object": "error",
	"message": "Rate limit exceeded",
	"type": "rate_limited",
	"code": "1300",
	"raw_status_code": 429
}

Die interessantere Evidenz stand in den Headern:

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

Das war kein vorübergehend ausgeschöpftes Minutenkontingent.

Für dieses Konto lag das effektive Anfrage-Limit von Medium 3.5 genau bei:

0 RPM

Das ist ein grundlegend anderer Zustand als „du wirst rate-limited, weil du zu viele Anfragen geschickt hast“.

Einen Kontrolltest einrichten

Bevor ich schloss, dass der Free-Account oder die API grundsätzlich kaputt war, brauchte ich einen Kontrolltest.

Ich schickte dieselbe Anfrage mit demselben API-Key an:

ministral-3b-latest

Das lieferte HTTP 200.

Die Response-Header enthielten:

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

Damit ließ sich der Zustand sauber isolieren:

credential valid           yes
API reachable              yes
chat completions working   yes
Free account usable        yes
Medium 3.5 usable          no: effective 0 RPM

An diesem Punkt wurde die ursprüngliche Frage zur Plattformübersicht deutlich interessanter.

Wenn Vibes Default-Modell kein Kontingent hatte, welche Modelle konnte der Free-Account dann tatsächlich nutzen?

/model konnte die Frage nicht beantworten

Der naheliegende nächste Schritt war, das Modell zu wechseln.

Vibes /model-Picker gab mir keinen operativen Blick auf die Verfügbarkeit des Kontos. Er zeigte eine kleine Auswahl, beantwortete aber nicht:

Welche gehosteten Modelle sind für dieses Konto verfügbar?
Welche davon sind Chat-/Coding-Modelle?
Welche sind Aliase?
Welche haben ein Limit größer als null?
Welche benötigen eine weitere Kontoeinstellung?

Also ging ich erneut unter Vibe:

curl -sS 
    https://api.mistral.ai/v1/models 
    -H "Authorization: Bearer $MISTRAL_API_KEY" 
    | jq .

Der authentifizierte Endpunkt lieferte:

46 Von /v1/models gelieferte Model-IDs Katalog-Discovery

Katalog-Discovery

46 model IDs Roher Provider-Bestand

Operatives Ergebnis

Empirical capability map Nutzbarkeit erfordert Berechtigung, Limits und den korrekten Endpunkt

Das waren mehr Informationen, aber noch keine Antwort.

Der Modellkatalog ist strukturierte Daten, kein Picker

Die Antwort von /v1/models war nützlich, weil sie mehr als nur Namen enthielt.

Sie legte maschinenlesbare Informationen zu Fähigkeiten offen, etwa:

{
	"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
}

Außerdem zeigte die Antwort Beziehungen zwischen den IDs.

Mehrere der 46 Einträge waren Aliase und keine eigenständigen Modellfamilien:

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

Das legt für Developer-Tooling sofort einen wichtigen Unterschied nahe:

Ein Provider-Katalog ist nicht dieselbe Abstraktion wie eine Model-Selection-UI.

Der Katalog enthält den rohen Provider-Zustand. Ein brauchbarer Picker muss diesen Zustand in konkrete Entscheidungen übersetzen.

Bevor ich jedoch etwas entwarf, wollte ich empirische Daten.

Das Konto instrumentieren statt zu raten

46 IDs manuell zu testen wäre mühsam und schwer reproduzierbar gewesen.

Also schrieb ich einen kleinen Fish-Probe, der:

  • den authentifizierten Modellkatalog abruft;
  • IDs dynamisch ermittelt;
  • die kleinstmögliche sinnvolle Chat-Anfrage sendet;
  • den HTTP-Status protokolliert;
  • Rate-Limit-Header erfasst;
  • strukturierte API-Fehlertypen erhält;
  • zwischen Anfragen wartet, damit der Probe selbst nicht zum Rate-Limit-Problem wird.

Der zentrale Probe:

probe-free-models.fish fish Quelltext anzeigen
#!/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
Der vollständige Probe mit 74 Zeilen bleibt verfügbar, dominiert aber nicht mehr den Lesefluss.

Die wichtige Designentscheidung ist, den Unterschied zwischen diesen Zuständen zu erhalten:

200
400 invalid_model
403 tier_not_allowed
403 labs_not_enabled
429 rate_limited

Alle diese Zustände in available = false zusammenzufassen würde genau die Information wegwerfen, die ich verstehen wollte.

Erste empirische Karte

Der erste Chat-orientierte Durchlauf lieferte diese nützliche Teilmenge:

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

Zwei weitere dokumentierte Modelle waren als Kontrolltests nützlich, obwohl sie nicht im zurückgegebenen 46-ID-Katalog des Kontos enthalten waren:

zai-glm-5-2
→ 403 tier_not_allowed

mistral-large-latest
→ 403 tier_not_allowed

Dieser Unterschied ist nützlich:

known model + direct 403 tier_not_allowed

model returned as available by authenticated /v1/models

model with non-zero operational allowance

Die Vibe-spezifischen IDs waren besonders interessant.

Der Katalog ordnete sie so zu:

mistral-vibe-cli-fast
→ Mistral Small 4

mistral-vibe-cli-latest
→ Mistral Medium 3.5

mistral-vibe-cli-with-tools
→ Mistral Medium 3.5

Es waren also keine drei mysteriösen versteckten Vibe-Modelle. Es waren Aliase auf dieselben Familien, die bereits null RPM zurückgaben.

Dann war mein Probe-Skript falsch

Das ist einer der wichtigsten Teile der Untersuchung.

Der generische Probe schickte jede ID über:

POST /v1/chat/completions

Mehrere Modelle lieferten:

400 invalid_model

Es wäre einfach gewesen, daraus eine sauber aussehende Tabelle zu machen:

400 = unavailable on Free

Sie wäre trotzdem falsch gewesen.

Die Capability-Metadaten von /v1/models enthielten den Hinweis bereits. Einige dieser IDs gehörten zu Embeddings, OCR, Moderation, Transkription, Text-to-Speech oder Realtime-Transkription.

Die Anfrage war ungültig, weil ich die falsche API-Frage stellte.

Das ändert die Interpretation:

400 invalid_model on /chat/completions

means:

this model cannot be used through this endpoint

und nicht:

this account cannot use this model

Statt den ersten Probe zu verteidigen, änderte ich deshalb die Teststrategie.

Erste Interpretation

400 invalid_model Free kann dieses Modell nicht verwenden

Korrekte Interpretation

Falscher Fähigkeits-Endpunkt Das Modell kann über eine andere API nutzbar sein

Von Modell-Probes zu Fähigkeits-Probes

Die bessere Abstraktion ist:

Capability to endpoint map
GET /v1/models
  • completion_chat /v1/chat/completions Chat und Coding
  • embeddings /v1/embeddings Vektorrepräsentationen
  • moderation /v1/moderations Klassifizierung
  • ocr /v1/ocr Dokumente und Bilder
  • audio_transcription /v1/audio/transcriptions Sprache zu Text
  • audio_speech /v1/audio/speech Text zu Sprache

Das ist sowohl für Tests als auch für Produktdesign ein deutlich nützlicheres Modell.

Embeddings

Der Embedding-Probe verwendete /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

Alle vier funktionierten:

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

Die OCR-Familie wurde über /v1/ocr getestet.

Eine beispielhafte Anfrage:

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

Für die eigentliche Untersuchung verwendete ich ein gültiges Testdokument. Das wichtige Ergebnis: Alle getesteten OCR-IDs lieferten 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

Für OCR beobachtete ich nicht dieselben RPM-/TPM-Header, daher erfand ich keinen Quota-Wert.

Das klingt offensichtlich, ist aber ein wichtiges Testprinzip:

Das Fehlen eines Messwerts ist keine Erlaubnis, einen Wert zu unterstellen.

Labs war eine weitere Zustandsdimension

Zwei Leanstral-IDs lieferten zunächst:

403 labs_not_enabled

Das ist ein besserer Fehler als ein generisches Scheitern, weil er einen konkreten, bearbeitbaren Zustand ausdrückt.

Pfad zur Labs-Aktivierung
  1. 01 403 labs_not_enabled
  2. 02 Fehler verweist auf Privacy
  3. 03 Vibe Privacy: kein Schalter
  4. 04 API Privacy: Schalter vorhanden
  5. 05 Labs aktivieren
  6. 06 Leanstral funktioniert

Das verbleibende Problem war, die Einstellung zu finden.

Es gab zwei relevante Privacy-Seiten:

Vibe → Privacy
API  → Privacy

Der Labs-Schalter befand sich unter:

API → Privacy

Nach der Aktivierung von Labs führte ich einen minimalen Probe erneut aus:

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

Beide funktionierten sofort:

labs-leanstral-1-5
38 RPM
5,000,000 TPM

labs-leanstral-1-5-1
38 RPM
5,000,000 TPM

Zum Zeitpunkt dieses Tests hatte Leanstral das höchste TPM-Limit aller Chat-Modelle, die ich auf diesem Free-Account gemessen hatte.

Labs vorher

403 labs_not_enabled Fähigkeit ist hinter einer Kontoeinstellung verborgen

Labs nachher

Leanstral · 5M TPM 38 RPM nach der Aktivierung von Labs unter API → Privacy

Das ergab einen ziemlich absurden Kontrast:

Vibe default:
Mistral Medium 3.5
→ 0 RPM

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

Die Fähigkeit war vorhanden.

Der Discovery-Pfad für Entwickler kommunizierte sie jedoch nicht.

Audio ließ das Free Tier noch besser aussehen

Unter den verbleibenden 400 invalid_model-Ergebnissen waren Voxtral-Modelle.

Auch hier zeigten die Capability-Metadaten des Katalogs, welche APIs zu verwenden waren.

Für Offline-Transkription und TTS erzeugte ich eine lokale WAV-Datei von einer Sekunde. So hing der Probe nicht von einer externen Audioquelle ab:

ffmpeg -loglevel error -y 
    -f lavfi -i "sine=frequency=440:duration=1" 
    -ar 16000 -ac 1 
    /tmp/mistral-probe.wav

Offline-Transkription

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

Beide Aliase lieferten 200.

Die Header zeigten eine andere Quota-Dimension:

x-ratelimit-limit-audio-seconds-minute: 3600
x-ratelimit-limit-req-minute: 60

Das beobachtete Free-Kontingent war also:

60 RPM
3,600 audio seconds/minute

Die Sinuswelle von einer Sekunde wurde tatsächlich verarbeitet:

prompt_audio_seconds: 1
audio_tokens: 375

Die leere Transkription war erwartbar, denn die Eingabe enthielt keine Sprache.

TTS und Voice-Erkennung

Statt eine Voice-ID fest einzubauen, fragte ich zuerst die Voice-API ab:

curl -sS 
    "https://api.mistral.ai/v1/audio/voices?type=preset&limit=5" 
    -H "Authorization: Bearer $MISTRAL_API_KEY" 
    | jq .

Das Konto stellte Folgendes bereit:

30 preset voices

Dann verwendete ich eine der zurückgegebenen Voice-IDs für den TTS-Probe:

probe-tts.fish fish Quelltext anzeigen
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

Beide TTS-IDs lieferten 200.

Beobachtetes Limit:

12,000 input characters/minute

Auch das ursprüngliche 400 invalid_model war keine Einschränkung des Abonnements gewesen. Es war eine falsche Endpunktwahl in meinem ersten Probe.

Realtime benötigte ein vollständig anderes Protokoll

Die letzte spezialisierte Familie war Realtime-Transkription.

Das ließ sich nicht sinnvoll durch eine weitere REST-Anfrage testen. Stattdessen verwendete ich Mistrals Realtime-Python-Client.

Temporäre Umgebung:

python -m venv /tmp/mistral-realtime-probe
source /tmp/mistral-realtime-probe/bin/activate.fish
pip install 'mistralai[realtime]'

Rohes PCM erzeugen:

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

Der Probe:

probe-realtime.py python Quelltext anzeigen
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())
Der vollständige Streaming-Probe bleibt zur Reproduzierbarkeit verfügbar, ohne den umgebenden Erzählfluss zu unterbrechen.

Alle drei IDs schlossen eine echte Streaming-Session ab:

session.created
session.updated
transcription.done
RESULT: stream completed

Auch die Nutzung wurde gemeldet:

prompt_tokens=23
completion_tokens=23
total_tokens=46
prompt_audio_seconds=2

Zu diesem Zeitpunkt funktionierte jede spezialisierte Modellfamilie, die im generischen Chat-Probe 400 invalid_model geliefert hatte, über die korrekte API oder das korrekte Protokoll.

Das ist ein deutlich nützlicheres Ergebnis als die ursprünglichen 400er.

Die daraus entstandene empirische Fähigkeitskarte

Für diese konkrete Mistral-Free-EU-Organisation am 10.09.2026 ergab sich folgender beobachteter Zustand:

Chat und 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

Spezialisierte 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

Diese Zahlen sind Beobachtungen, keine Versprechen.

Sie beschreiben ein Konto, eine Organisation, eine Region und einen Zeitpunkt.

Dieser Unterschied ist für jede empirische Plattformuntersuchung wichtig.

Was das Experiment tatsächlich über Mistral Free sagt

Die erste Vibe-Erfahrung ließ die Plattform schlechter aussehen, als sie war.

Der tiefere Probe zeigte fast das Gegenteil.

Der Free-Account hatte Zugriff auf:

  • mehrere nutzbare gehostete Chat-/Coding-Modelle;
  • ein Coding-orientiertes Labs-Modell mit 5M TPM;
  • Embeddings;
  • Moderation;
  • mehrere OCR-Aliase;
  • Offline-Audio-Transkription;
  • Realtime-Transkription;
  • TTS;
  • globale Preset-Voices.

Das Problem war vor allem keine Knappheit an Fähigkeiten.

Es war die Verständlichkeit der Fähigkeiten.

An diesem Punkt war dies kein Free-Tier-Benchmark mehr, sondern ein DevEx-Audit.

Die fehlende Abstraktion

Das wiederkehrende Fehlermuster war: Der rohe Plattformzustand war vorhanden, aber die Entwicklerin oder der Entwickler musste ihn manuell übersetzen.

Verfügbarkeit ist eine Kette verschiedener Zustände
  1. Entdeckt
  2. Berechtigt
  3. Limit größer als null
  4. Korrekte Fähigkeit
  5. In Vibe nutzbar

Bei einer einzigen Aufgabe zur Modellauswahl musste ich klären:

Hat die Browser-Authentifizierung die CLI tatsächlich eingerichtet?

Ist der API-Key gültig?

Ist das ein globaler API-Fehler oder betrifft er nur dieses Modell?

Bedeutet 429 ein erschöpftes Kontingent oder ein konfiguriertes Limit von null?

Wird dieses Modell für mein Konto zurückgegeben?

Ist diese ID kanonisch oder ein Alias?

Ist das ein Chat-Modell?

Welche Fähigkeit stellt es bereit?

Welcher Endpunkt implementiert diese Fähigkeit?

Benötigt es Labs?

Bedeutet 403, dass Labs deaktiviert ist, oder dass der Subscription-Tier den
Zugriff verweigert?

Welches alternative Modell kann ich jetzt verwenden?

Die meisten Bausteine existierten bereits irgendwo:

/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

Was fehlte, war eine normalisierte Repräsentation zwischen Provider-Zustand und Entscheidungen für Entwickler.

Konzeptionell:

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

Das ist eine größere Änderung, als /model hübscher zu machen.

Es ist eine UX- und DX-Korrektur.

Was aus /model werden könnte

Sobald der Client den Provider-Zustand normalisiert hat, wird der Picker vergleichsweise einfach.

Zum Beispiel:

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

Die konkrete UI ist weniger wichtig als das Zustandsmodell dahinter.

Aus einem Labs-Fehler kann werden:

Leanstral 1.5 is disabled for this organization.

Enable Labs under:
API → Privacy

Aus einer Anfrage mit null RPM kann werden:

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.

Das ist deutlich nützlicher, als eine Anfrage mit konfiguriertem Limit von null immer wieder zu wiederholen.

Aus Discovery keinen zusätzlichen API-Traffic machen

Dabei gibt es eine wichtige Implementierungsgrenze.

Ein besserer Picker sollte nicht bei jedem Öffnen von /model jedes Modell testen.

Der generische Probe mit 46 IDs war für eine Untersuchung angemessen. Als normales Interaktionsmuster wäre er schrecklich.

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

Damit sind die Verantwortlichkeiten außerdem sauber getrennt.

/model sollte eine Model-Availability-Abstraktion verwenden. Es sollte nicht zu einem Haufen Mistral-spezifischer API-Regeln werden.

Eine mögliche Repräsentation

Die konkrete Implementierung sollte Vibes bestehender Architektur folgen. Als Konzept stelle ich mir jedoch etwas in dieser Art vor:

Eine normalisierte Model-Availability-Pipeline
  1. 01 Provider
  2. 02 Erkennung
  3. 03 Fähigkeiten normalisieren
  4. 04 Berechtigung
  5. 05 Rate-Limit-Status
  6. 06 ModelAvailability
  7. 07 Picker · Setup · Fallback · Diagnose
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,
    ),
)

Und möglicherweise:

ModelAvailability(
    model_id="labs-leanstral-1-5",
    status="labs_disabled",
    action=EnableLabs(
        location="API → Privacy",
    ),
)

Die entwicklernahen Komponenten müssen dann nicht jeden Provider-Fehler unabhängig verstehen.

Sie konsumieren normalisierten Zustand.

Warum die Korrektur meines eigenen Probes wichtig ist

Der wertvollste technische Moment dieser Untersuchung war nicht die Entdeckung von 0 RPM.

Es war die Erkenntnis, dass mein erster Test die falsche Frage stellte.

Ein generischer Chat-Probe erzeugte eine Reihe sauberer 400 invalid_model- Antworten. Diese Ergebnisse schienen leicht einzuordnen zu sein. Die Modell-Metadaten widersprachen dieser Interpretation.

Also änderte ich den Test.

Jede spezialisierte Familie, die ich anschließend korrekt testete, funktionierte auf dem Free-Account.

Das ist eine nützliche Erinnerung für Developer-Tooling im Allgemeinen:

Ein Fehler ist Evidenz über die Anfrage, die ihn erzeugt hat, nicht automatisch Evidenz für die weitergehende Schlussfolgerung, die du ziehen willst.

Dasselbe Prinzip zeigte sich hier wiederholt:

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

Diese Unterschiede zu bewahren machte aus dem Debugging ein nützliches Plattformmodell.

Externe Validierung

Ich veröffentlichte die ersten Ergebnisse auf r/MistralAI, solange die Untersuchung noch frisch war. Nach den Labs-Tests und der Korrektur der Ergebnisse für spezialisierte Modelle aktualisierte ich den Thread.

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.

Nützlicher als das Ranking selbst war der Kommentar eines anderen Entwicklers. Er beschrieb unabhängig, dass er zu viel Zeit mit dem Debugging der Vibe- Authentifizierung verbracht hatte, und stimmte zu, dass der Picker die Modellverfügbarkeit direkt kommunizieren sollte.

Das ist qualitative Validierung.

Der öffentliche Write-up lieferte außerdem ein kleines, aber nützliches Signal, dass die Ergebnisse umsetzbar und nicht nur interessant waren. Eine Person fasste es schlicht zusammen:

„Let’s hope Mistral incorporates these findings.“

Da Vibe Open Source ist, liegt der nächste Schritt auf der Hand: das Audit in einen Beitrag zu verwandeln.

Die stärkere Evidenz bleibt der reproduzierbare technische Zustand:

HTTP responses
rate-limit headers
/v1/models metadata
capability-specific API calls
realtime protocol events

Aus der Übersicht wurde eine Fallstudie

Die ursprüngliche Frage lautete:

Was bietet Mistrals Free Tier Entwicklerinnen und Entwicklern tatsächlich?

Die empirische Antwort lautet: deutlich mehr, als die erste Vibe-Erfahrung vermuten ließ.

Um diese Antwort zu bekommen, musste ich:

  • einen fehlgeschlagenen lokalen Zustand für Zugangsdaten reparieren;
  • Authentifizierung von Modellverfügbarkeit trennen;
  • rohe Rate-Limit-Header untersuchen;
  • den authentifizierten Modellkatalog finden;
  • Aliase normalisieren;
  • 46 zurückgegebene IDs instrumentieren;
  • einen Fehler in meinem eigenen generischen Probe erkennen;
  • von modellbasierten zu fähigkeitsbasierten Tests wechseln;
  • die Labs-Aktivierung auf Organisationsebene finden;
  • REST- und Realtime-Audio-Schnittstellen testen;
  • und schließlich eine nutzbare Fähigkeitskarte des Kontos rekonstruieren.

Das ist das DevEx-Problem.

Ein Developer-Tool muss die zugrunde liegende Plattform nicht verstecken.

Es sollte den Plattformzustand so verständlich machen, dass Entwicklerinnen und Entwickler die nächste richtige Entscheidung treffen können, ohne ihn zuerst reverse-engineeren zu müssen.

Ein Developer-Tool sollte nicht verlangen, dass Entwicklerinnen und Entwickler Informationen reverse-engineeren, die die eigene Plattform bereits programmatisch bereitstellen kann.