Skip to content
INFRO

Documentation

Request traces

Every INFRO request leaves a trace record — route, fallback attempts, latency, spans, and exact cost — queryable via GET /v1/requests and enrichable with metadata.


Every request through INFRO produces a trace record: the model and provider that served it, how routing chose them, every fallback attempt along the way, latency down to the first token, and the exact USD charged next to what the same call would have cost at the direct price. Records exist for every endpoint — chat, images, video, audio, and async jobs — whether the request succeeded or failed.

This page covers the record itself, the metadata extension that ties records to your own users and sessions, and the two endpoints that read them back. For aggregates over these records, see Analytics; to move raw records into your own stack, see Exports.

What INFRO keeps per request

A record is written for every API call, successful or not, and becomes queryable within a few seconds. Every response also carries an X-INFRO-Request-Id header whose value is the record's id — log it next to your own request IDs and you (or INFRO support) can pull up exactly what happened to any call.

idstring
req_-prefixed record ID. Returned on every API response in the X-INFRO-Request-Id header.
createdinteger
Unix timestamp (seconds) at which the gateway accepted the request.
endpointstring
The API path called, such as /v1/chat/completions.
projectstring
ID of the project the API key belongs to.
keystring
Label of the API key that made the request — the same label shown in the console and in GET /v1/key (Rate & spend limits).
statusstring
ok for a 2xx response, error for anything else. The exact status code is in http_status.
errorobject | null
For failed requests, the standard error envelope — message, type, code — exactly as returned to the caller. See Errors. null on success.
model_requestedstring
The model ID you sent in the request body.
modelstring
The model that actually served the request. Differs from model_requested when a fallback ran.
providerstring
Slug of the provider that served the request, as chosen by routing — the same value the response's provider field reported.
regionstring
Datacenter region that served the request: us, eu, or ap.
routing_policystring
The routing policy in effect: cheapest, fastest, or balanced.
fallback_attemptsarray
One entry per attempt that failed before the served one, in order. Each carries model, provider, error (the error type), and latency_ms. Empty when the first attempt succeeded.
latency_msinteger
Total gateway latency in milliseconds, from accepting the request to the last byte out.
first_token_msinteger | null
Milliseconds until the first content token left the gateway (Streaming). null for non-streamed responses.
spansarray
Timing breakdown of where the latency went — see the spans section below.
usageobject
Token counts (prompt_tokens, completion_tokens, total_tokens) plus cost — the exact USD charged — and cost_at_direct, what the same call would have cost at the provider's direct price. Media endpoints report unit counts (images, seconds of audio or video) instead of tokens.
metadataobject
Whatever you sent in the request's metadata field, verbatim — see below.
contentobject | null
The request and response payloads — messages in, choices out, or the media equivalent. Present only while content logging is on and within the content retention window; see Privacy.

The metadata extension

Every endpoint accepts an optional top-level metadata object. INFRO stores it on the trace record verbatim and indexes it, so you can filter the request log by your own end-user or session and group analytics by any tag. The gateway never interprets metadata — it changes nothing about routing, models, or billing.

metadataobject
Top-level request field, accepted on every endpoint. All three members are optional.
metadata.user_idstring
Your identifier for the end user behind the request. Filterable via user_id on GET /v1/requests and groupable in GET /v1/usage.
metadata.session_idstring
Your identifier for a session or conversation, so a multi-turn interaction can be pulled up as one thread.
metadata.tagsobject
Up to 16 free-form pairs. Keys and values are strings; values up to 256 characters. Each tag is filterable as tag.<key> and groupable in analytics.

A metadata object that breaks these rules — more than 16 tags, non-string values, an oversized value — fails the whole request with 400 invalid_request_error before it reaches a model. The OpenAI SDKs type metadata as a flat string map, so pass INFRO's richer shape via extra_body in Python and as an untyped extra field in TypeScript, the same way as routing:

curl https://api.infro.io/v1/chat/completions \
  -H "Authorization: Bearer $INFRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5",
    "messages": [{"role": "user", "content": "Summarize this contract: ..."}],
    "metadata": {
      "user_id": "usr_8842",
      "session_id": "sess_a91k3",
      "tags": {"feature": "summarize", "plan": "pro"}
    }
  }'

