Request Lifecycle
A chat completion is authenticated, checked against quota, routed to a candidate provider chain, and logged — without any of that bookkeeping blocking the response the client sees.
Step by step
- Request ID (
middleware/01.request-id.ts) — every incoming request is stamped with a server-generatedBun.randomUUIDv7(), stored in a per-Requestcontext (see Security), used to correlate structured log lines. - Auth (
middleware/02.auth.ts,/v1/**only) — theauthorization: Bearer <key>header is required. Missing →401 missing_api_key. The key is hashed and looked up; no match, or the owning application is disabled →401 invalid_api_key. On success, the resolvedApplicationrow is attached to the request context. - Rate limit (
middleware/03.rate-limit.ts,/v1/**only) — if the application has arateLimitPerMinute, a fixed 60-second window counter is incremented; exceeding it →429 rate_limit_exceeded. See Rate Limits & Quotas. - Body validation — the JSON body is parsed and validated against
chatCompletionRequestSchema(a ValibotstrictObject, so unrecognized fields liketoolsare rejected outright rather than silently ignored). Failure →400 invalid_request. - Model permission (
utils/policy.ts) — if the application has anallowedModelslist, the rawmodelstring from the request must be in it (checked before alias resolution). Violation →403 model_not_permitted. - Quota reservation (
utils/quota.ts) — if the application has amonthlyTokenQuota, an upper-bound token estimate (min(max_tokens ?? 4096, 4096)) is atomically reserved against the current month's counter in the same conditional database write used to check it, so concurrent requests can't all pass a stale check. Exceeded →429 quota_exceeded. The reservation is refunded on failure and reconciled down to real usage once the provider responds. - Dispatch — resolve the model to a candidate provider chain (registry order, filtered by circuit state, reordered by the application's routing strategy) and attempt each in order. See Model Registry & Routing and Failover & Circuit Breaking.
- Response — the client gets its response (or a final error) as soon as dispatch resolves.
- Background logging — usage accounting and the request log row are written after the response is already on its way to the client, wrapped so a logging failure can never surface as a client-visible error.
Streaming is different after the first chunk
For stream: true requests, failover can only happen before the first chunk reaches the client. The gateway pulls exactly one chunk from a candidate before committing to it: if the provider fails to produce even one chunk, that counts as a retryable failure and the next candidate is tried. Once a chunk has been sent to the client, the connection is committed — a failure from that point just ends the stream, since content already delivered can't be un-sent or silently replaced by a different provider's answer.
Everything downstream of dispatch is fire-and-forget
Recording usage (recordUsage) and the request log entry (recordRequestLog) both happen in void (async () => { ... })() blocks wrapped in their own try/catch. This is a deliberate reliability property: a Postgres hiccup while writing observability data must never turn into a 500 for a request that otherwise succeeded.