Skip to content

fix(dashboard): query session_model_usage for accurate model analytics (#71778) - #71802

Open
JonthanaHanh wants to merge 1 commit into
NousResearch:mainfrom
JonthanaHanh:fix/dashboard-models-analytics-session-model-usage
Open

fix(dashboard): query session_model_usage for accurate model analytics (#71778)#71802
JonthanaHanh wants to merge 1 commit into
NousResearch:mainfrom
JonthanaHanh:fix/dashboard-models-analytics-session-model-usage

Conversation

@JonthanaHanh

Copy link
Copy Markdown
Contributor

Summary

Fixes #71778

The Models dashboard page (/api/analytics/models) queried the sessions table with GROUP BY model, billing_provider. The sessions table only records the final billing_provider after a mid-session /model switch, so all tokens were attributed to a single provider even when multiple providers were used during the session.

This PR switches the main query to session_model_usage which tracks every API call individually with its own model+provider attribution. This matches the approach already used by the Insights engine (_compute_model_breakdown).

Changes

  • hermes_cli/web_server.py: Replace FROM sessions query in _get_models_analytics() with FROM session_model_usage u JOIN sessions s
  • Adds try/except fallback to old sessions query for older DBs without the session_model_usage table
  • Uses COUNT(DISTINCT u.session_id) instead of COUNT(*) for accurate session count
  • Filters u.task = '' to exclude auxiliary usage rows (already handled separately by _aux_usage_rows)

Test Plan

  • python3 -m py_compile hermes_cli/web_server.py
  • Dashboard Models page shows correct per-provider attribution for sessions that switched models mid-conversation
  • Older DBs without session_model_usage table still work (fallback query)

NousResearch#71778)

The Models dashboard page (`/api/analytics/models`) queried the `sessions`
table with `GROUP BY model, billing_provider`. The `sessions` table only
records the *final* billing_provider after a mid-session `/model` switch,
so all tokens were attributed to one provider even when multiple were used.

Switch the main query to `session_model_usage` which tracks every API call
individually with its own model+provider attribution. This matches the
approach already used by the Insights engine (`_compute_model_breakdown`).

Includes a try/except fallback to the old `sessions` query for older DBs
that may not have the `session_model_usage` table.

Fixes NousResearch#71778
@JonthanaHanh
JonthanaHanh force-pushed the fix/dashboard-models-analytics-session-model-usage branch from c70d245 to c4ad87c Compare July 26, 2026 06:40
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/dashboard Web dashboard / control panel UI (dashboard/, landing) area/usage-cost Token accounting, usage reporting, billing, cost tracking labels Jul 26, 2026
@PRATHAMESH75

Copy link
Copy Markdown
Contributor

Reviewed against #71778. The root cause is right and the core fix is correct: _get_models_analytics() now aggregates from session_model_usage (per-API-call model+provider attribution) instead of the sessions table (which holds only the final billing_provider after a /model switch), so mid-session switches stop being misattributed to one provider. WHERE u.task = '' correctly scopes to main-agent usage so it doesn't double-count the auxiliary rows added below. COUNT(DISTINCT u.session_id) and MAX(u.last_seen) are the right shapes. I verified every queried column exists on the table (hermes_state.py:1158-1178).

Two things worth tightening before merge:

  1. tool_calls is now always 0. session_model_usage has no tool_call_count column (schema at hermes_state.py:1158-1178), so 0 as tool_calls hardcodes the Models-page tool-call count to zero for every card — the old query summed the real sessions.tool_call_count. That column is displayed on the page, so this is a visible regression. Tool calls aren't tracked per (model, provider), so exact per-card attribution isn't available, but a per-session tool_call_count sum joined back on session_id would at least preserve a meaningful total instead of a flat 0 (or, if that's out of scope, calling out the 0 explicitly so a reviewer signs off on dropping the column).

  2. The except Exception fallback is too broad. The only legitimate reason to fall back to the old sessions query is a pre-session_model_usage DB, i.e. sqlite3.OperationalError: no such table. A bare except Exception also swallows a genuine bug in the new query and silently resurrects the exact misattribution this PR is fixing — the failure mode would be invisible. Narrowing to except sqlite3.OperationalError (or a pragma_table_info existence check like the migrations use at hermes_state.py:3405) keeps the old-DB safety net while letting real errors surface.

Neither blocks the core fix; both are about not masking a regression. Thanks for tracing this to the right table.

@PRATHAMESH75

Copy link
Copy Markdown
Contributor

Reviewed against #71778 — the root-cause diagnosis and the core fix are right. sessions.billing_provider only holds the final provider after a /model switch, so aggregating there misattributes every token to one provider; moving the aggregation to session_model_usage (which is keyed per model, billing_provider, task per API call) is the correct source. Filtering u.task = '' to keep main-agent usage while the auxiliary rows are added separately below is also right — it avoids double-counting aux.

One concrete regression to flag before merge:

  • tool_calls is now hardcoded to 0. session_model_usage has no tool_call_count column (hermes_state.py:1158), so the query can't source it there. But the frontend still renders it: web/src/pages/ModelsPage.tsx:520 shows entry.tool_calls when > 0, so with this change the per-model tool-call stat disappears from every model card — a silent data loss for a field that previously worked. Per-model tool-call attribution after a mid-session switch is genuinely ambiguous (same reason the tokens were wrong), so dropping it may be the right call — but it should be an explicit decision, e.g. keep a session-level SUM(tool_call_count) joined from sessions, or drop the tool_calls UI row rather than leave it permanently hidden. Either way worth a line in the PR body so a maintainer knows the stat is going away.

Minor: avg_tokens_per_session is now AVG(input+output) over session_model_usage rows, not over sessions, so for sessions spanning multiple models it averages per model-row rather than per session — the label slightly overstates what it measures now.

Neither blocks the core correctness fix; the provider/token attribution is the important part and this gets it right.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tracing the Models dashboard to the per-route accounting table. Current main still aggregates this endpoint from sessions (hermes_cli/web_server.py:13952-13969), while the write path records the active model/provider per API delta in session_model_usage (hermes_state.py:4224-4290), so the core direction is correct.

Problems

  • hermes_cli/web_server.py:16835 hardcodes tool_calls to 0. session_model_usage has no such column (hermes_state_common.py:218-238), but the dashboard renders positive entry.tool_calls values (web/src/pages/ModelsPage.tsx:520-524), so this removes the stat from every main-agent card.
  • hermes_cli/web_server.py:16845 catches every exception and silently falls back to the known-inaccurate sessions query. The analogous Insights fallback is limited to sqlite3.OperationalError (agent/insights.py:525-541).
  • The updated test only changes an existing zero-API-call expectation; it does not cover a single session switching between two model/provider routes.

Suggested changes

  • Preserve or explicitly redesign session-level tool-call attribution rather than returning zeros.
  • Narrow the fallback to the expected SQLite compatibility error.
  • Add an endpoint regression test for two model/provider rows written under one session.

Automated hermes-sweeper review.

Comment thread hermes_cli/web_server.py
COALESCE(SUM(u.actual_cost_usd), 0) as actual_cost,
COUNT(DISTINCT u.session_id) as sessions,
SUM(COALESCE(u.api_call_count, 0)) as api_calls,
0 as tool_calls,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

session_model_usage has no tool-call field, so this makes every main-agent Models card report zero and hides the UI statistic at ModelsPage.tsx:520. Please preserve a deliberate session-level attribution (as Insights does) or remove/redesign the displayed metric explicitly.

Comment thread hermes_cli/web_server.py
GROUP BY u.model, u.billing_provider
ORDER BY SUM(u.input_tokens) + SUM(u.output_tokens) DESC
""", (cutoff,))
except Exception:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please narrow this fallback to the expected SQLite compatibility failure. Catching every exception hides defects in this new query and silently reverts users to the inaccurate sessions aggregation; Insights limits its analogous fallback to sqlite3.OperationalError.

@teknium1 teknium1 added sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform area/sessions Session lifecycle, resume, persistence, history labels Jul 30, 2026
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

Seven PRs address this issue complex across three layers: persisting the latest model switch, recording per-route usage, and consuming that accounting in Insights and the Models dashboard. #35256 landed latest-model persistence, #62610 landed per-route accounting and Insights aggregation, while #71802 is the remaining dashboard-consumer fix but has documented gaps.

Related pull requests

Duplicates

#35181 is the source implementation salvaged by #35256; #51634 is the core per-route design salvaged and hardened by #62610. #28842 overlaps #51634/#62610 on per-model accounting but uses a different JSON storage design, while #49682 overlaps the stale-model symptom addressed correctly by #35256; #71802 is not a duplicate because it fixes the downstream Models dashboard consumer.

Suggested consolidation

Keep #71802 open with a salvage path, consistent with the contributor keep_open review: retain its session_model_usage aggregation and u.task = '' scoping, narrow the fallback to sqlite3.OperationalError, deliberately preserve or redesign tool-call attribution, and add a regression test with one session switching across two model/provider routes. Treat #35256 and #62610 as merged reference implementations, and keep #28842, #35181, #49682, and #51634 closed under the explicit supersession chains above.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I28637(["issue #28637 (closed)"])
    I51607(["issue #51607 (closed)"])
    I71778(["issue #71778 (open)"])
    P71802["PR #71802 (open)"]
    P71802 -.->|partial| I28637
    P71802 -.->|partial| I51607
    P71802 -->|best fix| I71778
    class I28637 closed
    class I51607 closed
    class I71778 open
    class P71802 open
    class P71802 best
    class P71802 target
    click I28637 "https://github.com/NousResearch/hermes-agent/issues/28637"
    click I51607 "https://github.com/NousResearch/hermes-agent/issues/51607"
    click I71778 "https://github.com/NousResearch/hermes-agent/issues/71778"
    click P71802 "https://github.com/NousResearch/hermes-agent/pull/71802"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 7 pull requests and 4 issues in this complex. Each diff was read against this issue; Assessment working set: 76 kB of PR diffs, 28 kB of issue/PR text, 15 kB of discussion (22 comments), 34 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history area/usage-cost Token accounting, usage reporting, billing, cost tracking comp/dashboard Web dashboard / control panel UI (dashboard/, landing) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform type/bug Something isn't working

Projects

None yet

5 participants