fix(dashboard): align token stats by API key/channel/model with totals#1524
Conversation
`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 SummaryThis PR fixes two independent bugs in the dashboard token stats resolvers: (1) Confidence Score: 5/5Safe 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 ( No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
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)
Reviews (2): Last reviewed commit: "fix(dashboard): drop reasoning from per-..." | Re-trigger Greptile |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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.
| 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.
Summary
tokenStatsByAPIKeyjoinedusage_logstorequestsonly to readapi_key_id, butusage_logs.api_key_idis the same column. Once therequeststable 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 onusage_logs.api_key_id(mirroringrequestStatsByAPIKey/costStatsByAPIKey), and bump the per-key limit from 3 → 10 to match the other by-X resolvers and the chart's existingslice(0, 10).totalTokensin by-API-key, by-channel and by-model frominput + output + reasoningtoinput + output. Reasoning tokens are already a subset ofcompletion_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 matchingORDER BYformulas are simplified the same way.Test plan
requestshas been pruned butusage_logsis retained, confirm "Tokens by API key" panel populates and the sum matches "All token stats" / API-Keys page totals.totalTokensno longer exceeds the input+output total.