Span attributes
Send recognized OpenTelemetry attributes for models, tools, agents, users, and content.
The ingest API reads OpenTelemetry span attributes into searchable fields, token totals, input and output content, and cost. This page lists every attribute it recognizes and the order it checks them in.
A minimal model span
These attributes are enough for a model call with token totals and a server-calculated cost:
{
"gen_ai.operation.name": "chat",
"gen_ai.provider.name": "openai",
"gen_ai.request.model": "gpt-4o",
"gen_ai.usage.input_tokens": 1200,
"gen_ai.usage.output_tokens": 350
}
Add gen_ai.input.messages and gen_ai.output.messages only if you want content capture. Add gen_ai.usage.cache_read.input_tokens and gen_ai.usage.cache_creation.input_tokens if the provider reports prompt caching.
Every attribute is also kept in the span’s stored attributes, subject to capture settings and redaction. The exception is a content attribute that was moved into the input or output field; see Input and output content.
How aliases are resolved
Many fields have several recognized attributes, listed in priority order on this page. For each field the ingest API takes the first attribute that is present and valid, and skips the rest.
- A string field skips empty strings.
- A token field skips values that are not valid token counts; see Token usage.
- A cost field skips values that are not valid costs; see Client cost.
The first-party SDKs and the standard gen_ai.* names come first in each list. Compatibility names from other frameworks follow.
Operations
gen_ai.operation.name sets the span’s operation. The ingest API stores the value as sent. The operation decides which content attributes are read:
| Value | Meaning | Content read from |
|---|---|---|
chat |
Chat generation | Model attributes |
text_completion |
Text completion | Model attributes |
generate_content |
Content generation | Model attributes |
stream |
Streaming generation | Model attributes |
embeddings |
Embedding | Model attributes |
rerank |
Reranking | Model attributes |
evaluate |
Evaluation | Model attributes, plus the ai.evaluation.* attributes |
create_agent |
Agent creation | Model attributes |
invoke_agent |
Agent invocation | Model attributes |
invoke_workflow |
Workflow invocation | Model attributes |
llm |
Generic model call | Model attributes |
execute_tool |
Tool execution | Tool attributes |
function |
Plain instrumented function | gen_ai.input.messages and gen_ai.output.messages only |
Any other value is stored, but no content is extracted for it.
The first-party SDKs always send this attribute: chat for generations, execute_tool for tools, invoke_agent for agents, embeddings for embeddings, and function for plain spans. Set it yourself when you use your own OpenTelemetry exporter.
Derived operations
If gen_ai.operation.name is absent, the ingest API applies these rules in order and uses the first that matches:
| Order | Rule | Operation |
|---|---|---|
| 1 | An attribute starts with gen_ai.tool. |
execute_tool |
| 2 | An attribute is ai.toolCall or starts with ai.toolCall., or the span name is ai.toolCall or starts with ai.toolCall. |
execute_tool |
| 3 | mcp.method.name is tools/call |
execute_tool |
| 4 | mcp.method.name has any other value |
mcp |
| 5 | The span name is ai.eve.turn, or starts with ai.eve.turn and the span has eve.session.id |
invoke_agent |
| 6 | An attribute starts with gen_ai. |
llm |
| 7 | An attribute is db.system or starts with db.system., or the span name contains retriev in any letter case |
retrieval |
| 8 | The span name is ai.streamText or starts with ai.streamText. |
stream |
| 9 | The span name is ai.embed or ai.embedMany, or starts with either followed by . |
embeddings |
| 10 | The span name is ai.generateText or ai.generateObject, or starts with either followed by . |
llm |
| 11 | The span kind is CLIENT and an attribute starts with http. |
http |
| 12 | No rule matches | No operation |
Rule 6 is broad: any gen_ai.* attribute, even a propagated gen_ai.conversation.id, makes a span an llm call. This is why the first-party SDKs send function explicitly for plain spans.
A few integrations have their own rules:
- OpenClaw
openclaw.model.callspans becomellm, andopenclaw.model.usageaccounting spans get no operation. - Claude Code
claude_code.llm_requestspans becomellm. - A Claude Platform or Claude Code span without
gen_ai.operation.namethat would be derived asllmbut has no token usage gets no operation. These spans always carrygen_ai.provider.name, so rule 6 would otherwise count sessions, hooks, and similar spans as model calls.
Model, provider, and response
| Field | Attributes, in priority order | Notes |
|---|---|---|
| Model | gen_ai.request.model, gen_ai.response.model, ai.model.id, llm.model_name |
Used for the cost lookup |
| Provider | gen_ai.provider.name, gen_ai.system, ai.model.provider, llm.system |
Stored as sent; normalized only for the cost lookup |
| Response model | gen_ai.response.model, ai.response.model |
|
| Response ID | gen_ai.response.id |
|
| Finish reason | gen_ai.response.finish_reasons, ai.response.finishReason, gen_ai.response.finish_reason, anthropic.message.stop_reason, anthropic.session.turn.stop_reason |
A string or an array of strings. From an array, the first nonempty string is stored. |
| Output type | gen_ai.output.type |
The first-party SDKs send gen_ai.response.finish_reasons as an array with one value.
Token usage
Send token counts as integers. Each token field uses the first valid attribute in this order:
| Token field | Attributes, in priority order |
|---|---|
| Input | gen_ai.usage.input_tokens, ai.usage.inputTokens, ai.usage.promptTokens, gen_ai.usage.prompt_tokens, llm.token_count.prompt |
| Output | gen_ai.usage.output_tokens, ai.usage.outputTokens, ai.usage.completionTokens, gen_ai.usage.completion_tokens, llm.token_count.completion |
| Total (fallback only) | gen_ai.usage.total_tokens, ai.usage.tokens |
| Cache read | gen_ai.usage.cache_read.input_tokens, ai.usage.cachedInputTokens, gen_ai.usage.cache_read_input_tokens, gen_ai.usage.cached_input_tokens |
| Cache creation | gen_ai.usage.cache_creation.input_tokens, gen_ai.usage.cache_write.input_tokens, ai.usage.cacheCreationInputTokens, gen_ai.usage.cache_creation_input_tokens |
| One-hour cache creation | anthropic.usage.cache_creation.ephemeral_1h_input_tokens (the part of cache creation written to the one-hour cache) |
| Reasoning output | gen_ai.usage.reasoning.output_tokens, gen_ai.usage.reasoning_tokens, ai.usage.reasoningTokens |
Input is the total input, including cache reads and cache creation. Output includes reasoning tokens. The cost formula depends on this.
Valid token counts
A token count is valid when it is a whole number from 0 to 253 − 1 (9,007,199,254,740,991). It can be an integer value, a double with no fractional part, or a string of decimal digits only, such as "1200". Negative, fractional, and larger values, and strings such as "1e3" or "12.0", are skipped, and the next alias is tried.
The ingest API records the diagnostic unrecognized_usage_attribute when a span has a numeric gen_ai.usage.*, ai.usage.*, or llm.token_count.* token attribute it does not recognize and the matching token field stays empty. It does the same for a near-miss cost name, such as gen_ai.usage.cost_usd or td.cost, when no cost is stored. It records inconsistent_usage_counts when cache read plus cache creation is larger than input.
Fallbacks for missing totals
When a span has no input or output attribute from the table, the ingest API fills the field this way:
| Missing field | Filled from |
|---|---|
Input, on an embeddings or rerank span with no output or zero output |
Total |
| Input, otherwise | Cache read plus cache creation, if either is present; else total, if the span reports no output |
| Output | Reasoning output |
When a span reports modality counts for a direction but no aggregate attribute, these fallbacks are not used. Token totals and cost use the sum of the modality counts instead.
Modality token usage
Providers that bill text, image, and audio tokens at different rates report usage per modality. The ingest API recognizes one attribute for each modality and token kind:
| Modality | Input | Output | Cache read |
|---|---|---|---|
| Text | gen_ai.usage.text.input_tokens |
gen_ai.usage.text.output_tokens |
gen_ai.usage.text.cache_read.input_tokens |
| Image | gen_ai.usage.image.input_tokens |
gen_ai.usage.image.output_tokens |
gen_ai.usage.image.cache_read.input_tokens |
| Audio | gen_ai.usage.audio.input_tokens |
gen_ai.usage.audio.output_tokens |
gen_ai.usage.audio.cache_read.input_tokens |
For example, a call with 900 text and 300 image input tokens, where 100 of the text tokens were read from cache, sends:
{
"gen_ai.usage.input_tokens": 1200,
"gen_ai.usage.cache_read.input_tokens": 100,
"gen_ai.usage.text.input_tokens": 900,
"gen_ai.usage.text.cache_read.input_tokens": 100,
"gen_ai.usage.image.input_tokens": 300,
"gen_ai.usage.image.cache_read.input_tokens": 0,
"gen_ai.usage.output_tokens": 80,
"gen_ai.usage.text.output_tokens": 80
}
Modality counts are parts of the aggregate counts, not additions to them:
- Send
gen_ai.usage.input_tokensandgen_ai.usage.output_tokensas the totals, and keep each modality sum at or below its aggregate. - A modality’s input count includes its own cache reads.
- When the aggregate cache-read count is above zero, send each modality’s cache-read count, even when it is
0. Without it, that modality’s input is priced at the base input rate; see Modality pricing.
When an aggregate is absent, token totals and cost use the sum of the modality counts that are present.
The first-party SDKs accept modality fields, and several provider integrations set them when the provider reports modality detail. See Cost for how modality counts change the calculation.
Cost attributes
A valid client cost replaces the server calculation. The ingest API checks these attributes in order: gen_ai.usage.cost, ai.usage.cost, td.cost_usd, telemetry.dev.cost_usd. For valid values and the calculation itself, see Cost calculation.
Streaming latency
| Field | Attributes, in priority order |
|---|---|
| Time to first chunk | gen_ai.client.operation.time_to_first_chunk, gen_ai.response.time_to_first_chunk, gen_ai.response.time_to_first_token, gen_ai.server.time_to_first_token, llm.time_to_first_token, llm.ttft, ai.response.msToFirstChunk, ai.stream.firstChunkMs |
| Time per output chunk | gen_ai.client.operation.time_per_output_chunk, gen_ai.response.time_per_output_chunk, gen_ai.server.time_per_output_token, llm.time_per_output_chunk, llm.time_per_output_token |
The unit comes from the attribute name. If the key, in lowercase, contains ms, the value is milliseconds. Otherwise it is seconds and the ingest API multiplies it by 1,000. The result is rounded to the nearest millisecond. For example, gen_ai.response.time_to_first_chunk = 0.25 is stored as 250 ms, and ai.response.msToFirstChunk = 250 is also 250 ms.
Time to first chunk is stored as a span field. Both values are also added to the stored attributes as telemetry.dev.streaming.time_to_first_chunk_ms and telemetry.dev.streaming.time_per_output_chunk_ms.
The first-party SDK property timeToFirstChunkMs (time_to_first_chunk_ms in Python) is sent as gen_ai.response.time_to_first_chunk in seconds.
Errors
A span is stored with status error if its OpenTelemetry status is ERROR or it has an error.type attribute. Set the status, and add error.type for error classification and trace error totals.
An exception or gen_ai.client.operation.exception span event adds exception.type, exception.message, and exception.stacktrace to the span’s error details, and is also stored as an error log record.
Tools and agents
| Attribute | Stored as |
|---|---|
gen_ai.tool.name |
Tool name field |
gen_ai.agent.name |
Agent name field |
gen_ai.tool.call.arguments |
Tool input content; see Tool operations |
gen_ai.tool.call.result |
Tool output content |
gen_ai.tool.call.id |
Attribute only |
gen_ai.tool.description |
Attribute only |
gen_ai.agent.id |
Attribute only |
“Attribute only” means the value is kept in the span’s stored attributes but has no field of its own.
User, session, and metadata
| Field | Attributes, in priority order |
|---|---|
| User ID | user.id, ai.telemetry.metadata.userId, ai.telemetry.metadata.user_id, ai.settings.context.user.id, ai.settings.context.userId, ai.settings.context.user_id, td.user_id, td.metadata.userId, td.metadata.user_id |
| Session ID | anthropic.session.id, gen_ai.conversation.id, session.id, ai.telemetry.metadata.sessionId, ai.telemetry.metadata.session_id, eve.session.id, ai.settings.context.eve.session.id, ai.settings.context.sessionId, ai.settings.context.session_id, td.session_id, td.metadata.sessionId, td.metadata.session_id |
| Metadata | Every attribute that starts with td.metadata., ai.telemetry.metadata., or ai.settings.context., with the prefix removed |
For user and session IDs, the ingest API checks the whole list on the span first, then on the resource. A span’s td.user_id therefore wins over a resource’s user.id. If redaction removes the value it finds, the span contributes no ID, and another span in the trace can still supply one.
Metadata is read from both the resource and the span; a span key overrides a resource key with the same name. These keys are not added to metadata because they are user or session IDs:
- the suffixes
userId,user_id,sessionId, andsession_idunder any metadata prefix user.idandeve.session.idunderai.settings.context.
The first-party SDKs send user.id and gen_ai.conversation.id on every span and log in a propagated scope, and send per-span metadata as td.metadata.<key>.
Input and output content
The span’s operation selects which attributes become its input and output content. For string values marked “parsed” below, the ingest API parses the string as JSON when it starts with { or [ after leading whitespace. If parsing fails, it stores the raw string and records the diagnostic extraction_failed.
Model operations
For the model operations in the operations table:
| Direction | Attributes, in priority order |
|---|---|
| Input | gen_ai.input.messages (parsed), gen_ai.prompt.N.*, gen_ai.prompt, ai.prompt.messages (parsed), ai.prompt (parsed), langchain.inputs (parsed), traceloop.entity.input (parsed), llm.prompts (parsed) |
| Output | gen_ai.output.messages (parsed), gen_ai.completion.N.*, gen_ai.completion, ai.response.text, ai.response.object (parsed), ai.response.toolCalls (parsed), langchain.outputs (parsed), traceloop.entity.output (parsed), llm.completions (parsed), anthropic.session.outcome_evaluation.explanation, then response metadata |
gen_ai.prompt.N.*andgen_ai.completion.N.*are indexed attributes, such asgen_ai.prompt.0.roleandgen_ai.prompt.0.content. The ingest API rebuilds them into a list of messages ordered byN.gen_ai.system_instructions(parsed) is added as asystemmessage before the input.- If no output attribute is present, the output is one
assistantmessage built fromgen_ai.response.id,gen_ai.response.finish_reasons, andgen_ai.output.type, when any of them is set. - For an
evaluatespan,ai.evaluation.stateandai.evaluation.questionsare read as input aftergen_ai.input.messages, andai.evaluation.answersas output aftergen_ai.output.messages.
Tool operations
For execute_tool spans:
| Direction | Attributes, in priority order |
|---|---|
| Input | gen_ai.tool.call.arguments (parsed), ai.toolCall.args (parsed) |
| Output | gen_ai.tool.call.result (parsed), anthropic.tool_result.content (parsed), ai.toolCall.result (parsed) |
Function operations
For function spans, only gen_ai.input.messages and gen_ai.output.messages (both parsed) become content.
What is stored
Content passes through the project’s capture settings and redaction before it is stored. When an attribute’s content is stored in the input or output field, the ingest API removes that attribute from the stored attributes so that the content is not stored twice. If capture is off or redaction removes all of the content, the attribute is not removed this way. The stored attributes then go through the same capture and redaction rules, which can remove or redact it. For details, see Privacy.
Request parameters
The first-party SDKs send these request attributes. The ingest API keeps them in the stored attributes; they have no separate fields.
| Attribute | Value type |
|---|---|
gen_ai.request.temperature |
Number |
gen_ai.request.top_p |
Number |
gen_ai.request.top_k |
Number |
gen_ai.request.max_tokens |
Number |
gen_ai.request.stop_sequences |
String array |
gen_ai.request.seed |
Number |
gen_ai.request.frequency_penalty |
Number |
gen_ai.request.presence_penalty |
Number |
Framework detection
Each span gets a framework from its instrumentation scope name and attributes. The ingest API compares the scope name in lowercase, except for the Claude scopes, and uses the first rule that matches:
| Order | Rule | Framework |
|---|---|---|
| 1 | Scope com.anthropic.claude-platform, com.anthropic.sdk, or a scope that starts with com.anthropic.sdk. |
claude |
| 2 | Scope ai or @telemetry-dev/ai-sdk, or a scope that starts with @ai-sdk |
vercel-ai-sdk |
| 3 | Scope @telemetry-dev/tanstack-ai or @tanstack/ai, or a scope that starts with @tanstack/ai/ |
tanstack-ai |
| 4 | Any span attribute starts with ai. |
vercel-ai-sdk |
| 5 | The scope name contains langchain |
langchain |
| 6 | No rule matches | otel |
A trace takes the first framework other than otel that the ingest API sees among its spans, or otel if there is none.
The first-party core SDKs use exact scope names:
| SDK | Instrumentation scope | Framework |
|---|---|---|
| TypeScript | @telemetry-dev/sdk |
otel |
| Python | telemetry_dev |
otel |
Compatibility attributes by framework
The tables above already include these attributes in priority order. This section groups them by the framework that sends them, so you can check what the ingest API reads from a given instrumentation.
Vercel AI SDK
| Purpose | Attributes |
|---|---|
| Input tokens | ai.usage.inputTokens, ai.usage.promptTokens |
| Output tokens | ai.usage.outputTokens, ai.usage.completionTokens |
| Total tokens | ai.usage.tokens |
| Cache-read tokens | ai.usage.cachedInputTokens |
| Cache-creation tokens | ai.usage.cacheCreationInputTokens |
| Reasoning tokens | ai.usage.reasoningTokens |
| Cost | ai.usage.cost |
| Model | ai.model.id |
| Response model | ai.response.model |
| Provider | ai.model.provider |
| Finish reason | ai.response.finishReason |
| Time to first chunk, in ms | ai.response.msToFirstChunk, ai.stream.firstChunkMs |
| Input | ai.prompt.messages, ai.prompt |
| Output | ai.response.text, ai.response.object, ai.response.toolCalls |
| Tool input and output | ai.toolCall.args, ai.toolCall.result |
| User, session, and metadata | ai.telemetry.metadata.* and ai.settings.context.*; see User, session, and metadata |
OpenInference and Traceloop llm.*
| Purpose | Attributes |
|---|---|
| Input tokens | llm.token_count.prompt |
| Output tokens | llm.token_count.completion |
| Model | llm.model_name |
| Provider | llm.system |
| Time to first token, in seconds | llm.time_to_first_token, llm.ttft |
| Time per output chunk, in seconds | llm.time_per_output_chunk, llm.time_per_output_token |
| Input | llm.prompts |
| Output | llm.completions |
LangChain and Traceloop entities
| Purpose | Attributes |
|---|---|
| Input | langchain.inputs, traceloop.entity.input |
| Output | langchain.outputs, traceloop.entity.output |
Claude Platform and Claude Code
| Purpose | Attributes |
|---|---|
| Session ID | anthropic.session.id |
| Finish reason | anthropic.message.stop_reason, anthropic.session.turn.stop_reason |
| One-hour cache creation tokens | anthropic.usage.cache_creation.ephemeral_1h_input_tokens |
| Compaction tokens on SDK request spans | anthropic.usage.compaction.input_tokens, anthropic.usage.compaction.output_tokens |
| Tool output | anthropic.tool_result.content |
| Evaluation output | anthropic.session.outcome_evaluation.explanation |
Claude Platform telemetry can repeat one request’s usage on several spans. The ingest API counts each request’s tokens and cost once, and prefers the Claude Platform API’s request span over the client SDK’s span for the same request. For setup, see Claude Platform.
Claude Code claude_code.llm_request spans report usage in their own keys. The standard attributes above win when present:
| Claude Code key | Read as |
|---|---|
input_tokens |
Uncached input. The ingest API adds cache read and cache creation to get total input. |
output_tokens |
Output |
cache_read_tokens |
Cache read |
cache_creation_tokens |
Cache creation |
ttft_ms |
Time to first chunk, in ms |
model |
Model, when no model attribute is present |
speed |
fast turns on fast-mode pricing |
These spans get provider anthropic when no provider attribute is present. For setup, see Claude Code.