Semantic Conventions¶
TraceVerde follows OpenTelemetry semantic conventions for GenAI with additional custom attributes.
Span Attributes¶
GenAI Core (OTel Standard)¶
| Attribute | Type | Description |
|---|---|---|
gen_ai.system |
string | Provider name (e.g., "openai", "anthropic") |
gen_ai.request.model |
string | Requested model identifier |
gen_ai.response.model |
string | Actual model used in response |
gen_ai.request.type |
string | Call type ("chat", "embedding", "completion") |
gen_ai.usage.input_tokens |
int | Input token count |
gen_ai.usage.output_tokens |
int | Output token count |
gen_ai.usage.total_tokens |
int | Total token count |
gen_ai.usage.cache_read.input_tokens |
int | Prompt-cache hits (Anthropic cache_read_input_tokens, OpenAI prompt_tokens_details.cached_tokens). Included in gen_ai.usage.input_tokens. |
gen_ai.usage.cache_write.input_tokens |
int | Prompt-cache writes (Anthropic cache_creation_input_tokens). Current spelling: semantic-conventions-genai#440 renamed cache_creation to cache_write. |
gen_ai.usage.cache_creation.input_tokens |
int | Superseded spelling of the row above. Emitted alongside it only under OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai/dup (the default). |
gen_ai.usage.reasoning.output_tokens |
int | Reasoning tokens (OpenAI o1/o3-style completion_tokens_details.reasoning_tokens). Included in gen_ai.usage.output_tokens. |
server.address |
string | Host of the model endpoint, derived from the SDK client's base URL. Omitted when the SDK exposes no base URL -- absent means "endpoint unknown", never a guessed vendor host. |
server.port |
int | Port of the model endpoint. Explicit port when the base URL carries one, otherwise the scheme default. |
gen_ai.usage.{text,image,audio}.input_tokens |
int | Per-modality input tokens (semantic-conventions-genai#440). A subset of gen_ai.usage.input_tokens -- do not sum alongside it. Absent when the provider reports no breakdown. |
gen_ai.usage.{text,image,audio}.output_tokens |
int | Per-modality output tokens; subset of gen_ai.usage.output_tokens. |
gen_ai.usage.{text,image,audio}.cache_read.input_tokens |
int | Per-modality prompt-cache hits. |
gen_ai.response.finish_reasons |
string[] | Finish reasons as an array (registry spelling). |
gen_ai.response.finish_reason |
string | Superseded singular form, emitted alongside the array under gen_ai/dup. |
gen_ai.embeddings.dimension.count |
int | Requested embedding dimensions (registry spelling; supersedes gen_ai.request.dimensions). |
gen_ai.request.encoding_formats |
string[] | Requested encoding formats as an array (registry spelling; supersedes the singular gen_ai.request.encoding_format). |
gen_ai.request.seed |
int | Sampling seed, when the caller passed one. |
gen_ai.request.stream |
bool | Whether the caller requested streaming. |
gen_ai.request.top_k |
int | Top-k sampling parameter, when passed. |
gen_ai.request.choice.count |
int | Number of completions requested (n, or Google's candidate_count). |
gen_ai.output.type |
string | json or text, derived from the caller's response_format. |
| gen_ai.agent.token_budget | int | Cumulative token cap configured for the invocation (semantic-conventions-genai#425). |
| gen_ai.agent.token_budget.consumed | int | Tokens actually consumed across the invocation's inference calls. |
| gen_ai.agent.iteration_budget | int | Configured iteration / turn / recursion limit. |
| gen_ai.agent.iteration_budget.consumed | int | Inference calls completed during the invocation. |
| gen_ai.latency.time_in_queue | float | Scheduler queue wait, seconds (self-hosted engines). |
| gen_ai.latency.time_to_first_token | float | TTFT, seconds. |
| gen_ai.latency.e2e | float | End-to-end request latency, seconds. |
| gen_ai.latency.time_in_model_prefill | float | Prefill phase duration, seconds. |
| gen_ai.latency.time_in_model_decode | float | Decode phase duration, seconds. |
| gen_ai.latency.time_in_model_inference | float | Total on-engine inference duration, seconds. |
| gen_ai.request.id | string | Engine-assigned request id (vLLM / SGLang). |
Engine latency keys are the de-facto vocabulary, not ours
gen_ai.latency.* is emitted verbatim as vLLM and SGLang define it, and as
semantic-conventions-genai#408
proposes standardising. Running vLLM's own OTLP tracing alongside this
library therefore yields one vocabulary rather than two to join. These keys
are not yet in the registry.
Agent budget consumption and threads
Consumption is accumulated in a context-local frame, so it survives head
sampling that drops the child inference spans. A call made on a thread that
does not carry the context (a bare threading.Thread) is not counted --
consistent with tracing itself, which would also lose the parent/child span
relationship for such a call.
Request parameters are recorded only when the caller passes them
An absent gen_ai.request.* attribute means the application did not set that
parameter. The library does not materialise provider defaults, which would
report a top_k or seed the application never chose.
| gen_ai.usage.token_count_estimated | bool | true when prompt/completion token counts came from a fallback estimate (e.g. multimodal Ollama responses lacking prompt_eval_count, or HuggingFace vision/audio pipelines that don't surface usage). Absent on spans whose tokens come from the provider response. |
| gen_ai.usage.image_count | int | Number of input images counted by the instrumentor for multimodal calls (vision pipelines). |
| gen_ai.usage.audio_seconds | float | Total input audio duration in seconds for ASR / audio pipelines. |
| gen_ai.usage.audio_duration_seconds | float | Canonical total input audio duration in seconds for speech-to-text and audio pipelines. |
| gen_ai.operation.name | string | Operation being performed (chat, embeddings) |
Cost lives under gen_ai.usage.cost.*
Earlier revisions of this page listed gen_ai.cost.amount as the cost
attribute. Cost is reported under
gen_ai.usage.cost.total and its breakdown. Only the
Hyperbolic instrumentor also emits gen_ai.cost.amount, for backwards
compatibility with dashboards built against it; do not query it as a
general cost attribute, because no other provider sets it.
Embeddings¶
Emitted on embedding spans (openai.embeddings), which are the retrieval half
of a RAG trace.
| Attribute | Type | Description |
|---|---|---|
gen_ai.request.type |
string | embedding - the value cost lookup dispatches on |
gen_ai.request.input_count |
int | Number of texts in the request; a batch of chunks counts each one, pre-tokenised input counts as one |
gen_ai.request.dimensions |
int | Requested vector dimension, when the caller asked for one |
gen_ai.request.encoding_format |
string | Requested encoding format, when set |
gen_ai.response.embedding_count |
int | Number of vectors returned |
gen_ai.response.vector_size |
int | Dimension of the returned vectors |
embedding.model_name |
string | Embedding model; set only when content capture is enabled |
embedding.text |
string | The embedded text, truncated to GENAI_CONTENT_MAX_LENGTH; set only when content capture is enabled |
embedding.vector |
string | JSON-encoded vector; off unless capture_embedding_vectors is set, since vectors dominate span size |
Token usage and cost are recorded exactly as for chat, priced against the
embeddings table rather than the chat one.
Retrieval quality¶
The rag.* attributes are additive to retrieval.* and db.vector.*. They
describe embedding provenance, score quality, corpus version, and downstream
context policy. top_k and result count remain db.vector.top_k and
retrieval.document_count; see the retrieval quality guide.
gen_ai.rag.context is application-owned input for evaluation processors. The
library reads it when present but does not emit it automatically.
Degradation events¶
Capability downgrades are recorded as a gen_ai.degraded span event rather
than an error. The event attributes are gen_ai.degraded.component,
gen_ai.degraded.from, gen_ai.degraded.to, gen_ai.degraded.reason, and
gen_ai.degraded.recoverable. The span status remains OK when the request
successfully completes through the degraded path.
Cost Attributes¶
| Attribute | Type | Description |
|---|---|---|
gen_ai.usage.cost.total |
float | Total cost in USD |
gen_ai.usage.cost.prompt |
float | Prompt token cost |
gen_ai.usage.cost.completion |
float | Completion token cost |
gen_ai.usage.cost.decision |
float | Decision-model cost, on decision spans only. Equal to cost.total there, so a reader can keep decision spend apart without reading the span kind |
gen_ai.usage.cost.pricing_source |
string | Where the price came from: table, estimated or unpriced (decision spans: table or unpriced, never estimated) |
gen_ai.request.model.deprecated |
bool | Set only when the provider has announced the model's retirement |
gen_ai.request.model.deprecation_note |
string | Retirement date and migration target |
Read pricing_source before summing cost
A gen_ai.usage.cost.total of 0.0 is ambiguous on its own — it means
either "this call really was free" or "no price could be found for this
model". Always read pricing_source alongside it:
| Value | Meaning |
|---|---|
table |
Matched an entry in the shipped pricing file. Billable. |
estimated |
No entry; price inferred from the parameter count in the model name via the local-model size tier. Indicative only — do not treat as a billable figure. |
unpriced |
No price could be determined. cost.total will be 0.0 and must not be counted as free spend. |
A dashboard that sums cost.total without filtering on pricing_source
will silently under-report spend for every unpriced model.
Decision Spans¶
Decision-model calls are OpenInference DECISION spans
(openinference.span.kind = DECISION). The full attribute table, and which attributes
need content capture, is in Decision models.
| Attribute | Type | Recorded |
|---|---|---|
decision.system |
string | always: openai, typesafe or custom |
decision.provider |
string | recognised host, or GENAI_DECISION_PROVIDER |
decision.model_name, decision.request.model_name, decision.response.model_name |
string | always / when they differ |
decision.token_count.input, decision.token_count.output |
int | when reported |
genai.decision.protocol |
string | always: openai_decisions, systemone, granite_adapter, manual |
genai.decision.question_count, genai.decision.refusal_count |
int | always |
genai.decision.question.types |
string[] | always |
genai.decision.input.modalities |
string[] | always: text, image, audio |
genai.decision.input.audio_seconds |
double | WAV audio inputs |
genai.decision.option_count |
int | one-question calls |
genai.decision.question.names |
string[] | content capture on |
genai.decision.min_confidence |
double | content capture on |
genai.decision.choice |
string | content capture on, one-question calls |
genai.decision.confidence, .probability, .score, .margin |
double | content capture on, one-question calls |
evaluation.skipped_reason |
string | decision_span when the response detectors had no text to read |
Streaming Latency¶
Emitted only on streamed calls. On a non-streamed call these attributes are absent, not zero.
| Attribute | Type | Description |
|---|---|---|
gen_ai.server.time_to_first_token |
float | Seconds from request start to the first streamed chunk (TTFT) |
gen_ai.server.time_per_output_token |
float | Seconds per output token after the first (TPOT), (duration - ttft) / max(output_tokens - 1, 1) |
gen_ai.server.ttft |
float | Superseded spelling of TTFT, still emitted for consumers already reading it |
gen_ai.streaming.token_count |
int | Number of chunks yielded by the stream (chunks, not tokens) |
gen_ai.streaming.tpot_unavailable_reason |
string | Why TPOT was omitted; currently only output_token_count_unavailable |
An absent TTFT is not a zero
Span duration cannot substitute for TTFT: a fast-starting long answer and a
slow-starting short one can share a duration. But a fabricated 0 is worse
than nothing — it is indistinguishable from an instant first token and drags
down every average it enters.
So these attributes are omitted whenever they were not genuinely measured:
- Non-streamed calls carry neither attribute.
- TPOT needs a real output-token count. Some providers only send usage
in the final chunk when asked. With OpenAI, for example, pass
stream_options={"include_usage": True}; without it the span gets TTFT plusgen_ai.streaming.tpot_unavailable_reason, and no TPOT. The chunk count is not used as a stand-in — chunks are not tokens. - Text-to-speech streams report TTFT (time to first audio byte) and never TPOT, having no output tokens to divide by.
Report a missing attribute as "not measured" rather than substituting zero.
Agent Graph¶
Added to every span that carries gen_ai.agent.name, so OpenInference backends
(Arize AX, Phoenix) can draw the agent graph. Keys a span already carries are
kept. Disable with GENAI_ENABLE_AGENT_GRAPH_ATTRIBUTES=false.
| Attribute | Type | Description |
|---|---|---|
agent.name |
string | Agent name, copied from gen_ai.agent.name |
graph.node.id |
string | Graph node; the agent name, so repeat runs share one node |
graph.node.name |
string | Node label (OpenInference spelling) |
graph.node.display_name |
string | Node label (Arize AX spelling) |
graph.node.parent_id |
string | Node of the nearest enclosing agent; unset on a root agent |
openinference.span.kind |
string | AGENT |
LangGraph node spans (langgraph.node.<name>) carry:
| Attribute | Type | Description |
|---|---|---|
langgraph.node |
string | Node name |
langgraph.step |
int | Superstep the node ran in |
langgraph.triggers |
string[] | Channels that triggered the node |
langgraph.node.interrupted |
bool | Set when the node paused with interrupt() |
graph.node.id |
string | Node name (agent graph attributes enabled) |
metadata |
string | JSON {"langgraph_node", "langgraph_step"}, the OpenInference key Arize reads for LangGraph |
openinference.span.kind |
string | CHAIN (also on the graph run span, whose graph.node.id is the graph name) |
Session Tracking¶
| Attribute | Type | Description |
|---|---|---|
session.id |
string | Session identifier; groups a conversation's traces into one session |
gen_ai.conversation.id |
string | Same value as session.id (OTel GenAI spelling) |
user.id |
string | User identifier |
Every span gets these from the enclosing genai_otel.session() block, or else from
its parent span, so a session resolved on a framework span reaches every span
beneath it. A span that already carries a key keeps it. Disable with
GENAI_ENABLE_SESSION_PROPAGATION=false.
Between services the ids can travel as W3C baggage under the same keys, when the
caller sets GENAI_SESSION_BAGGAGE_KEYS and the callee GENAI_SESSION_BAGGAGE_ACCEPT.
Resource Attributes¶
Every resource attribute is a name from the OpenTelemetry registry. They identify where a span came from, which is what makes it possible to separate traffic per host and per instance when several copies of an application run at once.
| Attribute | Description |
|---|---|
service.name |
Service name |
service.instance.id |
Instance identifier. Taken from OTEL_SERVICE_INSTANCE_ID when set, otherwise generated - see Host and Instance Identity |
deployment.environment.name |
Environment name |
telemetry.distro.name |
"genai-otel-instrument" |
telemetry.distro.version |
Package version |
host.name |
Hostname of the machine |
host.arch |
CPU architecture |
host.ip |
Non-loopback IP addresses of the host (array) |
os.type / os.version |
Operating system |
process.pid / process.parent_pid |
Process identifiers |
process.command |
Program that was started |
process.command_line |
Full startup command line |
process.command_args |
Startup arguments (array) |
process.executable.name / process.executable.path |
Interpreter location |
process.owner |
User the process runs as (requires psutil) |
process.runtime.name / .version / .description |
Python runtime |
The host.*, os.* and process.* groups come from the OpenTelemetry SDK's
own detectors. This library enables host and os by default via
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS; process is opt-in (add it to that
variable). host.ip has no upstream detector and is resolved by the library
only when GENAI_OTEL_RESOURCE_HOST_IP=true.
Credential values in process.command_args are redacted
A value following a flag whose name looks like a credential
(--password, --api-key, --token, --client-secret, ...) is replaced
with ***REDACTED***, in both the --flag value and --flag=value forms,
as are name=value pairs and scheme://user:password@host URLs. This has
to happen in the SDK: process.command_args is an array, so a flag and its
value become separate elements and no downstream name=value rule can
reconnect them. Redaction is pattern-based and cannot catch a credential
passed under an unrecognised flag name - see
Host and Instance Identity
for how to drop the process detector entirely.
Superseded spellings, still emitted alongside the current names for the same reason the GenAI token attributes are dual-emitted, and removed at 2.0:
| Superseded | Current |
|---|---|
environment |
deployment.environment.name |
telemetry.auto.name |
telemetry.distro.name |
telemetry.auto.version |
telemetry.distro.version |
Metrics¶
GenAI Metrics¶
| Metric | Type | Unit | Description |
|---|---|---|---|
gen_ai.requests |
Counter | requests | Request count by provider/model |
gen_ai.client.token.usage |
Counter | tokens | Token usage, labelled token_type (prompt/completion). No longer defined by the conventions; see Token usage metrics |
gen_ai.client.operation.duration |
Histogram | seconds | Request latency |
gen_ai.cost |
Counter | USD | Estimated costs |
gen_ai.errors |
Counter | errors | Error count by type |
gen_ai.server.time_to_first_token |
Histogram | seconds | TTFT, recorded on streamed calls only |
gen_ai.server.time_per_output_token |
Histogram | seconds | TPOT, recorded when the output-token count is known |
gen_ai.server.ttft |
Histogram | seconds | Superseded spelling of TTFT |
gen_ai.server.tbt |
Histogram | seconds | Time between streamed chunks: one sample per stream, its mean gap. With GENAI_METRICS_PROFILE=full, one sample per gap |
Token Usage Metrics¶
semantic-conventions-genai#374
replaced the gen_ai.client.token.usage histogram and its gen_ai.token.type attribute with
these instruments. They are recorded for every inference call whose provider reported usage.
| Metric | Type | Unit | Records |
|---|---|---|---|
gen_ai.client.inference.usage.input_tokens |
Counter | {token} |
Input tokens, cached ones included |
gen_ai.client.inference.usage.output_tokens |
Counter | {token} |
Output tokens, reasoning ones included |
gen_ai.client.inference.usage.cache_read.input_tokens |
Counter | {token} |
Input tokens served from the provider's cache. Part of input_tokens |
gen_ai.client.inference.usage.cache_write.input_tokens |
Counter | {token} |
Input tokens written to the provider's cache. Part of input_tokens |
gen_ai.client.inference.usage.reasoning.output_tokens |
Counter | {token} |
Output tokens spent on reasoning. Part of output_tokens |
gen_ai.client.inference.operation.input_tokens |
Histogram | {token} |
One call's input tokens |
gen_ai.client.inference.operation.output_tokens |
Histogram | {token} |
One call's output tokens |
Attributes, each only when the span has it: gen_ai.operation.name, gen_ai.provider.name
(read from gen_ai.system when only that is set), gen_ai.request.model, gen_ai.response.model,
server.address, server.port. The counters also carry gen_ai.token.modality; the histograms do
not, because percentiles taken per modality do not add up to a call.
- Modality. Where the provider breaks usage down (
text,image,audio), each part is counted under its modality and whatever the breakdown leaves over is counted underunknown, so the modalities of one counter always add up to its total. Usage with no breakdown is allunknown. - Zero and missing counts record nothing on the counters, as
semantic-conventions-genai#602
proposes: a call with no cache activity adds no
cache_readpoint. The histograms record one value per call whenever usage was reported, zero included. - Agent runs. When a framework span (an agent, crew, graph or workflow run) is the one that accounts a run's tokens, they are added to the counters but not to the histograms: a run's total is not the size of any one model call.
- Cached input is counted as input. Anthropic reports
input_tokenswithout its cache reads and writes; the input counter adds them back so that the cache counters stay subsets of it. Thegen_ai.usage.input_tokensspan attribute is unchanged. - Not recorded: embeddings calls (the conventions are to give them their own instrument), token
counts this library estimated because the provider returned none (they stay on the span, marked
gen_ai.usage.token_count_estimated, and ongen_ai.client.token.usage), and failed calls.
gen_ai.client.token.usage is still recorded alongside these with its existing token_type
label, so dashboards built on it keep working. It is removed at 2.0.
GPU Metrics¶
See GPU Metrics Guide for the complete list.
Evaluation Metrics¶
See Evaluation Guide for detector-specific metrics.
Evaluation Runtime Attributes¶
Written on a span by the evaluation runtime, whichever detectors are enabled.
| Attribute | Type | Description |
|---|---|---|
evaluation.completed |
boolean | The detectors ran for this span |
evaluation.skipped |
string | Why they did not: overloaded (the queue was full) or shutdown (the process was exiting) |
evaluation.truncated |
boolean | Detectors read only part of the prompt or response |
evaluation.prompt.length |
int | Characters in the prompt, when it was cut |
evaluation.prompt.analyzed_length |
int | Characters of it the detectors read |
evaluation.response.length |
int | Characters in the response, when it was cut |
evaluation.response.analyzed_length |
int | Characters of it the detectors read |
evaluation.pii.prompt.pattern_analyzed_length |
int | Characters the PII patterns covered |
evaluation.pii.prompt.model_analyzed_length |
int | Characters the PII entity model read |
evaluation.<detector>.prompt.analyzed_length |
int | Characters read by prompt_injection, restricted_topics or bias when it has a block policy: the whole prompt |
Multimodal Content-Part Attributes (v1.0.0)¶
Emitted only when GENAI_OTEL_MEDIA_CAPTURE_MODE is set to reference_only or full.
Default behaviour (off) emits nothing additional.
| Attribute | Type | Notes |
|---|---|---|
gen_ai.prompt.{n}.role |
string | user, system, assistant, tool |
gen_ai.prompt.{n}.content.{m}.type |
enum | text | image | audio | video | document |
gen_ai.prompt.{n}.content.{m}.text |
string | text parts only |
gen_ai.prompt.{n}.content.{m}.media_uri |
string | URI returned by the configured store |
gen_ai.prompt.{n}.content.{m}.media_mime_type |
string | e.g. image/png |
gen_ai.prompt.{n}.content.{m}.media_byte_size |
int | size of the captured payload |
gen_ai.prompt.{n}.content.{m}.media_source |
enum | inline_offloaded | external_url | reference_only |
gen_ai.completion.{n}.* |
— | mirror namespace for generated content |
gen_ai.media.stripped_reason |
enum | size_exceeded, modality_not_allowed, redactor_error, upload_error |
Not yet part of upstream OTel GenAI semconv. See open-telemetry/semantic-conventions#3672 for the upstream contribution.