Track grading spend per user
Attribution comes from the proxy's basic-auth username, which nginx already forwards. Recorded as a ledger of billed calls rather than a column on the grade: a regrade overwrites the grade row, so a per-row cost would forget what earlier runs cost, and deleting a card would erase that it was ever paid for. Splits server-key spend (money the host actually owes) from own-key spend (billed to the visitor) — the two must never be summed. Attribution only, never authorization: the app is reachable directly on the LAN, so these headers are not trustworthy for access control.
This commit is contained in:
parent
762b394c8a
commit
9d5c68b093
4 changed files with 186 additions and 0 deletions
76
store.py
76
store.py
|
|
@ -50,6 +50,29 @@ CREATE TABLE IF NOT EXISTS grades (
|
|||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_grades_created ON grades(created_at DESC);
|
||||
|
||||
-- One row per paid vision call, including every regrade. Deliberately NOT a
|
||||
-- column on `grades`: a regrade overwrites that row, so a per-row cost would
|
||||
-- silently forget what the earlier runs cost, and deleting a card would erase
|
||||
-- the fact that it was ever paid for. Spend is a ledger, not a property of the
|
||||
-- current result.
|
||||
-- grade_id is only a breadcrumb (no foreign key) — the event must outlive the
|
||||
-- grade it came from.
|
||||
CREATE TABLE IF NOT EXISTS grade_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
created_at TEXT NOT NULL,
|
||||
grade_id INTEGER, -- the row it produced, if it was saved
|
||||
username TEXT, -- from the proxy's basic auth, or NULL
|
||||
model TEXT,
|
||||
input_tokens INTEGER,
|
||||
output_tokens INTEGER,
|
||||
cost REAL,
|
||||
own_key INTEGER NOT NULL DEFAULT 0, -- 1 = caller paid, 0 = server key
|
||||
kind TEXT -- 'grade' | 'regrade'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_events_user ON grade_events(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_created ON grade_events(created_at DESC);
|
||||
"""
|
||||
|
||||
DEFAULT_SETTINGS = {
|
||||
|
|
@ -270,6 +293,59 @@ def update_grade_result(grade_id, grade, thumbnail=None, source_images=None):
|
|||
return get_grade(grade_id)
|
||||
|
||||
|
||||
def log_grade_event(grade, username=None, grade_id=None, own_key=False, kind="grade"):
|
||||
"""Record one paid vision call. Called for first grades AND regrades."""
|
||||
usage = grade.get("usage") or {}
|
||||
conn = connect()
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT INTO grade_events "
|
||||
"(created_at, grade_id, username, model, input_tokens, "
|
||||
" output_tokens, cost, own_key, kind) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?)",
|
||||
(now(), grade_id, username, usage.get("model"),
|
||||
usage.get("input_tokens"), usage.get("output_tokens"),
|
||||
grade.get("estimated_cost"), 1 if own_key else 0, kind),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def usage_by_user(limit=100):
|
||||
"""Per-user grading spend, most expensive first.
|
||||
|
||||
Splits by whose key paid: `server_cost` is what the host is actually out
|
||||
of pocket for, `own_cost` is what someone spent on their own key. Only
|
||||
the first is money the owner of this instance ever sees a bill for, so
|
||||
the two must never be added together and shown as one number.
|
||||
"""
|
||||
conn = connect()
|
||||
try:
|
||||
rows = conn.execute(
|
||||
"SELECT COALESCE(username, '(unknown)') AS username, "
|
||||
" COUNT(*) AS grades, "
|
||||
" SUM(CASE WHEN own_key = 0 THEN COALESCE(cost, 0) ELSE 0 END) AS server_cost, "
|
||||
" SUM(CASE WHEN own_key = 1 THEN COALESCE(cost, 0) ELSE 0 END) AS own_cost, "
|
||||
" MAX(created_at) AS last_used "
|
||||
"FROM grade_events GROUP BY COALESCE(username, '(unknown)') "
|
||||
"ORDER BY server_cost DESC, grades DESC LIMIT ?",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
totals = conn.execute(
|
||||
"SELECT COUNT(*) AS grades, "
|
||||
" SUM(CASE WHEN own_key = 0 THEN COALESCE(cost, 0) ELSE 0 END) AS server_cost, "
|
||||
" SUM(CASE WHEN own_key = 1 THEN COALESCE(cost, 0) ELSE 0 END) AS own_cost "
|
||||
"FROM grade_events"
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
return {
|
||||
"users": [dict(r) for r in rows],
|
||||
"totals": dict(totals) if totals else {},
|
||||
}
|
||||
|
||||
|
||||
def prune_source_images(days=None):
|
||||
"""Drop stored photos older than the retention window. Returns the count.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue