Version 1.2.2. Added usage overview. Shows token used and cost in $.

This commit is contained in:
2026-04-15 10:00:39 +02:00
parent 752691fe54
commit d4c6420481
18 changed files with 1657 additions and 86 deletions

View File

@@ -380,6 +380,58 @@ async def get_queue_status(request: Request):
return agent_runner.queue_status
# ── Browser trusted domains ───────────────────────────────────────────────────
class BrowserDomainIn(BaseModel):
domain: str
note: Optional[str] = None
@router.get("/my/browser-trusted")
async def list_browser_trusted(request: Request):
from ..database import get_pool as _gp
user = _require_auth(request)
pool = await _gp()
rows = await pool.fetch(
"SELECT id, domain, note, created_at FROM browser_approved_domains "
"WHERE owner_user_id = $1 ORDER BY domain",
user.id,
)
return [dict(r) for r in rows]
@router.post("/my/browser-trusted")
async def add_browser_trusted(request: Request, body: BrowserDomainIn):
from ..database import get_pool as _gp
user = _require_auth(request)
domain = body.domain.lower().strip().lstrip("*.")
if not domain:
raise HTTPException(status_code=400, detail="Invalid domain")
pool = await _gp()
try:
await pool.execute(
"INSERT INTO browser_approved_domains (owner_user_id, domain, note) "
"VALUES ($1, $2, $3) ON CONFLICT (owner_user_id, domain) DO NOTHING",
user.id, domain, body.note or None,
)
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
return {"ok": True, "domain": domain}
@router.delete("/my/browser-trusted/{domain:path}")
async def remove_browser_trusted(request: Request, domain: str):
from ..database import get_pool as _gp
user = _require_auth(request)
domain = domain.lower().strip().lstrip("*.")
pool = await _gp()
await pool.execute(
"DELETE FROM browser_approved_domains WHERE owner_user_id = $1 AND domain = $2",
user.id, domain,
)
return {"ok": True}
@router.get("/settings/provider")
async def get_default_provider(request: Request):
_require_admin(request)
@@ -814,6 +866,179 @@ async def stop_run(request: Request, run_id: str):
return {"ok": True, "run_id": run_id}
# ── Usage / cost overview ─────────────────────────────────────────────────────
@router.get("/usage")
async def get_usage(
request: Request,
since: str = "7d",
start: str = "",
end: str = "",
):
"""Aggregate token and cost usage by agent for the usage overview page."""
user = _require_auth(request)
if not user.is_admin:
from ..database import user_settings_store as _uss
if await _uss.get(user.id, "use_admin_keys"):
raise HTTPException(status_code=403, detail="Not available on admin API keys")
has_own = (
await _uss.get(user.id, "anthropic_api_key") or
await _uss.get(user.id, "openrouter_api_key") or
await _uss.get(user.id, "openai_api_key")
)
if not has_own:
raise HTTPException(status_code=403, detail="Not available on admin API keys")
from ..database import get_pool as _gp
pool = await _gp()
now = datetime.now(timezone.utc)
since_dt: str | None = None
if start:
since_dt = start
elif since == "today":
since_dt = now.replace(hour=0, minute=0, second=0, microsecond=0).isoformat()
elif since == "7d":
since_dt = (now - timedelta(days=7)).isoformat()
elif since == "30d":
since_dt = (now - timedelta(days=30)).isoformat()
# since == "all" → no date filter
# Build WHERE clauses
clauses: list[str] = ["ar.status IN ('success', 'error', 'stopped')"]
params: list = []
n = 1
# Exclude email handler agents
handler_ids_rows = await pool.fetch(
"SELECT agent_id FROM email_accounts WHERE agent_id IS NOT NULL"
)
handler_ids = [str(r["agent_id"]) for r in handler_ids_rows]
if handler_ids:
placeholders = ", ".join(f"${n + i}" for i in range(len(handler_ids)))
clauses.append(f"ar.agent_id NOT IN ({placeholders})")
params.extend(handler_ids)
n += len(handler_ids)
if since_dt:
clauses.append(f"ar.started_at >= ${n}"); params.append(since_dt); n += 1
if end:
clauses.append(f"ar.started_at <= ${n}"); params.append(end); n += 1
# Non-admin sees only their own agents
if not user.is_admin:
own_agents = await agent_store.list_agents(owner_user_id=user.id)
own_ids = [a["id"] for a in own_agents]
if own_ids:
placeholders = ", ".join(f"${n + i}" for i in range(len(own_ids)))
clauses.append(f"ar.agent_id IN ({placeholders})")
params.extend(own_ids)
n += len(own_ids)
else:
# No agents → empty result
return {"summary": {"runs": 0, "input_tokens": 0, "output_tokens": 0, "cost_usd": None}, "by_agent": []}
where = "WHERE " + " AND ".join(clauses)
rows = await pool.fetch(
f"""
SELECT
ar.agent_id,
a.name AS agent_name,
a.model AS agent_model,
COUNT(*) AS runs,
SUM(ar.input_tokens) AS input_tokens,
SUM(ar.output_tokens) AS output_tokens,
SUM(ar.cost_usd) AS cost_usd
FROM agent_runs ar
LEFT JOIN agents a ON a.id = ar.agent_id
{where}
GROUP BY ar.agent_id, a.name, a.model
ORDER BY cost_usd DESC NULLS LAST, (SUM(ar.input_tokens) + SUM(ar.output_tokens)) DESC
""",
*params,
)
by_agent = []
total_input = 0
total_output = 0
total_cost: float | None = None
total_runs = 0
for row in rows:
inp = int(row["input_tokens"] or 0)
out = int(row["output_tokens"] or 0)
cost = float(row["cost_usd"]) if row["cost_usd"] is not None else None
runs = int(row["runs"])
total_input += inp
total_output += out
total_runs += runs
if cost is not None:
total_cost = (total_cost or 0.0) + cost
by_agent.append({
"agent_id": str(row["agent_id"]),
"agent_name": row["agent_name"] or "",
"model": row["agent_model"] or "",
"runs": runs,
"input_tokens": inp,
"output_tokens": out,
"cost_usd": cost,
})
# ── Chat session usage ────────────────────────────────────────────────────
chat_clauses: list[str] = ["task_id IS NULL"] # chat only, not agent/task runs
chat_params: list = []
cn = 1
if since_dt:
chat_clauses.append(f"started_at >= ${cn}"); chat_params.append(since_dt); cn += 1
if end:
chat_clauses.append(f"started_at <= ${cn}"); chat_params.append(end); cn += 1
if not user.is_admin:
chat_clauses.append(f"user_id = ${cn}"); chat_params.append(user.id); cn += 1
chat_where = "WHERE " + " AND ".join(chat_clauses)
chat_row = await pool.fetchrow(
f"""
SELECT
COUNT(*) AS sessions,
SUM(input_tokens) AS input_tokens,
SUM(output_tokens) AS output_tokens,
SUM(cost_usd) AS cost_usd
FROM conversations
{chat_where}
""",
*chat_params,
)
chat_inp = int(chat_row["input_tokens"] or 0)
chat_out = int(chat_row["output_tokens"] or 0)
chat_cost = float(chat_row["cost_usd"]) if chat_row["cost_usd"] is not None else None
chat_sessions = int(chat_row["sessions"] or 0)
total_input += chat_inp
total_output += chat_out
total_runs += chat_sessions
if chat_cost is not None:
total_cost = (total_cost or 0.0) + chat_cost
return {
"summary": {
"runs": total_runs,
"input_tokens": total_input,
"output_tokens": total_output,
"cost_usd": total_cost,
},
"by_agent": by_agent,
"chat": {
"sessions": chat_sessions,
"input_tokens": chat_inp,
"output_tokens": chat_out,
"cost_usd": chat_cost,
},
}
# ── Inbox triggers ────────────────────────────────────────────────────────────
class InboxTriggerIn(BaseModel):