Agent traces without a price: auditing the OpenTelemetry GenAI semconv
An agent run now has a standard shape in OpenTelemetry: a workflow span over agent spans over plan, inference and tool spans. What it does not have is a price.
We cloned open-telemetry/semantic-conventions-genai on 2026-08-14, at HEAD 30182ac (2026-08-13), and read the model files and the reference reports rather than the blog posts about them. The conventions are further along than most people assume: five distinct agent span types, a full MCP tracing chapter, a conformance matrix against 28 real libraries. They are also missing something structural. There is no attribute anywhere in the registry for what an operation cost.
That gap matters more for an agent that pays per call than for a chatbot that bills a human monthly. This is an audit of what exists, what was added in the last two weeks, and where the telemetry layer stops being able to answer the question an autonomous agent actually asks: how much of my balance did that consume.
A repo of its own, four months old
Until May the GenAI conventions lived inside the main semantic-conventions repo. The commit ebe3d1f, "Prepare standalone GenAI semantic conventions repo", is dated 2026-05-04, and the GitHub API reports the repository created on 2026-05-05. History was carried over rather than restarted: commits before the split still reference main-repo pull request numbers, the last inherited one being the v1.41.0 release chore of 2026-04-27. Since the split there have been 198 commits.
The rest of the metadata is worth stating plainly, because the badges are the whole story. Apache-2.0. 249 stars, 77 forks, 175 open issues at the time of the clone. No releases — the tag list is empty. And in model/manifest.yaml:
# model/manifest.yaml
name: semantic-conventions-genai
schema_url: https://opentelemetry.io/schemas/gen-ai-dev/1.42.0-dev
stability: development
dependencies:
- schema_url: https://opentelemetry.io/schemas/1.44.0
registry_path: https://github.com/open-telemetry/[email protected][model]
stability: development, and a schema URL with -dev in it twice. Every single gen_ai.* and mcp.* attribute in the docs carries the blue Development badge. The stable badges you see in the generated tables — error.type, server.address, server.port, network.transport — all come from the core registry it depends on, pinned at v1.44.0. Nothing GenAI-specific has graduated.
gen_ai.workflow.duration became gen_ai.invoke_workflow.duration on 2026-08-04, and a fetch_response operation was added on 2026-07-31. Pin the version you instrument against.
The shape of an agent run
The registry defines 63 gen_ai.* attributes. The interesting structure is in the spans, and in particular in a distinction the repo made in April: invoke_agent was split into a client span and an internal span.
An invoke agent client span describes calling an agent that runs somewhere else — the documented examples are the OpenAI Assistants API and AWS Bedrock Agents. Span kind CLIENT. An invoke agent internal span describes an agent running in your own process — LangChain, CrewAI. Span kind INTERNAL. Same gen_ai.operation.name value, invoke_agent, different span kind, different attribute sets. That split is the first admission in the conventions that "agent" is both a remote resource you pay to call and a loop you run yourself.
Above it sits invoke_workflow, INTERNAL, named invoke_workflow {gen_ai.workflow.name}. The criteria were tightened on 2026-08-10: a workflow span is for a coordinated process over multiple agents or GenAI calls, it SHOULD NOT be reported for standalone agent invocations, and it SHOULD NOT be reported when the workflow is an internal implementation detail of another operation — an agent that spins up a runner to delegate to a sub-agent does not get one. Application-defined workflows do get one even when nested. The doc names concrete entry points per framework: Crew.kickoff(), LangGraph's invoke, ADK's Runner.run, OpenAI Agents' Runner.run with handoffs.
Below it, two more. plan, INTERNAL, "the decision phase where an agent formulates a strategy before executing it" — the LLM call that generates the plan is a child of the plan span, and the resulting tool spans are siblings under the same invoke_agent. And execute_tool, INTERNAL, named execute_tool {gen_ai.tool.name}. Inference spans keep the older shape: {gen_ai.operation.name} {gen_ai.request.model}, kind CLIENT.
The gen_ai.operation.name enum is where the scope of the effort becomes visible. Beyond chat, embeddings and text_completion, it now carries create_agent, invoke_agent, invoke_workflow, plan, execute_tool, retrieval, fetch_response, and eight memory operations: search_memory, create_memory, update_memory, upsert_memory, delete_memory, create_memory_store, delete_memory_store. Memory got first-class vocabulary before money did — which tracks with where the ecosystem has been putting its attention, as we covered in the memory architectures update.
Two operational details worth copying into any instrumentation you write. First, a set of attributes is marked sampling-relevant and SHOULD be set at span creation time: gen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, gen_ai.agent.name, server.address, server.port. Set them late and head sampling cannot see them. Second, gen_ai.agent.id is specified as the provider-assigned stable identifier — a Bedrock agent ARN, a GCP Agent Registry id — and the doc explicitly says it is NOT RECOMMENDED to record in-memory agent instance ids there, because they are transient.
MCP moved into the same repo
The MCP conventions live in this repo now, which means MCP calls and the agent making them share one vocabulary. The MCP chapter opens by rejecting both obvious alternatives: don't use the RPC conventions, don't lean on HTTP conventions, because multiple MCP requests can share one HTTP request and one MCP request can span several.
Context propagation is the part with teeth. Instrumentations SHOULD inject trace context into the MCP request's params._meta bag, not into the transport. MCP normally requires DNS-prefixed keys in _meta; SEP-414 carves out W3C keys explicitly, so they go in unprefixed:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "get-weather",
"_meta": {
"traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"tracestate": "rojo=00f067aa0ba902b7"
}
},
"id": 1
}
The consequence is stated directly in the spec: the MCP client span becomes the parent of the MCP server span regardless of transport, and the transport context — the HTTP request that happened to carry it — is recorded as a span link instead. That is the right call for a protocol whose core is now stateless, as the 2026-07-28 revision made it. Sessions become an attribute (mcp.session.id), not a structural assumption.
There are only four mcp.* attributes: mcp.method.name, mcp.protocol.version, mcp.resource.uri, mcp.session.id. Everything else is borrowed — gen_ai.tool.name, gen_ai.prompt.name, jsonrpc.request.id, rpc.response.status_code. Span names follow {mcp.method.name} {target} where target is the tool or prompt name; mcp.resource.uri is kept out of span names by default because of cardinality. And there is an explicit anti-duplication rule: if MCP instrumentation can reliably detect that outer GenAI instrumentation is already tracing the tool execution, it SHOULD NOT create a second span — it should decorate the existing one.
What actually gets emitted
The most useful thing in the repo is not the spec text. It is reference/: 28 scenarios, one per library, each exercising a real SDK against a deterministic mock server, with the captured telemetry validated by a conformance runner and the results committed as per-signal reports. This turns "the conventions support X" into a countable claim.
Counting from those reports: the inference span is emitted by 13 libraries, including openai, anthropic, litellm, aws-bedrock, vertexai and claude-agent-sdk. All 13 set gen_ai.usage.input_tokens and gen_ai.usage.output_tokens on the span. The internal invoke-agent span is emitted by 7 frameworks — agent-framework, autogen, crewai, google-adk, langchain, openai-agents, pydantic-ai. The workflow span by 4. The plan span by 2 (crewai, langchain). Memory spans by 2 (aws-bedrock-agentcore, google-adk).
Then the metrics column collapses. gen_ai.client.token.usage — the histogram in {token} units, with gen_ai.operation.name, gen_ai.provider.name and gen_ai.token.type (input or output) as required attributes — is emitted by exactly two of the 28 scenarios: agent-framework and anthropic. gen_ai.invoke_agent.tool_calls and gen_ai.invoke_agent.inference_calls, by one each (google-adk).
That asymmetry is the practical finding. Token counts are widely available as span attributes and almost nowhere as metrics. If your consumption accounting reads from the metrics pipeline, you are reading from an instrument that today's instrumentation libraries mostly do not write to. If it reads from spans, it depends on a sampling decision. Neither is a billing feed, which is the point we made in the observability craft post and which the conformance matrix now makes measurable.
The column that isn't there
Grep the entire model directory for cost, price, billing, payment or usd. You get one hit, and it is the word "costs" inside a prose note about storage. There is no monetary attribute in the GenAI semantic conventions.
This is not an oversight nobody noticed. Issue #287, "Add convention for operation costs", has been open since 2025-05-30. What changed is PR #443, opened 2026-08-09 and still open with 7 comments, which proposes the concrete schema:
// proposed in PR #443 — not merged
gen_ai.usage.cost.amount double // monetary cost of the operation
gen_ai.usage.cost.currency string // ISO 4217, required when amount is set
gen_ai.usage.cost.source enum // provider | pricing_table | estimate
// plus one histogram metric
gen_ai.client.operation.cost
The design notes are more interesting than the field list. The attributes go on the shared attributes.gen_ai.usage group, so they land on inference, embeddings, agent and workflow spans without duplication. Hard-coded pricing tables inside instrumentation are explicitly forbidden — the value either comes from the provider or from a user-supplied table. The source enum exists so dashboards never mix billed actuals with estimates in the same chart. And the type is a float, justified in the PR with a sentence worth quoting: "This is observability, not accounting."
A companion PR, #439 (opened 2026-08-07), adds budget governance to the invoke_agent span: gen_ai.agent.token_budget.limit and .consumed, gen_ai.agent.iteration_budget.limit and .consumed, plus a gen_ai.invoke_agent.token_budget.utilization histogram. Consumed tokens explicitly exclude sub-agent consumption, so the values stay summable across a tree. One PR tells you how close to the cap you are; the other tells you what the consumption cost.
Both are open. Neither is merged. As of today, an agent framework that wants to emit spend has no standard key to emit it under.
A float is not a settlement
"Observability, not accounting" is the correct scope decision for OpenTelemetry, and it is exactly why an agent that pays per call cannot treat the telemetry pipeline as its ledger.
Compare the shapes. In the x402 v2 specification — we re-cloned x402-foundation/x402 today at a1af647 to check — the amount in PaymentRequirements is a string in atomic token units, and the SettleResponse returned by the facilitator has required fields success, transaction (the blockchain transaction hash) and network (CAIP-2), with an optional amount carrying the actual settled amount, again in atomic units as a string.
Every design choice there is the opposite of #443, and correctly so. Integers in base units, because a float that loses a rounding at the sixth decimal is a rounding you cannot reconcile against USDC. Strings, because JSON numbers are IEEE-754 and 10000 atomic units must survive a round trip. A transaction hash, because the authoritative record is on a chain and not in a sampled span. The facilitator's verify and settle responses, which we walked through in the facilitator API deep dive, are the accounting; a span is the story of what happened around it.
So an agent operator ends up with two ledgers, and the useful discipline is to keep them separate and joined by an identifier rather than merged:
Settlement — authoritative, integer, on-chain
Atomic units, EIP-3009 authorization or the equivalent per network, a transaction hash, a CAIP-2 network id. This is what the balance is derived from. It never comes from a span, and it is never sampled away.
Telemetry — explanatory, float, sampled
Token counts, latencies, model and provider names, tool and MCP call trees, and — when #443 lands — an estimated cost with an explicit source. This is what answers "why did this run cost 3x the previous one".
The join key is the interesting engineering problem. A span already carries gen_ai.conversation.id, gen_ai.response.id and mcp.session.id. What it has no place for is the settlement identifier: the transaction hash, or the reservation id that a gateway issues before it proxies the call. Until the conventions have one, that link has to live in a custom attribute — and every operator will pick a different name for it, which is precisely the failure mode semantic conventions exist to prevent.
What it means for LLM4Agents
Three concrete consequences.
First, the gateway is in the rare position of being able to emit both ledgers from one place. An OpenAI-compatible endpoint that reserves, proxies and settles — the flow we documented in the billing internals post — already knows the model, the provider it routed to, the token counts returned upstream, and the settled amount. Emitting gen_ai.* spans from that path costs almost nothing incremental and makes the platform legible to any OTel backend a customer already runs. The alternative, a private telemetry format, guarantees a migration later and interoperates with nothing in the meantime.
Second, model routing and fallback are only debuggable with these attributes set honestly. The convention says gen_ai.provider.name is a discriminator for the telemetry flavor and MAY differ from the actual upstream provider when a proxy is involved, while gen_ai.request.model is what was asked for and gen_ai.response.model is what answered. For a gateway with fallback chains, that trio is the whole audit trail of a downgrade. Setting them loosely turns a fallback into an invisible event.
Third, the cost gap is an opportunity, not just a hole. LLM4Agents settles in stablecoins against an on-chain authorization, so it has something almost no participant in that PR thread has: a source value stronger than provider. Not an estimate, not a pricing table — a settled amount with a transaction hash. If the conventions land gen_ai.usage.cost.* as a float for estimates, the natural complement is a settlement-grade sibling, and the operator with real settlement data is the one who should propose it.
Staying on the frontier
In order, from cheapest to most ambitious.
Start by emitting the spans that already have consensus, pinned to a version. Inference spans with gen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, gen_ai.response.model, gen_ai.usage.input_tokens and gen_ai.usage.output_tokens, plus the sampling-relevant attributes set at creation time. Pin to the schema URL in manifest.yaml and treat renames as a scheduled maintenance item, not a surprise — two landed in the last three weeks.
Then emit gen_ai.client.token.usage as a metric, not only as a span attribute. Only two of 28 reference scenarios do this today. It is a histogram with three required attributes, it survives sampling, and it is the instrument every OTel backend will chart by default. This is the cheapest way to be ahead of the ecosystem rather than behind it.
Third, instrument the MCP surface with params._meta propagation. A gateway that exposes MCP tools and also serves inference is the exact place where a trace either connects or breaks; injecting and extracting there means a customer's agent trace reaches into the platform and back out without a hole in the middle. Honor the anti-duplication rule so tool executions are not double-counted.
Fourth, define the join key now and publish it. A single attribute carrying the settlement reference — reservation id on the request span, transaction hash on the settled one — makes the two ledgers reconcilable. Use a vendor-prefixed name, document it, and be ready to migrate to a standard key if one appears.
Fifth, engage upstream. PRs #443 and #439 are open with active review. A participant who can say "here is what a settled, non-estimated cost looks like on the wire, in atomic units, with a transaction hash" is contributing something the thread currently lacks. The window to shape a convention closes when it stabilizes, and this one has not shipped a single release yet.
The conventions will stabilize on someone's model of what an agent run is. Right now that model has memory operations, planning phases and workflow trees — and no concept of a balance going down. For agents that pay per call, that is not a detail. It is the missing half of the trace.
Run agents on a gateway that knows what each call cost
OpenAI-compatible endpoint, stablecoin settlement per request, and usage you can reconcile against a transaction hash.
Register your agent