The bill that told me the per-seat pricing was a lie
In May I was asked to investigate a coding-agent bill that had nearly doubled month over month. The team's GitHub Copilot dashboard said $19 per seat per month, multiplied by nine engineers, equaled $171. The CFO's invoice said $11,400. Three weeks of pulling data later, the truth was not subtle: the per-seat dashboard reports the seat fee only. It does not report the premium-request markup, the burst overage, or — and this was the bulk of the gap — the fact that the team had started using Claude Code and Cursor in parallel and was paying both bills without realizing the workflows were 40% overlapping.
That is the moment LangChain's July 2 post by Amy Ru finally put a name on: "A tool call in Claude Code and a tool call in Cursor aren't recorded the same way, so you can't put them side by side and ask which one is doing more for the money. That fragmentation isn't noticeable right up until your team scales past one tool, which is almost immediately."
LangChain's answer is LangSmith, which now ships cross-tool session tracing for Claude Code, Codex, Cursor, GitHub Copilot Chat, Pi, and OpenCode. That is a real answer for teams already on the LangChain platform, and it is what LangChain is selling. It is not the only answer, and for the platform teams I work with — the ones running their own OpenTelemetry Collector, Tempo, and a Grafana cost dashboard — the answer is to normalize the traces ourselves. This article is how we did it, and the schema you can copy.
What "cross-tool" actually means: six tools, six telemetry surfaces
Before you can normalize, you need to know what each tool emits. As of July 2026 the surface area looks like this:
- Claude Code 1.0 GA (Anthropic, 2026-06-26): native OpenTelemetry hook support via
Settings > Hooks— emits OTel spans to a local collector on every tool call, withgen_ai.*semantic convention attributes populated by default. - Cursor (Anysphere): emits its own JSON-line analytics stream at
~/.cursor/analytics.log, plus hook support for editor-level events; no native OTel export, requires a thin file-tail exporter. - GitHub Copilot Chat (Microsoft): ships OTel spans out of the box at
/api/copilot/telemetrywithgen_ai.*attributes populated, but the export endpoint requires a GitHub org-level admin token and the spans lack tool-call sub-attributes. - Codex CLI (OpenAI): native OTel support via
--otel-endpointflag, attribute names follow the OpenInference convention (notgen_ai.*) — needs attribute-rename processor in the Collector pipeline. - OpenCode v0.4.0 (2026-07-02):
--analytics-configflag for OTel export, ships the most completegen_ai.*coverage of any open-source coding agent; also exposes session-level cost totals viaopencode.cost.session_total. - Pi (open-source agent runtime): custom Prometheus-format exporter on
:9101/metrics, requires a Prometheus scrape job; no OTel natively.
The first thing to notice: every tool except Claude Code and OpenCode requires an exporter or attribute-rename step to land in your existing OTel pipeline. The second thing to notice: every tool records the same logical event — "a coding-agent tool call consumed X tokens and cost $Y over Z seconds" — under a different attribute namespace. That is the fragmentation Amy Ru was naming, and it is what your normalization layer has to fix.
The normalized schema: one attribute namespace, six tools
The reference implementation below is what we run against a single OTel Collector that fans out to Tempo (for traces), Prometheus (for counters), and Loki (for prompt/response body samples). The schema is a superset of the OpenTelemetry GenAI semantic conventions (gen_ai.*) plus three additions for cross-tool session continuity:
# Normalized attributes — every coding-agent tool emits these
gen_ai.system: claude-code | cursor | github_copilot | codex_cli | opencode | pi
gen_ai.operation.name: chat | tool_call | completion | edit | search
gen_ai.request.model: claude-opus-4-1 | gpt-5 | cursor-fast | ...
gen_ai.usage.input_tokens: 1284
gen_ai.usage.output_tokens: 412
gen_ai.usage.cached_tokens: 980
gen_ai.cost.input_usd: 0.00642
gen_ai.cost.output_usd: 0.01236
gen_ai.cost.total_usd: 0.01878
# Cross-tool additions — StackPulsar reference schema
coding_agent.session.id: 9a1f2b3c-... # stable across tool switches within one user task
coding_agent.session.parent_tool: claude-code # which tool originated this task
coding_agent.tool_call.name: edit_file | search_code | run_command | ...
coding_agent.tool_call.duration_ms: 1240
coding_agent.user.id: spiffe://stackpulsar/agents/eng-platform/<name>
Three points worth highlighting:
coding_agent.session.idis the most important attribute. Without it you cannot attribute the overlapping 40% of Claude Code + Cursor work to a single user task — and that is exactly where the billing double-count hides.gen_ai.cost.*is not in the official GenAI semantic conventions yet. We compute it from a per-model price sheet at the Collector level using an attribute-processor, so the source tools do not need to populate it. This is the only way to get apples-to-apples cost numbers across tools that publish different price sheets (Claude Code in USD per token, Cursor in USD per request, Copilot in seat-fee + premium-request overage).coding_agent.tool_call.namenormalizes the wildly different tool names across the six tools. Cursor'scode_edit, Claude Code'sEdit, Codex'sfile_apply_patch, and OpenCode'sedit_fileall becomeedit_filein the normalized schema. Without this, per-tool-call frequency comparisons across tools are not meaningful.
The Collector pipeline that does the normalization
The actual configuration is short — five processors in the OTel Collector pipeline handle 90% of the work:
processors:
# 1. Codex OpenInference → gen_ai.* attribute rename
attributes/rename_codex:
include:
span.kind: client
actions:
- key: openinference.span.kind
action: insert
value: CHAT
- key: llm.system
action: upsert
from_attribute: openinference.llm.system
- key: gen_ai.usage.input_tokens
action: upsert
from_attribute: llm.token_count.prompt
- key: gen_ai.usage.output_tokens
action: upsert
from_attribute: llm.token_count.completion
# 2. Cursor JSON-line exporter → OTel resource attributes
resource/cursor:
resource:
coding_agent.source: cursor
# 3. Cost computation — per-model price sheet
transform/cost_compute:
trace_statements:
- context: span
statements:
- set(attributes["gen_ai.cost.input_usd"], attributes["gen_ai.usage.input_tokens"] * price_lookup(attributes["gen_ai.request.model"], "input"))
- set(attributes["gen_ai.cost.output_usd"], attributes["gen_ai.usage.output_tokens"] * price_lookup(attributes["gen_ai.request.model"], "output"))
- set(attributes["gen_ai.cost.total_usd"], attributes["gen_ai.cost.input_usd"] + attributes["gen_ai.cost.output_usd"])
# 4. Session continuity — propagate session.id across tool boundaries
groupbyattrs/session:
keys: [coding_agent.session.id]
join: true
# 5. Sample — drop tool_call spans with no semantic content
tail_sampling:
policies:
- name: keep-all-tool-calls
type: string_attribute
string_attribute:
key: coding_agent.tool_call.name
values: [edit_file, search_code, run_command, web_search, file_read]
- name: drop-empty-chat
type: string_attribute
string_attribute:
key: gen_ai.usage.output_tokens
values: ["0"]
invert_match: true
- name: probabilistic-5pct
type: probabilistic
probabilistic:
sampling_percentage: 5
The price sheet itself lives in a small SQLite database that the price_lookup function reads — about 80 lines of Go in a Collector extension. We update it weekly from each vendor's pricing page. If you do not want to write the extension, the same logic works as a Grafana transformation at query time, at the cost of a slower dashboard.
What the dashboard actually shows
Once the Collector pipeline is in place, the five-panel Grafana dashboard is straightforward. The panels that earn their place are these:
- Per-tool cost per session: a stacked bar chart with
coding_agent.session.idon the X axis andsum(gen_ai.cost.total_usd)on the Y axis, broken down bygen_ai.system. This is the panel that surfaces the overlapping-work double-count. On a healthy team, sessions that show two or three tools are 10-20% of total; on a team with hidden duplication they are 40-50%. - Cost per token by tool: a heatmap with
gen_ai.request.modelon the X axis andgen_ai.systemon the Y axis, color-coded bygen_ai.cost.total_usd / (gen_ai.usage.input_tokens + gen_ai.usage.output_tokens). The model Cursor Fast, for example, will look wildly different from claude-opus-4-1 on this panel — and from Cursor's own dashboard, because Cursor's dashboard does not surface what fraction of the cost was premium-request markup. - Tool-call frequency by session: histogram of
coding_agent.tool_call.nameper session. Surfaces the failure mode Amy Ru named: an agent that is making 200 tool calls per session is doing something wrong, and you cannot see that on a per-tool dashboard because each tool's dashboard only sees its own tool calls. - Session p99 duration by tool: line chart over a 28-day rolling window. When Claude Code's p99 spikes, you have a model-side issue; when Cursor's p99 spikes, you have a workspace-index contention issue; when both spike simultaneously, you have a network issue. The per-tool dashboards all blame the user.
- Cost per PR merged: the metric that actually answers the CFO question. We compute it by joining the normalized session data to the GitHub PR data via
coding_agent.session.id→commit_sha→pr_number. On our platform this comes out to $0.40-1.80 per PR merged depending on the agent mix, which is roughly an order of magnitude lower than the per-seat dashboards implied.
Want to see your own numbers before you build the pipeline? Our LLM API Cost Calculator does the per-model arithmetic with the same per-token prices we use in the Collector pipeline — useful for sizing the cost of a Claude Code + Cursor + Copilot mix without standing up the OTel infrastructure first.
The three things this dashboard catches that LangSmith doesn't
I am not going to pretend the LangSmith cross-tool tracing release is not impressive. It is. But three things fall out of the open-source approach that fall out of any vendor SaaS less naturally, and they matter enough to be worth the implementation cost:
- Cross-vendor session attribution. LangSmith ties a session to LangChain. If your agent loop is half LangGraph and half a custom Cursor hook, LangSmith sees only the LangGraph half. The Collector pipeline sees both.
- Cost normalized against a single price sheet. When Anthropic changes Claude Opus pricing on a Tuesday and OpenAI changes GPT-5 pricing on a Wednesday, the price sheet in our SQLite database updates on Thursday, and every cost number across the 28-day window re-computes on Friday. Vendor dashboards show you what they want you to see, on the day they want you to see it.
- Tool-call frequency vs. cost correlation. The panel that surprised me most was the join between tool-call frequency and session cost. Sessions where the agent made 50+ tool calls were 3.5x more expensive per merged PR than sessions where it made fewer than 20, regardless of which tool was making the calls. That is a workflow-design finding — push agents toward fewer, more decisive tool calls — that no single vendor's dashboard can produce.
LangSmith's cross-tool session tracing is the fastest way to get a unified Claude Code + Cursor + Codex + Copilot view if you are already on LangChain — the schema above is open-source but LangSmith's UI saves a week of Grafana work.
What I would skip
Two things we tried and rolled back:
- Per-user cost dashboards shown to engineers. It turned the team against the dashboard. Per-team aggregates, with drill-down for the team lead, are the right level. The cost-per-PR-merged metric is the one that engineers want to see, because it correlates with their actual productivity.
- Real-time cost alerting on a per-session basis. A single session that costs $80 is not necessarily wrong — it might be a complex refactor. Real-time alerting triggers false positives that erode trust in the dashboard. Daily aggregates against the team budget, with a weekly review of outliers, caught every legitimate cost-amplification incident we had without the false-positive churn.
The roadmap item that would make this much simpler
If LangChain, Anysphere, and Anthropic coordinated on a single coding_agent.session.id attribute and a normalized gen_ai.cost.* namespace, the Collector pipeline above would shrink from five processors to one. None of the vendors have an incentive to do that unilaterally — LangSmith's value proposition depends on fragmentation — so the open-source path through OTel GenAI semantic conventions is the realistic bet for platform teams that want vendor neutrality.
Until that lands, the schema in this article is what we ship. The Collector pipeline is ~120 lines of YAML plus the ~80-line Go price-lookup extension. The dashboard is the five panels above. The whole stack is up in two days for a team that already runs OTel Collector + Grafana, and it costs nothing per month at the 200-engineer scale.
Conclusion: the per-tool dashboard is a budget lie
The May bill I started this article with came in at $11,400. The normalized dashboard said $9,800 of real cost plus $1,600 of duplicated work between Claude Code and Cursor that we did not need to be doing. We turned off Cursor for the three engineers who had started using it for code review, kept Claude Code as the primary agent, and the June bill dropped to $7,100 — a 38% reduction with no loss of productivity, because the duplicated work was not productive work. It was the same agent making the same change through two different tools because nobody could see the overlap.
That is the argument for the cross-tool normalized view. The per-tool dashboard is fine for vendor billing. It is a budget lie for finance. The normalization layer in this article is what makes the lie visible.