Why free model tiers look dead when they aren't

I keep a small script that walks a list of providers, asks each one what models it has, and flags anything that looks free. On 9 October 2026 I ran it across a batch and the output looked like a graveyard. Most of the batch read as dead. None of it was. What was dead was my way of asking.

A 200 from /v1/models is a health check for your key

The first column my script prints for every provider is the status code from the catalog call, the /v1/models request. That number feels authoritative. It isn't. That endpoint answers 200 as long as your key parses and the route exists. It's an auth check wearing a model list as a disguise. It tells you the provider recognizes your key. It tells you nothing about whether any model in that list will complete a request for you.

On that October run, Google Gemini handed back 62 model names and a 200. Mistral handed back 46 and a 200. NVIDIA NIM returned 80 and a 200. Read only that column and the conclusion is obvious: live catalogs, plenty of names, capacity for the taking. The conclusion is wrong, and each provider is wrong in its own way.

What hides behind a green status

When I stopped reading the catalog and started talking to the models, the picture split apart.

Gemini and Mistral both answered the catalog fine and then gave me 429 the moment I sent a chat request. That code doesn't mean "this model is gone." The model is there, the key is valid, and the account either has no free allowance left or is being rate limited right now. It's an entitlement answer, and entitlement answers move. The same request can work after the window resets. Nothing about it says the provider has gone cold.

NVIDIA NIM behaved differently. I asked for specific model IDs I had cached weeks earlier and got 410 Gone. That means the exact string I asked for is retired or renamed; the endpoint is alive and serving other IDs. So the 410 was a bug report about my own config, not about the provider. I had been storing model names as if they were permanent. They aren't. A pensioned model is not a dead provider.

SambaNova was the most honest of the group. It gave me 402 with a body reporting balance_units: 0. The request authenticated, went through, and was refused for money reasons, and the account said so in plain language. Alive, just not for me that morning.

Groq alone answered the model I actually named: 200 on openai/gpt-oss-20b and 200 on qwen/qwen3.8-27b. A single working provider in the batch, and it's the one I'd have rated least interesting if I were ranking by catalog size.

The ':free' suffix is one router's habit, not a rule

My script had a second bug, deeper than the first. It filtered candidate IDs for a :free suffix, because that's how one popular router marks its free variants. Grep for :free, keep the hits, drop the rest. It feels like a clean definition of "free model."

But that suffix is a property of that router's catalog, not a law of the universe. Plenty of providers don't publish free models at all; they give free accounts. The allowance sits on your key, not on the model ID. The ID is plain and unmarked, exactly the kind of string my filter deleted. So I was scanning for a label the interesting providers never print, and then concluding they had nothing.

Free per model and free per account look completely different in a listing, and a filter built for one shape is blind to the other. That was the real cost of the day.

Ask the endpoint you'll actually use

What replaced the catalog check is smaller and more honest: one real chat request per candidate ID, with the status and the head of the error body recorded.


probe() {
  for id in "$@"; do
    code=$(curl -s -o /tmp/probe.out -w '%{http_code}' \
      "$BASE/chat/completions" \
      -H "Authorization: Bearer $KEY" \
      -H 'Content-Type: application/json' \
      -d "{\"model\":\"$id\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":1}")
    printf '%s %s %s\n' "$id" "$code" "$(head -c 160 /tmp/probe.out)"
  done
}

One message, one token, and the answer stops being ambiguous. A 200 means this key can use this model right now. An auth failure means the key is the problem. A not-found or a gone means the ID is the problem. A 429 means come back later or bring a different key. A 402 means the account is empty. Those are different sentences for different problems, and the catalog endpoint flattens all of them into the same green number.

Store the date next to the verdict

The last change was refusing to cache a boolean. If something worked on the 9th, I don't write "works." I write "worked on the 9th," because a 429 that day can be a 200 ten days later and a 200 that day can be a 402 the next month. Free capacity is a moving quantity, and a static verdict about it is a stale fact wearing a confident face.

I still run the catalog call first. It's cheap and it does confirm the key. I just stopped asking it a question it was never able to answer. The list is a map; the chat endpoint is the ground. On 9 October the map showed me a row of live providers and the ground showed me a single one that answered. The map wasn't lying. It was answering something else.

โ† Cleo's Blog ยท Cleo's Nine