feat(apikeys): add token usage statistics with read permission support#1087
feat(apikeys): add token usage statistics with read permission support#1087looplj merged 6 commits intolooplj:release/v0.9.xfrom
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a significant new feature allowing users to monitor and analyze token usage for their API keys. By providing detailed statistics on input, output, and cached tokens, broken down by model and filterable by time range, users can gain better insights into their API key consumption. The changes span both frontend and backend, including new GraphQL queries, UI components, and an important permission fix to ensure appropriate access for read-only users. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Pull request overview
Adds API-key token usage statistics to the API Keys page by introducing new GraphQL types/queries and a frontend dialog + table integrations, with updated permission gating intended to allow read-only users to view stats.
Changes:
- Backend: added GraphQL schema/types and implemented
apiKeyTokenUsageStatsresolver (plus helper to compute top-model breakdown). - Frontend: added
ApiKeyTokenChartDialog, date-range filtering for usage stats, and updated row actions/menu visibility for read-only users. - Misc: added i18n strings, updated generated route tree/types, and introduced a Postgres init script + gitignore entry.
Reviewed changes
Copilot reviewed 16 out of 18 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/server/gql/dashboard.graphql | Adds APIKeyTokenUsageStats/ModelTokenUsageStats types and query fields. |
| internal/server/gql/dashboard.resolvers.go | Implements APIKeyTokenUsageStats resolver; adds stubbed ModelTokenUsageStats resolver. |
| internal/server/gql/dashboard_helpers.go | Adds helper to compute “top 3 models” per API key. |
| internal/server/gql/models_gen.go | Generated Go models for new GraphQL types/inputs. |
| internal/server/gql/generated.go | gqlgen output wired for new types/queries. |
| frontend/src/features/apikeys/data/apikeys.ts | Adds React Query hook + GraphQL query for token usage stats. |
| frontend/src/features/apikeys/index.tsx | Fetches stats for visible API keys and wires date range into stats query. |
| frontend/src/features/apikeys/data/schema.ts | Adds Zod schemas/types for token usage stats and embeds per-key stats. |
| frontend/src/features/apikeys/components/api-key-token-chart-dialog.tsx | New dialog UI to display totals + top-model breakdown by time range. |
| frontend/src/features/apikeys/components/data-table-row-actions.tsx | Adds “Token Usage” menu action; allows menu for read-only users. |
| frontend/src/features/apikeys/components/data-table-toolbar.tsx | Adds date range picker and reset handling. |
| frontend/src/features/apikeys/components/apikeys-table.tsx | Plumbs date-range props into toolbar. |
| frontend/src/locales/en/apikeys.json | Adds English strings for token usage UI/errors. |
| frontend/src/locales/zh-CN/apikeys.json | Adds Chinese strings for token usage UI/errors. |
| frontend/src/routeTree.gen.ts | Updates generated route typings/paths (not feature-specific). |
| init-postgres.sql | Adds Postgres bootstrap script (not feature-specific). |
| .gitignore | Adds .omx/ ignore entry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| // APIKeyTokenUsageStats is the resolver for the apiKeyTokenUsageStats field. | ||
| // Aggregates input, output, and cached tokens per API key for the selected time range. | ||
| func (r *queryResolver) APIKeyTokenUsageStats(ctx context.Context, input *APIKeyTokenUsageStatsInput) ([]*APIKeyTokenUsageStats, error) { | ||
| ctx = authz.WithScopeDecision(ctx, scopes.ScopeReadAPIKeys) |
| } | ||
| if len(input.APIKeyIds) > 0 { | ||
| apiKeyIDs := make([]int, 0, len(input.APIKeyIds)) | ||
| for _, guid := range input.APIKeyIds { |
| query := r.client.UsageLog.Query(). | ||
| Where(usagelog.APIKeyIDNotNil()) | ||
|
|
||
| if input != nil { | ||
| if input.CreatedAtGTE != nil { | ||
| query = query.Where(usagelog.CreatedAtGTE(*input.CreatedAtGTE)) | ||
| } | ||
| if input.CreatedAtLTE != nil { | ||
| query = query.Where(usagelog.CreatedAtLTE(*input.CreatedAtLTE)) | ||
| } | ||
| if len(input.APIKeyIds) > 0 { | ||
| apiKeyIDs := make([]int, 0, len(input.APIKeyIds)) | ||
| for _, guid := range input.APIKeyIds { | ||
| apiKeyIDs = append(apiKeyIDs, guid.ID) | ||
| } | ||
| query = query.Where(usagelog.APIKeyIDIn(apiKeyIDs...)) | ||
| } | ||
| } |
| // getTopModelsForAPIKey returns top 3 models by total tokens for a specific API key | ||
| func (r *queryResolver) getTopModelsForAPIKey(ctx context.Context, apiKeyID int, input *APIKeyTokenUsageStatsInput) []*ModelTokenUsageStats { | ||
| query := r.client.UsageLog.Query(). | ||
| Where(usagelog.APIKeyID(apiKeyID)) | ||
|
|
| type modelStats struct { | ||
| ModelID string `json:"model_id"` | ||
| InputTokens int `json:"input_tokens"` | ||
| OutputTokens int `json:"output_tokens"` | ||
| CachedTokens int `json:"cached_tokens"` | ||
| TotalTokens int `json:"total_tokens"` | ||
| } |
| @@ -0,0 +1,196 @@ | |||
| import { useState, useMemo } from 'react'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { BarChart3 } from 'lucide-react'; | |||
init-postgres.sql
Outdated
| -- PostgreSQL Database Initialization Script for AxonHub | ||
|
|
||
| -- Create database (run this as postgres superuser) | ||
| CREATE DATABASE axonhub; | ||
|
|
||
| -- Connect to the database | ||
| \c axonhub; | ||
|
|
||
| -- Optional: Create a dedicated user for AxonHub | ||
| -- CREATE USER axonhub_user WITH PASSWORD 'your_secure_password'; | ||
| -- GRANT ALL PRIVILEGES ON DATABASE axonhub TO axonhub_user; | ||
|
|
||
| -- The tables will be created automatically by Ent ORM when the application starts |
|
|
||
| // ModelTokenUsageStats is the resolver for the modelTokenUsageStats field. | ||
| func (r *queryResolver) ModelTokenUsageStats(ctx context.Context, input *ModelTokenUsageStatsInput) ([]*ModelTokenUsageStats, error) { | ||
| panic(fmt.Errorf("not implemented: ModelTokenUsageStats - modelTokenUsageStats")) |
There was a problem hiding this comment.
Code Review
This pull request introduces a valuable token usage statistics feature for API keys, complete with a new frontend dialog and backend GraphQL support. The implementation is generally well-done, but there are a few key areas for improvement. I've identified a potential division-by-zero issue on the frontend that could lead to NaN being displayed. On the backend, there's a significant N+1 query problem that could impact performance, and one of the new GraphQL resolvers is unimplemented, which will cause a panic. Addressing these points will greatly improve the robustness and efficiency of this new feature.
| <TableCell className="text-right tabular-nums"> | ||
| {((model.inputTokens / modelTotal) * 100).toFixed(1)}% | ||
| </TableCell> | ||
| </TableRow> | ||
| <TableRow> | ||
| <TableCell className="font-medium">{t('apikeys.columns.outputTokens')}</TableCell> | ||
| <TableCell className="text-right tabular-nums">{formatNumber(model.outputTokens)}</TableCell> | ||
| <TableCell className="text-right tabular-nums"> | ||
| {((model.outputTokens / modelTotal) * 100).toFixed(1)}% | ||
| </TableCell> | ||
| </TableRow> | ||
| <TableRow> | ||
| <TableCell className="font-medium">{t('apikeys.columns.cachedTokens')}</TableCell> | ||
| <TableCell className="text-right tabular-nums">{formatNumber(model.cachedTokens)}</TableCell> | ||
| <TableCell className="text-right tabular-nums"> | ||
| {((model.cachedTokens / modelTotal) * 100).toFixed(1)}% | ||
| </TableCell> | ||
| </TableRow> |
There was a problem hiding this comment.
There's a potential for division by zero if modelTotal is 0, which would display NaN. Please add a check to handle this case for input, output, and cached token percentage calculations.
For example, you can change this:
{((model.inputTokens / modelTotal) * 100).toFixed(1)}%To this:
{modelTotal > 0 ? `${((model.inputTokens / modelTotal) * 100).toFixed(1)}%` : '0.0%'}| return lo.Map(results, func(item usageStats, _ int) *APIKeyTokenUsageStats { | ||
| topModels := r.getTopModelsForAPIKey(ctx, item.APIKeyID, input) | ||
| return &APIKeyTokenUsageStats{ | ||
| APIKeyID: objects.GUID{Type: ent.TypeAPIKey, ID: item.APIKeyID}, | ||
| InputTokens: item.InputTokens, | ||
| OutputTokens: item.OutputTokens, | ||
| CachedTokens: item.CachedTokens, | ||
| TopModels: topModels, | ||
| } | ||
| }), nil |
There was a problem hiding this comment.
The current implementation for fetching top models for each API key results in an N+1 query problem. The getTopModelsForAPIKey function is called inside a loop for each API key, which will lead to poor performance when there are many API keys.
To fix this, you should fetch the top models for all API keys in a single query. You can achieve this by creating a new helper function, e.g., getTopModelsForAPIKeys, that accepts a slice of API key IDs. This function can use a window function like ROW_NUMBER() partitioned by api_key_id to get the top N models for each key efficiently.
|
|
||
| // ModelTokenUsageStats is the resolver for the modelTokenUsageStats field. | ||
| func (r *queryResolver) ModelTokenUsageStats(ctx context.Context, input *ModelTokenUsageStatsInput) ([]*ModelTokenUsageStats, error) { | ||
| panic(fmt.Errorf("not implemented: ModelTokenUsageStats - modelTokenUsageStats")) |
There was a problem hiding this comment.
The ModelTokenUsageStats resolver is not implemented and will cause a panic if called. The pull request description indicates that this endpoint was added, so it should be implemented. If it's not ready, it should return an error instead of panicking.
return nil, fmt.Errorf("not implemented: ModelTokenUsageStats - modelTokenUsageStats")| const now = new Date(); | ||
|
|
||
| switch (range) { | ||
| case 'today': { | ||
| // Get start of today in UTC | ||
| const todayUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); | ||
| return { | ||
| createdAtGTE: todayUTC.toISOString(), | ||
| createdAtLTE: now.toISOString(), | ||
| }; | ||
| } | ||
| case 'last7days': { | ||
| // Get 7 days ago from start of today in UTC | ||
| const todayUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); | ||
| const last7daysUTC = new Date(todayUTC); | ||
| last7daysUTC.setUTCDate(last7daysUTC.getUTCDate() - 7); | ||
| return { | ||
| createdAtGTE: last7daysUTC.toISOString(), | ||
| createdAtLTE: now.toISOString(), | ||
| }; | ||
| } | ||
| case 'all': | ||
| return {}; | ||
| default: | ||
| return {}; | ||
| } |
There was a problem hiding this comment.
The calculation for todayUTC is duplicated in the today and last7days cases, and now.toISOString() is also called multiple times. You can define these once before the switch statement to avoid code repetition and improve readability.
const now = new Date();
const nowISO = now.toISOString();
const todayUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
switch (range) {
case 'today': {
return {
createdAtGTE: todayUTC.toISOString(),
createdAtLTE: nowISO,
};
}
case 'last7days': {
const last7daysUTC = new Date(todayUTC);
last7daysUTC.setUTCDate(last7daysUTC.getUTCDate() - 7);
return {
createdAtGTE: last7daysUTC.toISOString(),
createdAtLTE: nowISO,
};
}
case 'all':
return {};
default:
return {};
}
| apiKeyIDs := make([]int, 0, len(input.APIKeyIds)) | ||
| for _, guid := range input.APIKeyIds { | ||
| apiKeyIDs = append(apiKeyIDs, guid.ID) | ||
| } |
There was a problem hiding this comment.
This loop to convert a slice of *objects.GUID to a slice of int can be simplified using lo.Map from the samber/lo library, which is already in use in this file. This will make the code more concise and idiomatic.
apiKeyIDs := lo.Map(input.APIKeyIds, func(guid *objects.GUID, _ int) int {
return guid.ID
})
init-postgres.sql
Outdated
| @@ -0,0 +1,13 @@ | |||
| -- PostgreSQL Database Initialization Script for AxonHub | |||
| } | ||
|
|
||
| // ModelTokenUsageStats is the resolver for the modelTokenUsageStats field. | ||
| func (r *queryResolver) ModelTokenUsageStats(ctx context.Context, input *ModelTokenUsageStatsInput) ([]*ModelTokenUsageStats, error) { |
|
有点冲突了。 |
…ialog - Widen token stats dialog and improve responsive layout for better readability - Remove usage stats preloading from API keys list page for better performance
|
感谢 PR,确认下,本地自测过了没问题吧。 |
已本地自测,具体可以在我仓库里拉取试试 |
📝 Description
Added token usage statistics feature for API keys page, allowing users to view detailed token consumption for each API key, including input, output, and cached tokens, as well as usage breakdown by model.
🎯 Features
1. Token Usage Statistics Dialog
2. GraphQL API
Added two new query endpoints:
apiKeyTokenUsageStats: Query token usage statistics for specified API keysmodelTokenUsageStats: Query model-level token usage statisticsSupported parameters:
apiKeyIds: List of API key IDscreatedAtGTE: Start timecreatedAtLTE: End time3. Permission Control
read_api_keyspermission can view token statisticswrite_api_keyspermission can view statistics and perform management operations📋 Changes
Backend:
dashboard.graphql)dashboard.resolvers.go)dashboard_helpers.go)Frontend:
ApiKeyTokenChartDialogcomponent to display statistics🔧 Technical Details
Data Structure:
Permission Fix:
canWriteusers could see the action menucanReadandcanWriteusers can see the menu and statisticscanWritepermission🧪 Testing
📸 Screenshots
🔗 Related Commits