Documentslangfuse/langfuse
Trace-Level Token Aggregation
Trace-Level Token Aggregation
Type
Topic
Status
Published
Created
Jul 8, 2026
Updated
Jul 8, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

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:

  • observations table — the legacy/primary path, used in the UI traces table, Public API v1, and user metrics queries.
  • events table (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:

PathWhere sumMap is used
traces-ui-table-service.ts observations_stats CTEUI traces table metrics query
traces.ts observations_agg CTEcheckTraceExistsAndGetTimestamp
public-api/server/traces.ts observation_stats CTEPublic API List Traces endpoint
traces.ts getUserMetrics queryUser-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 use positionCaseInsensitive (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-summed Record<string, number>.

reduceUsageOrCostDetails is called in:


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#

FilePurpose
packages/shared/src/server/repositories/observations_converters.tsreduceUsageOrCostDetails definition and per-observation conversion
packages/shared/src/server/services/traces-ui-table-service.tsobservations_stats CTE with sumMap, convertToUITableMetrics
packages/shared/src/server/queries/clickhouse-sql/event-query-builder.tsEVENTS_AGGREGATION_FIELDS and EVENTS_SESSION_AGGREGATION_FIELDS with sumMap
packages/shared/src/server/tableMappings/mapTracesTable.tsColumn definitions with positionCaseInsensitive filter expressions for UI table
web/src/features/public-api/server/traces.tsPublic API observation_stats CTE
packages/shared/src/server/repositories/traces.tsUser metrics and trace existence checks with sumMap
Trace-Level Token Aggregation | Dosu