Skip to content

feat: query usage of token - #1161

Merged
Calcium-Ion merged 4 commits into
QuantumNous:alphafrom
lollipopkit:main
Aug 23, 2025
Merged

feat: query usage of token#1161
Calcium-Ion merged 4 commits into
QuantumNous:alphafrom
lollipopkit:main

Conversation

@lollipopkit

@lollipopkit lollipopkit commented Jun 5, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added authenticated endpoint GET /api/usage/token to view token usage: total granted, used, available, unlimited status, model limits, and expiration timestamp.
    • Enforces Bearer token authentication; requests without a valid Authorization header return 401.
  • Chores

    • Minor router cleanup with no functional impact.
  • Notes

    • Existing endpoints remain unchanged.

@lollipopkit

Copy link
Copy Markdown
Collaborator Author

@sourcery-ai review

@Calcium-Ion
Calcium-Ion changed the base branch from main to alpha August 23, 2025 07:22
@Calcium-Ion
Calcium-Ion self-requested a review August 23, 2025 07:23
@coderabbitai

coderabbitai Bot commented Aug 23, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Added GET /api/usage/token endpoint with CriticalRateLimit and TokenAuth middleware. Implemented controller.GetTokenUsage to parse Bearer Authorization header, validate and trim sk- prefix, call model.GetTokenByKey(..., false), normalize expires_at (-1 → 0, no ms scaling), and return token usage JSON or appropriate 401/200 error responses.

Changes

Cohort / File(s) Summary of changes
Token usage controller
controller/token.go
Added GetTokenUsage(c *gin.Context) and imported strings. Handler reads Authorization header, enforces Bearer format (401 on missing/invalid), trims sk-, calls model.GetTokenByKey(..., false), returns 200 with {"code": true, "message": "ok", "data": {...}} on success (normalizes expires_at: -1 → 0, raw ExpiredTime not multiplied), or 200 {"success": false, "message": error} on retrieval error.
API routing
router/api-router.go
Registered GET /api/usage/token under /api/usage with CriticalRateLimit() on usage group and TokenAuth() on token subgroup; removed a minor blank-line formatting in the /log group.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant Router as API Router (/api/usage)
  participant RateLimit as Middleware: CriticalRateLimit
  participant TokenAuth as Middleware: TokenAuth
  participant Ctrl as Controller: GetTokenUsage
  participant Model

  Client->>Router: GET /api/usage/token (Authorization: Bearer sk-...)
  Router->>RateLimit: Apply rate limit
  RateLimit->>TokenAuth: Forward to token auth
  TokenAuth->>Ctrl: Authenticated
  Ctrl->>Ctrl: Read Authorization header
  alt Missing header
    Ctrl-->>Client: 401 "No Authorization header"
  else Invalid Bearer
    Ctrl-->>Client: 401 "Invalid Bearer token"
  else Valid Bearer
    Ctrl->>Ctrl: tokenKey = TrimPrefix(header, "sk-")
    Ctrl->>Model: GetTokenByKey(tokenKey, false)
    alt Model error
      Ctrl-->>Client: 200 { "success": false, "message": error }
    else Model success
      Ctrl->>Ctrl: expires_at = (ExpiredTime == -1 ? 0 : ExpiredTime)
      Ctrl-->>Client: 200 { "code": true, "message":"ok", "data": { token_usage fields... } }
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I nibble keys and count each chew,
stripping sk- to find what's true.
Quotas tallied, expiry neat,
a tidy payload, carrot sweet.
Hop on /api/usage/token—new route, new beat. 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (4)
router/api-router.go (1)

142-142: Clarify auth contract for /api/token/usage endpoint

It looks like UserAuth() (which calls authHelper) both reads and overwrites the Authorization header to validate an API key before calling your handler, while GetTokenUsage itself parses that same header to identify the target token. This overlap will lead to inconsistent header values or unexpected 401s.

Please choose one of the following approaches to keep the contract unambiguous:

