[eric] usage: accurate tool calls + run time, drop empty-draft skew, quirky savings line

This commit is contained in:
ciregenz
2026-05-24 02:15:19 -07:00
parent 45fdfff3b7
commit cd694ce6f1
2 changed files with 74 additions and 22 deletions
+48 -21
View File
@@ -282,11 +282,24 @@ async def usage_summary():
for s in agent_manager.get_all_sessions():
sessions.append(s.model_dump(mode="json"))
def _is_real(sess: dict) -> bool:
# "Real" = actually ran. Empty draft/abandoned sessions (no assistant turn, no tokens,
# no active time) otherwise inflate the count and drag every average toward zero.
if (sess.get("agent_active_ms") or 0) > 0 or (sess.get("cost_usd") or 0) > 0:
return True
tk = sess.get("tokens") or {}
if (tk.get("input") or 0) > 0 or (tk.get("output") or 0) > 0:
return True
return any(m.get("role") == "assistant" for m in sess.get("messages", []))
sessions = [s for s in sessions if _is_real(s)]
total_sessions = len(sessions)
total_cost = sum(s.get("cost_usd", 0) for s in sessions)
total_messages = 0
total_tool_calls = 0
total_duration = 0.0
total_run_seconds = 0.0
timed_sessions = 0
model_counts: Counter = Counter()
provider_counts: Counter = Counter()
tool_counts: Counter = Counter()
@@ -294,30 +307,43 @@ async def usage_summary():
for s in sessions:
messages = s.get("messages", [])
user_msgs = [m for m in messages if m.get("role") in ("user", "assistant")]
tool_msgs = [m for m in messages if m.get("role") == "tool_call"]
total_messages += len(user_msgs)
total_tool_calls += len(tool_msgs)
total_messages += sum(1 for m in messages if m.get("role") in ("user", "assistant"))
model_counts[s.get("model", "unknown")] += 1
provider_counts[s.get("provider", "anthropic")] += 1
status_counts[s.get("status", "unknown")] += 1
created = s.get("created_at")
closed = s.get("closed_at")
if created and closed:
try:
dur = (datetime.fromisoformat(closed[:19]) - datetime.fromisoformat(created[:19])).total_seconds()
if dur > 0:
total_duration += dur
except Exception:
pass
for m in tool_msgs:
content = m.get("content", {})
if isinstance(content, dict):
tool_name = content.get("tool", "")
if tool_name:
tool_counts[tool_name] += 1
avg_duration = total_duration / total_sessions if total_sessions > 0 else 0
# Tool calls: tool_latencies carries authoritative per-tool counts; older sessions only have
# the sparse tool_call messages. Per session take whichever source recorded more so we never
# undercount what's on record (and so the total never drops below the old message-only count).
lat_counts: Counter = Counter()
for tool, d in (s.get("tool_latencies") or {}).items():
cnt = (d or {}).get("count", 0) or 0
if tool and cnt:
lat_counts[tool] += cnt
msg_counts: Counter = Counter()
for m in messages:
if m.get("role") == "tool_call":
content = m.get("content", {})
name = content.get("tool") if isinstance(content, dict) else None
msg_counts[name or "tool"] += 1
chosen = lat_counts if sum(lat_counts.values()) >= sum(msg_counts.values()) else msg_counts
total_tool_calls += sum(chosen.values())
tool_counts.update(chosen)
# Run time: real agent-active time when tracked, else session wall-clock as a rough proxy.
run_s = (s.get("agent_active_ms") or 0) / 1000.0
if run_s <= 0:
created, closed = s.get("created_at"), s.get("closed_at")
if created and closed:
try:
run_s = (datetime.fromisoformat(closed[:19]) - datetime.fromisoformat(created[:19])).total_seconds()
except Exception:
run_s = 0
if run_s > 0:
total_run_seconds += run_s
timed_sessions += 1
avg_duration = total_run_seconds / timed_sessions if timed_sessions > 0 else 0
completed = status_counts.get("completed", 0)
completion_rate = completed / total_sessions if total_sessions > 0 else 0
@@ -362,6 +388,7 @@ async def usage_summary():
"total_cost_usd": round(total_cost, 4),
"total_messages": total_messages,
"total_tool_calls": total_tool_calls,
"total_run_seconds": round(total_run_seconds, 1),
"avg_duration_seconds": round(avg_duration, 1),
"avg_cost_per_session": round(avg_cost, 4),
"completion_rate": round(completion_rate, 3),
+26 -1
View File
@@ -1037,7 +1037,7 @@ const UsageStats: React.FC = () => {
<PixelBarOuter {...props} tokens={c} />
);
const totalTime = stats.avg_duration_seconds * stats.total_sessions;
const totalTime = stats.total_run_seconds ?? (stats.avg_duration_seconds * stats.total_sessions);
const msgsPerSession = stats.total_sessions > 0 ? (stats.total_messages / stats.total_sessions).toFixed(1) : '0';
const toolsPerSession = stats.total_sessions > 0 ? (stats.total_tool_calls / stats.total_sessions).toFixed(1) : '0';
const formatTokens = (n: number) => {
@@ -1049,8 +1049,33 @@ const UsageStats: React.FC = () => {
const isSubscription = stats.cost_source === '9router';
const costSourceLabel = isSubscription ? 'saved with your subscription' : stats.cost_source === 'sdk' ? 'via API' : '';
// Quirky savings nudge (subscription users only): what their token usage would've cost at API
// rates, framed a little differently each day so it stays fun without nagging.
const savedAmt = stats.total_cost_usd || 0;
const sessionsLabel = (stats.total_sessions || 0).toLocaleString();
const lattes = Math.max(1, Math.round(savedAmt / 5.75));
const savingsQuips = [
`You've sidestepped ${formatCost(savedAmt)} in API fees by routing ${sessionsLabel} agent runs through your own subscriptions. The meter never blinked.`,
`${formatCost(savedAmt)} saved, about ${lattes} oat-milk latte${lattes === 1 ? '' : 's'} you didn't expense to a token meter.`,
`Your subscriptions quietly did the work of a ${formatCost(savedAmt)} API bill. OpenSwarm just drove around the toll booth.`,
`${formatCost(savedAmt)} that stayed in your wallet across ${sessionsLabel} sessions. Per-token guilt: zero.`,
];
const savingsQuip = savingsQuips[Math.floor(Date.now() / 86_400_000) % savingsQuips.length];
return (
<Box sx={{ mb: 2.5 }}>
{isSubscription && savedAmt > 1 && (
<Box sx={{
mb: 1.5, px: 1.5, py: 1, borderRadius: `${c.radius.md}px`,
bgcolor: `${c.accent.primary}0F`, border: `1px solid ${c.accent.primary}26`,
display: 'flex', alignItems: 'center', gap: 1,
}}>
<Typography sx={{ fontSize: '0.95rem', lineHeight: 1 }}></Typography>
<Typography sx={{ fontSize: '0.74rem', color: c.text.secondary, fontStyle: 'italic', lineHeight: 1.4 }}>
{savingsQuip}
</Typography>
</Box>
)}
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 1, mb: 1 }}>
<Box sx={cardSx}>
<Typography sx={labelSx}>Total Sessions</Typography>