rosdiff / api /budget.py
Chandrakiran-08
Deploy to HF Spaces
8501103
Raw History Blame Contribute Delete
5.59 kB
"""The rupee budget for AI usage: one shared pool for the test group, an equal allocation per tester.
Every model call is recorded (api.logstore `usage`). A call is billable when a provider charges for it;
OpenRouter-exempt calls (free models, or anything OpenRouter reports at zero cost) are recorded with the
reason and never deducted. GPU/CPU jobs are not part of this pool: they run on Modal's free credit and have
their own dollar limit (api.jobs, MONTHLY_CREDIT_LIMIT_USD).
Before an AI task runs, its estimate is checked against the tester's remaining allocation and the pool; after
it runs, the actual cost (as OpenRouter reports it, else computed from list prices) is what gets deducted.
"""
from __future__ import annotations
from dataclasses import dataclass
from .routing import ModelSpec, call_cost_usd
CATEGORIES = ("coding", "simulation_ai", "debugging", "testing")
class BudgetError(Exception):
def __init__(self, status: int, message: str, detail: dict | None = None):
super().__init__(message)
self.status = status
self.detail = detail or {}
@dataclass
class CallCost:
cost_usd: float
billable: bool
exempt_reason: str | None
estimated: bool
def cost_of_call(model: ModelSpec, usage: dict) -> CallCost:
"""What one model call cost, from the provider's own figure when it reports one."""
tin = int(usage.get("prompt_tokens") or usage.get("input_tokens") or 0)
tout = int(usage.get("completion_tokens") or usage.get("output_tokens") or 0)
reported = usage.get("cost")
if model.free:
return CallCost(0.0, False, "opencode_zen_free", False)
if model.id.endswith(":free"):
return CallCost(0.0, False, "free model (no OpenRouter charge)", False)
if reported is not None:
cost = float(reported)
if cost <= 0:
return CallCost(0.0, False, "OpenRouter reported no charge", False)
return CallCost(cost, True, None, False)
return CallCost(call_cost_usd(model, tin, tout), True, None, True)
def record_call(logs, settings, *, tester, generation_id, category, model: ModelSpec, usage: dict, estimate_inr=None):
c = cost_of_call(model, usage)
if tester == "owner":
c.billable = False
c.exempt_reason = "owner key usage"
logs.record_usage(
tester=tester,
generation_id=generation_id,
category=category,
model=model.id,
tier=model.tier,
provider="opencode-zen"
if model.free
else ("openrouter" if "openrouter" in settings.llm_base_url else settings.llm_base_url),
input_tokens=int(usage.get("prompt_tokens") or usage.get("input_tokens") or 0),
output_tokens=int(usage.get("completion_tokens") or usage.get("output_tokens") or 0),
cost_usd=round(c.cost_usd, 6),
cost_inr=round(c.cost_usd * settings.usd_inr, 4),
billable=int(c.billable),
exempt_reason=c.exempt_reason,
estimated=int(c.estimated),
estimate_inr=estimate_inr,
)
def summary(logs, settings, tester: str | None = None) -> dict:
"""Used / remaining for one tester (their allocation) and for the whole pool, split by category."""
rows = logs.usage_rows()
mine = [r for r in rows if tester is None or r["tester"] == tester]
billable = [r for r in mine if r["billable"]]
by_cat = {c: round(sum(r["cost_inr"] for r in billable if r["category"] == c), 2) for c in CATEGORIES}
used = round(sum(r["cost_inr"] for r in billable), 2)
pool_used = round(sum(r["cost_inr"] for r in rows if r["billable"]), 2)
return {
"currency": "INR",
"allocation": settings.budget_per_user_inr,
"used": used,
"remaining": round(max(settings.budget_per_user_inr - used, 0.0), 2),
"by_category": by_cat,
"exempt_calls": sum(1 for r in mine if not r["billable"]),
"calls": len(mine),
"pool": {
"total": settings.budget_total_inr,
"used": pool_used,
"remaining": round(max(settings.budget_total_inr - pool_used, 0.0), 2),
},
}
def team(logs, settings, testers: list[str]) -> list[dict]:
rows = logs.usage_rows()
out = []
for name in sorted(set(testers) | {r["tester"] for r in rows}):
if name == "owner":
continue
used = round(sum(r["cost_inr"] for r in rows if r["tester"] == name and r["billable"]), 2)
out.append(
{
"tester": name,
"allocation": settings.budget_per_user_inr,
"used": used,
"remaining": round(max(settings.budget_per_user_inr - used, 0.0), 2),
}
)
return out
def check(logs, settings, tester: str, estimate: tuple[float, float], confirmed: bool) -> None:
"""Refuse when the tester's allocation or the pool cannot cover the likely cost; ask above the threshold."""
if tester == "owner":
return
s = summary(logs, settings, tester)
likely, worst = estimate
if s["pool"]["remaining"] < likely:
raise BudgetError(402, "The team's AI budget is used up. Please tell the Orcanion team.", {"budget": s})
if s["remaining"] < likely:
raise BudgetError(402, "Your AI budget is used up. Please tell the Orcanion team.", {"budget": s})
if worst >= settings.confirm_above_inr and not confirmed:
raise BudgetError(
409,
"This task may cost more than usual; confirm to run it.",
{"needs_confirmation": True, "estimate_inr": list(estimate), "budget": s},
)