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, with gen_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/telemetry with gen_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-endpoint flag, attribute names follow the OpenInference convention (not gen_ai.*) — needs attribute-rename processor in the Collector pipeline.
  • OpenCode v0.4.0 (2026-07-02): --analytics-config flag for OTel export, ships the most complete gen_ai.* coverage of any open-source coding agent; also exposes session-level cost totals via opencode.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.

Advertisement
Advertisement

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:

  1. coding_agent.session.id is 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.
  2. 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).
  3. coding_agent.tool_call.name normalizes the wildly different tool names across the six tools. Cursor's code_edit, Claude Code's Edit, Codex's file_apply_patch, and OpenCode's edit_file all become edit_file in 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:

  1. Per-tool cost per session: a stacked bar chart with coding_agent.session.id on the X axis and sum(gen_ai.cost.total_usd) on the Y axis, broken down by gen_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%.
  2. Cost per token by tool: a heatmap with gen_ai.request.model on the X axis and gen_ai.system on the Y axis, color-coded by gen_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.
  3. Tool-call frequency by session: histogram of coding_agent.tool_call.name per 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.
  4. 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.
  5. 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.idcommit_shapr_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:

  1. 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.
  2. 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.
  3. 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.
Recommended Tool LangSmith

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.

The Hidden 54x: why your coding-agent bill is a prompt-cache tax

