feat: add tracking for auth request success and failure counts

- Introduced `Success` and `Failed` fields in auth records to track request outcomes.
- Updated `/v0/management/auth-files` and `/v0/management/api-key-usage` responses to include success and failure counts.
- Enhanced tests to validate tracking logic and API responses.
This commit is contained in:
Luis Pater
2026-05-02 03:40:00 +08:00
parent 8c2f1a80d3
commit b8bba053fc
7 changed files with 103 additions and 13 deletions

View File

@@ -9,6 +9,12 @@ import (
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
)
type apiKeyUsageEntry struct {
Success int64 `json:"success"`
Failed int64 `json:"failed"`
RecentRequests []coreauth.RecentRequestBucket `json:"recent_requests"`
}
func mergeRecentRequestBuckets(dst, src []coreauth.RecentRequestBucket) []coreauth.RecentRequestBucket {
if len(dst) == 0 {
return src
@@ -51,7 +57,7 @@ func (h *Handler) GetAPIKeyUsage(c *gin.Context) {
}
now := time.Now()
out := make(map[string]map[string][]coreauth.RecentRequestBucket)
out := make(map[string]map[string]apiKeyUsageEntry)
for _, auth := range manager.List() {
if auth == nil {
continue
@@ -80,14 +86,21 @@ func (h *Handler) GetAPIKeyUsage(c *gin.Context) {
recent := auth.RecentRequestsSnapshot(now)
providerBucket, ok := out[provider]
if !ok {
providerBucket = make(map[string][]coreauth.RecentRequestBucket)
providerBucket = make(map[string]apiKeyUsageEntry)
out[provider] = providerBucket
}
if existing, exists := providerBucket[compositeKey]; exists {
providerBucket[compositeKey] = mergeRecentRequestBuckets(existing, recent)
existing.Success += auth.Success
existing.Failed += auth.Failed
existing.RecentRequests = mergeRecentRequestBuckets(existing.RecentRequests, recent)
providerBucket[compositeKey] = existing
continue
}
providerBucket[compositeKey] = recent
providerBucket[compositeKey] = apiKeyUsageEntry{
Success: auth.Success,
Failed: auth.Failed,
RecentRequests: recent,
}
}
c.JSON(http.StatusOK, out)

View File

@@ -64,25 +64,31 @@ func TestGetAPIKeyUsage_GroupsByProviderAndAPIKey(t *testing.T) {
t.Fatalf("status = %d, want %d body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
var payload map[string]map[string][]coreauth.RecentRequestBucket
var payload map[string]map[string]apiKeyUsageEntry
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode payload: %v", err)
}
codexBuckets := payload["codex"]["https://codex.example.com|codex-key"]
if len(codexBuckets) != 20 {
t.Fatalf("codex buckets len = %d, want 20", len(codexBuckets))
codexEntry := payload["codex"]["https://codex.example.com|codex-key"]
if codexEntry.Success != 1 || codexEntry.Failed != 1 {
t.Fatalf("codex totals = %d/%d, want 1/1", codexEntry.Success, codexEntry.Failed)
}
codexSuccess, codexFailed := sumRecentRequestBuckets(codexBuckets)
if len(codexEntry.RecentRequests) != 20 {
t.Fatalf("codex buckets len = %d, want 20", len(codexEntry.RecentRequests))
}
codexSuccess, codexFailed := sumRecentRequestBuckets(codexEntry.RecentRequests)
if codexSuccess != 1 || codexFailed != 1 {
t.Fatalf("codex totals = %d/%d, want 1/1", codexSuccess, codexFailed)
}
claudeBuckets := payload["claude"]["https://claude.example.com|claude-key"]
if len(claudeBuckets) != 20 {
t.Fatalf("claude buckets len = %d, want 20", len(claudeBuckets))
claudeEntry := payload["claude"]["https://claude.example.com|claude-key"]
if claudeEntry.Success != 1 || claudeEntry.Failed != 0 {
t.Fatalf("claude totals = %d/%d, want 1/0", claudeEntry.Success, claudeEntry.Failed)
}
claudeSuccess, claudeFailed := sumRecentRequestBuckets(claudeBuckets)
if len(claudeEntry.RecentRequests) != 20 {
t.Fatalf("claude buckets len = %d, want 20", len(claudeEntry.RecentRequests))
}
claudeSuccess, claudeFailed := sumRecentRequestBuckets(claudeEntry.RecentRequests)
if claudeSuccess != 1 || claudeFailed != 0 {
t.Fatalf("claude totals = %d/%d, want 1/0", claudeSuccess, claudeFailed)
}

View File

@@ -388,6 +388,8 @@ func (h *Handler) buildAuthFileEntry(auth *coreauth.Auth) gin.H {
"source": "memory",
"size": int64(0),
}
entry["success"] = auth.Success
entry["failed"] = auth.Failed
entry["recent_requests"] = auth.RecentRequestsSnapshot(time.Now())
if email := authEmail(auth); email != "" {
entry["email"] = email

View File

@@ -62,6 +62,13 @@ func TestListAuthFiles_IncludesRecentRequestsBuckets(t *testing.T) {
t.Fatalf("expected file entry object, got %#v", filesRaw[0])
}
if _, ok := fileEntry["success"].(float64); !ok {
t.Fatalf("expected success number, got %#v", fileEntry["success"])
}
if _, ok := fileEntry["failed"].(float64); !ok {
t.Fatalf("expected failed number, got %#v", fileEntry["failed"])
}
recentRaw, ok := fileEntry["recent_requests"].([]any)
if !ok {
t.Fatalf("expected recent_requests array, got %#v", fileEntry["recent_requests"])