Version 1.2.2. Added usage overview. Shows token used and cost in $.
This commit is contained in:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user