feat(site): improve cost analytics view (#23069)

Surfaces cache token data in the analytics views and fixes table
spacing.

### Changes

- **Cache token columns**: Added cache read and cache write token counts
to all analytics views (user and admin), from SQL queries through Go SDK
types to the frontend tables and summary cards.
- **Table spacing fix**: Replaced the bare React fragment in
`ChatCostSummaryView` with a `space-y-6` container so the model and chat
breakdown tables no longer overlap.

### Data flow

`chat_messages` table already stores `cache_read_tokens` and
`cache_creation_tokens` (and uses them for cost calculation). This PR
aggregates and displays them alongside input/output tokens in:

- Summary cards (6 cards: Total Cost, Input, Output, Cache Read, Cache
Write, Messages)
- Per-model breakdown table
- Per-chat breakdown table
- Admin per-user table
This commit is contained in:
Michael Suchacz
2026-03-14 07:22:00 +01:00
committed by GitHub
parent f6976fd6c1
commit 969066b55e
11 changed files with 252 additions and 106 deletions
+62 -34
View File
@@ -3259,7 +3259,9 @@ WITH chat_costs AS (
OR cm.cache_read_tokens IS NOT NULL
)::bigint AS message_count,
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens
FROM chat_messages cm
JOIN chats c ON c.id = cm.chat_id
WHERE c.owner_id = $1::uuid
@@ -3274,7 +3276,9 @@ SELECT
cc.total_cost_micros,
cc.message_count,
cc.total_input_tokens,
cc.total_output_tokens
cc.total_output_tokens,
cc.total_cache_read_tokens,
cc.total_cache_creation_tokens
FROM chat_costs cc
LEFT JOIN chats rc ON rc.id = cc.root_chat_id
ORDER BY cc.total_cost_micros DESC
@@ -3287,12 +3291,14 @@ type GetChatCostPerChatParams struct {
}
type GetChatCostPerChatRow struct {
RootChatID uuid.UUID `db:"root_chat_id" json:"root_chat_id"`
ChatTitle string `db:"chat_title" json:"chat_title"`
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
MessageCount int64 `db:"message_count" json:"message_count"`
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
RootChatID uuid.UUID `db:"root_chat_id" json:"root_chat_id"`
ChatTitle string `db:"chat_title" json:"chat_title"`
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
MessageCount int64 `db:"message_count" json:"message_count"`
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
TotalCacheReadTokens int64 `db:"total_cache_read_tokens" json:"total_cache_read_tokens"`
TotalCacheCreationTokens int64 `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"`
}
// Per-root-chat cost breakdown for a single user within a date range.
@@ -3314,6 +3320,8 @@ func (q *sqlQuerier) GetChatCostPerChat(ctx context.Context, arg GetChatCostPerC
&i.MessageCount,
&i.TotalInputTokens,
&i.TotalOutputTokens,
&i.TotalCacheReadTokens,
&i.TotalCacheCreationTokens,
); err != nil {
return nil, err
}
@@ -3343,7 +3351,9 @@ SELECT
OR cm.cache_read_tokens IS NOT NULL
)::bigint AS message_count,
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens
FROM
chat_messages cm
JOIN
@@ -3368,14 +3378,16 @@ type GetChatCostPerModelParams struct {
}
type GetChatCostPerModelRow struct {
ModelConfigID uuid.UUID `db:"model_config_id" json:"model_config_id"`
DisplayName string `db:"display_name" json:"display_name"`
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
MessageCount int64 `db:"message_count" json:"message_count"`
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
ModelConfigID uuid.UUID `db:"model_config_id" json:"model_config_id"`
DisplayName string `db:"display_name" json:"display_name"`
Provider string `db:"provider" json:"provider"`
Model string `db:"model" json:"model"`
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
MessageCount int64 `db:"message_count" json:"message_count"`
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
TotalCacheReadTokens int64 `db:"total_cache_read_tokens" json:"total_cache_read_tokens"`
TotalCacheCreationTokens int64 `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"`
}
// Per-model cost breakdown for a single user within a date range.
@@ -3398,6 +3410,8 @@ func (q *sqlQuerier) GetChatCostPerModel(ctx context.Context, arg GetChatCostPer
&i.MessageCount,
&i.TotalInputTokens,
&i.TotalOutputTokens,
&i.TotalCacheReadTokens,
&i.TotalCacheCreationTokens,
); err != nil {
return nil, err
}
@@ -3429,7 +3443,9 @@ WITH chat_cost_users AS (
)::bigint AS message_count,
COUNT(DISTINCT COALESCE(c.root_chat_id, c.id))::bigint AS chat_count,
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens
FROM
chat_messages cm
JOIN
@@ -3460,6 +3476,8 @@ SELECT
chat_count,
total_input_tokens,
total_output_tokens,
total_cache_read_tokens,
total_cache_creation_tokens,
COUNT(*) OVER()::bigint AS total_count
FROM
chat_cost_users
@@ -3481,16 +3499,18 @@ type GetChatCostPerUserParams struct {
}
type GetChatCostPerUserRow struct {
UserID uuid.UUID `db:"user_id" json:"user_id"`
Username string `db:"username" json:"username"`
Name string `db:"name" json:"name"`
AvatarURL string `db:"avatar_url" json:"avatar_url"`
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
MessageCount int64 `db:"message_count" json:"message_count"`
ChatCount int64 `db:"chat_count" json:"chat_count"`
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
TotalCount int64 `db:"total_count" json:"total_count"`
UserID uuid.UUID `db:"user_id" json:"user_id"`
Username string `db:"username" json:"username"`
Name string `db:"name" json:"name"`
AvatarURL string `db:"avatar_url" json:"avatar_url"`
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
MessageCount int64 `db:"message_count" json:"message_count"`
ChatCount int64 `db:"chat_count" json:"chat_count"`
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
TotalCacheReadTokens int64 `db:"total_cache_read_tokens" json:"total_cache_read_tokens"`
TotalCacheCreationTokens int64 `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"`
TotalCount int64 `db:"total_count" json:"total_count"`
}
// Deployment-wide per-user cost rollup within a date range.
@@ -3520,6 +3540,8 @@ func (q *sqlQuerier) GetChatCostPerUser(ctx context.Context, arg GetChatCostPerU
&i.ChatCount,
&i.TotalInputTokens,
&i.TotalOutputTokens,
&i.TotalCacheReadTokens,
&i.TotalCacheCreationTokens,
&i.TotalCount,
); err != nil {
return nil, err
@@ -3552,7 +3574,9 @@ SELECT
)
)::bigint AS unpriced_message_count,
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens
FROM
chat_messages cm
JOIN
@@ -3571,11 +3595,13 @@ type GetChatCostSummaryParams struct {
}
type GetChatCostSummaryRow struct {
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
PricedMessageCount int64 `db:"priced_message_count" json:"priced_message_count"`
UnpricedMessageCount int64 `db:"unpriced_message_count" json:"unpriced_message_count"`
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
TotalCostMicros int64 `db:"total_cost_micros" json:"total_cost_micros"`
PricedMessageCount int64 `db:"priced_message_count" json:"priced_message_count"`
UnpricedMessageCount int64 `db:"unpriced_message_count" json:"unpriced_message_count"`
TotalInputTokens int64 `db:"total_input_tokens" json:"total_input_tokens"`
TotalOutputTokens int64 `db:"total_output_tokens" json:"total_output_tokens"`
TotalCacheReadTokens int64 `db:"total_cache_read_tokens" json:"total_cache_read_tokens"`
TotalCacheCreationTokens int64 `db:"total_cache_creation_tokens" json:"total_cache_creation_tokens"`
}
// Aggregate cost summary for a single user within a date range.
@@ -3589,6 +3615,8 @@ func (q *sqlQuerier) GetChatCostSummary(ctx context.Context, arg GetChatCostSumm
&i.UnpricedMessageCount,
&i.TotalInputTokens,
&i.TotalOutputTokens,
&i.TotalCacheReadTokens,
&i.TotalCacheCreationTokens,
)
return i, err
}
+17 -5
View File
@@ -526,7 +526,9 @@ SELECT
)
)::bigint AS unpriced_message_count,
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens
FROM
chat_messages cm
JOIN
@@ -554,7 +556,9 @@ SELECT
OR cm.cache_read_tokens IS NOT NULL
)::bigint AS message_count,
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens
FROM
chat_messages cm
JOIN
@@ -587,7 +591,9 @@ WITH chat_costs AS (
OR cm.cache_read_tokens IS NOT NULL
)::bigint AS message_count,
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens
FROM chat_messages cm
JOIN chats c ON c.id = cm.chat_id
WHERE c.owner_id = @owner_id::uuid
@@ -602,7 +608,9 @@ SELECT
cc.total_cost_micros,
cc.message_count,
cc.total_input_tokens,
cc.total_output_tokens
cc.total_output_tokens,
cc.total_cache_read_tokens,
cc.total_cache_creation_tokens
FROM chat_costs cc
LEFT JOIN chats rc ON rc.id = cc.root_chat_id
ORDER BY cc.total_cost_micros DESC;
@@ -626,7 +634,9 @@ WITH chat_cost_users AS (
)::bigint AS message_count,
COUNT(DISTINCT COALESCE(c.root_chat_id, c.id))::bigint AS chat_count,
COALESCE(SUM(cm.input_tokens), 0)::bigint AS total_input_tokens,
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens
COALESCE(SUM(cm.output_tokens), 0)::bigint AS total_output_tokens,
COALESCE(SUM(cm.cache_read_tokens), 0)::bigint AS total_cache_read_tokens,
COALESCE(SUM(cm.cache_creation_tokens), 0)::bigint AS total_cache_creation_tokens
FROM
chat_messages cm
JOIN
@@ -657,6 +667,8 @@ SELECT
chat_count,
total_input_tokens,
total_output_tokens,
total_cache_read_tokens,
total_cache_creation_tokens,
COUNT(*) OVER()::bigint AS total_count
FROM
chat_cost_users