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
Functions
@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.
@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.