metrics: energy and carbon per day, from measured GreenPT factors
Implements the approach Nathan settled on in co-op/support#26: per-token estimates based on what GreenPT publishes, rather than waiting on the upstream LiteLLM `impact` passthrough. - New daily columns: out_tokens, energy_wh, co2e_g. - Tokens come from OUR OWN spend logs (completion_tokens per model per day), so member-visible impact does not depend on any client persisting an `impact` object. GreenPT's metric counts OUTPUT tokens only — that is the unit — so input tokens are excluded deliberately. - Factors live in one table with a source per row. The greenpt/* rows are MEASURED: fitted from live calls to their API across prompt sizes (energy = overhead + marginal x output tokens, R2 >= 0.999; carbon from the grid intensity their own emissions imply — ug/mWh and g/kWh are the same ratio). Everything else falls back to a documented order-of-magnitude proxy, so a day's total is never silently missing models. - Migration: CREATE IF NOT EXISTS never adds columns, so both the script and the app ALTER TABLE on connect. Refining a factor is a data edit in IMPACT_FACTORS, not a code change.
This commit is contained in:
1 parent
5a9b0d2b0d
commit
af610fe25f
2 files changed
+153
-7
No files matched your search
+20
-3
@@ -622,9 +622,19 @@ def get_db() -> sqlite3.Connection:
|
||||
"matrix_space_members INTEGER, "
|
||||
"spend_usd REAL, "
|
||||
"requests INTEGER, "
|
||||
"out_tokens INTEGER, "
|
||||
"energy_wh REAL, "
|
||||
"co2e_g REAL, "
|
||||
"recorded_at TEXT"
|
||||
")"
|
||||
)
|
||||
# CREATE IF NOT EXISTS never adds columns to an existing table, so migrate:
|
||||
# impact columns arrived after the table did (see co-op/support#26).
|
||||
for _col, _decl in (("out_tokens", "INTEGER"), ("energy_wh", "REAL"), ("co2e_g", "REAL")):
|
||||
try:
|
||||
conn.execute(f"ALTER TABLE metrics_daily ADD COLUMN {_col} {_decl}")
|
||||
except sqlite3.OperationalError:
|
||||
pass # already present
|
||||
# Member-managed API keys (created via the broker). Each row is one API key
|
||||
# under the member's team, tracked so we can list/revoke by name without
|
||||
# exposing the chat key. `sk_token` is the full sk- key (revealed once).
|
||||
@@ -3059,15 +3069,19 @@ def metrics_series(days: int = 90) -> list[dict]:
|
||||
|
||||
One query, two callers: the admin panel (via /admin/metrics/series) and the
|
||||
member dashboard (via /broker/metrics). Columns are cumulative as of that
|
||||
day except spend_usd/requests, which are that day only; NULL means the
|
||||
source could not be read that day.
|
||||
day except spend_usd/requests/out_tokens/energy_wh/co2e_g, which are that
|
||||
day only; NULL means the source could not be read that day.
|
||||
|
||||
energy_wh and co2e_g are computed in metrics_snapshot.py from the day's
|
||||
output tokens against a factor table: measured for GreenPT models (their API
|
||||
returns an `impact` object), estimated for the rest. See co-op/support#26.
|
||||
"""
|
||||
limit = max(1, min(int(days or 90), 730))
|
||||
conn = get_db()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT day, members_active, members_total, members_new, newsletter_subs, "
|
||||
"matrix_space_members, spend_usd, requests, recorded_at "
|
||||
"matrix_space_members, spend_usd, requests, out_tokens, energy_wh, co2e_g, recorded_at "
|
||||
"FROM metrics_daily ORDER BY day DESC LIMIT ?",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
@@ -3082,6 +3096,9 @@ def metrics_series(days: int = 90) -> list[dict]:
|
||||
"matrix_space_members",
|
||||
"spend_usd",
|
||||
"requests",
|
||||
"out_tokens",
|
||||
"energy_wh",
|
||||
"co2e_g",
|
||||
"recorded_at",
|
||||
]
|
||||
return [dict(zip(cols, row)) for row in reversed(rows)]
|
||||
|
||||
Reference in new issue
Block a user