symfonic.core.usage¶
usage ¶
Canonical token-usage extraction from AIMessage responses (v8.6.8).
Single source of truth for reading LangChain's usage_metadata off an
AIMessage-like response. Consolidates three previously-duplicated readouts
(DRY):
callbacks/emit.py:_extract_usage-- the adopter-VALIDATED cost path, carrying real production cost numbers (v7.14.2).nodes/react.py:_extract_usage-- the original (emit was hoisted from it); differed only on the object-style fallback branch.streaming/transpiler.py-- an inline input/output-only readout with no cache handling (the v8.6.7getattr-on-dict bug lived here, in the divergence this module closes).
This function reproduces emit's validated logic byte-for-byte: the dict path real providers (Anthropic, OpenAI) exercise is unchanged, and the object-style fallback adopts emit's attribute read (the canonical/validated behaviour) rather than react's zero-return.
The module depends on the standard library and nothing else, so all three call sites can import it without an import cycle.
TA2.3 (LAY-ADR): this module is ruled kernel-contracts, because the
memory capability's three LLM call sites need the same validated readout and
capability -> facade-compiler is no with no port escape. The purity
that ruling requires is why :func:extract_cache_ttl now lives here instead
of on AnthropicProvider -- see its docstring for what that costs.
extract_cache_ttl ¶
Mine the Anthropic 1h-cache TTL from a LangChain
usage_metadata.input_token_details dict.
Anthropic's API surfaces the prompt-cache write rate via the
cache_creation: {ephemeral_5m_input_tokens, ephemeral_1h_input_tokens}
sub-object on the response usage block. langchain-anthropic
(>= 0.3.20) flattens that into input_token_details with the same key
names -- see langchain_anthropic.chat_models:2600-2610.
When ephemeral_1h_input_tokens > 0 (the 1h cache was used on this
turn), this returns "1h" so the cost layer picks the
cache_write_1h rate from the pricing row (T-7.20.0.5). Otherwise --
pure cache_read, only 5m cache, or no cache_creation at all -- returns
None so the cost layer falls through to cache_write (the 5-min
rate, which IS the documented default in the registry).
Where this lives, and why it moved (TA2.3). The 2026-06-05 architect
verdict SS5.c placed this read on AnthropicProvider -- "wire-shape
translation lives on the provider that owns the wire contract" -- so that
framework-level extractors stayed free of Anthropic key names. Under
LAY-ADR that placement is unreachable: symfonic.memory is ruled
capability, its three LLM call sites need :func:extract_usage, and
capability -> facade-compiler is no with no port cell. The read
therefore moved to this kernel-contracts module, which is the same
resolution TA2.2 applied to the provider-specific pricing table
(core/observability/pricing.py).
The cost is stated rather than hidden: an Anthropic wire key now appears
in a kernel-contracts module, so this extractor is no longer
provider-agnostic in the sense the verdict intended. What survives is the
verdict's interface: AnthropicProvider.extract_cache_ttl is
unchanged in name, signature and behaviour and remains the declared
provider-side entry point -- it delegates here, so there is exactly one
implementation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_token_details
|
Any
|
The |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
|
str | None
|
(5m cache only, no cache, or non-dict input). |
Source code in symfonic/core/usage.py
extract_usage ¶
Pull token usage from an AIMessage-like response.
Canonical extractor for ALL LLM call sites (react node, the seven
non-react bypass sites, and the streaming transpiler). Validated
against real adopter production cost numbers (hoisted from the v7.14.2
callbacks/emit.py implementation).
LangChain stores usage as usage_metadata on AIMessage -- a
UsageMetadata TypedDict that is a plain dict at runtime.
Anthropic cache tokens are NESTED under
input_token_details.{cache_read, cache_creation} -- a flat
{k: int(v) for k, v in raw.items()} walk drops them (the nested
dict fails the isinstance(v, int) type filter). The dict shape
returned here matches what TokenUsage.from_dict consumes
(cache_read_input_tokens / cache_creation_input_tokens,
see contracts/callbacks.py).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
Any
|
An |
required |
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
A mixed-value mapping, hence |
dict[str, Any]
|
is an |
dict[str, Any]
|
|
dict[str, Any]
|
rather than the return type, so every caller threading |
dict[str, Any]
|
onwards inherited an |
dict[str, Any]
|
error at the caller for a value this function had always produced |
dict[str, Any]
|
correctly. |
dict[str, Any]
|
Dict with |
dict[str, Any]
|
defaulting to 0). When the provider surfaces Anthropic cache |
dict[str, Any]
|
tokens, additionally includes |
dict[str, Any]
|
|
dict[str, Any]
|
materialise the typed breakdown and the pricing layer can compute |
dict[str, Any]
|
the discounted cost. When the Anthropic 1h cache TTL is in play, |
dict[str, Any]
|
a |
dict[str, Any]
|
tokens (Opus-4-x and forward-compat for Opus-5+) are surfaced as a |
dict[str, Any]
|
flat |
Source code in symfonic/core/usage.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |