OpenTelemetry GenAI semantic conventions are a vendor-neutral specification for attributing and structuring telemetry emitted by generative AI workloads, defining standard span names, attribute keys, and event shapes for operations like chat, embedding, and tool calls. They let you instrument an LLM application once and produce consistent traces and metrics regardless of whether you call OpenAI, Anthropic, a local model, or a routing gateway.
What the conventions actually specify
The OpenTelemetry GenAI semantic conventions live in the semantic-conventions repository under the genai namespace. They are not a library or agent; they are a contract. The contract covers three layers:
- Span identity: required attributes
genai.system(the provider or framework) andgenai.operation.name(e.g.,chat,text_completion,embedding). Span names follow a template such aschat {genai.request.model}so they are human-readable in a trace waterfall. - Request and response context: model identifiers, temperature, max tokens, response ID, and finish reasons.
- Token accounting:
genai.usage.input_tokens,genai.usage.output_tokens, andgenai.usage.total_tokens. These are the fields every cost dashboard needs.
The conventions are still marked experimental. Attribute names can shift between releases. Treat them as a moving target that is stabilizing, not a frozen standard.
Attributes you will use daily
{
"genai.system": "openai",
"genai.operation.name": "chat",
"genai.request.model": "gpt-4o",
"genai.request.max_tokens": 512,
"genai.response.model": "gpt-4o-2024-05-13",
"genai.usage.input_tokens": 128,
"genai.usage.output_tokens": 47
}
Note the split between request.model and response.model. Providers often return a dated snapshot (e.g., gpt-4o-2024-05-13). Recording both avoids ambiguity when reconciling billing.
How LLM calls become spans
A typical chat completion is modeled as a single CLIENT span. The span represents the round trip to the model provider. Nested spans for retrieval, tool execution, or prompt templating sit underneath it, exactly like any distributed trace.
The conversation content is not dumped into span attributes. Large payloads would blow up trace storage. Instead, the convention moves prompts and completions into span events with structured roles. This keeps the span attribute map small while preserving the full exchange for debugging.
{
"name": "chat gpt-4o",
"kind": "CLIENT",
"attributes": {
"genai.system": "openai",
"genai.operation.name": "chat",
"genai.request.model": "gpt-4o",
"genai.usage.input_tokens": 12,
"genai.usage.output_tokens": 34
},
"events": [
{
"name": "genai.user.message",
"attributes": { "role": "user", "content": "What is a span?" }
},
{
"name": "genai.choice",
"attributes": { "index": 0, "finish_reason": "stop", "content": "A span represents a unit of work." }
}
]
}
For agentic flows, genai.operation.name extends to tool_call and agent.run. The same token usage attributes apply, so a multi-step agent loop aggregates naturally into a token budget view.
Span hierarchy in an agent loop
agent.run (genai.operation.name=agent.run)
├── chat (genai.operation.name=chat, genai.system=openai)
├── tool_call (genai.operation.name=tool_call, genai.tool.name=search)
└── chat (genai.operation.name=chat, genai.system=anthropic)
Standard operation names let you filter or rank latency by genai.system without custom tagging on every node.
Why standardized tracing matters
Without a common schema, every LLM SDK emits its own telemetry. One logs prompt_tokens, another inputTokens, a third usage.prompt_tokens. You end up writing per-vendor parsers in your observability pipeline. The OpenTelemetry GenAI semantic conventions collapse that diversity into one set of keys.
Cross-provider correlation. If your app falls back from Anthropic to OpenAI mid-conversation, the trace still shows two chat spans with different genai.system values. No dashboard rewrite required.
Cost attribution. Token counts are first-class. When you pipe traces into a metrics backend, genai.usage.total_tokens becomes a single counter irrespective of source. A gateway like n4n.ai exposes an OpenAI-compatible endpoint across 240+ models and meters tokens at the edge; mirroring those counts in your app spans via these conventions closes the loop between billed usage and observed usage.
Debugging agent loops. Tool calls and multi-turn expansions generate deep span trees. Standard operation names let you filter for tool_call spans or rank latency by genai.system without custom tagging.
Future-proofing. Instrument once against the convention; switch model vendors or add a local inference server without touching your tracing code.
Concrete instrumentation example
Below is a minimal Python snippet using the OpenTelemetry SDK directly. It does not rely on a auto-instrumentation package, which makes the convention explicit.
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import ConsoleSpanExporter, SimpleSpanProcessor
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("my.llm.service")
def chat(prompt: str) -> str:
with tracer.start_as_current_span("chat gpt-4o") as span:
span.set_attribute("genai.system", "openai")
span.set_attribute("genai.operation.name", "chat")
span.set_attribute("genai.request.model", "gpt-4o")
span.set_attribute("genai.request.temperature", 0.2)
span.add_event("genai.user.message", {"role": "user", "content": prompt})
# Simulate provider call
completion = "A span is a timed operation in a trace."
span.set_attribute("genai.response.model", "gpt-4o-2024-05-13")
span.set_attribute("genai.usage.input_tokens", 10)
span.set_attribute("genai.usage.output_tokens", 15)
span.add_event("genai.choice", {"index": 0, "finish_reason": "stop", "content": completion})
return completion
chat("Define a span")
The span name embeds the model as the convention suggests. Attributes use the exact genai.* keys. If you later swap the body for a real openai or anthropic client, the surrounding telemetry stays identical.
TypeScript variant
import { trace } from "@opentelemetry/api";
const tracer = trace.getTracer("my-node-llm");
function chat(model: string, prompt: string) {
const span = tracer.startSpan(`chat ${model}`);
span.setAttribute("genai.system", "anthropic");
span.setAttribute("genai.operation.name", "chat");
span.setAttribute("genai.request.model", model);
span.addEvent("genai.user.message", { role: "user", content: prompt });
// ... call Claude ...
span.setAttribute("genai.usage.input_tokens", 22);
span.setAttribute("genai.usage.output_tokens", 9);
span.end();
}
Common misconceptions
“The conventions give me a hosted LLM dashboard”
They do not. They are a schema, not a backend. You still need a collector, a storage backend (Jaeger, Tempo, Honeycomb), and a query layer.
“They are finished and stable”
As of this writing, the GenAI conventions are experimental. Attribute names like genai.prompt have already been deprecated in favor of events. Pin your instrumentation to a specific convention version and review release notes.
“They dictate how I call the model”
No. They sit after the fact, describing what happened. Your client code, retry logic, and fallback routing are untouched. They only care about the metadata you attach to the span.
“They cover every agent framework nuance”
Partial. Operation names for agent.run and tool_call exist, but state management, memory stores, and graph execution are not fully specified. Expect gaps if you build complex orchestration.
“I must use a specific vendor’s SDK”
Any OpenTelemetry SDK in any language can emit these attributes. The point is portability. Auto-instrumentors from Traceloop, LangChain, or OpenLLMetry merely set the same keys for you.
Versioning and evolution
The semantic-conventions project versions experimentally under 1.x with breaking changes allowed in the genai namespace until stabilization. Early drafts used gen_ai as a prefix; current spec consolidates to genai. If you see older telemetry, write a span processor to rename gen_ai.* to genai.* at ingestion.
# Example rename processor (simplified)
def on_start(span):
for k, v in list(span.attributes.items()):
if k.startswith("gen_ai."):
span.set_attribute(k.replace("gen_ai.", "genai."), v)
span.remove_attribute(k)
Track the changelog. When genai moves to stable, the attribute set will freeze and you can drop the shims.
Integrating with existing pipelines
OpenTelemetry Collector can keep or drop attributes via transform or attributes processors. Ensure your export pipeline does not strip genai.* under a default “drop unknown” policy.
processors:
attributes:
include:
match_type: regexp
attributes: ["genai.*"]
Once retained, a metrics pipeline can derive genai_token_total by summing genai.usage.total_tokens grouped by genai.system and genai.request.model. That single view works across every provider you use.
Why not just log?
Structured logs can carry the same fields, but they lack causality. A trace connects the retriever, the model call, and the post-processor with timing and context propagation. When a user complaint says “the answer was wrong and slow,” only a span tree tells you which tool call injected bad context and how many tokens the recovery attempt burned.
Adopting the conventions today
Start by adding the core attributes manually to your existing LLM call wrappers. If you use a framework, check whether its OpenTelemetry integration already emits genai.* keys; many have aligned to the draft.
When you route through a gateway, propagate trace context via W3C headers and let the gateway honor your routing directives. n4n.ai forwards provider cache-control hints and meters per-token usage, which complements—rather than replaces—application-level spans that follow the OpenTelemetry GenAI semantic conventions.
Finally, export to a backend that lets you build metrics from attributes. A simple Prometheus pipeline can sum genai.usage.total_tokens grouped by genai.system to produce a provider cost breakdown without writing new code per model.
The conventions will keep evolving. Adopt the stable core now, avoid logging full prompts in attributes, and treat token usage as mandatory. That gets you 90% of the value with none of the vendor lock-in.