symfonic.core.observability.pricing¶
pricing ¶
Model pricing registry for cost calculation.
calculate_cost ¶
calculate_cost(
model: str,
input_tokens: int,
output_tokens: int,
cached_tokens: int = 0,
cache_creation_tokens: int = 0,
reasoning_tokens: int = 0,
cache_ttl: str | None = None,
) -> float
Calculate USD cost for a single LLM call.
Returns 0.0 if the model is not in the registry. Cached tokens are billed at cache_read rate; the remainder of input tokens are billed at the standard input rate.
v7.15.0: reasoning_tokens is observability-only by default --
the pricing layer only adds a separate reasoning line when the
model's pricing row explicitly opts in via a "reasoning" key.
See :func:_compute_cost docstring for the no-double-charge
invariant on Opus-4-x.
v7.20.0 (architect verdict 2026-06-05 §5): cache_ttl lets
Anthropic 1-hour ephemeral caches bill at the row's
cache_write_1h rate instead of the 5-minute cache_write
default. See :func:_compute_cost for the fallback contract.
For callers that need to distinguish "known model, zero cost"
from "unknown model, fallback to zero", use
:func:calculate_cost_with_meta -- it returns the same float plus
a pricing_unknown: bool discriminator. This function preserves
the historical float-return contract for BudgetTracker,
MetricsHandler, and other callers that sum / compare the value.
Source code in src/symfonic/core/observability/pricing.py
calculate_cost_with_meta ¶
calculate_cost_with_meta(
model: str,
input_tokens: int,
output_tokens: int,
cached_tokens: int = 0,
cache_creation_tokens: int = 0,
reasoning_tokens: int = 0,
cache_ttl: str | None = None,
) -> tuple[float, bool]
Same as :func:calculate_cost, but also returns a pricing_unknown flag.
Returns (cost_usd, pricing_unknown) where:
pricing_unknown=Truemeans the model name had no entry in the registry (empty model string OR no exact/prefix match).cost_usdis then0.0and a one-time WARNING was logged (same behaviour as the legacycalculate_costpath).pricing_unknown=Falsemeans the registry resolved a pricing row;cost_usdis the computed value (can still be0.0for zero-token calls or zero-rate entries).
Introduced in v7.4.5 (Jarvio Ask 6) so :class:TokenUsage and
:class:LLMEndEvent can ship a programmatic "unknown pricing" channel
distinct from the "known model, $0" case (e.g. zero-token continuation,
cache-only turn at a $0 cache rate).
v7.15.0: reasoning_tokens opt-in kwarg. See
:func:calculate_cost for the no-double-charge invariant.
Source code in src/symfonic/core/observability/pricing.py
clear_pricing_overrides ¶
Clear all adopter-supplied pricing overrides.
Test isolation helper -- production callers should re-register via
register_pricing_overrides({}) instead so the intent is explicit.
Source code in src/symfonic/core/observability/pricing.py
emit_overrides_audit_log ¶
Emit a single INFO line listing the loaded overrides at agent init.
v7.20.0 (architect verdict 2026-06-05, pricing-registry hardening
§4.a): called by SymfonicAgent.__init__ after override
registration so operators see what the deployment installed.
CRITICAL SECURITY INVARIANT: the line carries model IDs ONLY, NEVER rate values. Rate values can be commercially-sensitive (e.g. a negotiated $0.40/M Anthropic enterprise rate); logging them surfaces confidential data into observability pipelines where they get indexed by CloudWatch / Datadog / etc. and become accessible to anyone with log-read permission.
Empty mapping is a silent no-op -- adopters who set no overrides should see zero log noise.
Source code in src/symfonic/core/observability/pricing.py
get_pricing_overrides ¶
Return a deep-ish copy of the currently-installed override table.
Copy semantics: mutating the returned mapping (or its rows) does not affect the process-wide registry.
Source code in src/symfonic/core/observability/pricing.py
install_pricing_overrides ¶
install_pricing_overrides(
overrides: dict[str, dict[str, float]],
*,
skip_validation: bool = False,
) -> None
Agent-construction-safe install of pricing overrides (v8.7.1 M7).
Unlike :func:register_pricing_overrides (which treats an empty mapping
as an explicit CLEAR), this:
- SKIPS registration entirely when
overridesis empty, so a default-config agent cannot wipe a prior agent's process-wide overrides (the M7 clobber), and - WARNS ONCE when a non-empty override set REPLACES a DIFFERENT non-empty set (two agents with divergent pricing in one process — only the last install wins, violating the one-agent-per-process contract).
Non-empty installs still flow through register_pricing_overrides
(full validation) and emit_overrides_audit_log.
Source code in src/symfonic/core/observability/pricing.py
register_pricing_overrides ¶
register_pricing_overrides(
overrides: dict[str, dict[str, float]],
*,
skip_validation: bool = False,
) -> None
Install adopter-supplied pricing overrides.
v7.8.0 (Jarvio billing-pipeline ask). Each entry maps a model id
(or a strict prefix) to a complete pricing row with keys
input / output / cache_read / cache_write in USD
per 1M tokens. Applied by SymfonicAgent.__init__ from
FrameworkConfig.pricing_overrides.
Override lookup precedence is: exact match -> strict prefix match, BEFORE the built-in registry. This lets adopters either correct a stale built-in entry (exact match) or pre-empt a family-level lookup for an unreleased model (prefix match).
Partial-row overrides are rejected: a row that omits any of the
required keys raises ValueError at registration time rather
than silently defaulting to 0.0 at compute time. All values must
be non-negative numbers.
v7.20.0 (architect verdict 2026-06-05, pricing-registry hardening
§4): adds sanity bounds on input / output rates --
$1000/M (unit-confusion class) or < $0.0001/M (typo class) rejected at registration. Cache rates are exempted; they can be 0.0 for non-caching providers and can vary widely (Anthropic 1h multipliers, DashScope no-cache). Adopters with legitimate outlier rates (specialty embeddings, audio-token billing extras) pass
skip_validation=Trueand own the consequences -- required-key and non-negative checks STILL fire even with skip_validation set.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
overrides
|
dict[str, dict[str, float]]
|
Mapping of model-id-or-prefix to pricing row. Empty mapping is a no-op (clears any prior overrides). |
required |
skip_validation
|
bool
|
When True, bypasses the input/output sanity bounds. Required-key check, type check, and non-negative check are unaffected (these are footgun guards, not unit guards). Default False. |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
When any row omits a required key, carries a
negative / non-numeric value, or (unless
|
Source code in src/symfonic/core/observability/pricing.py
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 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | |