Source code for cadrumo.adapters.outbound.llm.pricing

"""Provider pricing helpers for cost estimation.

Holds the public per-million-token pricing table for each supported
:class:`llm.LLMProvider` and exposes
:func:`estimate_cost_usd` so :class:`llm.LLMClient`
can stamp every :class:`llm.LLMResponse` with a
deterministic USD cost estimate. The local provider always reports zero.
"""

from __future__ import annotations

from decimal import ROUND_HALF_UP, Decimal

from ....core.config_support import LLMProvider
from ....core.decimal.constants import ZERO

_PRICING_PER_MILLION: tuple[tuple[LLMProvider, str, Decimal, Decimal], ...] = (
    (LLMProvider.ANTHROPIC, "claude-opus-4-6", Decimal("5.00"), Decimal("25.00")),
    (LLMProvider.ANTHROPIC, "claude-sonnet-4-6", Decimal("3.00"), Decimal("15.00")),
    (LLMProvider.ANTHROPIC, "claude-opus-4-5", Decimal("5.00"), Decimal("25.00")),
    (LLMProvider.ANTHROPIC, "claude-sonnet-4-5", Decimal("3.00"), Decimal("15.00")),
    (LLMProvider.OPENAI, "gpt-4.1", Decimal("2.00"), Decimal("8.00")),
    (LLMProvider.OPENAI, "gpt-4o", Decimal("2.50"), Decimal("10.00")),
    (LLMProvider.GEMINI, "gemini-2.5-pro", Decimal("1.25"), Decimal("10.00")),
    (LLMProvider.GEMINI, "gemini-2.5-flash", Decimal("0.30"), Decimal("2.50")),
)
_TOKEN_SCALE = Decimal("1000000")
_QUANTIZE = Decimal("0.000001")


[docs] def estimate_cost_usd(provider: LLMProvider, model: str, input_tokens: int, output_tokens: int) -> Decimal | None: """Estimate request cost in USD from known public pricing, or refuse. Looks up the first pricing entry whose ``model`` field is a prefix of the lowercased ``model`` argument so model variants (versioned suffixes, dated minor releases) inherit their family's rates. **A model this table does not price returns ``None``, never zero.** It used to return zero so every usage record carried a well-typed ``Decimal``, and that is exactly how a cost surface reports an absence as a positive answer: a caller budgeting from ``$0.0000`` concludes the call was free rather than unpriced. The table is a snapshot of public rates and goes stale by construction, so the models it does not carry are a permanent, moving population -- widening it cannot fix this and re-introduces the silent zero for whatever is added next. **Free and unpriceable stay distinct.** The local provider genuinely costs nothing and keeps returning ``Decimal('0')``. Collapsing the two would put the defect back under a different name. Args: provider: The :class:`llm.LLMProvider` that produced the response. model: Provider-resolved model identifier (e.g. ``"claude-sonnet-4-6-20260101"``). input_tokens: Prompt-side token count. output_tokens: Completion-side token count. Returns: Estimated cost in USD, quantised to 6 decimal places with :data:`decimal.ROUND_HALF_UP` -- half away from zero, matching how this tree rounds money everywhere else, and distinct from banker's rounding (:data:`decimal.ROUND_HALF_EVEN`), which resolves a tie to the even digit instead; :data:`decimal.Decimal('0')` for the local provider, which is free rather than unpriced; and ``None`` when no table entry prices this provider and model. """ if provider is LLMProvider.LOCAL: return ZERO normalized_model = model.lower() for known_provider, prefix, input_rate, output_rate in _PRICING_PER_MILLION: if provider is known_provider and normalized_model.startswith(prefix): input_cost = (Decimal(input_tokens) / _TOKEN_SCALE) * input_rate output_cost = (Decimal(output_tokens) / _TOKEN_SCALE) * output_rate return (input_cost + output_cost).quantize(_QUANTIZE, rounding=ROUND_HALF_UP) return None