Skip to content

Request Log

The Request Log records every request that reached /v1/*: what was asked for, what answered, what it cost, and — the part worth coming here for — what was tried, in what order, and what was never called.

Administrative changes are not here. They are in the Audit Trail.

What counts as a request

Four outcomes, and all four are recorded:

OutcomeWhen
AnsweredThe request succeeded
From cacheServed from the semantic cache without reaching a provider
RefusedA guard blocked it
FailedThe gateway returned 4xx or 5xx, a stream failed mid-flight, or the client disconnected

Requests that failed or were abandoned are kept. The recorder fires both when a response finishes and when the connection closes, guarded so a normal response is not counted twice. Previously only the first fired, so a client that timed out before the gateway finished its retry ladder produced no row at all — the requests you most wanted to see were the ones that were not there.

An abandoned request is recorded with status 499 and an error of:

{ "code": "CLIENT_DISCONNECTED",
"message": "The client closed the connection before a response was sent." }

It appears on the Failed tab reading Failed · 499, with that message inline on the row. There is no separate “aborted” tab; the status enum has four values and inventing a fifth for the console alone would put the page out of step with the data.

A stream that failed after its headers were already flushed is also recorded as failed, despite the 200 on the wire.

The routing trail

This is what the detail drawer is for. Every request carries an ordered list of the providers in its chain, each classified as one of three things:

RungMeaning
AnsweredThis provider served the request
FailedThis provider was called and returned an error — the error is shown verbatim
Not calledThis provider was in the chain and was never reached

“Not called” covers two different situations, and the drawer distinguishes them. A provider later in a chain that succeeded earlier was simply not needed. A provider skipped because its circuit breaker was open was not called even though its turn came — the drawer says so, and notes the breaker closes again once a trial request succeeds.

Above the list, a one-line summary: “Served by openai on attempt 2 of 3, after 1 failed attempt and 1 never called”, or “The chain was exhausted — 2 tried and failed, 1 never called”. When the chain is empty it says why: served from cache, refused by a guard, or failed before routing ran.

Per-attempt failure reasons are recorded. The error string on each rung is a declared field; it was previously undeclared and silently stripped on write, so the trail recorded that an attempt failed and never why.

Guard results

Each entry records what the guards decided, rendered as prose rather than enum values: Refused the request, Flagged it, Recorded only, or No match — with the confidence as a percentage.

Cost

Cost is computed when the row is written, from the model’s configured pricing at that moment. Per-unit capabilities add their own rate — per image, per minute of audio.

A model with no pricing row records a cost of 0, not null. That is the same trap least-cost routing has: an unpriced model looks free rather than looking unknown. If your cost figures look implausibly low, check the model catalogue for unpriced rows before you trust them.

Request and response bodies

Not captured by default. Set requestLogging.captureBodies on Settings to turn it on. With it off, the drawer says so rather than showing an empty panel.

Retention

This is the part that surprises people, so it is stated plainly.

A daily job hard-deletes request log entries older than seven days, at 02:00, on any deployment running the worker. It ignores the per-deployment retention setting.

The Settings page offers a retention field accepting 1 to 3650 days, and it does control the expiry stamp written onto each row — but the cleanup job runs independently and takes the shorter of the two in practice. Treat the request log as a rolling seven-day window and export anything you need to keep for longer.

Audit entries are unaffected; they have their own, much longer retention.

If the s3-backups entitlement is present, a weekly job archives audit_logs, request_logs and usage_records to S3 on Sundays at 03:00.

Reading it in the console

Tabs: All, Failed, Refused, From cache, Succeeded. The count badge shows on the selected tab only — it is the total for the query that produced the rows, and the gateway is not asked for four more counts nobody looked at.

Filters: search (request ID, model or provider), provider, a “slower than” preset, and a date range. The end date covers the whole day in local time. Filters are mirrored into the URL, so a filtered view is a shareable link. Two further filters, model and API key, are set by deep link rather than typed.

Columns: When, Outcome, Provider / model, Latency, Tokens, Cost, Request ID. When, Latency and Cost are sortable.

Detail drawer: time, latency, provider, model, HTTP status, cost, token split, request dialect (OpenAI or Anthropic), the routing rule and strategy that chose the chain, the API key and user, the request ID, then the routing trail, guard results, cache key, and the bodies if captured.

Endpoints

MethodPathPermission
GET/api/admin/requestsusage:read
GET/api/admin/requests/:requestIdusage:read

The detail route is keyed on the gateway request ID, not the database ID.

GET /requests accepts page, limit (capped at 100), sort (timestamp, latencyMs, cost, model, provider, statusCode), order, status (success, error, cached, blockederrors is accepted as a synonym for error), provider, model, apiKeyId, userId, startDate, endDate, minLatency, maxLatency, minCost, maxCost and q.

The list response is a projection — bodies, the routing trail, guard results and the cache key are only on the detail route.

There is no export endpoint and no delete endpoint. Entries are removed by retention alone.

Own-only callers

A caller holding usage:read with an own-only grant — the built-in user role, and any API key with the usage:read capability — has userId forced to their own on both routes, and a userId query parameter from them is ignored. They see their own traffic and nothing else.

  • Audit Trail — administrative changes
  • Routing — the rules that build the chain
  • Cache — why an entry reads “From cache”
  • Models — where cost figures come from