Metadata is operational metadata, not content: it survives logging: false, appears in analytics and exports, and is visible to your whole org. Send opaque IDs — never emails, names, or anything you would not put in a log line.

List requests

GET /v1/requests returns trace records newest-first, filtered by any combination of the parameters below. Filters combine with AND — user_id plus tag.feature returns only requests matching both. List entries are the full record minus spans and content; fetch a single record for those.

modelstring
Filter to one model ID, such as anthropic/claude-sonnet-5. Matches the model that served, not the model requested.
projectstring
Filter to one project ID.
keystring
Filter to one API key by its label.
user_idstring
Filter to records whose metadata.user_id matches exactly.
session_idstring
Filter to records whose metadata.session_id matches exactly.
tag.<key>string
Filter on a metadata tag, e.g. tag.feature=summarize. Repeat with different keys to require several tags at once.
statusstring
ok or error.
fromstring
Window start, inclusive. RFC 3339 UTC timestamp like 2026-08-01T00:00:00Z. Defaults to 24 hours ago.
tostring
Window end, exclusive. Same format. Defaults to now.
cursorstring
Opaque pagination cursor from a previous response's next_cursor.
limitinteger
Records per page, 1–100. Default 25.
curl "https://api.infro.io/v1/requests?user_id=usr_8842&status=ok&limit=2" \
  -H "Authorization: Bearer $INFRO_API_KEY"
Response
{
  "object": "list",
  "data": [
    {
      "id": "req_7f3k9q2v8n01",
      "created": 1787481600,
      "endpoint": "/v1/chat/completions",
      "project": "proj_4kq8b2",
      "key": "prod-checkout",
      "status": "ok",
      "http_status": 200,
      "error": null,
      "model_requested": "openai/gpt-5.1",
      "model": "anthropic/claude-sonnet-5",
      "provider": "anthropic",
      "region": "us",
      "routing_policy": "cheapest",
      "fallback_attempts": [
        {
          "model": "openai/gpt-5.1",
          "provider": "openai",
          "error": "upstream_error",
          "latency_ms": 774
        }
      ],
      "latency_ms": 2645,
      "first_token_ms": 1204,
      "usage": {
        "prompt_tokens": 1315,
        "completion_tokens": 208,
        "total_tokens": 1523,
        "cost": 0.007065,
        "cost_at_direct": 0.007959
      },
      "metadata": {
        "user_id": "usr_8842",
        "session_id": "sess_a91k3",
        "tags": { "feature": "summarize", "plan": "pro" }
      }
    },
    {
      "id": "req_5t8mw1zq4c22",
      "created": 1787481374,
      "endpoint": "/v1/images/generations",
      "project": "proj_4kq8b2",
      "key": "prod-checkout",
      "status": "ok",
      "http_status": 200,
      "error": null,
      "model_requested": "bfl/flux-2-pro",
      "model": "bfl/flux-2-pro",
      "provider": "bfl",
      "region": "us",
      "routing_policy": "cheapest",
      "fallback_attempts": [],
      "latency_ms": 6412,
      "first_token_ms": null,
      "usage": {
        "units": 1,
        "cost": 0.04,
        "cost_at_direct": 0.05
      },
      "metadata": {
        "user_id": "usr_8842",
        "session_id": "sess_a91k3",
        "tags": { "feature": "cover-image", "plan": "pro" }
      }
    }
  ],
  "has_more": true,
  "next_cursor": "cur_qz8xkm41"
}

When has_more is true, request the next page by passing next_cursor as cursor with the same filters — cursors are opaque and tied to the filter set, so changing a filter mid-pagination restarts the listing. has_more: false means the window is exhausted.

Fetch one request

GET /v1/requests/{id} returns the full record, including spans and — when content logging was on — the content payloads. IDs come from the list endpoint or from the X-INFRO-Request-Id response header. An unknown ID, or one older than the retention window, returns 404.

Request
curl https://api.infro.io/v1/requests/req_7f3k9q2v8n01 \
  -H "Authorization: Bearer $INFRO_API_KEY"
