Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions hermes_cli/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2299,6 +2299,99 @@ async def get_usage_analytics(days: int = 30):
db.close()


@app.get("/api/analytics/models")
async def get_models_analytics(days: int = 30):
"""Rich per-model analytics for the Models dashboard page.

Returns token/cost/session breakdown per model plus capability metadata
from models.dev (context window, vision, tools, reasoning, etc.).
"""
from hermes_state import SessionDB

db = SessionDB()
try:
cutoff = time.time() - (days * 86400)

cur = db._conn.execute("""
SELECT model,
billing_provider,
SUM(input_tokens) as input_tokens,
SUM(output_tokens) as output_tokens,
SUM(cache_read_tokens) as cache_read_tokens,
SUM(reasoning_tokens) as reasoning_tokens,
COALESCE(SUM(estimated_cost_usd), 0) as estimated_cost,
COALESCE(SUM(actual_cost_usd), 0) as actual_cost,
COUNT(*) as sessions,
SUM(COALESCE(api_call_count, 0)) as api_calls,
SUM(tool_call_count) as tool_calls,
MAX(started_at) as last_used_at,
AVG(input_tokens + output_tokens) as avg_tokens_per_session
FROM sessions WHERE started_at > ? AND model IS NOT NULL AND model != ''
GROUP BY model, billing_provider
ORDER BY SUM(input_tokens) + SUM(output_tokens) DESC
""", (cutoff,))
rows = [dict(r) for r in cur.fetchall()]

models = []
for row in rows:
provider = row.get("billing_provider") or ""
model_name = row["model"]
caps = {}
try:
from agent.models_dev import get_model_capabilities
mc = get_model_capabilities(provider=provider, model=model_name)
if mc is not None:
caps = {
"supports_tools": mc.supports_tools,
"supports_vision": mc.supports_vision,
"supports_reasoning": mc.supports_reasoning,
"context_window": mc.context_window,
"max_output_tokens": mc.max_output_tokens,
"model_family": mc.model_family,
}
except Exception:
pass

models.append({
"model": model_name,
"provider": provider,
"input_tokens": row["input_tokens"],
"output_tokens": row["output_tokens"],
"cache_read_tokens": row["cache_read_tokens"],
"reasoning_tokens": row["reasoning_tokens"],
"estimated_cost": row["estimated_cost"],
"actual_cost": row["actual_cost"],
"sessions": row["sessions"],
"api_calls": row["api_calls"],
"tool_calls": row["tool_calls"],
"last_used_at": row["last_used_at"],
"avg_tokens_per_session": row["avg_tokens_per_session"],
"capabilities": caps,
})

totals_cur = db._conn.execute("""
SELECT COUNT(DISTINCT model) as distinct_models,
SUM(input_tokens) as total_input,
SUM(output_tokens) as total_output,
SUM(cache_read_tokens) as total_cache_read,
SUM(reasoning_tokens) as total_reasoning,
COALESCE(SUM(estimated_cost_usd), 0) as total_estimated_cost,
COALESCE(SUM(actual_cost_usd), 0) as total_actual_cost,
COUNT(*) as total_sessions,
SUM(COALESCE(api_call_count, 0)) as total_api_calls
FROM sessions WHERE started_at > ? AND model IS NOT NULL AND model != ''
""", (cutoff,))
totals = dict(totals_cur.fetchone())

return {
"models": models,
"totals": totals,
"period_days": days,
}
finally:
db.close()


