Skip to content

fix(dashboard): align token stats by API key/channel/model with totals#1524

Merged
looplj merged 2 commits into
looplj:unstablefrom
raikyou:fix/dashboard-token-stats-by-api-key
Apr 28, 2026
Merged

fix(dashboard): align token stats by API key/channel/model with totals#1524
looplj merged 2 commits into
looplj:unstablefrom
raikyou:fix/dashboard-token-stats-by-api-key

Conversation

@raikyou

@raikyou raikyou commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • tokenStatsByAPIKey joined usage_logs to requests only to read api_key_id, but usage_logs.api_key_id is the same column. Once the requests table is pruned by GC retention while usage_logs are still kept, the dashboard's by-API-key panel silently zeroes out while the all-time totals stay non-zero. Aggregate directly on usage_logs.api_key_id (mirroring requestStatsByAPIKey / costStatsByAPIKey), and bump the per-key limit from 3 → 10 to match the other by-X resolvers and the chart's existing slice(0, 10).
  • Fix totalTokens in by-API-key, by-channel and by-model from input + output + reasoning to input + output. Reasoning tokens are already a subset of completion_tokens, so the previous formula double-counted them and made the per-row total drift above the all-time stats card on reasoning-heavy traffic. The matching ORDER BY formulas are simplified the same way.

Test plan

  • On a DB where requests has been pruned but usage_logs is retained, confirm "Tokens by API key" panel populates and the sum matches "All token stats" / API-Keys page totals.
  • On reasoning-heavy traffic, confirm the by-key/channel/model totalTokens no longer exceeds the input+output total.
  • On an account with >3 active keys, confirm up to 10 keys now appear in the chart.

`tokenStatsByAPIKey` joined `usage_logs` to `requests` only to read
`api_key_id`, even though `usage_logs.api_key_id` is the same column.
Once the requests table is pruned by GC retention while usage_logs are
retained longer, the dashboard's by-API-key panel silently zeroes out
while the all-time totals stay non-zero. Aggregate directly on
`usage_logs.api_key_id` (mirroring `requestStatsByAPIKey` /
`costStatsByAPIKey`), and bump the per-key limit from 3 to 10 to match
the other by-X resolvers and the chart's existing `slice(0, 10)`.

Also fix `totalTokens` in by-API-key, by-channel and by-model from
`input + output + reasoning` to `input + output`. Reasoning tokens are
already a subset of `completion_tokens`, so the previous formula
double-counted them and made the per-row total drift above the
all-time stats card on reasoning-heavy traffic.
@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes two independent bugs in the dashboard token stats resolvers: (1) TokenStatsByAPIKey previously joined usage_logs to requests only to read a column already present on usage_logs, causing the panel to silently zero out after GC prunes the requests table; and (2) all three by-key/channel/model resolvers double-counted reasoning tokens in totalTokens (and in the ORDER BY expression), since completion_tokens already includes reasoning tokens.

Confidence Score: 5/5

Safe to merge — both fixes are clearly correct and well-scoped with no regressions introduced.

All changes are targeted correctness fixes: eliminating a broken JOIN, correcting double-counting arithmetic, and pushing sort/limit into the database. No new logic paths, no schema changes, and the approach mirrors existing patterns in the codebase (requestStatsByAPIKey, costStatsByAPIKey). No P0/P1 findings remain.

No files require special attention.

Important Files Changed

Filename Overview
internal/server/gql/dashboard.resolvers.go Removes unnecessary JOIN to requests in TokenStatsByAPIKey, fixes double-counting of reasoning tokens in totalTokens for by-key/channel/model resolvers, pushes sort+limit into SQL, and bumps per-key limit from 3→10.
internal/server/gql/dashboard_helpers.go Fixes total_tokens SQL expression in getTopModelsForAPIKeys to exclude reasoning tokens from the computed column used for ordering, aligning it with the corrected resolver logic.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Resolver as TokenStatsByAPIKey
    participant UL as usage_logs table
    participant R as requests table (old)
    participant AK as api_keys table

    note over Resolver,R: Before (broken after GC prunes requests)
    Client->>Resolver: tokenStatsByAPIKey(timeWindow)
    Resolver->>UL: JOIN requests ON usage_logs.request_id = requests.id
    Resolver->>R: WHERE requests.api_key_id IS NOT NULL
    R-->>Resolver: empty after GC prunes requests

    note over Resolver,UL: After (directly on usage_logs)
    Client->>Resolver: tokenStatsByAPIKey(timeWindow)
    Resolver->>UL: WHERE api_key_id IS NOT NULL
    UL-->>Resolver: GROUP BY api_key_id (input+output, no double-count)
    Resolver->>AK: fetch names for top-10 key IDs
    AK-->>Resolver: key names
    Resolver-->>Client: [TokenStatsByAPIKey] (up to 10)
Loading

Reviews (2): Last reviewed commit: "fix(dashboard): drop reasoning from per-..." | Re-trigger Greptile

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request optimizes token statistics retrieval by aggregating directly on the usage_logs table, removing redundant joins, and moving sorting and limiting to the database level. It also corrects token calculations to prevent double-counting reasoning tokens. Feedback identifies a potential mapping failure during database scanning and suggests explicitly aliasing the api_key_id column to ensure it matches the struct field.

sql.As(requestTable.C(request.FieldAPIKeyID), "api_key_id"),
sql.As(sql.Sum(s.C(usagelog.FieldPromptTokens)), "input_tokens"),
sql.As(sql.Sum(s.C(usagelog.FieldCompletionTokens)), "output_tokens"),
s.C(usagelog.FieldAPIKeyID),

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.

high

The api_key_id column should be explicitly aliased to match the struct field tag. Without an alias, s.C() returns a qualified name (e.g., usage_logs.api_key_id) which may cause the Scan operation to fail to map the value to the APIKeyID field in the tokenStats struct.

Suggested change
s.C(usagelog.FieldAPIKeyID),
sql.As(s.C(usagelog.FieldAPIKeyID), "api_key_id"),

`getTopModelsForAPIKeys` still ordered the per-API-key model breakdown
by `input + output + reasoning`, which double-counts reasoning the same
way the by-X token resolvers did before this PR. The result isn't shown
to the client (struct doesn't expose `TotalTokens`), but the ranking is
biased — a reasoning-heavy model can displace a higher-throughput model
in the top-3 list. Switch the ordering alias to `input + output`.

Also explicitly alias `api_key_id` in `TokenStatsByAPIKey`'s SELECT to
guarantee the column matches the struct's JSON tag regardless of how
the underlying driver qualifies the column.
@looplj looplj merged commit 6d56570 into looplj:unstable Apr 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants