The calculation lived only in code comments. Adds docs/energy-metrics.md and wires it into the README, per the issue's proposed scope. No calculation changes. Contents: the input (completion_tokens, output tokens only, grouped on model_group) and why output-only understates total compute; the factor table with its three provenance tiers (measured / same-family / estimated) and the fact that the tiers are never exposed; the formulas; NULL-not-zero and the reason (the first backfill claimed every day used no energy); the implied-grid-intensity tripwire and the 1000x carbon error it exists to catch; the 03:15 schedule and that --backfill REWRITES history. States plainly what is measured and what is not: GreenPT is measured, glm-5.3 is assumed from its flash sibling, and all Tinfoil and Regolo traffic currently falls to the proxy estimate — with the consequence that the 250 gCO2e/kWh proxy OVERSTATES Regolo, which runs on renewable energy. README: "Two jobs" -> "Three", the metrics_snapshot task documented, and a short Energy & carbon metrics subsection linking the new doc. Also corrects "listing both tasks" in the post-change verification step.
5.9 KiB
Member Portal — membership middleware for the Inference Cooperative
Reconciles three systems into one membership lifecycle:
| System | Role | Source of truth for |
|---|---|---|
| Open Collective | Billing | Who is a paying member |
| Cloudron | Identity/SSO | Who can log in |
| LiteLLM | Inference | Who can use models, and how much |
Architecture
Open Collective (billing)
│ webhook
▼
┌─────────────────────────────┐
│ Member Portal (this app) │
│ - OC webhook handler │
│ - Cloudron user management │
│ - LiteLLM key management │
│ - key-injection proxy │
└─────────────────────────────┘
▲
│ X-User-Email header (LibreChat forwards identity)
│
LibreChat ──→ Member Portal ──→ LiteLLM ──→ backend
The three responsibilities
- Webhook handler (
POST /webhook/opencollective) — listens for Open Collective membership events and activates/deactivates members. - Key injector (
/v1/*) — reads the member's email from theX-User-Emailheader, looks up their LiteLLM key, and forwards the request to the gateway with that key. - Admin (
/health,/) — health and status.
Configuration (environment variables)
| Variable | Purpose |
|---|---|
CLOUDRON_API |
Cloudron API origin (auto-set by Cloudron) |
CLOUDRON_TOKEN |
Cloudron API token (Read+Write) |
MEMBERS_GROUP_ID |
Cloudron group ID for members (grants chat access) |
LITELLM_BASE |
LiteLLM gateway URL |
LITELLM_MASTER_KEY |
LiteLLM master key |
OPENCOLLECTIVE_WEBHOOK_SECRET |
Optional webhook signature secret |
PORTAL_KEK |
Required. Base64-encoded 32-byte key-encryption key. Member chat keys (members.key_token) are stored AES-256-GCM-encrypted at rest (enc1: prefix) and decrypted in memory only when served (dashboard, LibreChat key feed, LiteLLM management). Generate with python3 -c "import base64,os;print(base64.b64encode(os.urandom(32)).decode())". If unset, the portal runs but refuses to encrypt new keys, keeps legacy plaintext, and logs CRITICAL. Rotating it requires re-encrypting every row (decrypt with the old key, encrypt with the new). |
Tier budgets
Governance decision (set in Loomio). Defaults:
free— $2/month of tokensmember— $15/monthsupporter— $30/month
LibreChat wiring
Point LibreChat's custom endpoint at this portal (not directly at LiteLLM), and add the identity header:
endpoints:
custom:
- name: "Inference Cooperative"
apiKey: "${LITELLM_KEY}"
baseURL: "https://portal.inference.coop/v1"
headers:
X-User-Email: "{{LIBRECHAT_USER_EMAIL}}"
models:
default: ["deepseek-v4-flash", "gpt-oss-120b"]
fetch: true
Status
Scaffold — the three handlers are stubbed with the correct integration points. Remaining work before production:
- Verify Open Collective webhook event names + payload shape
→
order.processed(every payment),new member(first only),firstPaymentflag - Verify Cloudron user-creation API payload (role/group assignment)
→
POST /api/v1/userswith{username, email, displayName, role, active}→ group assignment viaPUT /api/v1/users/:userId/groupswith{groupIds: [...]} - Add a persistent store (SQLite) for email→key mapping
→
key/listreturns token strings (not objects), so we store email→token locally - Verify LiteLLM key/generate + key/list payload shapes against live gateway
→
key/generatereturns{key: "sk-..."};key/listreturns{keys: ["<token>", ...]} - Add HMAC signature verification for the OC webhook (if OC supports it)
- Wire the OIDC addon for admin access
Scheduled tasks (scheduler addon)
Three jobs run inside the container via CloudronManifest.json → addons.scheduler:
- membership_sweep (nightly 3am) —
sweep.shcalls the portal's own/admin/sweep(auth:X-Admin-Token=WEBHOOK_TOKEN) to reconcile members against the live Open Collective list: provisions missed signups, deactivates lapsed members, syncs Loomio + Matrix. Idempotent; a missed webhook self-heals within 24 hours. - rag_refresh (nightly 3:30am) —
rag_refresh.pypulls this repo's sibling docs from the Gitea raw API (public, no credentials), uploads changed files to Open WebUI (OWUI_API_KEY), attaches them to the shared knowledge collection (DOCS_KNOWLEDGE_ID), reindexes, and re-grants the members group. State persists in/app/data/rag_refresh_state.json. - metrics_snapshot (nightly 3:15am) —
metrics_snapshot.pyrecords one row per day into themetrics_dailytable: members, newsletter subscribers, Matrix membership, spend, request counts, and energy/CO₂. A source that cannot be read yields NULL for that metric rather than a zero.--backfillrebuilds history idempotently. Methodology: Energy and carbon metrics.
Verify after any manifest change: the deploy log must show
setupAddons: ... scheduler ... listing all three tasks, and each script must
succeed when run manually inside the container.
Energy & carbon metrics
The daily snapshot also computes energy (Wh) and carbon (gCO₂e) from output tokens, using per-model factors whose provenance varies: measured for GreenPT, assumed equal for one sibling model, and an explicit order-of-magnitude estimate for providers that disclose nothing. Only output tokens count, and a day with no measured tokens is recorded as NULL rather than zero.
Energy and carbon metrics documents the factor table, the
formulas, the tripwire that catches unit errors, the schedule, and — most importantly —
which parts are measurements and which are estimates. What members are told is in
co-op/docs/energy.md.