oc_chat
Loading...
Searching...
No Matches
prices.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/benchmarks/prices.py
4
5"""
6Dated, provenance-stamped price table + cost computation.
7
8Purpose
9-------
10Tokens are the source of truth; ``$`` is a best-effort convenience
11layer on top (plan 136 ADR-136-3). This module loads a ``prices.json``
12that carries an ``as_of`` capture date and a ``source`` URL, and turns
13a token split into dollars — costing cache **hit** and **miss** at
14their distinct rates (never summed). When a model has no entry, callers
15fall back to tokens-only.
16
17The ``source`` URL lives in the data file, not in code, so a moved
18pricing page is a one-field edit; ``--prices <path>`` (the CLI) points
19at an alternate table entirely.
20
21Responsibilities
22----------------
23- :class:`PriceTable` — loaded rates + provenance, with
24 :meth:`cost_for` returning a per-category USD breakdown or ``None``.
25- :func:`load_prices` — read + validate a ``prices.json``.
26
27Diagnostics
28-----------
29Domain: OC_CHAT.MAIN
30Levels:
31 L2 — lifecycle events
32 L3 — semantic details
33 L4 — deep tracing
34
35Contracts
36---------
37- ``load_prices`` never raises on a missing/garbage file — it returns
38 an empty table (``models == {}``) so the run degrades to tokens-only.
39- ``cost_for`` returns ``None`` when the model is absent (the report
40 then shows "no price table"); rates are per-million-tokens USD.
41- Hit and miss are costed independently; the breakdown never merges
42 them.
43
44Dependencies
45------------
46- oc_diagnostics (structured debug logging via _dbg)
47"""
48from __future__ import annotations
49
50import json
51from dataclasses import dataclass, field
52from pathlib import Path
53
54from oc_diagnostics import _dbg
55
56_dbg("OC_CHAT.MAIN", "module", "prices loaded", 4)
57
58# Bundled default table shipped with the suite.
59DEFAULT_PRICES_PATH = Path(__file__).with_name("prices.json")
60
61
62@dataclass(frozen=True)
64 """Per-(provider,model) rates with provenance.
65
66 ``models`` maps ``"<provider>/<model>"`` (and bare ``"<model>"`` as
67 a fallback key) to a rate dict with keys ``input_miss`` /
68 ``input_hit`` / ``output`` (and optional ``reasoning``), all in USD
69 per million tokens.
70 """
71
72 as_of: str = ""
73 source: str = ""
74 path: str = ""
75 models: dict = field(default_factory=dict)
76
77 def _lookup(self, provider: str, model: str) -> dict | None:
78 return self.models.get(f"{provider}/{model}") or self.models.get(model)
79
80 def has(self, provider: str, model: str) -> bool:
81 return self._lookup(provider, model) is not None
82
84 self,
85 provider: str,
86 model: str,
87 *,
88 cache_hit_tokens: int = 0,
89 cache_miss_tokens: int = 0,
90 output_tokens: int = 0,
91 reasoning_tokens: int = 0,
92 ) -> dict | None:
93 """Return a USD breakdown for the given token split, or ``None``.
94
95 Reasoning tokens are billed at the explicit ``reasoning`` rate
96 when present, else at the ``output`` rate (DeepSeek bills
97 reasoning as output). Hit and miss are costed at distinct rates.
98 """
99 rates = self._lookup(provider, model)
100 if rates is None:
101 return None
102
103 def _r(key: str, default: float = 0.0) -> float:
104 try:
105 return float(rates.get(key, default))
106 except (TypeError, ValueError):
107 return default
108
109 per_mtok = 1_000_000.0
110 hit_usd = cache_hit_tokens / per_mtok * _r("input_hit")
111 miss_usd = cache_miss_tokens / per_mtok * _r("input_miss")
112 reasoning_rate = _r("reasoning", _r("output"))
113 reasoning_usd = reasoning_tokens / per_mtok * reasoning_rate
114 # Output tokens excluding reasoning (which is billed above).
115 plain_output = max(0, output_tokens - reasoning_tokens)
116 output_usd = plain_output / per_mtok * _r("output")
117 return {
118 "input_hit_usd": round(hit_usd, 6),
119 "input_miss_usd": round(miss_usd, 6),
120 "output_usd": round(output_usd, 6),
121 "reasoning_usd": round(reasoning_usd, 6),
122 "total_usd": round(hit_usd + miss_usd + output_usd + reasoning_usd, 6),
123 }
124
125 def miss_rate(self, provider: str, model: str) -> float:
126 """The input cache-miss rate ($/Mtok) used for the cold-turn cost."""
127 rates = self._lookup(provider, model)
128 if rates is None:
129 return 0.0
130 try:
131 return float(rates.get("input_miss", 0.0))
132 except (TypeError, ValueError):
133 return 0.0
134
135
136def load_prices(path: str | Path | None = None) -> PriceTable:
137 """Load a ``prices.json`` into a :class:`PriceTable`.
138
139 Falls back to an empty table (tokens-only) on any read/parse error.
140 """
141 func = "load_prices"
142 p = Path(path) if path is not None else DEFAULT_PRICES_PATH
143 try:
144 data = json.loads(p.read_text(encoding="utf-8"))
145 except (OSError, ValueError) as exc:
146 _dbg("OC_CHAT.MAIN", func, f"no usable price table at {p}: {exc}", 2)
147 return PriceTable(path=str(p))
148 models = data.get("models")
149 if not isinstance(models, dict):
150 models = {}
151 return PriceTable(
152 as_of=str(data.get("as_of", "")),
153 source=str(data.get("source", "")),
154 path=str(p),
155 models=models,
156 )
157
158
159__all__ = ["PriceTable", "load_prices", "DEFAULT_PRICES_PATH"]
dict|None cost_for(self, str provider, str model, *, int cache_hit_tokens=0, int cache_miss_tokens=0, int output_tokens=0, int reasoning_tokens=0)
Definition prices.py:92
bool has(self, str provider, str model)
Definition prices.py:80
float miss_rate(self, str provider, str model)
Definition prices.py:125
dict|None _lookup(self, str provider, str model)
Definition prices.py:77
PriceTable load_prices(str|Path|None path=None)
Definition prices.py:136