Skip to content

Captured fields

One captured LLM call produces one row. This page lists every field on that row: what it holds, what type it is, whether it can be absent, where its value comes from, and which of the two servers keeps it.

Column Means
Hosted The Metergraph cloud, and the customer-local Docker stack, which share a schema
Self-hosted The Apache-2.0 server in the metergraph OSS repository, which has a narrower column allowlist
meta Kept, but folded into the row’s meta JSON rather than given a column of its own
— Not stored. The field is dropped on arrival

The self-hosted column is short by design. That server has no content column for a prompt to land in, so content is discarded structurally rather than by policy. See Content and privacy.

“Nullable” below means the field can be absent on the wire and null in storage. Almost everything is nullable, because a capture layer that refuses a row for a missing optional field is worse than one that records what it saw.

Field Type Nullable Source Hosted Self-hosted
ts ISO-8601 string on the wire, timestamptz in storage No Set by the SDK when the call starts, in UTC. An unparseable value becomes the ingest time column column
repository_id UUID Yes Server-stamped. Resolved from the session token. Any client-supplied value is stripped first column —
protocol_version smallint Yes Server-stamped, 2, alongside repository_id. Any client-supplied value is stripped first column —
Field Type Nullable Source Hosted Self-hosted
route text, 512 characters Yes The innermost route() scope. When absent, the server substitutes template:<template_hash>, so an unlabeled call still clusters column column
func text, 512 characters Yes track(), else the first qualifying stack frame. The two SDKs format this differently: Python writes module:qualname, TypeScript writes module:function:line column column
module text, 512 characters Yes track(module=...), else the first qualifying frame’s module column column
template_hash text, 512 characters Yes A content-free structural hash of the request, computed in the SDK column column
unit_name text, 512 characters Yes route(unit=...). The business unit this work produces column column
unit_count numeric(14,4) Yes route(unit_count=...), defaulting to 1.0 when a unit is named column column
tags JSON object of string to string Yes set_default_tags() merged with every enclosing tags(), context() and route() scope column column
environment text, 512 characters Yes init(environment=...) or METERGRAPH_ENV column column
frames_json array of {m, f, l, p} objects Yes Up to 5 stack frames under app_root, outermost last. p is the repository-relative path, present only for a frame inside the repository root meta —
normalized_path text, 512 characters Yes Import pipelines only. Not set by the SDKs column —
Field Type Nullable Source Hosted Self-hosted
provider text, 512 characters Yes wrap() detection, or the creator half of a creator/model identifier on a gateway call column column
model text, not truncated Yes The model field of the outgoing request, kept verbatim. Aliases are resolved only for pricing, never in place column column
endpoint text, 512 characters Yes The patched seam, for example chat.completions or messages.stream column column
canonical_model text Yes Server-derived. The catalog model the raw model resolved to column column
served_model text, 512 characters Yes Gateway calls only. The model the gateway reports actually serving meta —
gateway text Yes openrouter when the gateway contract matched. A Vercel AI Gateway call carries no gateway key meta —

Every count is a non-negative integer or null. Null means the provider did not report it, which is not the same as zero. A streaming OpenAI chat call reports nothing unless stream_options.include_usage is set, which the SDK adds for you unless METERGRAPH_PATCH_STREAM_USAGE=0.

Field Type Nullable Source Hosted Self-hosted
input_tokens bigint Yes prompt_tokens, input_tokens, prompt_token_count or promptTokenCount column column
output_tokens bigint Yes completion_tokens, output_tokens, candidates_token_count or candidatesTokenCount column column
cache_read_tokens bigint Yes cache_read_input_tokens, prompt_tokens_details.cached_tokens, or cached_content_token_count column column
cache_write_tokens bigint Yes cache_creation_input_tokens, or the details-level cache-write count. The total, when the provider does not split it by TTL column column
cache_write_5m_tokens bigint Yes cache_creation.ephemeral_5m_input_tokens column —
cache_write_1h_tokens bigint Yes cache_creation.ephemeral_1h_input_tokens column —
reasoning_tokens bigint Yes completion_tokens_details.reasoning_tokens or thoughts_token_count column column