• Expose GET /api/token/usage without UserAuth() and let the handler authenticate purely via the Bearer token it receives.
– Remove or narrow the middleware for this route in router/api-router.go:142.
– e.g.
```diff
func setupTokenRoutes(router *gin.Engine, controller *TokenController) {

  •   tokenRoute := router.Group("/api/token", middleware.UserAuth())
    
  •   tokenRoute := router.Group("/api/token")
      {
          tokenRoute.GET("", controller.CreateToken)
    
  •       tokenRoute.GET("/usage", controller.GetTokenUsage)
    
  •       tokenRoute.GET("/usage", controller.GetTokenUsage) // auth via Bearer token only
      }
    
    }
    
    

• Keep UserAuth() in place and change the handler to receive the token key via a separate field (query param or custom header) instead of Authorization.
– Update GetTokenUsage to read from e.g. ?key= or X-Token (in controller.GetTokenUsage).
– Document the new parameter in your API spec so clients know where to pass the token key.

Locations to update:

  • router/api-router.go:142 — where tokenRoute.GET("/usage", …) is wired under UserAuth().
  • middleware/auth.go around lines 193–216 — where authHelper mutates and then re-reads Authorization for API key validation.
controller/token.go (3)

86-95: Revisit requiring Authorization inside a UserAuth-protected route

Because this route already sits behind UserAuth(), returning 401 when Authorization is not present conflates user auth with the token lookup parameter. Prefer:

  • Keep UserAuth(), accept the token key via query/body or a non-Authorization header (e.g., X-Token), and treat absence as 400 Bad Request; or
  • Remove UserAuth() for this route and authenticate solely via the Bearer token.

This avoids double use of Authorization and makes the contract clear.

Would you like a patch to switch to query/header param (key) and update the router accordingly?


96-104: Harden Bearer parsing

Split on arbitrary whitespace and compare case-insensitively to reduce edge-case 401s.

Apply:

-    parts := strings.Split(authHeader, " ")
-    if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
+    parts := strings.Fields(authHeader)
+    if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
         c.JSON(http.StatusUnauthorized, gin.H{
             "success": false,
             "message": "Invalid Bearer token",
         })
         return
     }

86-134: Add tests for success, invalid header, and cross-user access

Please add handler tests that cover:

  • 200 path for own token, verifying object fields and ms units for expires_at.
  • 401 for malformed/missing Bearer header (if keeping Bearer contract).
  • 403 when attempting to access another user's token by key.

I can scaffold these with gin's httptest if helpful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e3a38d2 and df1ec48.

📒 Files selected for processing (2)
  • controller/token.go (2 hunks)
  • router/api-router.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
controller/token.go (1)
model/token.go (1)
  • GetTokenByKey (144-166)
router/api-router.go (1)
controller/token.go (1)
  • GetTokenUsage (86-134)
🔇 Additional comments (1)
controller/token.go (1)

8-8: Import looks correct

Adding strings is appropriate for header parsing below.

Comment thread controller/token.go Outdated
Comment on lines +106 to +113
token, err := model.GetTokenByKey(tokenKey, true)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
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.

⚠️ Potential issue

Authorization bypass across users — validate ownership and use cache path

Any authenticated user can currently supply any known token key and retrieve its usage. Enforce that the token belongs to the current user. While here, prefer the cache path for lookups.

Apply:

-    token, err := model.GetTokenByKey(tokenKey, true)
+    // Try cache first to reduce DB load
+    token, err := model.GetTokenByKey(tokenKey, false)
     if err != nil {
         c.JSON(http.StatusOK, gin.H{
             "success": false,
             "message": err.Error(),
         })
         return
     }
+    // Ensure the token belongs to the current user
+    userId := c.GetInt("id")
+    if token.UserId != userId {
+        c.JSON(http.StatusForbidden, gin.H{
+            "success": false,
+            "message": "forbidden",
+        })
+        return
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
token, err := model.GetTokenByKey(tokenKey, true)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
// Try cache first to reduce DB load
token, err := model.GetTokenByKey(tokenKey, false)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
// Ensure the token belongs to the current user
userId := c.GetInt("id")
if token.UserId != userId {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "forbidden",
})
return
}
🤖 Prompt for AI Agents
In controller/token.go around lines 106 to 113, the code currently returns token
info for any supplied token key; change the lookup to use the cache path (call
model.GetTokenByKey with the cache flag instead of the DB flag) and then verify
token ownership by comparing token.UserID to the authenticated user ID from the
request context (fetch current user ID from the Gin context/middleware). If the
lookup fails return the existing error response, and if the token exists but
token.UserID does not match the current user return a 403 JSON response
(success: false, message: "forbidden" or similar) and stop processing.

Comment thread controller/token.go
Comment on lines +120 to +133
c.JSON(http.StatusOK, gin.H{
"code": true,
"message": "ok",
"data": gin.H{
"object": "token_usage",
"id": token.Id,
"name": token.Name,
"total_granted": token.RemainQuota + token.UsedQuota,
"total_used": token.UsedQuota,
"total_available": token.RemainQuota,
"unlimited_quota": token.UnlimitedQuota,
"expires_at": expiredAt,
},
})

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.

🛠️ Refactor suggestion

Align response envelope and expires_at units with existing endpoints

Other token endpoints use "success" and return expires_at in milliseconds (see GetTokenStatus). Keep this consistent to avoid client breakage.

Apply:

-    c.JSON(http.StatusOK, gin.H{
-        "code": true,
-        "message": "ok",
-        "data": gin.H{
+    c.JSON(http.StatusOK, gin.H{
+        "success": true,
+        "message": "",
+        "data": gin.H{
             "object":          "token_usage",
             "id":              token.Id,
             "name":            token.Name,
             "total_granted":   token.RemainQuota + token.UsedQuota,
             "total_used":      token.UsedQuota,
             "total_available": token.RemainQuota,
             "unlimited_quota": token.UnlimitedQuota,
-            "expires_at":      expiredAt,
+            "expires_at":      expiredAt * 1000,
         },
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
c.JSON(http.StatusOK, gin.H{
"code": true,
"message": "ok",
"data": gin.H{
"object": "token_usage",
"id": token.Id,
"name": token.Name,
"total_granted": token.RemainQuota + token.UsedQuota,
"total_used": token.UsedQuota,
"total_available": token.RemainQuota,
"unlimited_quota": token.UnlimitedQuota,
"expires_at": expiredAt,
},
})
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": gin.H{
"object": "token_usage",
"id": token.Id,
"name": token.Name,
"total_granted": token.RemainQuota + token.UsedQuota,
"total_used": token.UsedQuota,
"total_available": token.RemainQuota,
"unlimited_quota": token.UnlimitedQuota,
"expires_at": expiredAt * 1000,
},
})
🤖 Prompt for AI Agents
In controller/token.go around lines 120-133, the response envelope and
expires_at units must match other token endpoints: replace the "code" field with
"success" (boolean) and return "expires_at" as milliseconds since epoch instead
of the current unit; compute expires_at via expiredAt.UnixMilli() (or
equivalent) and ensure the JSON value is an integer, leaving other fields
unchanged.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

♻️ Duplicate comments (2)
controller/token.go (2)

86-113: Avoid auth re-parsing; enforce ownership via middleware context

This handler reparses Authorization and lets any valid token query its own usage, but it duplicates TokenAuth and doesn’t bind the returned token to the authenticated user/context. Use token_id/id from Gin context (set by TokenAuth) and fetch via GetTokenByIds to enforce ownership and cut an extra cache/DB hop. Also standardize error handling via common.ApiError.

Apply:

-func GetTokenUsage(c *gin.Context) {
-	authHeader := c.GetHeader("Authorization")
-	if authHeader == "" {
-		c.JSON(http.StatusUnauthorized, gin.H{
-			"success": false,
-			"message": "No Authorization header",
-		})
-		return
-	}
-
-	parts := strings.Split(authHeader, " ")
-	if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
-		c.JSON(http.StatusUnauthorized, gin.H{
-			"success": false,
-			"message": "Invalid Bearer token",
-		})
-		return
-	}
-	tokenKey := parts[1]
-
-	token, err := model.GetTokenByKey(strings.TrimPrefix(tokenKey, "sk-"), false)
-	if err != nil {
-		c.JSON(http.StatusOK, gin.H{
-			"success": false,
-			"message": err.Error(),
-		})
-		return
-	}
-
-	expiredAt := token.ExpiredTime
-	if expiredAt == -1 {
-		expiredAt = 0
-	}
+func GetTokenUsage(c *gin.Context) {
+	// Rely on TokenAuth middleware for identity; avoid re-parsing Authorization
+	tokenId := c.GetInt("token_id")
+	userId := c.GetInt("id")
+	if tokenId == 0 {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"success": false,
+			"message": "unauthorized",
+		})
+		return
+	}
+	token, err := model.GetTokenByIds(tokenId, userId)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+	expiredAt := token.ExpiredTime
+	if expiredAt == -1 {
+		expiredAt = 0
+	}

120-134: Align response envelope and expires_at units with existing endpoints

Other token endpoints use "success" and return expires_at in ms (see GetTokenStatus). Keep consistent and include id for clients.

-	c.JSON(http.StatusOK, gin.H{
-		"code":    true,
-		"message": "ok",
-		"data": gin.H{
-			"object":               "token_usage",
-			"name":                 token.Name,
-			"total_granted":        token.RemainQuota + token.UsedQuota,
-			"total_used":           token.UsedQuota,
-			"total_available":      token.RemainQuota,
-			"unlimited_quota":      token.UnlimitedQuota,
-			"model_limits":         token.GetModelLimitsMap(),
-			"model_limits_enabled": token.ModelLimitsEnabled,
-			"expires_at":           expiredAt,
-		},
-	})
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data": gin.H{
+			"object":               "token_usage",
+			"id":                   token.Id,
+			"name":                 token.Name,
+			"total_granted":        token.RemainQuota + token.UsedQuota,
+			"total_used":           token.UsedQuota,
+			"total_available":      token.RemainQuota,
+			"unlimited_quota":      token.UnlimitedQuota,
+			"model_limits":         token.GetModelLimitsMap(),
+			"model_limits_enabled": token.ModelLimitsEnabled,
+			"expires_at":           expiredAt * 1000,
+		},
+	})
🧹 Nitpick comments (3)
controller/token.go (3)

8-8: Remove unused import if adopting middleware-based auth

After switching to context-based auth, strings is unused. Drop it to satisfy the compiler.

 import (
   "net/http"
   "one-api/common"
   "one-api/model"
   "strconv"
-  "strings"
 
   "github.com/gin-gonic/gin"
 )

96-104: If you keep header parsing, make it whitespace-robust

Use strings.Fields to tolerate extra spaces/tabs and compare scheme case-insensitively.

-	parts := strings.Split(authHeader, " ")
-	if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
+	parts := strings.Fields(authHeader)
+	if len(parts) != 2 || strings.EqualFold(parts[0], "Bearer") == false {
 		c.JSON(http.StatusUnauthorized, gin.H{
 			"success": false,
 			"message": "Invalid Bearer token",
 		})
 		return
 	}

106-113: Don’t echo raw DB/cache errors to clients

Leaking err.Error() can expose internals. Prefer common.ApiError (as in other handlers) or map to a generic message.

-	if err != nil {
-		c.JSON(http.StatusOK, gin.H{
-			"success": false,
-			"message": err.Error(),
-		})
-		return
-	}
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between df1ec48 and 93ce48a.

📒 Files selected for processing (2)
  • controller/token.go (2 hunks)
  • router/api-router.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • router/api-router.go
🧰 Additional context used
🧬 Code graph analysis (1)
controller/token.go (1)
model/token.go (1)
  • GetTokenByKey (144-166)

Comment thread controller/token.go
Comment on lines +86 to +135
func GetTokenUsage(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "No Authorization header",
})
return
}

parts := strings.Split(authHeader, " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "Invalid Bearer token",
})
return
}
tokenKey := parts[1]

token, err := model.GetTokenByKey(strings.TrimPrefix(tokenKey, "sk-"), false)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}

expiredAt := token.ExpiredTime
if expiredAt == -1 {
expiredAt = 0
}

c.JSON(http.StatusOK, gin.H{
"code": true,
"message": "ok",
"data": gin.H{
"object": "token_usage",
"name": token.Name,
"total_granted": token.RemainQuota + token.UsedQuota,
"total_used": token.UsedQuota,
"total_available": token.RemainQuota,
"unlimited_quota": token.UnlimitedQuota,
"model_limits": token.GetModelLimitsMap(),
"model_limits_enabled": token.ModelLimitsEnabled,
"expires_at": expiredAt,
},
})
}

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.

💡 Verification agent

🧩 Analysis chain

Add endpoint tests for contract and auth behavior

Cover: 401 no header (if header-parsing kept), 401 invalid scheme, happy path shape (envelope + ms for expires_at), and ownership enforcement (403) if you continue accepting arbitrary token keys.

Proposed verification script to ensure routing and consistency:


🏁 Script executed:

#!/bin/bash
# Confirm the route is protected by TokenAuth
rg -nC3 'GetTokenUsage' -- router | cat
rg -n 'TokenAuth\(' router | cat

# Confirm GetTokenStatus returns expires_at in ms for consistency reference
rg -nC2 'GetTokenStatus|expires_at.*\* *1000' controller | cat

Length of output: 1989


Add tests for GetTokenUsage and normalize expires_at units

Please address the following before merging:

  • Ensure expires_at in GetTokenUsage is returned in milliseconds, to match GetTokenStatus (which does expiredAt * 1000 in controller/token.go:82)
  • Add endpoint tests covering:
    • 401 when the Authorization header is missing
    • 401 when the scheme is not Bearer
    • 403 when a valid token key is used by a non-owner (ownership enforcement)
    • 200 “happy path” response shape, including the JSON envelope and expires_at in ms

Files/locations to update:

  • controller/token.go (normalize expires_at unit)
  • tests for token endpoints (e.g. in controller/token_test.go or equivalent)

@Calcium-Ion
Calcium-Ion merged commit 4200edb into QuantumNous:alpha Aug 23, 2025
3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Dec 31, 2025
x22x22 pushed a commit to x22x22/new-api that referenced this pull request Apr 24, 2026
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.

2 participants