@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.
- package/CHANGELOG.md +95 -0
- package/README.md +6 -5
- package/dist/roll.mjs +21533 -14095
- package/package.json +1 -2
- package/skills/roll-build/SKILL.md +22 -10
- package/skills/roll-design/SKILL.md +3 -2
- package/skills/roll-fix/SKILL.md +18 -7
- package/bin/roll +0 -15361
- package/lib/backfill-pi-usage.py +0 -243
- package/lib/changelog_audit.py +0 -149
- package/lib/changelog_generate.py +0 -470
- package/lib/consistency_check.py +0 -409
- package/lib/context_feed_budget.sh +0 -194
- package/lib/github_sync.py +0 -876
- package/lib/i18n.sh +0 -211
- package/lib/loop-exit-summary.py +0 -393
- package/lib/loop-fmt.py +0 -589
- package/lib/loop_pick_agent.py +0 -316
- package/lib/loop_result_eval.py +0 -469
- package/lib/loop_unstick.py +0 -180
- package/lib/model_prices.py +0 -194
- package/lib/prices_fetcher.py +0 -534
- package/lib/roll-backlog.py +0 -225
- package/lib/roll-brief.py +0 -286
- package/lib/roll-help.py +0 -158
- package/lib/roll-home.py +0 -556
- package/lib/roll-init.py +0 -156
- package/lib/roll-loop-status.py +0 -1691
- package/lib/roll-loop-story.py +0 -191
- package/lib/roll-peer.py +0 -252
- package/lib/roll-setup.py +0 -102
- package/lib/roll-status.py +0 -367
- package/lib/roll_git.py +0 -41
- package/lib/roll_render.py +0 -414
- package/lib/slides-render.py +0 -778
- package/lib/slides-validate.py +0 -357
- package/lib/test_quality_gate.py +0 -143
package/lib/model_prices.py
DELETED
|
@@ -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)
|