Final Router

API design

Unknown fields should be errors, not silence

We used to drop API fields we did not recognise. It cost a developer an afternoon, so we made it a 400. A short argument for loud failure.

The Final Router team · 29 July 2026 · 5 min read

Zod strips unrecognised keys by default. It is a sensible default for a form and a terrible one for a public API, and it took us exactly one support conversation to find out why.

A developer sent a request with a tools array. We did not support tool calling. Zod removed the field, the request validated cleanly, and the model - which had never been told there were any tools - answered in prose. They got a 200. They got a plausible-looking completion. They spent the afternoon reading their prompt, convinced the model was ignoring their instructions.

The failure was ours, twice

Once for not supporting the feature, which is fine and fixable. And once for hiding it, which is not. The second failure is the expensive one, because it moved the cost from us to them and gave them no way to find it.

A 400 naming the field is a minute of work. A silent drop is an afternoon of debugging the wrong thing.

So the schema is strict now. Every unrecognised key is a 400 that names it. And there is a second layer: fields we recognise but do not implement yet get their own message, because unrecognised key top_p tells a developer nothing they had not already worked out.

400 Bad Request
{
  "error": {
    "message": "top_p is not supported yet. Use \"temperature\".",
    "type": "invalid_request_error",
    "code": 400,
    "param": "top_p"
  }
}

Tool calling, structured output, stop sequences, seeding, logit bias, presence and frequency penalties - every one of them has a message that says what we do not do and what to reach for instead. That table is a dozen lines of code and it has removed more support email than any feature we have shipped.

The objection

Strictness breaks clients when you add fields, the argument goes. Be liberal in what you accept.

It does not, for a gateway. We are the server: adding a field we now understand turns a request that used to 400 into one that succeeds, which breaks nobody. The failure mode strictness prevents runs the other way - a client sending something meaningful and us pretending it was never there. Postel's law is about tolerating malformed input, not about discarding well-formed input you have not implemented.

The general rule

If a request means something different from what the caller intended, say so. Loudly, immediately, and with the field name attached. The cost of a wrong error message is a confused minute. The cost of a wrong success is however long it takes someone to stop trusting your 200.

We apply the same rule elsewhere. An unknown model id is a 400 rather than a fallback to auto. A request that would exceed your spend cap is refused before it is billed rather than after. A provider key that fails verification is rejected at the point you paste it, not at three in the morning when it is serving traffic.