diff --git a/backend/apps/service/service.py b/backend/apps/service/service.py
index 4e9e9d31..e3502daf 100644
--- a/backend/apps/service/service.py
+++ b/backend/apps/service/service.py
@@ -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),
diff --git a/frontend/src/app/pages/Settings/Settings.tsx b/frontend/src/app/pages/Settings/Settings.tsx
index 8f03c04b..f3983542 100644
--- a/frontend/src/app/pages/Settings/Settings.tsx
+++ b/frontend/src/app/pages/Settings/Settings.tsx
@@ -1037,7 +1037,7 @@ const UsageStats: React.FC = () => {
);
- 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 (
+ {isSubscription && savedAmt > 1 && (
+
+ ✨
+
+ {savingsQuip}
+
+
+ )}
Total Sessions