# ---------------------------------------------------------------------------
# /api/pty — PTY-over-WebSocket bridge for the dashboard "Chat" tab.
#
Expand Down
1 change: 1 addition & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"beesr@bee.localdomain": "beesrsj2500",
"mtf201013@gmail.com": "ma-pony",
"sonoyuncudmr@gmail.com": "Sonoyunchu",
"43525405+yatesjalex@users.noreply.github.com": "yatesjalex",
"maks.mir@yahoo.com": "say8hi",
"27719690+Mirac1eSky@users.noreply.github.com": "Mirac1eSky",
"web3blind@users.noreply.github.com": "web3blind",
Expand Down
10 changes: 10 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
BookOpen,
Clock,
Code,
Cpu,
Database,
Download,
Eye,
Expand Down Expand Up @@ -61,6 +62,7 @@ import EnvPage from "@/pages/EnvPage";
import SessionsPage from "@/pages/SessionsPage";
import LogsPage from "@/pages/LogsPage";
import AnalyticsPage from "@/pages/AnalyticsPage";
import ModelsPage from "@/pages/ModelsPage";
import CronPage from "@/pages/CronPage";
import SkillsPage from "@/pages/SkillsPage";
import ChatPage from "@/pages/ChatPage";
Expand Down Expand Up @@ -96,6 +98,7 @@ const BUILTIN_ROUTES_CORE: Record<string, ComponentType> = {
"/": RootRedirect,
"/sessions": SessionsPage,
"/analytics": AnalyticsPage,
"/models": ModelsPage,
"/logs": LogsPage,
"/cron": CronPage,
"/skills": SkillsPage,
Expand Down Expand Up @@ -125,6 +128,12 @@ const BUILTIN_NAV_REST: NavItem[] = [
label: "Analytics",
icon: BarChart3,
},
{
path: "/models",
labelKey: "models",
label: "Models",
icon: Cpu,
},
{ path: "/logs", labelKey: "logs", label: "Logs", icon: FileText },
{ path: "/cron", labelKey: "cron", label: "Cron", icon: Clock },
{ path: "/skills", labelKey: "skills", label: "Skills", icon: Package },
Expand All @@ -142,6 +151,7 @@ const ICON_MAP: Record<string, ComponentType<{ className?: string }>> = {
Activity,
BarChart3,
Clock,
Cpu,
FileText,
KeyRound,
MessageSquare,
Expand Down
13 changes: 13 additions & 0 deletions web/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const en: Translations = {
documentation: "Documentation",
keys: "Keys",
logs: "Logs",
models: "Models",
sessions: "Sessions",
skills: "Skills",
},
Expand Down Expand Up @@ -172,6 +173,18 @@ export const en: Translations = {
inOut: "{input} in / {output} out",
},

models: {
modelsUsed: "Models Used",
estimatedCost: "Est. Cost",
tokens: "tokens",
sessions: "sessions",
avgPerSession: "avg/session",
apiCalls: "API calls",
toolCalls: "tool calls",
noModelsData: "No model usage data for this period",
startSession: "Start a session to see model data here",
},

logs: {
title: "Logs",
autoRefresh: "Auto-refresh",
Expand Down
14 changes: 14 additions & 0 deletions web/src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export interface Translations {
documentation: string;
keys: string;
logs: string;
models: string;
sessions: string;
skills: string;
};
Expand Down Expand Up @@ -174,6 +175,19 @@ export interface Translations {
inOut: string;
};

// ── Models page ──
models: {
modelsUsed: string;
estimatedCost: string;
tokens: string;
sessions: string;
avgPerSession: string;
apiCalls: string;
toolCalls: string;
noModelsData: string;
startSession: string;
};

// ── Logs page ──
logs: {
title: string;
Expand Down
13 changes: 13 additions & 0 deletions web/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const zh: Translations = {
documentation: "文档",
keys: "密钥",
logs: "日志",
models: "模型",
sessions: "会话",
skills: "技能",
},
Expand Down Expand Up @@ -170,6 +171,18 @@ export const zh: Translations = {
inOut: "输入 {input} / 输出 {output}",
},

models: {
modelsUsed: "使用模型数",
estimatedCost: "预估费用",
tokens: "Token",
sessions: "会话",
avgPerSession: "平均/会话",
apiCalls: "API 调用",
toolCalls: "工具调用",
noModelsData: "该时间段暂无模型使用数据",
startSession: "开始会话后将在此显示模型数据",
},

logs: {
title: "日志",
autoRefresh: "自动刷新",
Expand Down
42 changes: 42 additions & 0 deletions web/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ export const api = {
},
getAnalytics: (days: number) =>
fetchJSON<AnalyticsResponse>(`/api/analytics/usage?days=${days}`),
getModelsAnalytics: (days: number) =>
fetchJSON<ModelsAnalyticsResponse>(`/api/analytics/models?days=${days}`),
getConfig: () => fetchJSON<Record<string, unknown>>("/api/config"),
getDefaults: () => fetchJSON<Record<string, unknown>>("/api/config/defaults"),
getSchema: () => fetchJSON<{ fields: Record<string, unknown>; category_order: string[] }>("/api/config/schema"),
Expand Down Expand Up @@ -370,6 +372,46 @@ export interface AnalyticsResponse {
};
}

export interface ModelsAnalyticsModelEntry {
model: string;
provider: string;
input_tokens: number;
output_tokens: number;
cache_read_tokens: number;
reasoning_tokens: number;
estimated_cost: number;
actual_cost: number;
sessions: number;
api_calls: number;
tool_calls: number;
last_used_at: number;
avg_tokens_per_session: number;
capabilities: {
supports_tools?: boolean;
supports_vision?: boolean;
supports_reasoning?: boolean;
context_window?: number;
max_output_tokens?: number;
model_family?: string;
};
}

export interface ModelsAnalyticsResponse {
models: ModelsAnalyticsModelEntry[];
totals: {
distinct_models: number;
total_input: number;
total_output: number;
total_cache_read: number;
total_reasoning: number;
total_estimated_cost: number;
total_actual_cost: number;
total_sessions: number;
total_api_calls: number;
};
period_days: number;
}

export interface CronJob {
id: string;
name?: string;
Expand Down
Loading
Loading