fix(discord-bridge): plafonul de cost e optional (COST_CAP_USD_DAY=off)
Costul raportat de CLI e pretul echivalent la API; pe abonament nu se factureaza, deci plafonul zilnic oprea puntea fara motiv (5.71 / 5.00 USD). - limits.parse_cap(): `off`/`none`/`nelimitat`/`0`/gol/gunoi => fara plafon - stopped()/record_cost() nu mai opresc si nu mai alerteaza cand e dezactivat - implicit devine `off`; /status arata „(fara plafon)" - dashboard: /api/status si doctor nu mai crapa pe valoare ne-numerica - README + ops/env.example explica de ce ramane off pe abonament Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
This commit is contained in:
@@ -37,6 +37,24 @@ class RateLimited(LimitError):
|
||||
super().__init__(f"prea multe mesaje; mai asteapta {retry_after:.0f}s")
|
||||
|
||||
|
||||
def parse_cap(raw, default: float = 0.0) -> float:
|
||||
"""Plafonul zilnic in USD. `off`/`none`/`nelimitat`/`0` => 0.0 (fara plafon).
|
||||
|
||||
Costul raportat de CLI e pretul echivalent la API; pe abonament nu se
|
||||
factureaza, deci plafonul are sens doar cand chiar platesti per token.
|
||||
"""
|
||||
if raw is None:
|
||||
return default
|
||||
text = str(raw).strip().lower()
|
||||
if text in ("", "off", "none", "no", "nu", "false", "unlimited", "nelimitat"):
|
||||
return 0.0
|
||||
try:
|
||||
val = float(text)
|
||||
except ValueError:
|
||||
return default
|
||||
return val if val > 0 else 0.0
|
||||
|
||||
|
||||
class CostCapReached(LimitError):
|
||||
def __init__(self, spent: float, cap: float):
|
||||
self.spent, self.cap = spent, cap
|
||||
@@ -63,7 +81,10 @@ class Limits:
|
||||
self.rate_per_min = (
|
||||
rate_per_min if rate_per_min is not None else config.get_int("RATE_PER_USER_PER_MIN", 10)
|
||||
)
|
||||
self.cost_cap = cost_cap if cost_cap is not None else config.get_float("COST_CAP_USD_DAY", 10.0)
|
||||
self.cost_cap = (
|
||||
parse_cap(cost_cap) if cost_cap is not None
|
||||
else parse_cap(config.get("COST_CAP_USD_DAY"), 0.0)
|
||||
)
|
||||
self.clock = clock
|
||||
self._alert = alerter or alerts.alert
|
||||
self._slots = asyncio.Semaphore(self.max_procs)
|
||||
@@ -115,11 +136,18 @@ class Limits:
|
||||
return float(self.store.cost_today())
|
||||
return self._local_cost
|
||||
|
||||
@property
|
||||
def cap_enabled(self) -> bool:
|
||||
"""Plafon dezactivat (0 sau `off`) — cazul abonamentului."""
|
||||
return self.cost_cap > 0
|
||||
|
||||
def stopped(self) -> bool:
|
||||
"""Plafonul e evaluat pe ziua curenta; reset-ul zilnic vine din `cost.day`."""
|
||||
return self.cost_today() >= self.cost_cap
|
||||
return self.cap_enabled and self.cost_today() >= self.cost_cap
|
||||
|
||||
def cost_remaining(self) -> float:
|
||||
if not self.cap_enabled:
|
||||
return float("inf")
|
||||
return max(0.0, self.cost_cap - self.cost_today())
|
||||
|
||||
def check_cost(self) -> None:
|
||||
@@ -136,7 +164,7 @@ class Limits:
|
||||
else:
|
||||
self._local_cost = round(self._local_cost + usd, 6)
|
||||
total = self._local_cost
|
||||
if total >= self.cost_cap and not self._cap_alerted:
|
||||
if self.cap_enabled and total >= self.cost_cap and not self._cap_alerted:
|
||||
self._cap_alerted = True
|
||||
with contextlib.suppress(Exception):
|
||||
self._alert(
|
||||
@@ -145,7 +173,7 @@ class Limits:
|
||||
f"Cheltuit azi: {total:.2f} USD, plafon {self.cost_cap:.2f}. Puntea nu mai accepta tururi.",
|
||||
"cost-cap",
|
||||
)
|
||||
elif total < self.cost_cap:
|
||||
elif not self.cap_enabled or total < self.cost_cap:
|
||||
self._cap_alerted = False
|
||||
return total
|
||||
|
||||
|
||||
Reference in New Issue
Block a user