Skip to content

fix(agent-sessions): normalise GenAI usage, cost, TTFT and agent name at ingest - #1123

Closed
JeremyFunk wants to merge 19 commits into
mainfrom
fix/agent-sessions-usage-keys
Closed

JeremyFunk wants to merge 19 commits into
mainfrom
fix/agent-sessions-usage-keys

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Agent Sessions read the wrong usage convention for some emitters, and missed usage, cost, TTFT and agent-name keys that frameworks actually write (bugs #8, #10, #11, #12, #23a, #24, #31, #35 from the agent-tracing docs verification, each from a real OTLP capture).

Design: normalise on the write side. The ingest gateway restates these facts once per stamped span; every reader (the ai_trace_index view, the session page, the MCP tools) reads one gen_ai.* key per fact with one meaning. The read-side alias lists, vendor refines and vendor/provider usage-convention tables are deleted.

Ingest: apps/ingest/src/ai_session/canonical.rs (070e5f9e2)

Runs on every vendor-stamped span, after the Claude Code restatement.

  • Usage: first parseable spelling per bucket → gen_ai.usage.{input_tokens, cache_read.input_tokens, cache_creation.input_tokens, output_tokens, reasoning.output_tokens}. Covers gen_ai.usage.prompt_tokens/completion_tokens, OpenRouter (input_tokens.cached, input_tokens.cache_write, output_tokens.reasoning), older Strands (cache_*_input_tokens), Mastra and Pydantic AI reasoning, the Vercel ai.usage.* and OpenInference llm.token_count.* dialects, and the registry cache_write.input_tokens spelling.
  • Convention: written usage always follows the semconv: input contains both cache buckets and output contains reasoning.
  • Cost: gen_ai.usage.cost ← gen_ai.usage.total_cost, llm.cost.total, litellm.cost.total, operation.cost (Enterprise tinybird selfhosted #10, chore(ci): SHA-pin actions, gate publish/deploy with environments, fix RCE in tag input #31).
  • TTFT: gen_ai.response.time_to_first_chunk (s) ← gen_ai.client.operation.time_to_first_chunk; OpenRouter trace.metadata.openrouter.first_token_ms and Strands gen_ai.server.time_to_first_token ÷ 1000 (Redesign #12).
  • Agent name: gen_ai.agent.name ← langsmith.metadata.lc_agent_name, ai.telemetry.functionId, CrewAI's graph.node.id (crewai only) (#23a, Cf migration #24).
  • Write rule: if the span already carries the canonical key (and no fold applies), its value is left as sent. Otherwise the canonical key is overwritten. Dialect keys are kept.
  • Tests: canonical.rs unit tests (one per rule). The Claude Code restatement test now expects the folded input (2 + 114,514 + 3,549).

Read side (cb1b2e764)

  • gen-ai.ts: usage-convention tables removed.
  • ai-integrations.ts / ai-vendors.ts: usage/cost/TTFT/agent-name aliases, the CrewAI refine and the ms-TTFT refines removed.
  • gen-ai-columns.ts: one key per usage bucket, cost and agent name. byConvention and the provider-key lists are gone. Tokens = the disjoint buckets summed under the one convention.
  • spanTokenBuckets always carves the cache out of input and the reasoning out of output.
  • ai-span-columns.test.ts pins that the list and the page read the same single key.

Warehouse: migration 0035 (8db109d70)

  • Drops and recreates ai_trace_index_mv with the simplified expressions. No column is added and nothing is backfilled.
  • The generated schema, Tinybird manifest and local chDB v26 are regenerated.
  • The e2e emitter-dialect fixtures are removed (that coverage is now in the Rust tests). The mirror span uses canonical keys.

Old data (accepted)

  • Spans ingested before the gateway change keep their old spellings. The detail page and get_agent_session no longer read those spellings, so such spans show no usage/cost/TTFT/agent name.
  • Index rows keep their pre-0035 values until the 30-day TTL ages them out.

Deploy

  • Deploy ingest before the view rebuild. Spans ingested in between lose their non-canonical usage in the index.
  • Manual after merge: tinybird:deploy (prod schema).

Overlap

…ive whatever provider it names

Symptom: a session of Claude calls forwarded by OpenRouter Broadcast read
32,573 tokens where OpenRouter billed 28,325 (session f0f992b0): every
cache read was counted twice.

Cause: Broadcast stamps the upstream as `gen_ai.provider.name`
(`anthropic` for Claude models) while reporting OpenRouter's own
OpenAI-shaped usage, whose prompt figure already contains the cached
tokens. `openrouter` was only in GENAI_PROVIDER_USAGE_CONVENTIONS, so the
vendor lookup missed and the provider lookup applied Anthropic's
excludes-cache rule.

Fix: `openrouter` joins GENAI_VENDOR_USAGE_CONVENTIONS as NESTED, so the
vendor decides before the provider does, on the detail page and in the
index expression.

Seen in: OpenRouter Broadcast capture (`openrouter`), 153 spans stamped
`gen_ai.provider.name=anthropic`.
…ude Code, not on the provider

Symptom: an Anthropic session traced by
`opentelemetry-instrumentation-genai-anthropic` read 78,144 tokens
(input 40,092 + cacheRead 30,324 + cacheWrite 7,581 + output 147) where
the instrumentation reported 40,239: every cache read and write was
counted twice, on the list and on the detail page.

Cause: GENAI_PROVIDER_USAGE_CONVENTIONS (packages/domain/src/gen-ai.ts)
applied the raw Messages API rule (`input_tokens` excludes both cache
buckets) to every span whose provider is `anthropic`. The semconv's
Anthropic mapping has the instrumentation add the cache buckets to
`gen_ai.usage.input_tokens`, and spec-conformant emitters do (the
genai-anthropic instrumentation; Pydantic AI, whose `input_tokens` is
documented as cache-inclusive; Vercel AI SDK spans that land in
`unknown:genai`). The only emitter in the captures that passes the raw
figures through is Claude Code, whose `claude_code.llm_request` usage the
gateway restates verbatim.

Fix: `anthropic` leaves the provider table (it takes the nesting
default), and `claude_agent_sdk` joins GENAI_VENDOR_USAGE_CONVENTIONS
with the excludes-cache rule, so the emitter decides.

Seen in: docs_provider-sdks_anthropic (genai-anthropic 1.2b0, chat span
input_tokens=7911, cache_write.input_tokens=7581); Claude Agent SDK
captures keep their current totals.
…lings of the reasoning and cache-write buckets

Symptom: reasoning tokens from Mastra and Pydantic AI, and OpenRouter
Broadcast's cache writes, never reached a session's usage; a cache write
read as plain prompt tokens.

Cause: the usage alias lists (GENAI_LEGACY_ALIASES in
ai-integrations.ts, GENAI_USAGE_KEYS in gen-ai-columns.ts) did not carry
`gen_ai.usage.reasoning_tokens` (Mastra), `gen_ai.usage.details.
reasoning_tokens` (Pydantic AI) or `gen_ai.usage.input_tokens.cache_write`
(OpenRouter Broadcast, documented beside the `.cached` read key Maple
already maps).

Fix: all three join the default integration's aliases and the index's
bucket key lists, after the canonical keys, so a span carrying both
spellings still reads the canonical one.

Seen in: docs_mastra_a/b, mastra_* (`gen_ai.usage.reasoning_tokens`);
docs_pydantic-ai_a/b/lf, pydantic_ai_* (`gen_ai.usage.details.
reasoning_tokens`); OpenRouter Broadcast OTel collector docs
(`gen_ai.usage.input_tokens.cache_write`).
… vendor, as the list does

Symptom: an Agno session was priced at $0.0042 on the Agent Sessions
list while its detail page (and `get_agent_session`) showed no cost.

Cause: the list's `Cost` column reads `llm.cost.total` on every span
(GENAI_COST_KEYS), but the detail page mapped it only through the
OpenInference integration (ai-vendors.ts), which is registered for
`openinference-openai` and `unknown:openinference` alone. Agno, DSPy,
smolagents, CrewAI and the OpenAI Agents SDK emit OpenInference under
their own vendor stamps.

Fix: `llm.cost.total` moves into the default integration's cost aliases,
so every vendor decodes it and the two pages read the same keys.

Seen in: docs_agno_a/b (`llm.cost.total` on `OpenRouter.invoke` /
`OpenRouter.ainvoke` spans, vendor `agno`).
Symptom: LiteLLM and Pydantic AI sessions showed as unpriced on the list
and on the detail page although every model call carried a price.

Cause: neither cost key was in any alias list: LiteLLM writes
`litellm.cost.total` (beside `litellm.cost.input`/`.output`/margins) and
Pydantic AI writes Logfire's `operation.cost`. Maple read only
`gen_ai.usage.cost`, `gen_ai.usage.total_cost` and `llm.cost.total`.

Fix: both join the default integration's cost aliases and the index's
GENAI_COST_KEYS, after the existing keys. Both are the instrumentation's
own price for the call from its price table (LiteLLM's model cost map
with any proxy margin, Pydantic AI's genai-prices), the same kind of
figure OpenLLMetry's `gen_ai.usage.cost` and OpenInference's
`llm.cost.total` already are, so they read into the same field.

Seen in: docs_litellm_a/b/proxy (`litellm.cost.total` 2.895e-05 on
`chat openai/gpt-4o-mini`); docs_pydantic-ai_a/b/lf and pydantic_ai_*
(`operation.cost` 4.755e-05 on `chat openai/gpt-4o-mini`).
…outer Broadcast and Strands

Symptom: no TTFT on the detail page's waterfall and vitals for Pydantic
AI, OpenRouter Broadcast and Strands model calls, all of which report it.

Cause: `responseTimeToFirstChunk` read `gen_ai.response.time_to_first_
chunk` for every vendor and `gen_ai.client.operation.time_to_first_chunk`
for the Vercel AI SDK alone (ai-vendors.ts). Pydantic AI writes the
latter too; OpenRouter Broadcast writes
`trace.metadata.openrouter.first_token_ms` and Strands writes
`gen_ai.server.time_to_first_token`, both in milliseconds (a Strands chat
span of 1,511 ms reports 1127).

Fix: `gen_ai.client.operation.time_to_first_chunk` (seconds) moves to
the default integration's aliases. `openrouter` and `strands` get
integrations whose refine lifts their millisecond key into the field as
seconds, only when no seconds-valued key already set it. The index
carries no TTFT, so no warehouse change is needed.

Seen in: docs_pydantic-ai_a/b/lf, docs_vercel-ai-sdk_a/b, eve_slack
(`gen_ai.client.operation.time_to_first_chunk`); openrouter
(`first_token_ms` 3784 on `LLM Generation`); docs_strands_a/b/g,
strands_* (`gen_ai.server.time_to_first_token`).
Symptom: a LangChain agent traced through LangSmith's OTel export showed
no agent name on the list (no Agent facet value) or on the detail page.

Cause: LangSmith names the agent in `langsmith.metadata.lc_agent_name`
on every span of the run and writes no `gen_ai.agent.name`; neither the
default integration nor GENAI_AGENT_NAME_KEYS read it.

Fix: the key joins the default integration's `agentName` aliases and the
index's agent-name key list, after the canonical key.

Seen in: docs_langchain_ls (50 spans with
`langsmith.metadata.lc_agent_name=assistant`, none with
`gen_ai.agent.name`). The second half of the report (LangSmith
`GraphInterrupt` read as a failure) is a failure-classification change
and is not part of this batch.
…dual-write is off

Symptom: a CrewAI crew traced with OpenInference's defaults showed no
agent names: the list's Agent facet and the detail page's agent scopes
were empty for every sub-agent.

Cause: without `enable_genai_semconv`, CrewAI's OpenInference agent
spans carry the agent's role only in `graph.node.id`, which no agent-name
key list read. Agno's OpenInference spans use the same key for an opaque
node id (`c8bddb16e7b7e3cc`), so a plain alias would name Agno agents
by hex ids.

Fix: a `crewai` integration whose refine lifts `graph.node.id` into
`agentName` when nothing else named the agent, and the index's
agent-name expression reads the same key under the same vendor gate
(CREWAI_AGENT_NAME_KEY, shared by both).

Seen in: crewai_agents / crewai_user (`graph.node.id` = orchestrator,
weather_worker, ...; no `gen_ai.agent.name`); agno_agents (`graph.node.id`
opaque, `graph.node.name` the name).
… usage convention and the added keys

The list reads `Tokens`, `Cost` and `AgentName` off `ai_trace_index`,
whose materialized view compiles the expressions in gen-ai-columns.ts at
creation. The preceding fixes changed those expressions (usage
convention keyed on the emitter; Mastra, Pydantic AI and OpenRouter
usage spellings; LiteLLM and Pydantic AI cost keys; LangSmith and CrewAI
agent names), so the view is recreated for the list to agree with the
detail page.

Migration 0035 drops and recreates `ai_trace_index_mv`; no column is
added. Nothing is backfilled, as in 0026/0029/0031/0032: rows
materialized before it keep their old values until raw `traces`' 30-day
TTL ages them out. The generated schema, the Tinybird manifest and the
local chDB schema (v25 -> v26) are regenerated; the local step only
drops and rebuilds the view.

The materialization e2e suite seeds one span per emitter under its own
org and asserts every changed column off the real view.
…ion summary too

The turn summary read off trace_detail_spans (summaryMeasures_ in
ai-sessions.ts) reads agentName through the integrations' source keys,
which leave out the refine-only CrewAI role key, so its agentNames stayed
empty for a CrewAI crew with the GenAI dual-write off while the index and
the detail page named the role. It now reads graph.node.id under the same
crewai vendor gate.
The vendor table now also holds an emitter that passes a provider's raw
figures through (Claude Code) and one that names a provider it did not
take its figures from (OpenRouter Broadcast), and the default
integration's alias table now holds cross-dialect keys; the comments say
so. Drops a requiredForIngest assertion the version test already makes.
@maple-review-bot

maple-review-bot Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Note

A newer push replaced ab753c3 before its review finished. The latest commit is reviewed in a new comment.

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5b634c4e-c882-46ee-94d9-afc211970b29

📥 Commits

Reviewing files that changed from the base of the PR and between 3b7241b and 616dbe5.

⛔ Files ignored due to path filters (2)
  • packages/domain/src/generated/clickhouse-schema.ts is excluded by !**/generated/**
  • packages/domain/src/generated/tinybird-project-manifest.ts is excluded by !**/generated/**
📒 Files selected for processing (27)
  • .github/workflows/ci.yml
  • apps/cli/src/server/local-schema-history.ts
  • apps/cli/src/server/local-schema-version.ts
  • apps/cli/src/server/local-store-migrations/steps.ts
  • apps/cli/src/server/schema-identity.ts
  • apps/cli/src/server/schema/local-inserts.json
  • apps/cli/src/server/schema/local-schema-v26.sql
  • apps/cli/src/server/schema/local-schema.sql
  • apps/cli/test/local-store-migrations.test.ts
  • apps/cli/test/native-local-store-migration.sh
  • apps/ingest/src/ai_session/claude_code.rs
  • apps/ingest/src/clickhouse_insert_mappings.rs
  • packages/agent-sessions/src/session-summary.test.ts
  • packages/backend/src/services/warehouse/ai-trace-index-materialization.clickhouse.e2e.test.ts
  • packages/domain/src/clickhouse/migrations/0035_ai_trace_index_usage_keys.ts
  • packages/domain/src/clickhouse/migrations/index.test.ts
  • packages/domain/src/clickhouse/migrations/index.ts
  • packages/domain/src/gen-ai.ts
  • packages/domain/src/tinybird/gen-ai-columns.ts
  • packages/query-engine-integrations/src/__sql_baseline__/integrations.sql
  • packages/query-engine-integrations/src/ai/ai-integrations.test.ts
  • packages/query-engine-integrations/src/ai/ai-integrations.ts
  • packages/query-engine-integrations/src/ai/ai-sessions.test.ts
  • packages/query-engine-integrations/src/ai/ai-sessions.ts
  • packages/query-engine-integrations/src/ai/ai-span-columns.test.ts
  • packages/query-engine-integrations/src/ai/ai-vendors.test.ts
  • packages/query-engine-integrations/src/ai/ai-vendors.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The change expands GenAI telemetry attribute handling across integrations, ClickHouse materialization, and session and trace queries. It adds vendor-specific usage conventions and mappings, updates the AI trace index migration and local schema to version 26, and adds migration and materialization coverage.

Changes

GenAI telemetry

Layer / File(s) Summary
Usage conventions and vendor mappings
packages/domain/src/gen-ai.ts, packages/domain/src/tinybird/gen-ai-columns.ts, packages/query-engine-integrations/src/ai/*, packages/agent-sessions/src/session-summary.test.ts
Usage conventions now distinguish emitter-reported figures. Default aliases and vendor refinements cover additional token, cost, timing, and agent-name keys. Tests cover those mappings and token accounting.
AI trace index migration
packages/domain/src/clickhouse/migrations/*, apps/cli/src/server/local-*, apps/cli/src/server/schema/*, apps/cli/test/local-store-migrations.test.ts, apps/cli/test/native-local-store-migration.sh, apps/ingest/src/*, packages/backend/src/services/warehouse/ai-trace-index-materialization.clickhouse.e2e.test.ts, .github/workflows/ci.yml
Migration 0035 recreates ai_trace_index_mv with expanded usage, cost, and agent-name rules. The local schema advances to version 26. Existing index rows are not backfilled. E2E fixtures and migration checks cover the updated behavior.
Session and trace query paths
packages/query-engine-integrations/src/__sql_baseline__/integrations.sql, packages/query-engine-integrations/src/ai/ai-sessions.ts, packages/query-engine-integrations/src/ai/ai-sessions.test.ts
Session and trace queries retain additional span attributes and use expanded cache-read, cost, and agent-name fallbacks. Session queries use graph.node.id for CrewAI spans when no mapped agent name is present.

Priority: ⚪ Not assessed

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Emitter
  participant QueryIntegration
  participant ClickHouse
  participant TraceIndex
  Emitter->>QueryIntegration: Provide span attributes
  QueryIntegration->>QueryIntegration: Map aliases and vendor-specific values
  Emitter->>ClickHouse: Insert spans into traces
  ClickHouse->>TraceIndex: Materialize index values from the updated view
Loading

Suggested reviewers: makisuo

Merge Risk: ⚪ Minimal · up to 616db

No merge-blocking issue is established. Complete the normal checks and planned Tinybird schema deployment.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 616db

The new calculations do not show an expanded authorization boundary, but a failed view update can leave new sessions absent from the index even when the schema update reports success. Older indexed values also retain their previous meaning until they expire.

Retained concerns

  • Medium · reliability · inferred: If view creation fails after migration 0035 drops the view, the optional migration can be skipped and the schema-apply run marked successful. Index materialization remains absent until a later successful apply, and this migration does not backfill spans received during the gap.
Security review details

Security Blast Radius

  • inferred — Telemetry producers can influence derived values in their trace rows, but the inspected expressions neither turn those values into SQL syntax nor select another storage destination. A failed view replacement affects indexing in the configured ClickHouse database; broader shared-cluster exposure was not established.

Trust Boundaries and Controls

  • observed — The inspected mapping keeps vendor-specific graph.node.id interpretation behind a CrewAI check, and schema-apply admission checks admin authority and atomically claims an active run per organization. Downstream query authorization was not inspected.

Resilience and Maintainability Implications

  • inferred — Step retries and later reapplication limit the duration of recoverable failures, but swallowing a failed optional view recreation allows a successful run status to coexist with an absent index producer. This weakens failure visibility and containment rather than demonstrating an authorization bypass.

Hardening Proposals

  • proposed — Verify that the view exists before reporting index readiness after an optional migration, and provide an explicit recovery path for spans received between DROP and successful recreation.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 22 files. (4 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary changes: normalizing GenAI usage, cost, time-to-first-token, and agent-name handling across ingest and related Agent Sessions processing.
Full details: Docstring Coverage

Explanation

Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 22 files. (4 skipped: 4 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

Comment on lines +106 to +110
export const GENAI_AGENT_NAME_KEYS = [
"gen_ai.agent.name",
"ai.telemetry.functionId",
"langsmith.metadata.lc_agent_name",
] as const

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Conflicting agent names across session views

When both agent-name keys appear, genAiAgentNameExpr chooses ai.telemetry.functionId before LangSmith's name. mapAiSpan chooses LangSmith's name first, so the session list and details disagree.

Learn more

The materialized index uses GENAI_AGENT_NAME_KEYS in order to select one AgentName for each span. The detail mapper builds its key order from genAiSources, then appends vendor keys through mergeSources. This puts LangSmith's key before the Vercel SDK function ID on the detail path, unlike the index. Both values can coexist when the Vercel SDK runs inside a LangSmith-traced agent.

Example: A span contains ai.telemetry.functionId=generateText and langsmith.metadata.lc_agent_name=assistant. The list labels it generateText; its details label it assistant.

Recommended fix: Put the two aliases in the same priority order in GENAI_AGENT_NAME_KEYS and the mapper, then add a test supplying both keys for vercel_ai_sdk.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b003a48: GENAI_AGENT_NAME_KEYS now follows the detail page's order (gen_ai.agent.name, lc_agent_name, then functionId), pinned in ai-span-columns.test.ts; 0035 regenerated.

…nction id in the index, as the detail page does

The detail page reads the default integration's agentName keys
(gen_ai.agent.name, then langsmith.metadata.lc_agent_name) before a
vendor's own (ai.telemetry.functionId for vercel_ai_sdk), while the index
read the function id second: a span carrying both would be named
differently on the list and the detail page. GENAI_AGENT_NAME_KEYS takes
the detail page's order, pinned by ai-span-columns.test.ts, and
migration 0035 and the local v26 schema are regenerated with it.
@maple-review-bot

maple-review-bot Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 4/5 · likely safe to merge
quality 100/100 · no findings · tests covered · risk medium

Re-keys Agent Sessions' usage convention on the emitter (openrouter, claude_agent_sdk) and teaches the shared key lists the Mastra, Pydantic AI, OpenRouter, LiteLLM and LangSmith spellings, on both the index view and the detail page. Coherent and safe to merge.

  • genAiUsageConvention moves anthropic out of the provider table and adds openrouter/claude_agent_sdk to the vendor table
  • gen-ai-columns.ts gains the reasoning, cache-write, cost and LangSmith agent-name keys plus the CrewAI graph.node.id fallback
  • Migration 0035 and local schema v26 rebuild ai_trace_index_mv with no column added and nothing backfilled
  • ai-vendors.ts adds crewai, openrouter and strands integrations lifting millisecond TTFT keys
What was checked
  • Claude Code's raw figures always carry the claude_agent_sdk stamp: normalize runs only inside that vendor branch (apps/ingest/src/ai_session.rs:155-166)
  • Index SQL, mapAiSpan sources and migration 0035 agree key-for-key and branch-for-branch in the regenerated ai_trace_index_mv DDL
  • crewai/openrouter/strands had no entry at the base, so the new vendor integrations only refine, never drop a default source key

b003a48 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

The suite that checks every ai_trace_index column against the real view
(ai-trace-index-materialization.clickhouse.e2e.test.ts) was never in the
ClickHouse E2E job, so its assertions only ran where someone started a
local ClickHouse. It now runs beside the trace facets rollup check.
…cache_write_input_tokens (#11)

Older Strands releases write both cache buckets as
gen_ai.usage.cache_read_input_tokens / cache_write_input_tokens (captures
strands_user, strands_agents), which no alias list read. Both join the
default integration's cache aliases and the index's bucket key lists;
migration 0035 and the local v26 schema are regenerated with them, and the
materialization e2e seeds a Strands span carrying them.
GENAI_LEGACY_ALIASES now also holds other dialects' keys any vendor's span
can carry, so it becomes GENAI_DEFAULT_ALIASES and its generated test
titles stop calling every key deprecated.
@maple-review-bot

maple-review-bot Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 4/5 · likely safe to merge
Dropping the anthropic provider rule changes displayed usage for every Anthropic span that is not Claude Code, and nothing is backfilled, so only new spans get it.
quality 100/100 · no findings · tests covered · risk medium

ai_trace_index and the detail page now resolve usage by emitter (Claude Code keeps Anthropic's excludes-cache rule, OpenRouter Broadcast nests) and read the LiteLLM/Pydantic AI/Mastra/OpenRouter/LangSmith/CrewAI keys. Internally consistent and covered by unit plus materialization e2e tests.

  • claude_agent_sdk carries the excludes-cache rule; anthropic leaves the provider table
  • openrouter joins the vendor conventions as cache-inclusive
  • AgentName reads langsmith.metadata.lc_agent_name and CrewAI's graph.node.id
  • Cost reads litellm.cost.total and operation.cost
What was checked
  • View DDL in migration 0035 matches the convention maps branch for branch (nested vendor list, claude_agent_sdk apart, Gemini output apart)
  • Index key lists match GENAI_AGENT_NAME_KEYS, GENAI_USAGE_KEYS and GENAI_COST_KEYS in the same order (gen-ai-columns.ts:388-427)
  • Detail-page sources: mergeSources appends vendor keys after the default's, so aiFieldSourceKeys("agentName") equals the index order (ai-integrations.ts:248-302)

281899d · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

…age-keys

# Conflicts:
#	packages/backend/src/services/warehouse/ai-trace-index-materialization.clickhouse.e2e.test.ts
#	packages/query-engine-integrations/src/__sql_baseline__/integrations.sql
#	packages/query-engine-integrations/src/ai/ai-sessions.ts
#	packages/query-engine-integrations/src/ai/ai-vendors.ts
@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 4/5 · likely safe to merge
Token and cost figures change for existing Anthropic-shaped emitters, but the rule, the key lists and the agent-name order are mirrored in every reader and pinned by tests and a new CI e2e.
quality 100/100 · no findings · tests covered · risk medium

Moves the gen_ai usage convention from the provider to the emitter (anthropic out, claude_agent_sdk and openrouter in), adds the Mastra, Pydantic AI, LiteLLM and LangSmith usage/cost/agent-name keys, and recreates ai_trace_index_mv in migration 0035 so the list and detail page agree. Safe to merge.

  • anthropic leaves GENAI_PROVIDER_USAGE_CONVENTIONS; the nested default now applies
  • claude_agent_sdk and openrouter join GENAI_VENDOR_USAGE_CONVENTIONS
  • New usage, cost and agent-name keys in GENAI_DEFAULT_ALIASES and GENAI_*_KEYS
  • crewai agent-role and Strands/OpenRouter TTFT refinements; migration 0035 recreates the view
What was checked
  • claude_agent_sdk is the vendor id ingest stamps on Claude Code spans (apps/ingest/src/ai_session/claude_code.rs:33), so its excludes-cache rule survives the provider-table change
  • Migration 0035's statement is the v26 snapshot the emitter produces (migrations/index.test.ts:845), and the local schema identity is bumped to 26 with its history step (local-schema-version.ts:4, …
  • Index key lists and the detail page's source lists agree in key and order for usage, cost and agent name (ai-integrations.ts:185, gen-ai-columns.ts:388, ai-span-columns.test.ts:73)

616dbe5 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

…e canonical key each

Frameworks spell these facts their own way (ai.usage.*, llm.token_count.*,
litellm.cost.total, a TTFT in milliseconds), and Claude Code's and Gemini's
usage figures leave the cache or reasoning buckets out where the semconv
folds them in. Every reader re-derived that per vendor and provider.

The gateway now settles it once per stamped span (ai_session/canonical.rs):
the first parseable spelling of each bucket, the cost, the TTFT and the
agent name is written under its gen_ai.* key, Claude Code's input gains
both cache buckets and a Gemini provider's output gains the reasoning, so
gen_ai.usage.* always carries the semconv meaning. A value already canonical
and unfolded is left as the emitter wrote it; dialect keys are kept.
… one key each

The ingest gateway now restates every spelling and the usage convention, so
the read side drops them: the vendor/provider usage-convention tables, the
usage/cost/TTFT/agent-name aliases in the default, Vercel AI SDK and
OpenInference integrations, the CrewAI and millisecond-TTFT refines, and the
matching key lists and convention branches behind ai_trace_index. Token
buckets are carved out under the one semconv convention on both the list and
the detail page.

Spans ingested before the gateway change keep their old spellings and read
as unpriced / without usage on the detail page; that data is not migrated.
…, cost and agent-name keys

Migration 0035 and the local chDB v26 step now carry the view compiled from
the simplified expressions: one key per fact, no dialect coalesces, no
vendor or provider branches. Still a drop-and-recreate of the view only;
nothing is backfilled.
@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 3/5 · needs attention
The ingest restatement and the migration are view-only and well tested, but the read path now mis-carves spans that were stamped before the deploy.
quality 88/100 · 1 warning · 1 note · tests covered · risk medium

Ingest now restates every AI span's usage, cost, TTFT and agent name under one canonical gen_ai.* key, and the index view, integrations and detail page stop asking the vendor/provider usage convention. The migration and local-schema step only recreate the view, but the live read path will mis-count spans already in traces until they age out.

  • canonical.rs restates usage, cost, TTFT and agent name under canonical gen_ai.* keys at ingest.
  • ai_trace_index_mv, spanTokenBuckets and the integrations drop the vendor/provider usage conventions.
  • openrouter is judged cache-inclusive, claude_agent_sdk cache-exclusive; anthropic takes the nesting default.
  • Local schema v26 recreates the view, keeping existing rows' v25 values.

Findings

Warning · F1 · Detail page under-counts tokens for spans ingested before the deploy

correctness · packages/agent-sessions/src/session-summary.ts:466

spanTokenBuckets now carves both cache buckets out of the prompt figure unconditionally, which is only correct once ingest has folded them in. Spans already in traces were stamped by the old gateway, so a Claude Code (claude_agent_sdk) span still carries Anthropic's exclusive gen_ai.usage.input_tokens: for raw input_tokens: 5000 with cache_read: 1000 the detail page and get_agent_session show 5000 instead of 6000, because the 1000 is subtracted from a figure that never held it. The index rows keep their v25 Tokens, so for the 30-day window the list and the detail page disagree — the opposite of what the change sets out to fix.

The pre-deploy rows are indistinguishable from restated ones at read time, so either state the dip in migration 0035 and the `local-schema-history` v26 entry (they only mention the index's stale values), or have `canonical::normalize` leave a marker the readers can test before carving.
Note · F2 · local-0025-to-0026 step ships the bump generator's TODO scaffolding

maintainability · apps/cli/src/server/local-store-migrations/steps.ts:1118-1121

The row is complete — beforeBootstrap, plan, verifies and dispositions are all filled — but the appended comment still tells the next reader the row is unfinished and that "the v26 physical verify fails an unfinished row". Every earlier step in this file carries no such block.

	{
		// Only the view's SELECT changes; existing index rows keep their v25 values.
		id: "local-0025-to-0026-ai-trace-index-usage-keys",
		from: 25,
		to: 26,
What was checked
  • Migration 0035 and step v25→v26 recreate the view only, no row rewritten (steps.ts:1128).
  • No readers of the removed exports remain (genAiUsageConvention, GENAI_LEGACY_ALIASES).
  • The detail-page SQL still projects the canonical keys its mapper reads (ai-sessions.ts:1357).
Copy all findings (2)
Findings from an automated review of commit 8db109d705fe6ea6d2df5c1ad3c813d5a0d32fe1. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F1 · Warning · correctness · packages/agent-sessions/src/session-summary.ts:466
Detail page under-counts tokens for spans ingested before the deploy
`spanTokenBuckets` now carves both cache buckets out of the prompt figure unconditionally, which is only correct once ingest has folded them in. Spans already in `traces` were stamped by the old gateway, so a Claude Code (`claude_agent_sdk`) span still carries Anthropic's exclusive `gen_ai.usage.input_tokens`: for raw `input_tokens: 5000` with `cache_read: 1000` the detail page and `get_agent_session` show 5000 instead of 6000, because the 1000 is subtracted from a figure that never held it. The index rows keep their v25 `Tokens`, so for the 30-day window the list and the detail page disagree — the opposite of what the change sets out to fix.
Suggested fix: The pre-deploy rows are indistinguishable from restated ones at read time, so either state the dip in migration 0035 and the `local-schema-history` v26 entry (they only mention the index's stale values), or have `canonical::normalize` leave a marker the readers can test before carving.

---

F2 · Note · maintainability · apps/cli/src/server/local-store-migrations/steps.ts:1118-1121
`local-0025-to-0026` step ships the bump generator's TODO scaffolding
The row is complete — `beforeBootstrap`, `plan`, `verifies` and `dispositions` are all filled — but the appended comment still tells the next reader the row is unfinished and that "the v26 physical verify fails an unfinished row". Every earlier step in this file carries no such block.
Replace those lines with:
	{
		// Only the view's SELECT changes; existing index rows keep their v25 values.
		id: "local-0025-to-0026-ai-trace-index-usage-keys",
		from: 25,
		to: 26,

8db109d · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@JeremyFunk JeremyFunk changed the title fix(agent-sessions): usage convention by emitter, and the usage, cost, TTFT and agent-name keys frameworks write fix(agent-sessions): normalise GenAI usage, cost, TTFT and agent name at ingest Sep 29, 2026

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 inline note from Maple's review. The score and summary are in the review comment above.

input: convention.inputIncludesCache
? Math.max(0, reportedInput - cacheRead - cacheWrite)
: reportedInput,
input: Math.max(0, reportedInput - cacheRead - cacheWrite),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Detail page under-counts tokens for spans ingested before the deploy

F1 · Warning · correctness

spanTokenBuckets now carves both cache buckets out of the prompt figure unconditionally, which is only correct once ingest has folded them in. Spans already in traces were stamped by the old gateway, so a Claude Code (claude_agent_sdk) span still carries Anthropic's exclusive gen_ai.usage.input_tokens: for raw input_tokens: 5000 with cache_read: 1000 the detail page and get_agent_session show 5000 instead of 6000, because the 1000 is subtracted from a figure that never held it. The index rows keep their v25 Tokens, so for the 30-day window the list and the detail page disagree — the opposite of what the change sets out to fix.

The pre-deploy rows are indistinguishable from restated ones at read time, so either state the dip in migration 0035 and the `local-schema-history` v26 entry (they only mention the index's stale values), or have `canonical::normalize` leave a marker the readers can test before carving.
Prompt for an AI agent
In `packages/agent-sessions/src/session-summary.ts:466`: Detail page under-counts tokens for spans ingested before the deploy.

`spanTokenBuckets` now carves both cache buckets out of the prompt figure unconditionally, which is only correct once ingest has folded them in. Spans already in `traces` were stamped by the old gateway, so a Claude Code (`claude_agent_sdk`) span still carries Anthropic's exclusive `gen_ai.usage.input_tokens`: for raw `input_tokens: 5000` with `cache_read: 1000` the detail page and `get_agent_session` show 5000 instead of 6000, because the 1000 is subtracted from a figure that never held it. The index rows keep their v25 `Tokens`, so for the 30-day window the list and the detail page disagree — the opposite of what the change sets out to fix.

Suggested fix: The pre-deploy rows are indistinguishable from restated ones at read time, so either state the dip in migration 0035 and the `local-schema-history` v26 entry (they only mention the index's stale values), or have `canonical::normalize` leave a marker the readers can test before carving.

Verify the problem exists at that location before changing it, and keep the fix to those lines.

@JeremyFunk JeremyFunk closed this Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant