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.
How to read the destination columns
Section titled “How to read the destination columns”| 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.
Identity and time
Section titled “Identity and time”| 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 | — |
Attribution
Section titled “Attribution”| 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 | — |
Provider and model
Section titled “Provider and model”| 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 | — |
Tokens
Section titled “Tokens”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 | 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 | — |
Latency and status
Section titled “Latency and status”| 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.
Trace and session
Section titled “Trace and session”| 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 |
Execution shape
Section titled “Execution shape”| 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 | — |
Content
Section titled “Content”| 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 any of the three content fields | 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 | — |
The two truncation stages
Section titled “The two truncation stages”| 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 | 100 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.
The opt-out path
Section titled “The opt-out path”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.
Credential scrubbing
Section titled “Credential scrubbing”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 |
SDK metadata
Section titled “SDK metadata”| 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 | — |
Import provenance
Section titled “Import provenance”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 | — |
Everything else
Section titled “Everything else”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.
Outcome rows
Section titled “Outcome rows”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 |
Retention
Section titled “Retention”Metadata is kept for 90 days by default. See Limits and allowances.