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.

ColumnMeans
HostedThe Metergraph cloud, and the customer-local Docker stack, which share a schema
Self-hostedThe Apache-2.0 server in the metergraph OSS repository, which has a narrower column allowlist
metaKept, 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.

FieldTypeNullableSourceHostedSelf-hosted
tsISO-8601 string on the wire, timestamptz in storageNoSet by the SDK when the call starts, in UTC. An unparseable value becomes the ingest timecolumncolumn
repository_idUUIDYesServer-stamped. Resolved from the session token. Any client-supplied value is stripped firstcolumn
protocol_versionsmallintYesServer-stamped, 2, alongside repository_id. Any client-supplied value is stripped firstcolumn
FieldTypeNullableSourceHostedSelf-hosted
routetext, 512 charactersYesThe innermost route() scope. When absent, the server substitutes template:<template_hash>, so an unlabeled call still clusterscolumncolumn
functext, 512 charactersYestrack(), else the first qualifying stack frame. The two SDKs format this differently: Python writes module:qualname, TypeScript writes module:function:linecolumncolumn
moduletext, 512 charactersYestrack(module=...), else the first qualifying frame’s modulecolumncolumn
template_hashtext, 512 charactersYesA content-free structural hash of the request, computed in the SDKcolumncolumn
unit_nametext, 512 charactersYesroute(unit=...). The business unit this work producescolumncolumn
unit_countnumeric(14,4)Yesroute(unit_count=...), defaulting to 1.0 when a unit is namedcolumncolumn
tagsJSON object of string to stringYesset_default_tags() merged with every enclosing tags(), context() and route() scopecolumncolumn
environmenttext, 512 charactersYesinit(environment=...) or METERGRAPH_ENVcolumncolumn
frames_jsonarray of {m, f, l, p} objectsYesUp to 5 stack frames under app_root, outermost last. p is the repository-relative path, present only for a frame inside the repository rootmeta
normalized_pathtext, 512 charactersYesImport pipelines only. Not set by the SDKscolumn
FieldTypeNullableSourceHostedSelf-hosted
providertext, 512 charactersYeswrap() detection, or the creator half of a creator/model identifier on a gateway callcolumncolumn
modeltext, not truncatedYesThe model field of the outgoing request, kept verbatim. Aliases are resolved only for pricing, never in placecolumncolumn
endpointtext, 512 charactersYesThe patched seam, for example chat.completions or messages.streamcolumncolumn
canonical_modeltextYesServer-derived. The catalog model the raw model resolved tocolumncolumn
served_modeltext, 512 charactersYesGateway calls only. The model the gateway reports actually servingmeta
gatewaytextYesopenrouter when the gateway contract matched. A Vercel AI Gateway call carries no gateway keymeta

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.

FieldTypeNullableSourceHostedSelf-hosted
input_tokensbigintYesprompt_tokens, input_tokens, prompt_token_count or promptTokenCountcolumncolumn
output_tokensbigintYescompletion_tokens, output_tokens, candidates_token_count or candidatesTokenCountcolumncolumn
cache_read_tokensbigintYescache_read_input_tokens, prompt_tokens_details.cached_tokens, or cached_content_token_countcolumncolumn
cache_write_tokensbigintYescache_creation_input_tokens, or the details-level cache-write count. The total, when the provider does not split it by TTLcolumncolumn
cache_write_5m_tokensbigintYescache_creation.ephemeral_5m_input_tokenscolumn
cache_write_1h_tokensbigintYescache_creation.ephemeral_1h_input_tokenscolumn
reasoning_tokensbigintYescompletion_tokens_details.reasoning_tokens or thoughts_token_countcolumncolumn

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.

FieldTypeNullableSourceHostedSelf-hosted
cost_usdnumeric(14,8)YesThe effective cost, chosen by the billing resolver at ingestcolumncolumn
catalog_cost_usdnumeric(16,8)YesWhat the catalog computed, kept separately from the effective costcolumn
catalog_price_idUUIDYesThe catalog price row usedcolumnprice_id
catalog_reasonstext array, never null, defaults to {}NoWhy the result is partial or unpriced, for example missing_input_tokens, unknown_model, batch_rate_unavailablecolumn
cost_statustextYesOne of priced, partial, unpriced. reported exists only on legacy rowscolumncolumn
reported_cost_usdnumeric, non-negativeYesThe gateway’s own billed amount, when it qualifies. Never overrides the catalog unless it is qualified OpenRouter chat-completions evidencecolumn and meta
reported_cost_sourcetextYesAn SDK-fixed provenance string, never gateway-suppliedmeta
reported_upstream_cost_usdnumericYesOpenRouter’s upstream inference cost, kept as provenance onlymeta
reported_upstream_cost_sourcetextYesAn SDK-fixed provenance stringmeta
FieldTypeNullableSourceHostedSelf-hosted
latency_msintegerYesMeasured 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 tokencolumncolumn
ttft_msintegerYesTime to first token, streaming calls onlycolumncolumn
statustext, 512 charactersYeserror on a raised exception, otherwise the provider’s stop reason, otherwise successcolumncolumn
status_codetextYeserror or unset. The OTel-shaped statusmetacolumn
finish_reasontextYesNormalized finish reasonmetacolumn
finish_reason_rawtextYesThe provider’s own string, unmodifiedmetacolumn
errorbooleanYesTrue when status_code is error. Stored only when the value really is a booleancolumncolumn
error_typetext, 512 charactersYesThe exception class name, not its message. No exception text is transmittedcolumncolumn

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

