From 592cf83656aea1ce51c61d080b85232dd329b874 Mon Sep 17 00:00:00 2001 From: Inferencebot Date: Thu, 29 Oct 2026 20:57:10 +0000 Subject: [PATCH] metrics + overview: stop counting utility accounts as members, and surface agent usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The panel has always excluded operator/utility emails from member counts (OPERATOR_EMAILS), but the metrics counted every row in the members table, so the panel said 75 while every metric surface said 76. The one affected row is info@inference.coop — the reading mailbox, not a person. - metrics_snapshot.py now applies the same OPERATOR_EMAILS definition (same variable name, same default) so the two cannot drift apart again. No backfill: history stays as published and the daily job writes the corrected value going forward. - admin_overview now also returns an `agents` list, built from the LiteLLM teams it already fetches (anything not member:*), so bot and sandbox spend is visible rather than structurally invisible. Kept out of member counts and member aggregates, like the operator category. --- app/main.py | 16 ++++++++++++++++ metrics_snapshot.py | 31 +++++++++++++++++++++---------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app/main.py b/app/main.py index 6ddf76b..296c97f 100644 --- a/app/main.py +++ b/app/main.py @@ -3131,9 +3131,25 @@ async def admin_overview(request: Request): total_spend += spend total_all_time += member_all_time + # Bot and sandbox teams: real spend, but not members. Surfaced in their own + # category so the co-op's own usage is accountable without inflating member + # counts — the same reasoning as the operator accounts above. + agents = [ + { + "alias": alias, + "spend": float(t.get("spend") or 0.0), + "budget": t.get("max_budget"), + "team_members": len(t.get("members_with_roles") or []), + } + for alias, t in teams.items() + if alias and not alias.startswith("member:") + ] + agents.sort(key=lambda a: a["spend"], reverse=True) + return { "members": members, "admins": admins, + "agents": agents, "totals": { "members": len(members), "active": sum(1 for m in members if m["active"]), diff --git a/metrics_snapshot.py b/metrics_snapshot.py index 133a168..c06effb 100644 --- a/metrics_snapshot.py +++ b/metrics_snapshot.py @@ -143,21 +143,32 @@ def listmonk_joins() -> list[str] | None: return joins or None +# Utility/operator accounts are not members. The panel has always excluded them +# (OPERATOR_EMAILS in app/main.py) while the metrics counted every row, so the two +# disagreed. Same variable name, same default, so they cannot drift apart again. +OPERATOR_EMAILS = { + e.strip().lower() + for e in os.environ.get("OPERATOR_EMAILS", "info@inference.coop,bot@inference.coop").split(",") + if e.strip() +} + + def from_sqlite() -> dict: - """Members, plus the provisioning dates that give the growth curve.""" + """Members, plus the provisioning dates that give the growth curve. + + Operator/utility accounts are excluded, matching the panel's member count. + """ conn = connect() try: - active = conn.execute("SELECT COUNT(*) FROM members WHERE active=1").fetchone()[0] - total = conn.execute("SELECT COUNT(*) FROM members").fetchone()[0] - joins = [ - row[0] - for row in conn.execute( - "SELECT welcome_sent_at FROM members WHERE welcome_sent_at IS NOT NULL AND welcome_sent_at != ''" - ) - ] + rows = conn.execute("SELECT email, active, welcome_sent_at FROM members").fetchall() finally: conn.close() - return {"active": active, "total": total, "joins": joins} + real = [r for r in rows if (r[0] or "").strip().lower() not in OPERATOR_EMAILS] + return { + "active": sum(1 for r in real if r[1]), + "total": len(real), + "joins": [r[2] for r in real if r[2]], + } def from_listmonk() -> int | None: