|
6 | 6 |
|
7 | 7 | from __future__ import annotations |
8 | 8 |
|
| 9 | +import json |
9 | 10 | import logging |
10 | 11 | import time |
11 | 12 | from dataclasses import dataclass, field |
| 13 | +from pathlib import Path |
12 | 14 |
|
13 | 15 | from pydantic import BaseModel, Field |
14 | 16 |
|
|
20 | 22 | # Anthropic |
21 | 23 | "claude-haiku-4-5-20251001": (0.80, 4.00), |
22 | 24 | "claude-sonnet-4-5-20250929": (3.00, 15.00), |
23 | | - "claude-opus-4-6": (15.00, 75.00), |
| 25 | + "claude-opus-4-6": (5.00, 25.00), |
24 | 26 | # OpenAI |
25 | 27 | "gpt-4o": (2.50, 10.00), |
26 | 28 | "gpt-4o-mini": (0.15, 0.60), |
@@ -217,6 +219,25 @@ def cache_hit_rate(self) -> float: |
217 | 219 | return 0.0 |
218 | 220 | return self._cache_read_tokens / total |
219 | 221 |
|
| 222 | + def summary(self) -> dict: |
| 223 | + """Return a dict summarizing all budget and cache metrics.""" |
| 224 | + return { |
| 225 | + "project_spend": self._project_spend, |
| 226 | + "project_cap": self.per_project_cap, |
| 227 | + "budget_remaining": self.budget_remaining, |
| 228 | + "spend_percentage": self.spend_percentage, |
| 229 | + "daily_spend": self.daily_spend, |
| 230 | + "tokens_in": self._project_tokens_in, |
| 231 | + "tokens_out": self._project_tokens_out, |
| 232 | + "cache_creation_tokens": self._cache_creation_tokens, |
| 233 | + "cache_read_tokens": self._cache_read_tokens, |
| 234 | + "cache_hit_rate": self.cache_hit_rate, |
| 235 | + } |
| 236 | + |
| 237 | + def as_dict(self) -> dict: |
| 238 | + """Alias for summary().""" |
| 239 | + return self.summary() |
| 240 | + |
220 | 241 |
|
221 | 242 | class PhaseBudget(BaseModel): |
222 | 243 | """Budget tracking broken down by pipeline phase.""" |
@@ -270,3 +291,39 @@ def from_config(cls, shaping_budget_pct: float = 0.15) -> "PhaseBudget": |
270 | 291 | if shaping_budget_pct > 0: |
271 | 292 | caps["shape"] = shaping_budget_pct |
272 | 293 | return cls(phase_caps=caps) |
| 294 | + |
| 295 | + |
| 296 | +# ── External pricing file ───────────────────────────────────────── |
| 297 | + |
| 298 | + |
| 299 | +DEFAULT_PRICING_PATH = Path("~/.config/pact/model_pricing.json").expanduser() |
| 300 | + |
| 301 | + |
| 302 | +def load_pricing_file(path: Path = DEFAULT_PRICING_PATH) -> dict[str, tuple[float, float]]: |
| 303 | + """Load model pricing from a JSON file. |
| 304 | +
|
| 305 | + File format: {"model_id": [input_cost, output_cost], ...} |
| 306 | + Returns dict suitable for set_model_pricing_table(). |
| 307 | + """ |
| 308 | + data = json.loads(path.read_text()) |
| 309 | + result: dict[str, tuple[float, float]] = {} |
| 310 | + for model_id, costs in data.items(): |
| 311 | + if isinstance(costs, list) and len(costs) == 2: |
| 312 | + result[model_id] = (float(costs[0]), float(costs[1])) |
| 313 | + return result |
| 314 | + |
| 315 | + |
| 316 | +def save_pricing_file( |
| 317 | + path: Path = DEFAULT_PRICING_PATH, |
| 318 | + pricing: dict[str, tuple[float, float]] | None = None, |
| 319 | +) -> Path: |
| 320 | + """Save pricing table to a JSON file. Defaults to current active pricing. |
| 321 | +
|
| 322 | + Returns the path written to. |
| 323 | + """ |
| 324 | + if pricing is None: |
| 325 | + pricing = get_model_pricing_table() |
| 326 | + path.parent.mkdir(parents=True, exist_ok=True) |
| 327 | + data = {model: list(costs) for model, costs in sorted(pricing.items())} |
| 328 | + path.write_text(json.dumps(data, indent=2) + "\n") |
| 329 | + return path |
0 commit comments