Trace-Level Token Aggregation#
Token counts (and cost details) from individual observations are aggregated up to trace level using ClickHouse's sumMap() function. The resulting map is then filtered in JavaScript using reduceUsageOrCostDetails to extract canonical input, output, and total values. This pattern runs across two storage architectures:
observationstable — the legacy/primary path, used in the UI traces table, Public API v1, and user metrics queries.eventstable (events_core/events_full) — the newer path, used in the events-backed traces aggregation and session aggregation.
ClickHouse: sumMap() aggregation#
In every observations CTE that backs trace-level metrics, the SQL produces:
sumMap(usage_details) as usage_details,
sumMap(cost_details) as cost_details,
grouped by (trace_id, project_id). This sums the per-key values across all observations belonging to a trace and returns a Map(String, Float64).
Key locations:
| Path | Where sumMap is used |
|---|---|
traces-ui-table-service.ts observations_stats CTE | UI traces table metrics query |
traces.ts observations_agg CTE | checkTraceExistsAndGetTimestamp |
public-api/server/traces.ts observation_stats CTE | Public API List Traces endpoint |
traces.ts getUserMetrics query | User-level usage roll-up |
For the events table path, sumMap() appears in the EVENTS_AGGREGATION_FIELDS constant and in EVENTS_SESSION_AGGREGATION_FIELDS :
usage_details: "sumMap(usage_details) as usage_details",
cost_details: "sumMap(cost_details) as cost_details",
These are selected by EventsAggregationQueryBuilder (trace-level) and EventsSessionAggregationQueryBuilder (session-level).
Case-insensitive key filtering (ClickHouse)#
After sumMap() accumulates the full map, column definitions in mapTracesTable.ts use positionCaseInsensitive to extract directional sums:
-- Input tokens (matches "input", "input_cached", etc.)
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'input') > 0, o.usage_details)))
-- Output tokens (matches "output", "output_reasoning", etc.)
arraySum(mapValues(mapFilter(x -> positionCaseInsensitive(x.1, 'output') > 0, o.usage_details)))
-- Total tokens (exact key match)
o.usage_details['total']
The same positionCaseInsensitive pattern applies to cost_details for input/output cost filtering . This makes the system resilient to provider-specific key variants (e.g., input_cached, outputTokens) without requiring schema changes.
For user metrics and session aggregation, the same pattern appears inline in the SQL .
JavaScript: reduceUsageOrCostDetails#
After the sumMap result is returned from ClickHouse, the JavaScript function reduceUsageOrCostDetails normalizes it into { input, output, total }:
export const reduceUsageOrCostDetails = (
details: Record<string, number> | null | undefined,
): { input: number | null; output: number | null; total: number | null } => {
return {
input: Object.entries(details ?? {})
.filter(([k]) => k.startsWith("input"))
.reduce((acc, [, v]) => (acc ?? 0) + Number(v), null as number | null),
output: Object.entries(details ?? {})
.filter(([k]) => k.startsWith("output"))
.reduce((acc, [, v]) => (acc ?? 0) + Number(v), null as number | null),
total: Number(details?.total ?? 0),
};
};
Note: JS filtering uses
startsWith(case-sensitive, prefix match), while ClickHouse column definitions usepositionCaseInsensitive(substring, case-insensitive). These are parallel implementations for the same concept at different layers. The JS version is called after ClickHouse has already merged all maps, so it operates on the already-summedRecord<string, number>.
reduceUsageOrCostDetails is called in:
convertToUITableMetrics— maps ClickHouseusage_detailstopromptTokens,completionTokens,totalTokensfor the UI.convertObservationPartial— per-observation conversion for both observations and events table records.
Data flow summary#
observations / events_core
└── GROUP BY (trace_id, project_id)
sumMap(usage_details) → usage_details Map
sumMap(cost_details) → cost_details Map
│
▼
ClickHouse filter (positionCaseInsensitive)
inputTokens, outputTokens, totalTokens columns
inputCost, outputCost, totalCost columns
│
▼
JavaScript reduceUsageOrCostDetails
{ input: N, output: N, total: N }
│
▼
Domain model / API response
promptTokens, completionTokens, totalTokens
inputCost, outputCost, totalCost
Key files for further exploration#
| File | Purpose |
|---|---|
packages/shared/src/server/repositories/observations_converters.ts | reduceUsageOrCostDetails definition and per-observation conversion |
packages/shared/src/server/services/traces-ui-table-service.ts | observations_stats CTE with sumMap, convertToUITableMetrics |
packages/shared/src/server/queries/clickhouse-sql/event-query-builder.ts | EVENTS_AGGREGATION_FIELDS and EVENTS_SESSION_AGGREGATION_FIELDS with sumMap |
packages/shared/src/server/tableMappings/mapTracesTable.ts | Column definitions with positionCaseInsensitive filter expressions for UI table |
web/src/features/public-api/server/traces.ts | Public API observation_stats CTE |
packages/shared/src/server/repositories/traces.ts | User metrics and trace existence checks with sumMap |