From c7ed24593990d20bc637410bae21f3d4a610ea76 Mon Sep 17 00:00:00 2001 From: inference-bot Date: Sat, 26 Sep 2026 16:39:23 -0600 Subject: [PATCH] Read usage aggregations from LiteLLM Postgres directly (HTTP fallback kept) --- app/__pycache__/main.cpython-313.pyc | Bin 0 -> 90787 bytes app/main.py | 170 ++++++++++++++++++++++++++- requirements.txt | 1 + 3 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 app/__pycache__/main.cpython-313.pyc diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e558e472f99dd4faee718e890f33769a1f4867d7 GIT binary patch literal 90787 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 literal 0 HcmV?d00001 diff --git a/app/main.py b/app/main.py index dbf1784..3de3dd5 100644 --- a/app/main.py +++ b/app/main.py @@ -167,6 +167,123 @@ LISTMONK_API_USER = os.environ.get("LISTMONK_API_USER", "portal-sync") LISTMONK_API_TOKEN = os.environ.get("LISTMONK_API_TOKEN", "") LISTMONK_LIST_ID = int(os.environ.get("LISTMONK_LIST_ID", "3")) # "Members" list +# Direct read access to LiteLLM's Postgres (for usage aggregations). +# READ-ONLY purpose: never write to LiteLLM's tables — its HTTP API remains the +# only interface for writes/management. Falls back to the HTTP path when unset. +LITELLM_DB_HOST = os.environ.get("LITELLM_DB_HOST", "") +LITELLM_DB_PORT = int(os.environ.get("LITELLM_DB_PORT", "5432")) +LITELLM_DB_USER = os.environ.get("LITELLM_DB_USER", "") +LITELLM_DB_PASSWORD = os.environ.get("LITELLM_DB_PASSWORD", "") +LITELLM_DB_NAME = os.environ.get("LITELLM_DB_NAME", "") + + +def litellm_db_ready() -> bool: + """True when LiteLLM DB read credentials are configured.""" + return bool(LITELLM_DB_HOST and LITELLM_DB_USER and LITELLM_DB_NAME) + + +def _pg_connect(): + """Open a short-lived connection to LiteLLM's Postgres (read-only use).""" + import psycopg2 + return psycopg2.connect( + host=LITELLM_DB_HOST, port=LITELLM_DB_PORT, user=LITELLM_DB_USER, + password=LITELLM_DB_PASSWORD, dbname=LITELLM_DB_NAME, + connect_timeout=10, sslmode="prefer", + ) + + +def aggregate_model_usage(days: int = 0, team_id: str | None = None) -> dict: + """Aggregate LiteLLM spend logs per model, straight from Postgres. + + Returns {"models": [...], "totals": {...}} in the same shape the HTTP path + produced, so callers don't change. `days` bounds the window (0 = all time); + `team_id` restricts to one member's team (used by the dashboard path). + + Prefers the `model_group` column, which carries our provider/model alias + (e.g. greenpt/green-r); falls back to `model` for pre-alias rows — matching + the logic the HTTP path used, expressed in SQL. + """ + if not litellm_db_ready(): + raise RuntimeError("LiteLLM DB credentials not configured") + + where = [] + params: list = [] + if days and days > 0: + from datetime import datetime, timedelta, timezone + cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).strftime( + "%Y-%m-%dT%H:%M:%S" + ) + where.append("COALESCE(\"startTime\", '') >= %s") + params.append(cutoff) + if team_id: + where.append("team_id = %s") + params.append(team_id) + where_sql = ("WHERE " + " AND ".join(where)) if where else "" + + sql = f""" + SELECT + COALESCE(NULLIF(model_group, ''), NULLIF(model, ''), 'unknown') AS model, + COALESCE(SUM(spend), 0) AS spend, + COALESCE(SUM(total_tokens), 0) AS tokens, + COUNT(*) AS requests + FROM "LiteLLM_SpendLogs" + {where_sql} + GROUP BY 1 + ORDER BY 2 DESC + """ + + conn = _pg_connect() + try: + with conn.cursor() as cur: + cur.execute(sql, params) + rows = cur.fetchall() + finally: + conn.close() + + models = [ + {"model": r[0], "spend": float(r[1] or 0.0), "tokens": int(r[2] or 0), + "requests": int(r[3] or 0)} + for r in rows + ] + return { + "models": models, + "totals": { + "total_spend": sum(m["spend"] for m in models), + "total_tokens": sum(m["tokens"] for m in models), + "total_requests": sum(m["requests"] for m in models), + }, + } + + +def get_team_budget_from_db(team_id: str) -> dict | None: + """Fetch a team's spend/budget/reset directly from LiteLLM's Postgres. + + Same data as the /team/list HTTP path (LiteLLM computes `spend`), without + downloading the whole team list. Returns None when the team is unknown. + """ + if not litellm_db_ready(): + return None + sql = """ + SELECT max_budget, spend, budget_reset_at + FROM "LiteLLM_TeamTable" + WHERE team_id = %s + LIMIT 1 + """ + conn = _pg_connect() + try: + with conn.cursor() as cur: + cur.execute(sql, (team_id,)) + row = cur.fetchone() + finally: + conn.close() + if not row: + return None + return { + "max_budget": float(row[0]) if row[0] is not None else None, + "spend": float(row[1] or 0.0), + "budget_reset_at": row[2], + } + async def listmonk_sync(email: str, subscribe: bool) -> None: """Subscribe or unsubscribe a member to the newsletter list. @@ -1330,9 +1447,20 @@ async def admin_model_usage(request: Request, days: int = 0): sorted by spend descending. Optional ?days=N limits to the last N days (0 or omitted = all-time). No per-member breakdown — this is co-op-level so admins can see which models the membership actually uses. + + Reads go straight to LiteLLM's Postgres when configured (fast, bounded); + otherwise falls back to the HTTP API path. """ _verify_admin_token(request) + # Fast path: direct Postgres aggregation. + if litellm_db_ready(): + try: + return aggregate_model_usage(days=days) + except Exception as e: + logger.warning("admin model-usage: DB path failed (%s); falling back to HTTP", e) + + # Fallback path: LiteLLM HTTP API. cutoff = None if days and days > 0: from datetime import datetime, timedelta, timezone @@ -1479,12 +1607,14 @@ async def broker_revoke(name: str, request: Request): return {"status": "revoked", "name": name} -async def broker_get_usage(email: str) -> dict: +async def broker_get_usage(email: str, days: int = 0) -> dict: """Return the member's spend vs. balance, plus per-model and token detail. - Reads the member's team (spend + max_budget) and aggregates their recent - spend logs by model for token counts. This is the read-only primitive the - member dashboard uses (no master key on the client). + This is the read-only primitive the member dashboard uses (no master key on + the client). Reads go straight to LiteLLM's Postgres when configured (fast, + bounded); otherwise falls back to the HTTP API path. `days` bounds the + per-model breakdown window (0 = all time); balance/spend always come from + the team's live totals. """ team_id = await litellm_get_or_create_team(email, get_member_balance(email)) @@ -1492,6 +1622,34 @@ async def broker_get_usage(email: str) -> dict: spend = 0.0 reset_at = None + # Fast path: direct Postgres reads. + if litellm_db_ready(): + try: + team = get_team_budget_from_db(team_id) + if team: + if team["max_budget"] is not None: + balance = float(team["max_budget"]) + spend = float(team["spend"] or 0.0) + reset_at = team["budget_reset_at"] + usage = aggregate_model_usage(days=days, team_id=team_id) + model_list = [ + {"model": m["model"], "spend": m["spend"], "tokens": m["tokens"], + "calls": m["requests"]} + for m in usage["models"] + ] + return { + "email": email, + "balance": balance, + "spend": spend, + "remaining": max(balance - spend, 0.0), + "total_tokens": usage["totals"]["total_tokens"], + "reset_at": reset_at, + "models": model_list, + } + except Exception as e: + logger.warning("broker_get_usage: DB path failed (%s); falling back to HTTP", e) + + # Fallback path: LiteLLM HTTP API. # /team/list and /spend/logs have grown slow as usage accumulates; the # httpx default 5s read timeout was too short and made /broker/usage 500. client_timeout = httpx.Timeout(connect=15.0, read=60.0, write=15.0, pool=15.0) @@ -1558,9 +1716,9 @@ async def broker_get_usage(email: str) -> dict: @app.get("/broker/usage") @limiter.limit("60/minute") -async def broker_usage(request: Request): +async def broker_usage(request: Request, days: int = 0): email = _verify_broker_secret(request) - return await broker_get_usage(email) + return await broker_get_usage(email, days=days) async def reconcile_memberships() -> dict: diff --git a/requirements.txt b/requirements.txt index 4f514b5..5ed9ad2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ httpx==0.27.2 pydantic==2.9.2 PyJWT==2.9.0 slowapi==0.1.9 +psycopg2-binary==2.9.9