@seanyao/roll 3.608.1 → 3.610.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,194 +0,0 @@
1
- """
2
- model_prices — list-price table for AI model API pricing.
3
-
4
- Pricing is per million tokens (MTok), in the vendor's native currency.
5
- These are the public list rates; discounts (Pro subscription, prepay
6
- credits, etc.) are intentionally not modeled — IDEA-025 is about
7
- cross-account / cross-project comparable cost.
8
-
9
- US-VIEW-013: prices are no longer hardcoded here. They live in versioned
10
- snapshot files under ``lib/prices/snapshot-YYYY-MM-DD.json`` and are loaded
11
- at module import time. ``roll prices refresh`` produces new snapshots; this
12
- module never writes — it only loads all snapshots and merges them.
13
-
14
- FIX-116: multi-vendor support — snapshots carry ``vendor`` and ``currency``
15
- fields. All snapshots are loaded and merged into a single PRICES map, with
16
- each model entry carrying its native ``currency``. Vendor-prefixed model
17
- names (``deepseek/deepseek-chat``) are resolved by stripping the vendor
18
- segment when no exact match exists.
19
-
20
- Unknown models fall back to the snapshot's ``default_model`` with a stderr
21
- warning so dashboards don't blank out.
22
- """
23
-
24
- import json
25
- import os
26
- import sys
27
- from typing import Any, Dict, List, Optional, Tuple
28
-
29
- _LIB_DIR = os.path.dirname(os.path.abspath(__file__))
30
- SNAPSHOT_DIR = os.path.join(_LIB_DIR, "prices")
31
-
32
-
33
- def list_snapshots(snapshot_dir: str = SNAPSHOT_DIR) -> List[str]:
34
- """Return absolute paths of all snapshot files, sorted oldest → newest by filename."""
35
- if not os.path.isdir(snapshot_dir):
36
- return []
37
- entries = [
38
- os.path.join(snapshot_dir, name)
39
- for name in os.listdir(snapshot_dir)
40
- if name.startswith("snapshot-") and name.endswith(".json")
41
- ]
42
- return sorted(entries)
43
-
44
-
45
- def load_snapshot(path: str) -> Dict[str, Any]:
46
- """Load a snapshot file and validate its shape."""
47
- with open(path, "r", encoding="utf-8") as f:
48
- data = json.load(f)
49
- for key in ("version", "effective_at", "source_url", "prices"):
50
- if key not in data:
51
- raise ValueError(f"snapshot {path!r} missing required key {key!r}")
52
- if not isinstance(data["prices"], dict) or not data["prices"]:
53
- raise ValueError(f"snapshot {path!r} has empty or invalid prices map")
54
- data.setdefault("default_model", next(iter(data["prices"])))
55
- data.setdefault("vendor", "anthropic")
56
- data.setdefault("currency", "USD")
57
- return data
58
-
59
-
60
- def load_all_snapshots(snapshot_dir: str = SNAPSHOT_DIR) -> List[Dict[str, Any]]:
61
- """Load all snapshots, sorted oldest → newest. Raises FileNotFoundError if none."""
62
- snaps = list_snapshots(snapshot_dir)
63
- if not snaps:
64
- raise FileNotFoundError(
65
- f"no price snapshots found in {snapshot_dir}; run `roll prices refresh`"
66
- )
67
- return [load_snapshot(p) for p in snaps]
68
-
69
-
70
- _SNAPSHOTS: List[Dict[str, Any]] = load_all_snapshots()
71
- _DEFAULT_SNAP: Dict[str, Any] = _SNAPSHOTS[-1]
72
-
73
- # Merge PRICES from all snapshots, injecting currency per model.
74
- # Later snapshots override earlier ones for the same model name.
75
- PRICES: Dict[str, Dict[str, float]] = {}
76
- _CURRENCY: Dict[str, str] = {}
77
- for _snap in _SNAPSHOTS:
78
- _snap_currency = _snap.get("currency", "USD")
79
- for _model, _rates in _snap["prices"].items():
80
- PRICES[_model] = dict(_rates)
81
- PRICES[_model]["currency"] = _snap_currency
82
- _CURRENCY[_model] = _snap_currency
83
-
84
- DEFAULT: str = _DEFAULT_SNAP["default_model"]
85
- VERSION: str = _DEFAULT_SNAP["version"]
86
- EFFECTIVE_AT: str = _DEFAULT_SNAP["effective_at"]
87
- SOURCE_URL: str = _DEFAULT_SNAP["source_url"]
88
-
89
- _warned: set = set()
90
-
91
-
92
- def snapshot_meta() -> Tuple[str, str, str]:
93
- """Return (version, effective_at, source_url) of the active snapshot."""
94
- return VERSION, EFFECTIVE_AT, SOURCE_URL
95
-
96
-
97
- def _resolve(model: Optional[str], prices: Optional[Dict[str, Dict[str, float]]] = None,
98
- default: Optional[str] = None) -> Dict[str, float]:
99
- table = prices if prices is not None else PRICES
100
- fallback = default if default is not None else DEFAULT
101
- if not model:
102
- return table[fallback]
103
- base = model.split("[")[0].rstrip("0123456789-")
104
-
105
- # Direct match: model starts with a known key
106
- candidates = [k for k in table if model.startswith(k) or base.startswith(k)]
107
- if candidates:
108
- return table[max(candidates, key=len)]
109
-
110
- # Vendor prefix: try stripping "vendor/" segment for proxy tools (pi, etc.)
111
- if "/" in model:
112
- inner = model.split("/", 1)[1]
113
- inner_base = inner.split("[")[0].rstrip("0123456789-")
114
- for k in table:
115
- if inner == k or inner_base == k or inner.startswith(k) or inner_base.startswith(k):
116
- return table[k]
117
-
118
- if model not in _warned:
119
- _warned.add(model)
120
- print(f"[model_prices] warn: unknown model {model!r}, falling back to {fallback}",
121
- file=sys.stderr)
122
- return table[fallback]
123
-
124
-
125
- def _resolve_name(model: Optional[str],
126
- prices: Optional[Dict[str, Dict[str, float]]] = None,
127
- default: Optional[str] = None) -> str:
128
- """Return the canonical model name (key in PRICES) for a given model string.
129
-
130
- Same resolution logic as _resolve, but returns the matched key name
131
- instead of the rate dict. Used by currency_for() to find the currency.
132
- """
133
- table = prices if prices is not None else PRICES
134
- fallback = default if default is not None else DEFAULT
135
- if not model:
136
- return fallback
137
- base = model.split("[")[0].rstrip("0123456789-")
138
-
139
- # Direct match: model starts with a known key
140
- candidates = [k for k in table if model.startswith(k) or base.startswith(k)]
141
- if candidates:
142
- return max(candidates, key=len)
143
-
144
- # Vendor prefix: try stripping "vendor/" segment
145
- if "/" in model:
146
- inner = model.split("/", 1)[1]
147
- inner_base = inner.split("[")[0].rstrip("0123456789-")
148
- for k in table:
149
- if inner == k or inner_base == k or inner.startswith(k) or inner_base.startswith(k):
150
- return k
151
-
152
- return fallback
153
-
154
-
155
- _NO_CURRENCY_MATCH = "\x00__no_currency_match__\x00"
156
-
157
-
158
- def currency_for(model: Optional[str]) -> str:
159
- """Return the native currency code (USD/CNY) for a model.
160
-
161
- Falls back to 'USD' when the model isn't in any snapshot. FIX-162: resolve
162
- with a sentinel default so a *genuinely unknown* model returns USD instead
163
- of inheriting the global DEFAULT model's currency (which is a CNY kimi
164
- entry — that would mislabel unrelated unknown models as CNY).
165
- """
166
- name = _resolve_name(model, default=_NO_CURRENCY_MATCH)
167
- if name == _NO_CURRENCY_MATCH:
168
- return "USD"
169
- return _CURRENCY.get(name, "USD")
170
-
171
-
172
- def compute_list_cost(model: Optional[str],
173
- *,
174
- input_tokens: int = 0,
175
- output_tokens: int = 0,
176
- cache_creation_tokens: int = 0,
177
- cache_read_tokens: int = 0,
178
- prices: Optional[Dict[str, Dict[str, float]]] = None,
179
- default: Optional[str] = None) -> float:
180
- """Return cost (in native currency) at list price for one cycle's token usage."""
181
- p = _resolve(model, prices=prices, default=default)
182
- total = (input_tokens * p["in"]
183
- + output_tokens * p["out"]
184
- + cache_creation_tokens * p["cache_create"]
185
- + cache_read_tokens * p["cache_read"]) / 1_000_000
186
- return round(total, 4)
187
-
188
-
189
- def total_tokens(*,
190
- input_tokens: int = 0,
191
- output_tokens: int = 0,
192
- cache_creation_tokens: int = 0,
193
- cache_read_tokens: int = 0) -> int:
194
- return int(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens)