Skip to content

feat(apikeys): add token usage statistics with read permission support#1087

Merged
looplj merged 6 commits intolooplj:release/v0.9.xfrom
hen7777777:release/v0.9.x
Mar 18, 2026
Merged

feat(apikeys): add token usage statistics with read permission support#1087
looplj merged 6 commits intolooplj:release/v0.9.xfrom
hen7777777:release/v0.9.x

Conversation

@hen7777777
Copy link
Copy Markdown
Contributor

📝 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

  • Display overall token usage for API keys (input/output/cached)
  • Show top models' token consumption details
  • Support time range filtering: Today, Last 7 Days, All Time

2. GraphQL API

Added two new query endpoints:

  • apiKeyTokenUsageStats: Query token usage statistics for specified API keys
  • modelTokenUsageStats: Query model-level token usage statistics

Supported parameters:

  • apiKeyIds: List of API key IDs
  • createdAtGTE: Start time
  • createdAtLTE: End time

3. Permission Control

  • Users with read_api_keys permission can view token statistics
  • Users with write_api_keys permission can view statistics and perform management operations

📋 Changes

Backend:

  • Added GraphQL schema definitions (dashboard.graphql)
  • Implemented token statistics query resolvers (dashboard.resolvers.go)
  • Added statistics helper functions (dashboard_helpers.go)

Frontend:

  • Added ApiKeyTokenChartDialog component to display statistics
  • Added "View Token Statistics" option in action menu
  • Added English and Chinese i18n texts
  • Fixed permission check logic to allow read-only users access

🔧 Technical Details

Data Structure:

type APIKeyTokenUsageStats {
  apiKeyId: ID!
  inputTokens: Int!
  outputTokens: Int!
  cachedTokens: Int!
  topModels: [ModelTokenUsageStats!]!
}

type ModelTokenUsageStats {
  modelId: String!
  inputTokens: Int!
  outputTokens: Int!
  cachedTokens: Int!
}

Permission Fix:

  • Before: Only canWrite users could see the action menu
  • After: Both canRead and canWrite users can see the menu and statistics
  • Write operations (edit, enable/disable, archive) still require canWrite permission

🧪 Testing

  • ✅ Read-only users can view token usage statistics
  • ✅ Statistics data correctly filtered by time range
  • ✅ Top models list displays correctly
  • ✅ Permission control works as expected

📸 Screenshots

Token Usage Statistics Dialog Top Models Breakdown

🔗 Related Commits

  • c3506df: feat(apikeys): add token usage statistics
  • 4d3c196: fix(apikeys): allow read-only users to view token statistics

Copilot AI review requested due to automatic review settings March 17, 2026 11:37
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • Token Usage Statistics Dialog: Introduced a new dialog to display detailed token consumption for each API key, including input, output, and cached tokens, with a breakdown by model. This dialog supports time range filtering (Today, Last 7 Days, All Time).
  • GraphQL API Endpoints: Added two new GraphQL query endpoints: apiKeyTokenUsageStats for overall API key token usage and modelTokenUsageStats for model-level token consumption, both supporting filtering by API key IDs and creation date range.
  • Permission Control Enhancement: Updated permission logic to allow users with read_api_keys permission to view token statistics, while write_api_keys permission is still required for management operations. This fixes an issue where read-only users couldn't access the action menu.
  • Frontend Integration: Developed a new ApiKeyTokenChartDialog component, integrated a 'View Token Statistics' option into the API key action menu, and updated the API keys table to display and filter data based on token usage statistics.
  • Internationalization Support: Added English and Chinese translation texts for the new token usage statistics features.