cache_write_tokens and the two TTL-specific fields can all be present. The pricing rules in Model catalog and pricing explain how an ambiguous remainder is charged.

Every cost field except reported_cost_usd is server-derived. The SDK never computes a price.

Field Type Nullable Source Hosted Self-hosted
cost_usd numeric(14,8) Yes The effective cost, chosen by the billing resolver at ingest column column
catalog_cost_usd numeric(16,8) Yes What the catalog computed, kept separately from the effective cost column —
catalog_price_id UUID Yes The catalog price row used column price_id
catalog_reasons text array, never null, defaults to {} No Why the result is partial or unpriced, for example missing_input_tokens, unknown_model, batch_rate_unavailable. channel_inferred marks a cost priced on an inferred channel column —
cost_status text Yes One of priced, partial, unpriced. reported exists only on legacy rows column column
reported_cost_usd numeric, non-negative Yes The gateway’s own billed amount, when it qualifies. Never overrides the catalog unless it is qualified OpenRouter chat-completions evidence column and meta —
reported_cost_source text Yes An SDK-fixed provenance string, never gateway-supplied meta —
reported_upstream_cost_usd numeric Yes OpenRouter’s upstream inference cost, kept as provenance only meta —
reported_upstream_cost_source text Yes An SDK-fixed provenance string meta —
Field Type Nullable Source Hosted Self-hosted
latency_ms integer Yes Measured in the SDK from just before the provider call to completion, using a monotonic clock. On a stream this is total time, not time to first token column column
ttft_ms integer Yes Time to first token, streaming calls only column column
status text, 512 characters Yes error on a raised exception, otherwise the provider’s stop reason, otherwise success column column
status_code text Yes error or unset. The OTel-shaped status meta column
finish_reason text Yes Normalized finish reason meta column
finish_reason_raw text Yes The provider’s own string, unmodified meta column
error boolean Yes True when status_code is error. Stored only when the value really is a boolean column column
error_type text, 512 characters Yes The exception class name, not its message. No exception text is transmitted column column

The dashboard’s error rate treats a row as errored when error is true or status is one of error, failed, failure, cancelled, canceled.

Field Type Nullable Source Hosted Self-hosted
trace_id text, lowercased, 128 characters No after ingest The enclosing trace(), else 16 random bytes in hex per call. Synthesized from the batch key and row index if absent column column
span_id text, lowercased, 128 characters No after ingest 8 random bytes in hex per call. Synthesized if absent column —
parent_span_id text, lowercased, 128 characters Yes trace(parent_span_id=...), or the OTLP span’s parent column —
trace_name text, 512 characters Yes The trace name, else the route, else the function, else the endpoint, else the literal LLM call column —
session_id text, 512 characters Yes The innermost session(), context() or set_session() scope column column
conversation_id text, 512 characters Yes A duplicate of session_id, kept for compatibility column —
request_id text, 512 characters Yes The provider’s own request identifier, from _request_id, response_id or id column column
Field Type Nullable Source Hosted Self-hosted
stream boolean Yes True when the call was a streaming call column column
batch boolean Yes True when the request carried batch: true, meaning a provider Batch API submission column column
batch_custom_id string Yes The provider’s per-item custom identifier inside a batch meta —
Field Type Nullable Source Hosted Self-hosted
request_json JSON string Yes The serialized request after credential scrubbing, redaction and truncation. Stored in the request_text column request_text —
response_text JSON string Yes The serialized response envelope: content, aggregated stream text, tool calls, error and status column —
content_opted_in boolean Yes The effective capture_text for this call. Exactly false triggers the opt-out path consumed consumed
text_truncated boolean Yes True when the SDK clipped a content field content_truncated —
content_captured boolean, not null, defaults to false No Server-derived. True when either content field survived column —
content_truncated boolean, not null, defaults to false No Server-derived. True when the SDK truncated, or the server did column —
Stage Cap Marker Configurable
In the SDK, before transmission 1 MiB per field by default \n<metergraph:truncated> Yes, METERGRAPH_TEXT_MAX_BYTES, floor of 1 and no ceiling
At ingest, before storage 500 KiB per field \n<metergraph:truncated> No

