From feb31eaabe4a914a06dd6a66938e4c29ac8cb6c4 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Sun, 30 Aug 2026 21:02:43 +0000 Subject: [PATCH] fix(discord-bridge): plafonul de cost e optional (COST_CAP_USD_DAY=off) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7 --- .../discord-bridge/README.md | 16 ++++++--- .../lxc171-claude-agent/discord-bridge/bot.py | 3 +- .../discord-bridge/config.py | 2 +- .../discord-bridge/dashboard/api.py | 8 +++-- .../discord-bridge/limits.py | 36 ++++++++++++++++--- .../discord-bridge/ops/env.example | 5 ++- .../discord-bridge/tests/test_limits.py | 21 +++++++++++ 7 files changed, 76 insertions(+), 15 deletions(-) diff --git a/proxmox/lxc171-claude-agent/discord-bridge/README.md b/proxmox/lxc171-claude-agent/discord-bridge/README.md index 15f3c12..056f7f8 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/README.md +++ b/proxmox/lxc171-claude-agent/discord-bridge/README.md @@ -170,7 +170,9 @@ Nu se poate automatiza: cere un om logat in Discord. **Allowlist gol = nimic permis** (fail-closed). Mesajele de la webhook-uri si de la alti boti sunt ignorate din principiu. 7. **Pune destinatarul alertelor**: `ALERT_RECIPIENT=` in acelasi env. -8. **Verifica plafonul de cost**: `COST_CAP_USD_DAY=` (implicit `5.00`). +8. **Verifica plafonul de cost**: `COST_CAP_USD_DAY=` (implicit `off`). Pe abonament + (Pro/Max) lasa `off`: costul raportat de CLI e doar pretul echivalent la API, nu o + factura. Pune un numar (ex. `5.00`) doar daca platesti per token. 9. Abia acum: `./ops/install.sh --start`. --- @@ -212,9 +214,13 @@ Fara Tailscale: `ssh -L 18790:127.0.0.1:18790 -N claude@10.0.20.171`. ### Cost Costul se vede in trei locuri: in subsolul fiecarui raspuns (turul curent + cumulat pe -fir), in `/status`, si in `state.json` la cheia `cost`. La atingerea plafonului zilnic -(`COST_CAP_USD_DAY`) botul nu mai accepta tururi noi si trimite email. Plafonul se -reseteaza la schimbarea zilei. +fir), in `/status`, si in `state.json` la cheia `cost`. E pretul echivalent la API +raportat de CLI: pe abonament nu se factureaza, deci ramane doar o masura de consum. + +Plafonul zilnic e optional. `COST_CAP_USD_DAY=off` (implicit, si `0` sau gol) inseamna +fara plafon: cheltuiala se contorizeaza mai departe, dar nimic nu se opreste. Cu un +numar pozitiv, la atingerea lui botul nu mai accepta tururi noi si trimite email; +plafonul se reseteaza la schimbarea zilei. ### Alerte pe email @@ -265,7 +271,7 @@ tail -2 ~/.claude-discord/logs/alerts.log | Unitul se invarte in restart | Dupa 5 porniri esuate in 300s systemd renunta si lasa unitul `failed` (e voit). Repara, apoi `systemctl --user reset-failed claude-discord && systemctl --user start claude-discord`. | | Firul e blocat pe hourglass | Botul a fost restartat la mijlocul unui tur. Turul **nu** se reia automat (risc de dubla executie sub `bypassPermissions`); sweep-ul de la pornire pune un avertisment in fir. Trimite mesajul din nou. | | Memoria containerului creste | `/cleanup` (sec), apoi `/cleanup force:True`. Vezi si `systemctl --user show claude-discord -p MemoryCurrent`. | -| „Plafon de cost atins" | E limita zilnica, nu o eroare. Ridica `COST_CAP_USD_DAY` in env si reporneste, sau asteapta ziua urmatoare. | +| „Plafon de cost atins" | E limita zilnica, nu o eroare. Pe abonament pune `COST_CAP_USD_DAY=off` in env si reporneste; altfel ridica valoarea sau asteapta ziua urmatoare. | | Nu vin emailuri de alerta | `command -v mail`; `mailq`; `tail ~/.claude-discord/logs/alerts.log`. Un `NESENT` in log iti spune exact de ce. | | Dupa reboot serviciul nu porneste | `loginctl show-user claude -p Linger` trebuie sa fie `yes`. Daca nu: `sudo loginctl enable-linger claude`. | diff --git a/proxmox/lxc171-claude-agent/discord-bridge/bot.py b/proxmox/lxc171-claude-agent/discord-bridge/bot.py index 8e3c1c2..a9d901f 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/bot.py +++ b/proxmox/lxc171-claude-agent/discord-bridge/bot.py @@ -609,7 +609,8 @@ class Bridge: f"in coada : {'da' if self.limits.queued(thread_id) else 'nu'}", f"procese : {self.runner.live_count()} vii, {self.limits.free_slots} sloturi libere", f"cost fir : ${float(rec.get('cost_usd_total') or 0.0):.4f}", - f"cost azi : ${self.limits.cost_today():.4f} / ${cap:.2f}" + f"cost azi : ${self.limits.cost_today():.4f}" + + (f" / ${cap:.2f}" if self.limits.cap_enabled else " (fara plafon)") + (" ⛔ PLAFON ATINS" if self.limits.stopped() else ""), "```", ] diff --git a/proxmox/lxc171-claude-agent/discord-bridge/config.py b/proxmox/lxc171-claude-agent/discord-bridge/config.py index 9df8d2a..152313a 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/config.py +++ b/proxmox/lxc171-claude-agent/discord-bridge/config.py @@ -24,7 +24,7 @@ _env: dict[str, str] = {} # Valori implicite pentru cheile pe care le foloseste nucleul. DEFAULTS: dict[str, str] = { "MODEL_DEFAULT": "sonnet", - "COST_CAP_USD_DAY": "10.0", + "COST_CAP_USD_DAY": "off", # `off` = fara plafon (abonament); numar = plafon zilnic USD "MAX_PROCS": "4", "TURN_TIMEOUT_S": "900", "IDLE_REAP_S": "1200", diff --git a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py index 3e02e17..127a11a 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py +++ b/proxmox/lxc171-claude-agent/discord-bridge/dashboard/api.py @@ -41,6 +41,7 @@ for _p in (str(_BRIDGE), str(_DASH)): sys.path.insert(0, _p) import config # noqa: E402 +from limits import parse_cap # noqa: E402 # ── constante ─────────────────────────────────────────────────────────── SERVICE = "claude-discord.service" @@ -288,12 +289,13 @@ def doctor() -> list[dict]: "detail": f'{len(st.get("threads") or {})} fire' if st else "ilizibil sau gol", }) - cap = float(config.get("COST_CAP_USD_DAY") or 0) + cap = parse_cap(config.get("COST_CAP_USD_DAY"), 0.0) spent = float((st.get("cost") or {}).get("usd") or 0) checks.append({ "name": "Plafon de cost pe zi", "pass": cap <= 0 or spent < cap, - "detail": f"{spent:.2f} / {cap:.2f} USD", + "detail": f"{spent:.2f} / {cap:.2f} USD" if cap > 0 + else f"{spent:.2f} USD cheltuiti, fara plafon (abonament)", }) try: @@ -500,7 +502,7 @@ class Handler(SimpleHTTPRequestHandler): "dashboard": unit_info(SELF_SERVICE), "threads": threads_view(state), "cost": state.get("cost") or {}, - "cost_cap": float(config.get("COST_CAP_USD_DAY") or 0), + "cost_cap": parse_cap(config.get("COST_CAP_USD_DAY"), 0.0), "pending_approvals": len(pending_approvals()), "auth": not auth_disabled(), "user": tailnet_user(self.headers), diff --git a/proxmox/lxc171-claude-agent/discord-bridge/limits.py b/proxmox/lxc171-claude-agent/discord-bridge/limits.py index 125cbdc..9f13fd2 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/limits.py +++ b/proxmox/lxc171-claude-agent/discord-bridge/limits.py @@ -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 diff --git a/proxmox/lxc171-claude-agent/discord-bridge/ops/env.example b/proxmox/lxc171-claude-agent/discord-bridge/ops/env.example index c3ff86c..fdfa750 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/ops/env.example +++ b/proxmox/lxc171-claude-agent/discord-bridge/ops/env.example @@ -28,7 +28,10 @@ DISCORD_USER_IDS= CLAUDE_MODEL=sonnet # Plafon de cost pe zi, in USD. La atingere botul refuza tururi noi si alerteaza. # Reper masurat: un tur banal pe opus a costat $0.1547. -COST_CAP_USD_DAY=5.00 +# Plafon zilnic de cost. Pe abonament (Pro/Max) costul raportat de CLI e doar +# pretul echivalent la API, nu o factura: lasa `off`. Un numar (ex. 5.00) +# activeaza plafonul si opreste tururile cand e atins. +COST_CAP_USD_DAY=off # --- Limite ---------------------------------------------------------------- # Maxim de procese `claude` vii simultan (406 MB RSS fiecare, masurat). diff --git a/proxmox/lxc171-claude-agent/discord-bridge/tests/test_limits.py b/proxmox/lxc171-claude-agent/discord-bridge/tests/test_limits.py index cf803e8..a118409 100644 --- a/proxmox/lxc171-claude-agent/discord-bridge/tests/test_limits.py +++ b/proxmox/lxc171-claude-agent/discord-bridge/tests/test_limits.py @@ -78,6 +78,27 @@ def test_plafon_de_cost_opreste_botul(store): lim.admit("u1") +def test_plafonul_off_nu_opreste_nimic(store, state_dir): + import config + (state_dir / "env").write_text("COST_CAP_USD_DAY=off\n") + config.reload(state_dir) + alerte = [] + lim = L.Limits(store, alerter=lambda *a, **k: alerte.append(a)) + assert lim.cost_cap == 0.0 and not lim.cap_enabled + lim.record_cost(99.0, "1") + assert not lim.stopped() and lim.cost_remaining() == float("inf") + lim.admit("u1") # nu ridica CostCapReached + assert alerte == [] + + +@pytest.mark.parametrize("raw, asteptat", [ + ("off", 0.0), ("", 0.0), ("nelimitat", 0.0), ("0", 0.0), ("-3", 0.0), + ("5.00", 5.0), (" 2.5 ", 2.5), ("gunoi", 0.0), (None, 0.0), +]) +def test_parse_cap(raw, asteptat): + assert L.parse_cap(raw, 0.0) == pytest.approx(asteptat) + + def test_plafon_alerteaza_o_singura_data(store): alerte = [] lim = mkl(store, cost_cap=0.5, alerter=lambda *a, **k: alerte.append(a))