Changelog
  • .gitignore
    • Added '.omx/' to the ignore list.
  • frontend/src/features/apikeys/components/api-key-token-chart-dialog.tsx
    • Added a new React component ApiKeyTokenChartDialog to display API key token usage statistics, including overall usage and top models, with time range filtering.
  • frontend/src/features/apikeys/components/apikeys-table.tsx
    • Updated ApiKeysTable component to accept dateRange and onDateRangeChange props.
    • Passed new date range props to the DataTableToolbar component.
  • frontend/src/features/apikeys/components/data-table-row-actions.tsx
    • Added a 'View Token Usage' option to the API key row action dropdown menu.
    • Updated the permission check for displaying the action menu to include canRead permissions.
    • Integrated the ApiKeyTokenChartDialog component, allowing it to be opened from the action menu.
  • frontend/src/features/apikeys/components/data-table-toolbar.tsx
    • Integrated a DateRangePicker component into the toolbar for filtering API keys by creation date.
    • Modified the isFiltered logic to account for active date range filters.
    • Updated the reset filters functionality to also clear the date range.
  • frontend/src/features/apikeys/data/apikeys.ts
    • Added a new GraphQL query APIKEY_TOKEN_USAGE_STATS_QUERY to fetch token usage data.
    • Implemented the useApiKeyTokenUsageStats React Query hook for fetching API key token usage statistics.
    • Imported keepPreviousData for better data fetching behavior and apiKeyTokenUsageStatsSchema for data validation.
  • frontend/src/features/apikeys/data/schema.ts
    • Extended the apiKeySchema with an optional usageStats field to include input, output, and cached tokens.
    • Defined new Zod schemas for apiKeyTokenUsageStatsSchema and ModelTokenUsageStats to validate token usage data structures.
  • frontend/src/features/apikeys/index.tsx
    • Introduced dateRange state and setDateRange handler for filtering API keys.
    • Integrated useApiKeyTokenUsageStats to fetch token usage data based on current filters.
    • Created usageStatsMap and tableData to combine API key information with their respective token usage statistics.
    • Updated the useEffect dependencies to trigger data refetching when dateRange changes.
    • Modified handleResetFilters to clear the dateRange state.
  • frontend/src/locales/en/apikeys.json
    • Added new English translation keys for API key token usage columns (input, output, cached, reasoning tokens).
    • Included new translation keys for the token usage chart title, description, time ranges, and table headers.
  • frontend/src/locales/zh-CN/apikeys.json
    • Added new Chinese translation keys for API key token usage columns and chart-related texts.
  • frontend/src/routeTree.gen.ts
    • Updated generated route definitions, primarily by adding trailing slashes to many full paths and reordering the root route entry.
  • init-postgres.sql
    • Added a new SQL script for initializing a PostgreSQL database named 'axonhub'.
  • internal/server/gql/dashboard.graphql
    • Defined new GraphQL input types APIKeyTokenUsageStatsInput and ModelTokenUsageStatsInput.
    • Introduced new GraphQL types APIKeyTokenUsageStats and ModelTokenUsageStats to represent token usage data.
    • Extended the Query type with apiKeyTokenUsageStats and modelTokenUsageStats fields to expose the new data.
  • internal/server/gql/dashboard.resolvers.go
    • Implemented the APIKeyTokenUsageStats resolver to aggregate token usage per API key, including top models, based on provided filters.
    • Added a placeholder for the ModelTokenUsageStats resolver.
  • internal/server/gql/dashboard_helpers.go
    • Added a new helper function getTopModelsForAPIKey to retrieve the top 3 models by total tokens for a given API key and time range.
  • internal/server/gql/models_gen.go
    • Generated new Go types APIKeyTokenUsageStats, APIKeyTokenUsageStatsInput, ModelTokenUsageStats, and ModelTokenUsageStatsInput to match the updated GraphQL schema.
Activity
  • The pull request author, hen7777777, initially committed changes to add the token usage statistics feature.
  • A subsequent commit by hen7777777 fixed the permission check logic to allow read-only users to view token statistics.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 apiKeyTokenUsageStats resolver (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 {
Comment on lines +412 to +429
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...))
}
}
Comment on lines +182 to +186
// 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))

Comment on lines +196 to +202
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';
Comment on lines +1 to +13
-- 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"))
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

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 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.

Comment on lines +164 to +181
<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>
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

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%'}

Comment on lines +453 to +462
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
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 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"))
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 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")

Comment on lines +27 to +52
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 {};
}
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.

medium

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 {};
      }

Comment on lines +423 to +426
apiKeyIDs := make([]int, 0, len(input.APIKeyIds))
for _, guid := range input.APIKeyIds {
apiKeyIDs = append(apiKeyIDs, guid.ID)
}
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.

medium

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
			})

@@ -0,0 +1,13 @@
-- PostgreSQL Database Initialization Script for AxonHub
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个可以移除

}

// ModelTokenUsageStats is the resolver for the modelTokenUsageStats field.
func (r *queryResolver) ModelTokenUsageStats(ctx context.Context, input *ModelTokenUsageStatsInput) ([]*ModelTokenUsageStats, error) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个还没实现

@looplj
Copy link
Copy Markdown
Owner

looplj commented Mar 18, 2026

有点冲突了。
这个弹框有点小气,看是否可以宽一点。
然后看性能,应该不需要在列表页的时候记载。

…ialog

- Widen token stats dialog and improve responsive layout for better readability
- Remove usage stats preloading from API keys list page for better performance
@looplj
Copy link
Copy Markdown
Owner

looplj commented Mar 18, 2026

感谢 PR,确认下,本地自测过了没问题吧。

@hen7777777
Copy link
Copy Markdown
Contributor Author

感谢 PR,确认下,本地自测过了没问题吧。

已本地自测,具体可以在我仓库里拉取试试

@looplj looplj merged commit 5842179 into looplj:release/v0.9.x Mar 18, 2026
2 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.

3 participants