So up to 1 MiB per field can leave your process, and 500 KiB per field is what is retained. The request and the response are bounded independently.

A row whose content_opted_in is exactly false has request_json, request_text and response_text removed before anything durable is written, its tool_definitions removed, and its tool_calls reduced to call_id, id, name, status and idempotency. Any other value, including a missing field, means content is preserved if it is present.

Both content fields, and the tool-call structure, have these keys removed at every depth, case-insensitively, on both a JSON string and a nested object:

api-key, api_key, apikey, authorization, client_secret, cookie, headers, id_token, password, proxy-authorization, refresh_token, secret, set-cookie, token, x-api-key

This runs twice: once at the ingest boundary and once again in the worker.

Field Type Nullable Source Hosted Self-hosted
tool_calls JSON array Yes One entry per tool event with call_id, name, status, idempotency and, when content capture is on, its arguments and result. The two SDKs differ when no tool was called: Python leaves this null, TypeScript falls back to the tools the request offered column —
tool_names JSON array of strings Yes Deduplicated tool names, in first-seen order. Content-free, so this survives even with content capture off column column
tool_definitions JSON object Yes The hosted canonical view of caller-authored tool schemas. It records the declaration dialect, scope, fidelity, status and bounded schema values. It is content, so it is scrubbed and removed when content capture is off column —

tool_definitions is a versioned envelope rather than a provider wire payload. It can describe declarations from Anthropic, OpenAI Chat Completions, OpenAI Responses, Gemini, the AI SDK and OTLP. Invalid or oversized declarations are dropped without rejecting the row, and an oversized value marks the content as truncated.

Field Type Nullable Source Hosted Self-hosted
sdk text, 512 characters Yes python from the Python SDK, js from the TypeScript SDK column column
sdk_version text, 512 characters Yes The SDK package version column column
runtime text, 512 characters Yes cpython-3.12.4 style from Python, node-22.11.0 or edge from TypeScript column —
runtime_version text, 512 characters Yes Not set by either SDK. Reserved for importers column —

Set only by the import pipelines, never by the SDKs. The three are constrained to be all present or all absent.

Field Type Nullable Hosted Self-hosted
import_source text Yes column —
import_source_scope text Yes column —
import_event_id text Yes column —

Any field on a submitted row that is not one of the columns above is preserved verbatim into the row’s meta JSON rather than rejected. That is how status_code, finish_reason, finish_reason_raw, frames_json, batch_custom_id and the gateway evidence fields are kept, and it is how a forward-compatible SDK can add a field without an ingest deploy.

An otel object on the row is special-cased: trace_id, span_id, parent_span_id and span_name are lifted out into their own columns, and only the remainder stays in meta.

record_outcome() sends a different row shape, distinguished by event_type: "outcome". It carries no content at all.

Field Type Notes
event_type string Always outcome
event_id string, 128 characters Defaults to a UUID4
ts ISO-8601 string UTC
route string, 512 characters Required, non-empty
session_id string, 512 characters Required, non-empty
model string, 512 characters Required, non-empty
task_completed boolean Required
feedback_score float or null -1 to 1
turns_to_resolution integer or null 1 to 1000000
escalated boolean or null
abandoned boolean or null
edit_distance_ratio float or null 0 to 1
regeneration_count integer or null 0 to 1000000

Metadata is kept for 90 days by default. See Limits and allowances.