From 1035a4daf0cbcc19b5662670cd6be72fb57851db Mon Sep 17 00:00:00 2001 From: SavitarC <71271200+SavitarC@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:16:32 +0800 Subject: [PATCH 1/2] feat(ops): support token request stats across all platforms --- .../handler/admin/ops_dashboard_handler.go | 3 +- .../repository/ops_repo_openai_token_stats.go | 1 - .../ops_repo_openai_token_stats_test.go | 77 ++++++++++++++++++ frontend/src/i18n/locales/en/admin/ops.ts | 10 +-- frontend/src/i18n/locales/zh/admin/ops.ts | 10 +-- frontend/src/views/admin/ops/OpsDashboard.vue | 2 +- .../__tests__/OpsOpenAITokenStatsCard.spec.ts | 79 +++++++++++++++++++ 7 files changed, 169 insertions(+), 13 deletions(-) diff --git a/backend/internal/handler/admin/ops_dashboard_handler.go b/backend/internal/handler/admin/ops_dashboard_handler.go index 01f7bc2b55..ad16cfa08b 100644 --- a/backend/internal/handler/admin/ops_dashboard_handler.go +++ b/backend/internal/handler/admin/ops_dashboard_handler.go @@ -219,7 +219,8 @@ func (h *OpsHandler) GetDashboardErrorDistribution(c *gin.Context) { response.Success(c, data) } -// GetDashboardOpenAITokenStats returns OpenAI token efficiency stats grouped by model. +// GetDashboardOpenAITokenStats returns token efficiency stats for all platforms, grouped by model. +// The endpoint name is retained for compatibility; platform and group_id optionally narrow the scope. // GET /api/v1/admin/ops/dashboard/openai-token-stats func (h *OpsHandler) GetDashboardOpenAITokenStats(c *gin.Context) { if h.opsService == nil { diff --git a/backend/internal/repository/ops_repo_openai_token_stats.go b/backend/internal/repository/ops_repo_openai_token_stats.go index 6aea416eda..d700c5af83 100644 --- a/backend/internal/repository/ops_repo_openai_token_stats.go +++ b/backend/internal/repository/ops_repo_openai_token_stats.go @@ -32,7 +32,6 @@ func (r *opsRepository) GetOpenAITokenStats(ctx context.Context, filter *service } join, where, baseArgs, next := buildUsageWhere(dashboardFilter, dashboardFilter.StartTime, dashboardFilter.EndTime, 1) - where += " AND ul.model LIKE 'gpt%'" baseCTE := ` WITH stats AS ( diff --git a/backend/internal/repository/ops_repo_openai_token_stats_test.go b/backend/internal/repository/ops_repo_openai_token_stats_test.go index bb01d820f2..e872bb6f7a 100644 --- a/backend/internal/repository/ops_repo_openai_token_stats_test.go +++ b/backend/internal/repository/ops_repo_openai_token_stats_test.go @@ -2,6 +2,8 @@ package repository import ( "context" + "database/sql/driver" + "regexp" "testing" "time" @@ -10,6 +12,81 @@ import ( "github.com/stretchr/testify/require" ) +func TestOpsRepositoryGetOpenAITokenStats_PlatformScope(t *testing.T) { + start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + end := start.Add(time.Hour) + groupID := int64(9) + tests := []struct { + name string + platform string + groupID *int64 + models []string + }{ + {name: "all platforms", models: []string{"claude-sonnet-4", "deepseek-chat", "gemini-2.5-pro", "gpt-4o", "o3"}}, + {name: "anthropic group", platform: "anthropic", groupID: &groupID, models: []string{"claude-sonnet-4"}}, + {name: "gemini", platform: "gemini", models: []string{"gemini-2.5-pro"}}, + {name: "openai reasoning model", platform: "openai", models: []string{"o3"}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db, mock := newSQLMock(t) + repo := &opsRepository{db: db} + filter := &service.OpsOpenAITokenStatsFilter{ + TimeRange: "1h", + StartTime: start, + EndTime: end, + Platform: tt.platform, + GroupID: tt.groupID, + TopN: 20, + } + + // Match the complete filtering clause in both queries so an implicit + // model restriction cannot silently narrow the selected platform scope. + where := "WHERE ul.created_at >= $1 AND ul.created_at < $2" + args := []driver.Value{start, end} + if tt.groupID != nil { + where += " AND ul.group_id = $3" + args = append(args, *tt.groupID) + } + if tt.platform != "" { + placeholder := "$3" + if tt.groupID != nil { + placeholder = "$4" + } + where += " AND COALESCE(NULLIF(g.platform,''), a.platform) = " + placeholder + args = append(args, tt.platform) + } + queryPrefix := regexp.QuoteMeta(where) + `\s+GROUP BY ul.model\s+\)` + mock.ExpectQuery(queryPrefix + `\s+SELECT COUNT\(\*\) FROM stats`). + WithArgs(args...). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(len(tt.models))) + + rows := sqlmock.NewRows([]string{ + "model", "request_count", "avg_tokens_per_sec", "avg_first_token_ms", + "total_output_tokens", "avg_duration_ms", "requests_with_first_token", + }) + for _, model := range tt.models { + rows.AddRow(model, int64(2), 10.0, 100.0, int64(20), int64(1000), int64(2)) + } + mock.ExpectQuery(queryPrefix + `\s+SELECT model,`). + WithArgs(append(args, 20)...). + WillReturnRows(rows) + + resp, err := repo.GetOpenAITokenStats(context.Background(), filter) + require.NoError(t, err) + require.Equal(t, tt.platform, resp.Platform) + require.Equal(t, tt.groupID, resp.GroupID) + require.Equal(t, int64(len(tt.models)), resp.Total) + require.Len(t, resp.Items, len(tt.models)) + for i, model := range tt.models { + require.Equal(t, model, resp.Items[i].Model) + } + require.NoError(t, mock.ExpectationsWereMet()) + }) + } +} + func TestOpsRepositoryGetOpenAITokenStats_PaginationMode(t *testing.T) { db, mock := newSQLMock(t) repo := &opsRepository{db: db} diff --git a/frontend/src/i18n/locales/en/admin/ops.ts b/frontend/src/i18n/locales/en/admin/ops.ts index 2ce0bb38dc..afdbd36db6 100644 --- a/frontend/src/i18n/locales/en/admin/ops.ts +++ b/frontend/src/i18n/locales/en/admin/ops.ts @@ -157,15 +157,15 @@ export default { endTime: 'End Time' }, openaiTokenStats: { - title: 'OpenAI Token Request Stats', + title: 'Token Request Stats', viewModeTopN: 'TopN', viewModePagination: 'Pagination', prevPage: 'Previous', nextPage: 'Next', pageInfo: 'Page {page}/{total}', totalModels: 'Total models: {total}', - failedToLoad: 'Failed to load OpenAI token stats', - empty: 'No OpenAI token stats for the current filters', + failedToLoad: 'Failed to load token request stats', + empty: 'No token request stats for the current filters', table: { model: 'Model', requestCount: 'Requests', @@ -746,8 +746,8 @@ export default { dashboardCards: 'Dashboard Cards', displayAlertEvents: 'Display alert events', displayAlertEventsHint: 'Show or hide the recent alert events card on the ops dashboard. Enabled by default.', - displayOpenAITokenStats: 'Display OpenAI token request stats', - displayOpenAITokenStatsHint: 'Show or hide the OpenAI token request stats card on the ops dashboard. Hidden by default.', + displayOpenAITokenStats: 'Display token request stats', + displayOpenAITokenStatsHint: 'Show token request stats by model across all platforms, with platform and group filters. Hidden by default.', autoRefreshCountdown: 'Auto refresh: {seconds}s', validation: { title: 'Please fix the following issues', diff --git a/frontend/src/i18n/locales/zh/admin/ops.ts b/frontend/src/i18n/locales/zh/admin/ops.ts index 19a077fd0d..c7c017e4b9 100644 --- a/frontend/src/i18n/locales/zh/admin/ops.ts +++ b/frontend/src/i18n/locales/zh/admin/ops.ts @@ -153,15 +153,15 @@ export default { custom: '自定义' }, openaiTokenStats: { - title: 'OpenAI Token 请求统计', + title: 'Token 请求统计', viewModeTopN: 'TopN', viewModePagination: '分页', prevPage: '上一页', nextPage: '下一页', pageInfo: '第 {page}/{total} 页', totalModels: '模型总数:{total}', - failedToLoad: '加载 OpenAI Token 统计失败', - empty: '当前筛选条件下暂无 OpenAI Token 请求统计数据', + failedToLoad: '加载 Token 请求统计失败', + empty: '当前筛选条件下暂无 Token 请求统计数据', table: { model: '模型', requestCount: '请求数', @@ -747,8 +747,8 @@ export default { dashboardCards: '仪表盘卡片', displayAlertEvents: '展示告警事件', displayAlertEventsHint: '控制运维监控仪表盘中告警事件卡片是否显示,默认开启。', - displayOpenAITokenStats: '展示 OpenAI Token 请求统计', - displayOpenAITokenStatsHint: '控制运维监控仪表盘中 OpenAI Token 请求统计卡片是否显示,默认关闭。', + displayOpenAITokenStats: '展示 Token 请求统计', + displayOpenAITokenStatsHint: '按模型统计所有平台的 Token 请求,支持平台和分组筛选,默认关闭。', autoRefreshCountdown: '自动刷新:{seconds}s', validation: { title: '请先修正以下问题', diff --git a/frontend/src/views/admin/ops/OpsDashboard.vue b/frontend/src/views/admin/ops/OpsDashboard.vue index dc482ab074..d6240be522 100644 --- a/frontend/src/views/admin/ops/OpsDashboard.vue +++ b/frontend/src/views/admin/ops/OpsDashboard.vue @@ -84,7 +84,7 @@ /> - +