ReactiveDag.Rollup (reactive_dag v0.17.0-rc.40)

Copy Markdown View Source

Summing one key across many meta maps — the arithmetic behind every "what did this cost" line, wherever the numbers came from.

Both phases report what their work cost, and they report it the same way. A drain step carries meta (ReactiveDag.Drain.Report); a poll carries detail: (ReactiveDag.Source). The containers differ because the phases do, but the arithmetic over them does not, and it used to be written twice — two implementations of one fold, which have to agree and have no mechanism forcing them to.

ReactiveDag.Drain.Report.total/2 and ReactiveDag.Source.detail_total/2 both delegate here. A host calls whichever fits what it has in hand and gets the same answer.

The two shapes

A count may be reported flat, or broken down per bucket:

%{tokens_in: 1600}
%{tokens_in: %{"claude-haiku-4-5" => 1200, "openai/gpt-5.6-luna" => 400}}

total/2 sums either to one number, so a cost line does not have to know which shape a node chose. by/2 returns the breakdown.

The library does not interpret the buckets — a bucket is a model name only because a host chose to key by one. Mixing shapes is fine: a graph where one node reports per-model tokens and another reports a bare count totals correctly rather than refusing to show a number.

Summary

Types

A bucket a count can be attributed to — a model name, typically.

Functions

Sum key across metas, per bucket — the breakdown behind total/2.

Sum key across every meta map in metas.

Types

bucket()

@type bucket() :: String.t() | atom()

A bucket a count can be attributed to — a model name, typically.

Functions

by(metas, key)

@spec by(Enumerable.t(), atom()) :: %{optional(bucket()) => number()}

Sum key across metas, per bucket — the breakdown behind total/2.

Rollup.by(metas, :tokens_in)
#=> %{"claude-haiku-4-5" => 1200, "openai/gpt-5.6-luna" => 400}

This is what a cost line needs that a single number cannot give: models differ in price by an order of magnitude, so one summed token count cannot be turned into a cost, nor say which model is driving spend.

A flat number lands under :unattributed rather than being dropped — a node reporting tokens without saying which model produced them is a gap worth SEEING, and omitting it would make the breakdown disagree with total/2 for no visible reason:

Rollup.by(metas, :tokens_in)
#=> %{"claude-haiku-4-5" => 1200, unattributed: 90}

The returned values always sum to total/2 for the same key. %{} when nothing reported it.

total(metas, key)

@spec total(Enumerable.t(), atom()) :: number()

Sum key across every meta map in metas.

Maps lacking the key contribute nothing, so a mixed set — some nodes reporting tokens, most not — totals rather than raising. A key nothing reported is 0.