DocsObservability
Request traces
Inspect INFRO requests, provider connections, fallback attempts, latency and estimated cost using the request API, with history limited by your plan.
INFRO is in private early access. Running these examples requires an early-access account, an active trial or subscription entitlement, and your own provider connection. Self-serve purchasing is not generally open. See Early access.
The request log records what your applications sent through INFRO: the model, selected provider connection, outcome, timings, usage and estimated provider cost. It is operational evidence; it is not an invoice for model usage.
List and retrieve requests
curl "https://api.infro.io/v1/requests?limit=25&status=error" \
-H "Authorization: Bearer $INFRO_API_KEY"Use GET /v1/requests/{id} for a request detail. limit sets the page size, from 1 to 100 (default 20). List responses include data, has_more and next_cursor. Pass that cursor as after to fetch the next page, keeping the same filters and sort. sort is newest (the default), slowest (latency, highest first) or costliest (estimated provider cost, highest first; unpriced requests come last, never as $0). Every list also says what it searched: window_start (the oldest instant read), window_end (your to, or null for now), retention_days, and requested_from when your plan's window moved the from you sent.
Which keys can read history
A gateway key that can call models cannot automatically read every application's history. Each key carries its own history access, set when it is created or edited in the console:
| History access | What /v1/requests and /v1/usage return |
|---|---|
scoped (default) | Requests from the key's project, or only the key's own requests when it has no project, narrowed to the key's environment when it has one. |
organization | Every request in the organization. Only an owner or admin can grant it. |
none | Nothing; the key can call models and poll its own jobs, and history endpoints answer 403 with code observability_not_permitted. |
Environment is matched against the environment recorded when each request ran, not the key's current environment. Requests recorded before this was stored have no environment and are visible only to keys without an environment filter. Query filters narrow what a key can read; they never widen it. A request outside a key's scope answers exactly like an ID that does not exist.
Captured prompt and response bodies (content) need separate payload access on the key, granted by an owner or admin. Content is never returned after its retention deadline, even if deletion has not run yet, and shortening the retention setting applies to content already captured.
History is measured back from now, whatever to you send. An organization without an active or trialing subscription reads no more than the smallest plan's window.
| Filter | Meaning |
|---|---|
from, to | ISO timestamps; history is limited by the plan. |
status | ok or error. |
model, project, key | Model identifier, project ID or gateway key ID. |
provider, connection, route | Provider kind (openai, aws-bedrock), connection ID, or route ID or slug; route=none selects direct model calls. |
endpoint, modality | Narrow to an endpoint or text, image, video or audio. |
surface | Where the request came from: gateway (a gateway key), playground (the console Playground) or studio (INFRO Studio). Every request carries it as surface. Recorded from 2026-10-13; earlier rows were marked where their origin is certain and are otherwise gateway. |
trace_id, workflow, step, feature | Narrow to one run, one workflow, one step within it, or one product feature. workflow, step and feature take the same shape the request accepts (up to 64 letters, digits and _ . : -); any other value is a 400 invalid_attribution rather than an empty list. |
served_model, finish_reason | The model id your provider's reply named (the row's served_model), or why the model stopped: stop, length, tool_calls, content_filter or other. Recorded from 2026-09-25; older rows have neither. See Traffic changes. |
user_id, session_id, tag.<name> | Match values you attached in request metadata. |
billed | Legacy name: true selects positive estimated cost; false selects zero. It does not mean INFRO charged for inference. |
Every filter is an exact match on a value, and each is answered from the whole retained window, not from one page. There is no free-text search over request content. A filter naming something that does not exist in your organization (an unknown route slug, another organization's trace ID) returns an empty list, never an unfiltered one.
Response cache on a request
GET /v1/requests and its export accept cache=hit|miss|bypass|uncacheable|disabled. Each request object carries cache: null when the request never reached the cache step, otherwise {status, source_request_id, reason, provider_billed, estimated_provider_cost_avoided}. reason explains an uncacheable request, or a miss whose lookup failed (lookup_error, lookup_timeout). estimated_provider_cost_avoided is a USD number on hits only, null when the source request was unpriced. See Response cache.
Find a request in the console
The search box on Requests takes an identifier and searches the whole retained window:
| You type | What happens |
|---|---|
req_… | Opens that request. If it is not found, the console says there is no request with that ID in this organization's last N days, which covers a mistyped ID, an ID from another organization and a request older than your plan's window alike. |
trace:<id>, user:<id>, session:<id> | Filters by trace_id, user_id or session_id. A trace filter also offers the run's trace view. |
workflow:<name>, step:<name>, feature:<name>, served_model:<id> | Filters by that attribution value. |
tag.<key>:<value> or tag.<key>=<value> | Filters by a tag you attached in metadata.tags. |
| Any other value | The console asks whether to search it as a trace ID, user ID or session ID, rather than guessing. |
Each search becomes a removable filter chip and is written to the page address, so a narrowed view can be shared as a link.
Export requests as CSV
curl -o requests.csv "https://api.infro.io/v1/requests/export?trace_id=run_42" \
-H "Authorization: Bearer $INFRO_API_KEY"GET /v1/requests/export takes the same filters as GET /v1/requests and returns exactly the rows that list would return, in the same sort order (newest first by default), as one CSV file. It needs the same history access as the list, and a scoped key exports only its own scope. The console's Export CSV button on Requests downloads the current filters and period.
- Up to 50,000 requests per file. A larger result is refused before any data is sent, with
400and codeexport_too_large: narrow the window or add a filter. A file is never cut short silently.dry_run=truereturns the row count and the window as JSON without producing a file. - Content is never exported. Prompts, responses and captured payloads are not in the file, even when payload logging is on and the key may read content.
estimated_provider_cost_usdis an estimate of what your provider will bill you, not an INFRO charge. It is empty for an unpriced request, never0, andprice_sourceis empty with it. A request refused before anything ran is a known0.- When you send no
to, the window ends at the moment the export starts, so requests served while the file streams are not included. - Every export is recorded in your organization's audit log (who, the filters, the row count) and exports are rate limited per organization.
| Columns, in order |
|---|
id, created_at (ISO 8601, UTC, milliseconds), endpoint, modality, status, http_status, error_code, project_id, project_name, key_id, key_name, provider, connection_id, connection_name, route_slug, model_requested, model, served_model, finish_reason, prompt_tokens, completion_tokens, units, estimated_provider_cost_usd, price_source, latency_ms, ttft_ms, fallback_attempts (a count), trace_id, workflow, step, feature, user_id, session_id, tags (a JSON object) |
The column contract is versioned by the X-INFRO-Export-Schema-Version response header (currently 1); new columns are only ever added at the end. Headers also carry X-INFRO-Export-Rows, X-INFRO-Window-Start, X-INFRO-Window-End and X-INFRO-Retention-Days. Every field is quoted, lines end in CRLF, and the file has no byte-order mark. A text value that would start a spreadsheet formula (=, +, -, @, tab, carriage return) is prefixed with '; numbers are left as numbers. If Excel shows accented characters wrongly, open the file with Data → From Text/CSV and choose UTF-8.
Two columns are added at the end, after tags: cache_status and cache_source_request_id. The schema version stays 1, because columns are only ever added at the end.
Group requests into a run
A retrieval-augmented answer or an agent turn is several model calls. Attach three reserved keys to metadata and INFRO records them as indexed columns, so you can ask what one run of a workflow cost rather than what one request cost.
| Key | Meaning | Shape |
|---|---|---|
trace_id | One run. Generate it yourself, or propagate W3C trace context. | Up to 128 characters of letters, digits, _ . : - |
workflow | The stable name of the work: support-answer, nightly-indexer. | Up to 64 characters, same character set |
step | This call's role in the run: embed_query, rerank, generate. | Up to 64 characters, same character set |
feature | The product feature the call serves: summarize, search. Groups cost by feature and matches cost-center rules. See Cost attribution. | Up to 64 characters, same character set |
A malformed reserved key is refused with 400 and code invalid_attribution naming the key, rather than being dropped. Attribution that fails quietly is discovered weeks later as a hole in a cost report; a refusal is discovered on the first request of an integration. Every other metadata key stays free-form.
Requests that cannot carry a JSON body — multipart audio, for example — can send X-INFRO-Trace-Id, X-INFRO-Workflow, X-INFRO-Step, X-INFRO-Feature and X-INFRO-User-Id (for user_id) instead. A W3C traceparent supplies the trace ID when nothing else does, and always nests INFRO's OpenTelemetry spans inside your own trace. Precedence is body metadata, then headers, then traceparent.
GET /v1/traces/{trace_id} returns every request in one run in order, with cost by step and by modality. The total covers model calls made through INFRO and nothing else: your vector database, your retrieval and your own compute are not included. It is a lower bound whenever any successful call in the run is unpriced, and cost_is_partial says so rather than the figure being filled in.
GET /v1/usage?group_by=workflow — also step, feature, user and tag:<key> for a promoted tag — reports the same figures aggregated. These are the groupings whose cardinality you choose, so they return the hundred largest by estimated cost plus one __other__ row holding the remainder, with groups_truncated set. The rows still add up. Requests that carried no value are one group: null row, never folded into __other__, so you can see how much traffic is unattributed. Attribution is recorded from the request that first carries it; it is not applied retroactively. trace_id is not a grouping: a row per run is what GET /v1/traces/{trace_id} and Unit economics are for.
Tool calls in a trace
Tool calls made through the MCP gateway with the same trace id are returned by GET /v1/traces/{trace_id} beside the model requests, under the same key scope, history window and cap. The response gains tool_calls (in time order: id, created_at, server_id, server_slug, server_name, tool, outcome, error_code, latency_ms, step, workflow; metadata only), tool_call_count, tool_calls_truncated and covers_tool_calls: true. Every existing field keeps its meaning: requests, totals and the estimated cost cover model calls only, and covers is unchanged. outcome is ok, tool_error, refused, upstream_error, timeout or cancelled. A trace that has only tool calls is still found; its start, end and duration then come from the tool calls.
{
"trace_id": "run-7f3a",
"requests": [ ... ],
"tool_calls": [
{
"id": "gtc_01JA2Q8R5W",
"created_at": "2026-10-07T09:14:03.512Z",
"server_id": "mcps_01JA1V0K7M",
"server_slug": "github",
"server_name": "GitHub",
"tool": "github_list_issues",
"outcome": "ok",
"error_code": null,
"latency_ms": 412,
"step": "triage",
"workflow": "issue-triage"
}
],
"tool_call_count": 1,
"tool_calls_truncated": false,
"covers": "model_calls_through_infro",
"covers_tool_calls": true
}Read a request record
- Provider and connection details identify your own account that served the request. Fallback attempts record their models, route positions, errors and timings.
usage.costis an estimated provider cost in USD, with a price source when priced. An unpriced model is not a free model. Your provider invoice is authoritative.cost_breakdownitemizes the estimate when available. It can be null when a record was not priced or predates itemization.- Optional
contentis available only when capture was allowed and the payload has not expired. Customer-supplied metadata may itself contain personal data. guardrailsis{action, findings}when your organization's guardrails acted on the request, and null otherwise.actionisblocked_input,blocked_output,redactedorflagged; each finding is{detector, stage, action, count}. The matched text is never stored, so the record holds counts only.
History and export
Developer includes 7 days of queryable history, Startup 14, Team 30, Business 90 and Enterprise 365. The API reports the applied window. Optional payload logging is a separate opt-in with plan limits; see Privacy.
Use the request API or the CSV export above to keep your own history, or configure OpenTelemetry on an eligible plan. There are no scheduled or emailed exports. See Exports and OpenTelemetry.