The five-panel dashboard above catches the cross-tool overlap that drove the May bill. It does not catch the second cost axis that almost every team I have audited is paying and not seeing: the prompt-cache write tax. On a Claude Code + OpenCode comparison published July 12 by Systima Research (UK/EU agentic-AI consultancy, systima.ai, HN #2, 694 points at the time I am writing this) two coding agents running on the same model, the same machine, and the same task produced cache-write volumes that ranged from 5.9x to 54x apart depending on cache temperature. The cheaper bill was not the one that was billed at a lower per-token rate. It was the one that wrote its cache prefix once and read it back dozens of times. The other one kept re-writing tens of thousands of cache tokens mid-session, run after run, on the same task.

Cache writes bill at a premium — 1.25x base rate for the 5-minute TTL, 2x for the 1-hour TTL on Anthropic, with OpenAI's structure tracking similarly. A team that re-writes 50,000 cache tokens per session instead of 5,000 is paying a cache premium on the extra 45,000 every time the TTL expires (a five-minute think, a meeting, a lunch), on every parallel tool round trip, and on every subagent fan-out. The per-session cost difference compounds, and it does not show up in the per-tool dashboard because every per-tool dashboard quotes the input-token rate as if the cache write were a normal input token. It is not. It is a write.

Baseline bloat: 33k vs 7k before you have typed a word

Systima put both harnesses on claude-sonnet-4-5 with no MCP servers, no user settings, no memory, an empty workspace, and no instruction files. They then asked each harness for a one-line reply: Reply with exactly: OK. The first-turn metered payload was ~32,800 tokens for Claude Code and ~6,900 tokens for OpenCode — roughly a 4.7x baseline gap on Sonnet, shrinking to 3.3x on Claude Fable 5 (where Claude Code's system prompt dropped from 27,787 chars to 10,526 — the gap is harness design, not a model constant). The breakdown on Sonnet:

  • Claude Code system prompt: 27,344 chars, 3 blocks (Anthropic-specific instructions, behavior doctrine, environment description)
  • Claude Code tool schemas: 27 tools, 99,778 chars (including the CronCreate, Monitor, Task family, worktree management, push notifications — most of which a typical coding session never calls)
  • Claude Code first-message scaffolding: 7,997 chars of <system-reminder> blocks injected before the user prompt lands
  • OpenCode system prompt: 9,324 chars, 1 block ("You are OpenCode, the best coding agent on the planet" and the task framing)
  • OpenCode tool schemas: 10 tools, 20,856 chars (the coding core, no background-orchestration suite)
  • OpenCode first-message scaffolding: none

The dominant term in both harnesses is the tool schema: ~24,000 of Claude Code's ~33,000 tokens and ~4,800 of OpenCode's ~6,900 are tool definitions. Strip the tools and Claude Code's instruction set is still 3x the size of OpenCode's — the residual is the behavioral doctrine layer that Anthropic ships for safety and orchestration. With a 33k baseline, every turn starts a sixth of the way into a 200k window before any code enters the conversation. With a 7k baseline, the same turn starts at the 4% mark. That gap is what your prompt cache is paying for on every request, even when the cache is being read at a tenth of the input price.

Cache-write economics: a worked cost example

Both harnesses set cache breakpoints correctly — the payload is written once at the 1.25x premium for the 5-minute TTL and re-read at a tenth of the price thereafter. The difference is whether the prefix stays stable. Systima hashed the tools array and system blocks of every request in the dataset. The result, on an identical file-summarise task across five requests:

  • Claude Code wrote 53,839 cache tokens across those five requests, including one complete mid-task re-write of its full ~43k prefix (43,342 tokens in run one, 36,899 in run two). The re-write is not a response to anything the model did — it is harness-side, visible in the captured bytes before any gateway involvement.
  • OpenCode wrote 1,003 cache tokens across the same task. Byte-identical prefixes across every request and every run. Three separate runs of the same one-line-reply task produced the same tool bytes, the same system bytes, and the same message bytes. The repeat runs wrote zero cache tokens and read everything.

To put that in dollars on Anthropic's published rates for Sonnet 4.5 at the time of writing (input $3/Mtok, cache-write $3.75/Mtok for the 5-minute tier, cache-read $0.30/Mtok):

# Five-request file-summarise task
# Claude Code: 53,839 cache-write + ~145,000 cache-read = ~199k cumulative input tokens
# OpenCode:    1,003 cache-write + ~40,000 cache-read  = ~41k cumulative input tokens

claude_code_cache_write_cost_usd:  0.2019   # 53,839 * 3.75 / 1,000,000
claude_code_cache_read_cost_usd:   0.0435   # 145,000 * 0.30 / 1,000,000
claude_code_input_cost_usd:        0.0000   # the rest of the input was cache-read
claude_code_total_cache_tax_usd:   0.2454

opencode_cache_write_cost_usd:     0.0038   # 1,003 * 3.75 / 1,000,000
opencode_cache_read_cost_usd:      0.0120   # 40,000 * 0.30 / 1,000,000
opencode_input_cost_usd:           0.0000
opencode_total_cache_tax_usd:      0.0158

# Claude Code spent 15.5x more on prompt-cache tax alone for the same task
# The per-tool dashboard reports this as "input tokens" — it does not break
# the cache-write tier out, so the 1.25x premium is invisible.

Put your monthly volume through our LLM API Cost Calculator with cache-write and cache-read sliders set to the same fractions and the multiplier is the same shape — it is not model-specific. On GPT-5 or Claude Opus the dollar numbers scale up; the ratio does not change.

Subagent fan-out: 121k → 513k, a 4.2x cost multiplier

The baseline tax is large. The subagent fan-out tax is the multiplier that catches most teams off guard. Systima asked each harness to fan the same task out to two parallel subagents. Direct execution on Claude Code: 9 model requests, ~121,000 cumulative metered input tokens, $0.36 of cache + input combined. Fan-out execution: 9 model requests across three distinct request classes, ~513,000 cumulative metered input tokens, $1.92. That is a 4.2x multiplier for a single modest fan-out, and the mechanism is mechanical: each subagent is a fresh agent that re-reads its own bootstrap (a 3,554-char agent system prompt plus 24 of the 27 tools) on every one of its own turns, so two subagents running several turns each add several more full-baseline requests to the session. The parent ingests only each subagent's returned result; the cost is the baselines the subagents re-read on the parent's behalf.

OpenCode's subagent design is notably leaner (a 1,379-char system prompt and 5 tools per subagent), so its fan-out is structurally less expensive than Claude Code's, but the principle holds on both: whole-task input ≈ baseline × request count + conversation growth. If your heavy sessions surprise you, this is the first place to look. The fix is not to stop using subagents — the agents are doing real work — it is to (a) instrument the cache-write volume per subagent and (b) cap the fan-out depth in the workflow itself, not in the model router.

What to instrument: the OTel GenAI semconv attributes that catch this

To see the prompt-cache tax in your own deployment, you need the cache-write and cache-read counters broken out from the input counter, with both exposed as OTel span attributes. The OpenTelemetry GenAI semantic conventions cover this in the gen_ai.usage.* namespace; the Claude Code native OTel export adds the same numbers under claude_code.usage.*. The minimum attribute set:

# OpenTelemetry GenAI semconv (the canonical names)
gen_ai.usage.input_tokens:           1284
gen_ai.usage.output_tokens:           412
gen_ai.usage.cache_read_input_tokens:  980     # read at 0.1x input price
gen_ai.usage.cache_creation_input_tokens: 980  # written at 1.25x (5-min TTL) or 2x (1-hour TTL)

# Claude Code native OTel export (the names Claude Code actually emits)
claude_code.usage.input_tokens:           1284
claude_code.usage.output_tokens:           412
claude_code.usage.cache_read_input_tokens:  980
claude_code.usage.cache_creation_input_tokens: 980

# Cross-tool additions to add to the normalized schema from the section above
gen_ai.usage.cache_write_premium_ratio:   1.25    # 1.25 = 5-min tier, 2.0 = 1-hour tier
gen_ai.usage.cache_stable_prefix:         true    # computed: prefix hash matched the previous request

Two attributes that are not in the OTel GenAI semconv yet but should be: gen_ai.usage.cache_write_premium_ratio (so the price-lookup in the Collector can apply the correct multiplier for the 5-min vs 1-hour TTL) and gen_ai.usage.cache_stable_prefix (a boolean the Collector computes by hashing the request prefix and comparing to the previous request in the same session — a single attribute that surfaces the Claude Code vs OpenCode cache-stability difference at a glance). The first is small enough to propose in a PR; the second needs to be agreed across the OTel GenAI SIG before vendors emit it natively.

Once the cache attributes are flowing, the dashboard panels that catch the prompt-cache tax are these:

  1. Cache write rate per session: line chart of gen_ai.usage.cache_creation_input_tokens / gen_ai.usage.input_tokens over time, broken down by gen_ai.system. A healthy harness sits at 5-10% (one warmup write per session, all subsequent reads). A harness that re-writes mid-session sits at 30-60%. The Systima 54x number is the same shape — it is the ceiling of this panel, not a one-off spike.
  2. Cache write dollar cost per session: same data, multiplied by the per-tier premium. The panel that finally surfaces what the per-tool dashboard hides: how much of the bill is the cache-write premium, separately from the input rate. Most teams discover this number is 20-30% of the total coding-agent bill when they first turn the panel on.
  3. Subagent fan-out cost waterfall: stacked bar of parent vs subagent requests with gen_ai.system on the X axis and the per-class baseline on the Y axis. The Claude Code 4.2x fan-out multiplier shows up as a 2-3x bar-height delta between parent and subagent classes; OpenCode shows a much smaller delta because its subagent design is structurally leaner.

The EU AI Act angle: this is also your Article 12 audit trail

The same payload capture that surfaces the prompt-cache tax also satisfies a regulatory requirement that is increasingly landing on AI-platform teams in 2026. EU AI Act Article 12 (the logging obligation for high-risk AI systems) expects providers and deployers to keep "detailed records of the system's functioning … to allow post-market monitoring" and to retain them for an appropriate period. Systima's measurement rig — an HTTP proxy in front of the model endpoint, capturing every request body and response usage block, and writing each pair into a SHA-256 hash-chained audit log — is the reference implementation. The dataset is the 273 captured request/response records of the Claude Code vs OpenCode comparison itself, the chain verifies end to end, and their open-source @systima/aiact-audit-log library is what produced the integrity proof.

The pattern that connects prompt-cache cost observability to Article 12 compliance is the same: log every request body and every response usage block at the API boundary, hash-chain the records, and the audit trail is the same data set the cost dashboard queries. You do not need a separate logging stack and a separate observability stack. One Collector pipeline, one hash-chained store, two query patterns (cost dashboards + Article 12 reconstruction queries). For teams operating in the EU or selling into EU customers this is the path that turns the cost-observability work the rest of this article describes into a compliance deliverable, not a parallel initiative.

The cost angle and the compliance angle pull in the same direction. The prompt-cache tax is invisible in the per-tool dashboard and Article 12 makes that invisibility a regulatory problem: "we do not know what tokens our coding agent sent" is not a defensible position when the system is in scope. The same proxy that catches the 54x cache-write gap also catches the model-substitution failure mode Systima found (their gateway silently served a different model snapshot than the one they pinned, and answered alternately as claude-fable-5 and claude-opus-4-8 in the same lane). The proxy is the only place in the stack where both questions are answerable at the same time.

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.

Further reading: AI Coding Agent FinOps: Copilot, Cursor, Claude Code Cost Per EngineerAgentic Observability: Multi-Agent LLM MonitoringLLM Cost Monitoring Tools 2026