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