Spaces:
Sleeping
Sleeping
File size: 7,160 Bytes
5db30ac | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | """Optional spending limits for one run, shared with the basic-education runtime subprocess.
Nothing here is active unless a caller installs limits with ``spend_limits``.
The CLI never does, so a normal benchmark run behaves exactly as before.
Each paid request appends one line to a JSONL ledger, so the main process and the
runtime subprocess (which inherits the two environment variables below) count
against the same totals. Requests are checked before they are sent, so a limit
can be overshot by the requests already in flight.
"""
from __future__ import annotations
import json
import os
import threading
from contextlib import contextmanager
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Iterator
LEDGER_ENV = "ELBENCH_SPEND_LEDGER"
LIMITS_ENV = "ELBENCH_SPEND_LIMITS"
ROLE_LABELS = {
"target": "model under test",
"judge": "judge",
"student": "simulated student",
}
class BudgetExceeded(RuntimeError):
"""Raised instead of sending a request once a spending limit is reached.
``retryable = False`` keeps ``BenchmarkRunner`` from retrying it, and the message
says "quota exceeded" so the runner stops the way it does when a key runs out of
credit: finished items are kept and the run can be resumed with a higher limit.
"""
retryable = False
@dataclass
class SpendLimits:
total_usd: float | None = None
per_role_usd: dict[str, float] = field(default_factory=dict)
# USD per token, (prompt, completion), used when a response carries no cost.
prices: dict[str, tuple[float, float]] = field(default_factory=dict)
roles_by_model_id: dict[str, str] = field(default_factory=dict)
def to_json(self) -> str:
return json.dumps(
{
"total_usd": self.total_usd,
"per_role_usd": self.per_role_usd,
"prices": {role: list(pair) for role, pair in self.prices.items()},
"roles_by_model_id": self.roles_by_model_id,
}
)
@classmethod
def from_json(cls, raw: str) -> "SpendLimits":
data = json.loads(raw)
return cls(
total_usd=data.get("total_usd"),
per_role_usd={k: float(v) for k, v in (data.get("per_role_usd") or {}).items()},
prices={k: (float(v[0]), float(v[1])) for k, v in (data.get("prices") or {}).items()},
roles_by_model_id=dict(data.get("roles_by_model_id") or {}),
)
class SpendLedger:
def __init__(self, path: Path, limits: SpendLimits) -> None:
self.path = path
self.limits = limits
self._lock = threading.Lock()
self._offset = 0
self._spent: dict[str, float] = {}
def spent(self) -> dict[str, float]:
with self._lock:
self._read_new_lines()
return dict(self._spent)
def check(self, role: str) -> None:
spent = self.spent()
total = sum(spent.values())
limit = self.limits.total_usd
if limit is not None and total >= limit:
raise BudgetExceeded(
f"Spending limit reached (quota exceeded): ${total:.4f} spent of the ${limit:.2f} total limit."
)
role_limit = self.limits.per_role_usd.get(role)
if role_limit is not None and spent.get(role, 0.0) >= role_limit:
raise BudgetExceeded(
f"Spending limit reached (quota exceeded): ${spent.get(role, 0.0):.4f} spent on the "
f"{ROLE_LABELS.get(role, role)} of its ${role_limit:.2f} limit."
)
def record(self, role: str, usage: Any) -> float:
cost = usage_cost(usage, self.limits.prices.get(role))
if cost <= 0:
return 0.0
line = json.dumps({"role": role, "usd": cost}) + "\n"
with self._lock:
with self.path.open("a", encoding="utf-8") as handle:
handle.write(line)
return cost
def _read_new_lines(self) -> None:
if not self.path.exists():
return
with self.path.open("rb") as handle:
handle.seek(self._offset)
chunk = handle.read()
end = chunk.rfind(b"\n")
if end < 0:
return
self._offset += end + 1
for raw in chunk[: end + 1].splitlines():
try:
entry = json.loads(raw)
except json.JSONDecodeError:
continue
role = str(entry.get("role") or "")
self._spent[role] = self._spent.get(role, 0.0) + float(entry.get("usd") or 0.0)
def usage_cost(usage: Any, price: tuple[float, float] | None) -> float:
"""USD for one response: OpenRouter's reported ``cost`` when present, else tokens x price."""
if not isinstance(usage, dict):
return 0.0
cost = usage.get("cost")
if isinstance(cost, (int, float)) and not isinstance(cost, bool) and cost >= 0:
return float(cost)
if price is None:
return 0.0
prompt_tokens = usage.get("prompt_tokens", usage.get("input_tokens")) or 0
completion_tokens = usage.get("completion_tokens", usage.get("output_tokens")) or 0
try:
return float(prompt_tokens) * price[0] + float(completion_tokens) * price[1]
except (TypeError, ValueError):
return 0.0
_ACTIVE: SpendLedger | None = None
_ACTIVE_LOCK = threading.Lock()
def active_ledger() -> SpendLedger | None:
"""The installed ledger, or one rebuilt from the environment inside the runtime subprocess."""
global _ACTIVE
if _ACTIVE is not None:
return _ACTIVE
path = os.environ.get(LEDGER_ENV)
raw = os.environ.get(LIMITS_ENV)
if not path or not raw:
return None
with _ACTIVE_LOCK:
if _ACTIVE is None:
_ACTIVE = SpendLedger(Path(path), SpendLimits.from_json(raw))
return _ACTIVE
def role_for_model(model_id: str | None) -> str | None:
ledger = active_ledger()
if ledger is None or not model_id:
return None
return ledger.limits.roles_by_model_id.get(model_id)
def check(role: str | None) -> None:
ledger = active_ledger()
if ledger is not None and role:
ledger.check(role)
def record(role: str | None, usage: Any) -> None:
ledger = active_ledger()
if ledger is not None and role:
ledger.record(role, usage)
@contextmanager
def spend_limits(limits: SpendLimits, ledger_path: Path) -> Iterator[SpendLedger]:
"""Install ``limits`` for this process and any subprocess it starts."""
global _ACTIVE
ledger_path.parent.mkdir(parents=True, exist_ok=True)
ledger_path.touch(exist_ok=True)
ledger = SpendLedger(ledger_path, limits)
previous = {name: os.environ.get(name) for name in (LEDGER_ENV, LIMITS_ENV)}
os.environ[LEDGER_ENV] = str(ledger_path)
os.environ[LIMITS_ENV] = limits.to_json()
with _ACTIVE_LOCK:
_ACTIVE = ledger
try:
yield ledger
finally:
with _ACTIVE_LOCK:
_ACTIVE = None
for name, value in previous.items():
if value is None:
os.environ.pop(name, None)
else:
os.environ[name] = value
|