Your agent asks for a completion. The gateway answers 200. The usage block says you spent tokens. The content field is an empty string.
Nothing in that exchange is an error. Every dashboard counts it as a success, every retry loop treats it as done, and the bill arrives all the same. It is the most expensive kind of failure, because it does not look like one.
I have now made SpiderGate check for that before it spends anything.

The failure I could not see
Some models stop producing output well below their advertised limits. Ask for a 16-token completion and you can get a perfectly well-formed response whose content is empty. The model did not error. It answered, and the answer was nothing.
This is the sibling of a problem I wrote about when a model's capability list turned out not to be a capability test. There, models advertised tool support they did not have. Here, models advertise an output ceiling they do not reach. Both are the same shape: the catalogue says one thing, the behaviour says another, and only the behaviour bills you.
The part that bothered me was the reporting. Those requests were recorded status='success'. So the thing that should have told me was the thing telling me it was fine.
Check before you spend, not after
The obvious fix is to notice the empty response and retry. That is worthless. By the time you can see the empty body, the tokens are gone. A check that runs after the call cannot save you anything.
So the check moved in front of the call.

Before a request is dispatched, SpiderGate looks at each model that could serve it. It asks one narrow question: at this completion budget, has this model been measured returning nothing? If the answer is yes, that model is dropped from the pool for this one request.
Two things then happen, and only two.

If another model behind the same alias can serve it, it does, and you get your 200 from that one. If nothing can, you get a 422 with the code no_qualifying_model — and because the check ran before any provider was contacted, that refusal costs nothing.
{
"error": {
"type": "invalid_request_error",
"code": "no_qualifying_model",
"message": "No model behind 'MiniMax-M2.5' can serve a completion budget of 16 tokens. Every candidate is measured to fail at this budget — raise max_tokens or request a different model."
}
}It names each candidate it considered and the verdict that ruled it out, then tells you the remedy: raise max_tokens, or ask for a different model.
Measured, not declared
This is the part I think is actually interesting, and it is a small idea rather than a big one.
SpiderGate's router is built on LiteLLM, which has its own pre-call checks. They work on declared capability: does this deployment support vision, does it support function calling, what context window does it claim. Useful questions, and they are all questions about what a model says about itself.
The catalogue is exactly what was lying. So the gate does not read it. It reads what the models were observed doing: an empty completion at a given budget, recorded and counted. A verdict is only trusted once there are enough observations to mean something.

That has a consequence I want to state plainly, because it is a limit and not a feature. The gate only refuses on evidence it already holds. A model it has not yet measured at your budget is allowed straight through. It is not predicting failure; it is refusing to repeat one it has already seen.
I also had to resist making it stricter. There is a stricter posture available where a model must positively demonstrate it fits before it is allowed to serve. I measured what that would have done. Output ceilings are recorded on 6 of 1,241 catalogue rows, so it would have refused roughly 99.5% of all traffic on the first request. A gate that refuses everything is not a safe gate, it is an outage. So it denies only on measured refusals, and never on absence of information.
What it actually did
Since it went live on 18 August 2026, measured against the gateway request log to 25 August: 166,096 requests, and no request refused that another model could have served.
Four aliases changed behaviour. They used to return a 200 with an empty body; they now return the 422. That is the whole visible change, and it is a change from a lie to an error.
Two aliases that were burning tokens for zero characters stopped: opvs/coding went from 11 such requests to 0, and agent/creative from 3 to 0.
The honest edges
A single pinned model has nowhere to go. Rerouting works by narrowing a pool. If you name one concrete model, there is no alternative to move to, so a disqualification becomes a refusal immediately. Use a task alias if you want the reroute.
Rerouting never reorders your chain. It removes candidates from the pool for one request. Your alias, its fallbacks, its cooldowns and its load balancing all behave exactly as before. Nothing gets promoted or demoted.
Empty responses are still possible. On a model with no measurement history at your budget, the gate has nothing to act on and lets the request through. It shrinks the problem; it does not abolish it.
What to do about it
Mostly nothing. It is on for every chat request, and if your requests were already being served, they still are, by the same models, with no added hop.
If you do see a 422 no_qualifying_model, do not retry it. It carries no Retry-After because retrying is genuinely pointless — the same request will fail the same way every time. Raise max_tokens, or pick a different model or alias.
The full contract is in the error reference, and the how-to is in Refused & Empty Responses.