FieldTypeNullableSourceHostedSelf-hosted
trace_idtext, lowercased, 128 charactersNo after ingestThe enclosing trace(), else 16 random bytes in hex per call. Synthesized from the batch key and row index if absentcolumncolumn
span_idtext, lowercased, 128 charactersNo after ingest8 random bytes in hex per call. Synthesized if absentcolumn
parent_span_idtext, lowercased, 128 charactersYestrace(parent_span_id=...), or the OTLP span’s parentcolumn
trace_nametext, 512 charactersYesThe trace name, else the route, else the function, else the endpoint, else the literal LLM callcolumn
session_idtext, 512 charactersYesThe innermost session(), context() or set_session() scopecolumncolumn
conversation_idtext, 512 charactersYesA duplicate of session_id, kept for compatibilitycolumn
request_idtext, 512 charactersYesThe provider’s own request identifier, from _request_id, response_id or idcolumncolumn
FieldTypeNullableSourceHostedSelf-hosted
streambooleanYesTrue when the call was a streaming callcolumncolumn
batchbooleanYesTrue when the request carried batch: true, meaning a provider Batch API submissioncolumncolumn
batch_custom_idstringYesThe provider’s per-item custom identifier inside a batchmeta
FieldTypeNullableSourceHostedSelf-hosted
request_jsonJSON stringYesThe serialized request after credential scrubbing, redaction and truncation. Stored in the request_text columnrequest_text
response_textJSON stringYesThe serialized response envelope: content, aggregated stream text, tool calls, error and statuscolumn
content_opted_inbooleanYesThe effective capture_text for this call. Exactly false triggers the opt-out pathconsumedconsumed
text_truncatedbooleanYesTrue when the SDK clipped any of the three content fieldscontent_truncated
content_capturedboolean, not null, defaults to falseNoServer-derived. True when either content field survivedcolumn
content_truncatedboolean, not null, defaults to falseNoServer-derived. True when the SDK truncated, or the server didcolumn
StageCapMarkerConfigurable
In the SDK, before transmission1 MiB per field by default\n<metergraph:truncated>Yes, METERGRAPH_TEXT_MAX_BYTES, floor of 1 and no ceiling
At ingest, before storage100 KiB per field\n<metergraph:truncated>No

So up to 1 MiB per field can leave your process, and 100 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, 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.

FieldTypeNullableSourceHostedSelf-hosted
tool_callsJSON arrayYesOne 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 offeredcolumn
tool_namesJSON array of stringsYesDeduplicated tool names, in first-seen order. Content-free, so this survives even with content capture offcolumncolumn
FieldTypeNullableSourceHostedSelf-hosted
sdktext, 512 charactersYespython from the Python SDK, js from the TypeScript SDKcolumncolumn
sdk_versiontext, 512 charactersYesThe SDK package versioncolumncolumn
runtimetext, 512 charactersYescpython-3.12.4 style from Python, node-22.11.0 or edge from TypeScriptcolumn
runtime_versiontext, 512 charactersYesNot set by either SDK. Reserved for importerscolumn

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

FieldTypeNullableHostedSelf-hosted
import_sourcetextYescolumn
import_source_scopetextYescolumn
import_event_idtextYescolumn

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.

FieldTypeNotes
event_typestringAlways outcome
event_idstring, 128 charactersDefaults to a UUID4
tsISO-8601 stringUTC
routestring, 512 charactersRequired, non-empty
session_idstring, 512 charactersRequired, non-empty
modelstring, 512 charactersRequired, non-empty
task_completedbooleanRequired
feedback_scorefloat or null-1 to 1
turns_to_resolutioninteger or null1 to 1000000
escalatedboolean or null
abandonedboolean or null
edit_distance_ratiofloat or null0 to 1
regeneration_countinteger or null0 to 1000000

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