6Per-turn USD cost, computed in the render lane and honesty-gated — plan 188 T4,
7re-priced by :mod:`oc_pricing` (plan 202).
11The synthesis asks for a per-turn ``$`` line beside the token gauges.
12Everything needed already reaches the render lane: ``run_turn``
13accumulates the provider's own cache-hit / cache-miss / completion token
14split per iteration, and the **effective-dated, TOU-aware price table
15now ships as the standalone** :mod:`oc_pricing` **package** (plans
16206/210/211) — the one authoritative pricing source every Option C
17client imports. This module joins the two. No pricing data is invented
18here — if the package cannot price a sample *at the sample's instant*,
21The governing rule (plan 188 T4): **no line is better than a false
22line.** Every gate below prefers silence over a plausible number. Plan
23202 adds the converse hazard: a misfiring timestamp would make EVERY
24sample unpriceable — a silent pricing blackout that reads as honest
25"unknown" while hiding real spend. The pricing instant therefore
26arrives as an explicit, required ``at`` captured once per sample at the
27send seam (:mod:`oc_chat.core.react_loop`'s ``_now_utc``), never read
32- :class:`TurnCost` — the immutable result handed to presenters:
33 the figure plus the provenance that makes it auditable.
34- :func:`price_usage` — price ONE usage sample at ONE instant, or
35 ``None`` when any input makes the answer a guess.
36- :class:`TurnCostAccumulator` — fold a turn's samples exactly
37 (Decimal); one unpriceable sample suppresses the whole turn.
39Why per-sample rather than per-turn: a turn's iterations each carry
40their own model (``route.model``), their own cache split, and — now —
41their own pricing instant, and a sub-loop folds in usage of its own.
42Pricing each sample at the model AND tariff that actually produced it
43is exact; pricing the turn's summed tokens at one rate would silently
44mis-rate a routed turn or a turn straddling a tariff boundary.
50 L2 — a turn suppressed as unpriceable.
51 L3 — per-sample gate rejections.
52 L4 — module-load coverage marker.
56- ``price_usage`` returns ``None`` — never 0.0 as a stand-in — when:
57 the package has no rate for (provider, model) in force at ``at``; the
58 reported cache split does not ACCOUNT for the prompt
59 (``hit + miss != prompt_tokens``, so the input side is unknown —
60 costing it all as a miss would overstate); there are no tokens at
61 all; or ``at`` is ``None``/naive (oc_pricing prices a time-dependent
62 row only from an aware instant — a guessed tariff would be a wrong
64- Usage is reduced to :class:`oc_pricing.CanonicalUsage` DIRECTLY from
65 OC_Chat's own ``(hit, miss)`` extraction — NEVER via
66 ``oc_pricing.usage_from_deepseek``. That reducer reads only the
67 explicit ``prompt_cache_hit_tokens``/``prompt_cache_miss_tokens``
68 fields, but real DeepSeek-via-OpenAI-SDK usage reports cache via
69 ``prompt_tokens_details.cached_tokens`` — routing live usage through
70 it would price EVERY live turn to ``None`` (plan 202 decision 2).
71- A turn is priced only if EVERY sample priced. A partial total is a
72 wrong number, not a smaller one.
73- The turn total is accumulated as exact ``Decimal`` from
74 ``oc_pricing.breakdown_usd`` and quantized ONCE at :meth:`result` —
75 never a sum of per-sample floats. This exactness claim is the live
76 ``$`` line's alone; the event projection still folds the float
77 ``cost_usd`` published on the bus (plan 202 residual).
78- One ``at`` prices a whole sample; a turn straddling a TOU boundary
79 prices each sample at its own send instant (deliberate, observable
80 approximation — plan 202 residual).
81- Nothing here publishes an event or mutates session state.
85- oc_diagnostics (structured debug logging via _dbg)
86- oc_pricing (the one authoritative effective-dated, TOU-aware table)
88from __future__
import annotations
90from dataclasses
import dataclass
91from datetime
import datetime
92from decimal
import ROUND_HALF_EVEN, Context, Decimal, localcontext
93from typing
import Optional
95from oc_diagnostics
import _dbg
98from oc_pricing
import CanonicalUsage, RateQuote
100_dbg(
"OC_CHAT.MAIN",
"turn_cost",
"module", 4)
106MIXED_MODEL: str =
"mixed"
112_MONEY_CONTEXT = Context(prec=28, rounding=ROUND_HALF_EVEN)
116_USD_QUANTUM = Decimal(
"0.000001")
119@dataclass(frozen=True, slots=True)
121 """A priced turn plus the provenance that makes the figure auditable."""
124 """Modelled spend for the turn, in USD. Always an estimate, never a bill."""
127 """The model priced, or :data:`MIXED_MODEL` when the turn spanned several."""
129 """The provider whose published rates were applied."""
131 """Vendor-verification date of the SELECTED rate version
132 (``RateQuote.verified_at``) — the figure is only as fresh as this."""
134 """Where the selected version's rates came from
135 (``RateQuote.source``), so a reader can re-check them."""
139 cache_hit_tokens: int,
140 cache_miss_tokens: int,
142 completion_tokens: int,
143) -> Optional[CanonicalUsage]:
144 """Reduce one OC_Chat usage sample to a :class:`CanonicalUsage`, or ``None``.
146 The two fail-closed gates the DIRECT build must re-establish live here,
147 because bypassing ``usage_from_deepseek`` (deliberately — see the module
148 Contracts) also bypasses that reducer's own split gate:
150 - nothing happened (all-zero) → ``None``: preserves plan 188's
151 suppression — a ``$0.000000`` line for an empty sample is noise
152 presented as a price (oc_pricing would price it 0.0).
153 - ``hit + miss != prompt`` → ``None``: the input side is priced at two
154 different rates, so an unaccounted prompt cannot be costed — charging
155 the remainder at either rate would be a guess.
157 Completion maps whole to ``out``: DeepSeek bills chain-of-thought as
158 output (the table's rows carry ``reasoning: null``), which is why the
159 pre-202 distinct-reasoning-rate gate no longer exists.
161 func =
"_canonical_sample"
162 hit = max(0, int(cache_hit_tokens
or 0))
163 miss = max(0, int(cache_miss_tokens
or 0))
164 prompt = max(0, int(prompt_tokens
or 0))
165 completion = max(0, int(completion_tokens
or 0))
167 if hit + miss == 0
and completion == 0:
170 if hit + miss != prompt:
171 _dbg(
"OC_CHAT.MAIN", func,
172 f
"cache split {hit}+{miss} does not account for prompt {prompt}", 3)
175 return CanonicalUsage(
176 uncached_in=miss, cache_read=hit, cache_write=0, out=completion,
184 cache_hit_tokens: int,
185 cache_miss_tokens: int,
187 completion_tokens: int,
188 at: Optional[datetime],
190 """Price one usage sample in USD at instant ``at``, or ``None``.
192 ``at`` is required and must be the sample's ONE captured, tz-aware
193 instant (react_loop's ``_now_utc`` seam) — the tariff in force is a
194 function of it, and today's table is effective-dated AND time-of-use
195 banded, so ``at`` changes the number in production. A ``None`` or
196 naive ``at`` yields ``None`` (unpriceable), never a guessed tariff.
198 See the module Contracts for the exact gates. The caller must treat
199 ``None`` as "suppress the line", never as zero.
203 cache_hit_tokens, cache_miss_tokens, prompt_tokens, completion_tokens,
207 usd = oc_pricing.cost_usd(usage, provider=provider, model=model, at=at)
209 _dbg(
"OC_CHAT.MAIN", func,
210 f
"no rate in force for {provider}/{model} at={at!r}", 3)
215 """Folds a turn's usage samples into one :class:`TurnCost`, or nothing.
217 Each :meth:`add` prices at the model AND instant that actually produced
218 the sample. The first sample that cannot be priced latches the turn
219 unpriceable — reporting the priced subset would understate the turn
220 while looking authoritative, which is the exact failure mode plan 188
223 The running total is an exact ``Decimal`` sum of per-sample
224 ``breakdown_usd(...)["total"]`` values, quantized once at
225 :meth:`result` — the 200-sample fold and the one-aggregate figure are
226 equal by construction (the live ``$`` line's exactness win; the event
227 projection is NOT covered by this claim).
230 __slots__ = (
"_total",
"_models",
"_provider",
"_priceable",
"_samples",
246 cache_hit_tokens: int,
247 cache_miss_tokens: int,
249 completion_tokens: int,
250 at: Optional[datetime],
252 """Fold one usage sample priced at ``at``. No-op once unpriceable."""
256 cache_hit_tokens, cache_miss_tokens, prompt_tokens, completion_tokens,
261 breakdown = oc_pricing.breakdown_usd(
262 usage, provider=provider, model=model, at=at,
264 if breakdown
is None:
271 self.
_last_quote = oc_pricing.rate_at(provider, model, at=at)
272 with localcontext(_MONEY_CONTEXT):
273 self.
_total += breakdown[
"total"]
279 """The turn's cost, or ``None`` when any part of it was unpriceable."""
282 _dbg(
"OC_CHAT.MAIN", func,
"turn not priceable; no cost line", 2)
287 model = next(iter(self.
_models))
if len(self.
_models) == 1
else MIXED_MODEL
288 with localcontext(_MONEY_CONTEXT):
289 total = float(self.
_total.quantize(_USD_QUANTUM))
294 as_of=quote.verified_at
or "",
295 source=quote.source
or "",
299__all__ = [
"MIXED_MODEL",
"TurnCost",
"TurnCostAccumulator",
"price_usage"]
TurnCost|None result(self)
Optional[RateQuote] _last_quote
None add(self, str provider, str model, *, int cache_hit_tokens, int cache_miss_tokens, int prompt_tokens, int completion_tokens, Optional[datetime] at)
float|None price_usage(str provider, str model, *, int cache_hit_tokens, int cache_miss_tokens, int prompt_tokens, int completion_tokens, Optional[datetime] at)
Optional[CanonicalUsage] _canonical_sample(int cache_hit_tokens, int cache_miss_tokens, int prompt_tokens, int completion_tokens)