Normandy.Telemetry.Usage
(normandy v2.2.0)
View Source
Normalizes a provider token-usage map into the token counts telemetry reports.
Anthropic reports input_tokens as the uncached remainder only: cache reads
(cache_read_input_tokens) and cache writes (cache_creation_input_tokens)
are separate fields. OpenTelemetry's GenAI conventions want
gen_ai.usage.input_tokens to include all of them, so normalize/1 sums them.
The result uses these Normandy-side keys (all optional):
:input_tokens- input + cache read + cache write, summed over the fields that are present:output_tokens:cache_read_input_tokens:cache_write_input_tokens- Anthropic'scache_creation_input_tokens
A count the provider did not report (missing or nil) is left out of the
result, never 0 or nil. When it reports no count at all, the result is
%{}.
The input may be a map or struct with atom keys, string keys, or a mix
(Claudio's non-streaming usage uses atoms; streamed usage arrives with
string keys). When it carries a non-empty iterations list (Claudio's
refusal :fallbacks and compaction record every attempt there while the
top-level counts cover only the returned one), the billed totals are the sum
over the iterations and the top-level counts are ignored.
OpenAI-shaped usage (prompt_tokens, completion_tokens, optionally
prompt_tokens_details.cached_tokens) is recognized too, when the map has no
input_tokens: prompt_tokens already includes cached tokens, so it is the
:input_tokens total as is, completion_tokens is :output_tokens, and
cached_tokens (when present) is :cache_read_input_tokens. OpenAI reports no
cache writes.
The raw usage map on [:normandy, :agent, :llm_call, :stop] is unchanged;
these keys are added beside it.
Summary
Functions
Returns the normalized token counts for usage, or %{} when usage is not
a map or reports no counts.
Puts the raw provider usage under :usage in metadata when it is a non-empty map,
and returns metadata unchanged otherwise, so a call whose provider reported no usage
has no :usage key (never usage: nil).
Adds two raw provider usage maps (or structs), the one rule for combining the usage of
several calls. A nil side returns the other (as a map). Keys are atoms in the result, so a
string-keyed (wire) side adds to an atom-keyed one. Integer counts are summed and any other
value is the later side's. When either side carries iterations, the result's
iterations are both sides' entries in order (a side without them contributes one entry
built from its top-level counts), so normalize/1 bills their sum and not the top-level
counts.
Types
@type normalized() :: %{ optional(:input_tokens) => non_neg_integer(), optional(:output_tokens) => non_neg_integer(), optional(:cache_read_input_tokens) => non_neg_integer(), optional(:cache_write_input_tokens) => non_neg_integer() }
Functions
@spec normalize(term()) :: normalized()
Returns the normalized token counts for usage, or %{} when usage is not
a map or reports no counts.
Examples
iex> Normandy.Telemetry.Usage.normalize(%{
...> input_tokens: 10,
...> output_tokens: 5,
...> cache_read_input_tokens: 100,
...> cache_creation_input_tokens: 20
...> })
%{input_tokens: 130, output_tokens: 5, cache_read_input_tokens: 100, cache_write_input_tokens: 20}
iex> Normandy.Telemetry.Usage.normalize(nil)
%{}
Puts the raw provider usage under :usage in metadata when it is a non-empty map,
and returns metadata unchanged otherwise, so a call whose provider reported no usage
has no :usage key (never usage: nil).
Examples
iex> Normandy.Telemetry.Usage.put_raw(%{}, %{input_tokens: 3})
%{usage: %{input_tokens: 3}}
iex> Normandy.Telemetry.Usage.put_raw(%{}, nil)
%{}
Adds two raw provider usage maps (or structs), the one rule for combining the usage of
several calls. A nil side returns the other (as a map). Keys are atoms in the result, so a
string-keyed (wire) side adds to an atom-keyed one. Integer counts are summed and any other
value is the later side's. When either side carries iterations, the result's
iterations are both sides' entries in order (a side without them contributes one entry
built from its top-level counts), so normalize/1 bills their sum and not the top-level
counts.
Examples
iex> Normandy.Telemetry.Usage.sum(%{input_tokens: 3}, %{input_tokens: 4})
%{input_tokens: 7}
iex> Normandy.Telemetry.Usage.sum(nil, %{input_tokens: 4})
%{input_tokens: 4}