Response
{
  "id": "req_7f3k9q2v8n01",
  "created": 1787481600,
  "endpoint": "/v1/chat/completions",
  "project": "proj_4kq8b2",
  "key": "prod-checkout",
  "status": "ok",
  "http_status": 200,
  "error": null,
  "model_requested": "openai/gpt-5.1",
  "model": "anthropic/claude-sonnet-5",
  "provider": "anthropic",
  "region": "us",
  "routing_policy": "cheapest",
  "fallback_attempts": [
    {
      "model": "openai/gpt-5.1",
      "provider": "openai",
      "error": "upstream_error",
      "latency_ms": 774
    }
  ],
  "latency_ms": 2645,
  "first_token_ms": 1204,
  "spans": [
    { "name": "gateway", "start_ms": 0, "duration_ms": 2645 },
    { "name": "route_selection", "start_ms": 3, "duration_ms": 9 },
    { "name": "provider", "start_ms": 792, "duration_ms": 1846 },
    { "name": "stream", "start_ms": 1204, "duration_ms": 1434 }
  ],
  "usage": {
    "prompt_tokens": 1315,
    "completion_tokens": 208,
    "total_tokens": 1523,
    "cost": 0.007065,
    "cost_at_direct": 0.007959
  },
  "metadata": {
    "user_id": "usr_8842",
    "session_id": "sess_a91k3",
    "tags": { "feature": "summarize", "plan": "pro" }
  },
  "content": {
    "messages": [
      { "role": "user", "content": "Summarize this contract: ..." }
    ],
    "choices": [
      {
        "message": { "role": "assistant", "content": "This agreement covers..." },
        "finish_reason": "stop"
      }
    ]
  }
}

Reading this record: the request asked for openai/gpt-5.1, every provider for it errored, and the fallback chain moved to anthropic/claude-sonnet-5, served by Anthropic from a US datacenter. The failed attempt cost 774 ms, which is why first_token_ms is 1204 rather than the roughly 410 ms the serving provider actually took to start streaming. cost is the exact USD charged; cost_at_direct is what the served tokens would have cost at the provider's direct price.

Spans

spans breaks the total latency into where the time actually went. Each span has a name, a start_ms offset from created, and a duration_ms.

SpanCovers
gatewayThe whole request inside INFRO — auth, parsing, policy checks, and everything below. Its duration equals latency_ms.
route_selectionScoring healthy providers and picking a route — see Routing. Re-runs after a failed attempt; the span covers the total.
providerThe served attempt against the upstream provider, from connection to final byte. Failed attempts are summarized in fallback_attempts instead of getting spans.
streamFirst content token to last, for streamed responses. Absent when the response was not streamed.

The same span tree can be streamed to your own observability stack over OTLP/HTTP — one tree per request, service name infro-gateway, configured in the console. That setup lives in Exports.

Zero-logging and traces

logging: false — per request, or org-wide via the console toggle — controls content, not tracing. A zero-logged request still produces a complete trace record: route, fallback attempts, status, latency, spans, token counts, cost, and metadata are all present, because none of them contain your prompts. What changes is content: it is never written, so it never appears on the record, in exports, or to INFRO support. Metering and billing are unaffected.

The split is deliberate — you keep full operational visibility over workloads whose content must never touch a disk. The exact guarantees, including default content retention, are on Privacy.

Retention

DataWindow
Trace records (route, status, latency, spans, usage, metadata)90 days, queryable via GET /v1/requests
Aggregates (GET /v1/usage)Hourly buckets 90 days, daily buckets for the life of the account — see Analytics
content payloadsUp to 30 days with logging on; never written with logging: false — see Privacy

Records older than 90 days drop out of GET /v1/requests, but their aggregates stay in Analytics and billing totals are never lost. If you need raw records for longer — audits, customer billing, tuning your own routing — schedule recurring CSV exports or stream spans to your own collector over OTLP, where retention is yours to decide. And to act on this data in real time instead of querying it after the fact, set thresholds on spend, error rate, or p95 latency with Alerts.