Skip to content

Commit b8bba05

Browse files
committed
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.
1 parent 8c2f1a8 commit b8bba05

7 files changed

Lines changed: 103 additions & 13 deletions

File tree

internal/api/handlers/management/api_key_usage.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import (
99
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
1010
)
1111

12+
type apiKeyUsageEntry struct {
13+
Success int64 `json:"success"`
14+
Failed int64 `json:"failed"`
15+
RecentRequests []coreauth.RecentRequestBucket `json:"recent_requests"`
16+
}
17+
1218
func mergeRecentRequestBuckets(dst, src []coreauth.RecentRequestBucket) []coreauth.RecentRequestBucket {
1319
if len(dst) == 0 {
1420
return src
@@ -51,7 +57,7 @@ func (h *Handler) GetAPIKeyUsage(c *gin.Context) {
5157
}
5258

5359
now := time.Now()
54-
out := make(map[string]map[string][]coreauth.RecentRequestBucket)
60+
out := make(map[string]map[string]apiKeyUsageEntry)
5561
for _, auth := range manager.List() {
5662
if auth == nil {
5763
continue
@@ -80,14 +86,21 @@ func (h *Handler) GetAPIKeyUsage(c *gin.Context) {
8086
recent := auth.RecentRequestsSnapshot(now)
8187
providerBucket, ok := out[provider]
8288
if !ok {
83-
providerBucket = make(map[string][]coreauth.RecentRequestBucket)
89+
providerBucket = make(map[string]apiKeyUsageEntry)
8490
out[provider] = providerBucket
8591
}
8692
if existing, exists := providerBucket[compositeKey]; exists {
87-
providerBucket[compositeKey] = mergeRecentRequestBuckets(existing, recent)
93+
existing.Success += auth.Success
94+
existing.Failed += auth.Failed
95+
existing.RecentRequests = mergeRecentRequestBuckets(existing.RecentRequests, recent)
96+
providerBucket[compositeKey] = existing
8897
continue
8998
}
90-
providerBucket[compositeKey] = recent
99+
providerBucket[compositeKey] = apiKeyUsageEntry{
100+
Success: auth.Success,
101+
Failed: auth.Failed,
102+
RecentRequests: recent,
103+
}
91104
}
92105

93106
c.JSON(http.StatusOK, out)

internal/api/handlers/management/api_key_usage_test.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,31 @@ func TestGetAPIKeyUsage_GroupsByProviderAndAPIKey(t *testing.T) {
6464
t.Fatalf("status = %d, want %d body=%s", rec.Code, http.StatusOK, rec.Body.String())
6565
}
6666

67-
var payload map[string]map[string][]coreauth.RecentRequestBucket
67+
var payload map[string]map[string]apiKeyUsageEntry
6868
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
6969
t.Fatalf("decode payload: %v", err)
7070
}
7171

72-
codexBuckets := payload["codex"]["https://codex.example.com|codex-key"]
73-
if len(codexBuckets) != 20 {
74-
t.Fatalf("codex buckets len = %d, want 20", len(codexBuckets))
72+
codexEntry := payload["codex"]["https://codex.example.com|codex-key"]
73+
if codexEntry.Success != 1 || codexEntry.Failed != 1 {
74+
t.Fatalf("codex totals = %d/%d, want 1/1", codexEntry.Success, codexEntry.Failed)
7575
}
76-
codexSuccess, codexFailed := sumRecentRequestBuckets(codexBuckets)
76+
if len(codexEntry.RecentRequests) != 20 {
77+
t.Fatalf("codex buckets len = %d, want 20", len(codexEntry.RecentRequests))
78+
}
79+
codexSuccess, codexFailed := sumRecentRequestBuckets(codexEntry.RecentRequests)
7780
if codexSuccess != 1 || codexFailed != 1 {
7881
t.Fatalf("codex totals = %d/%d, want 1/1", codexSuccess, codexFailed)
7982
}
8083

81-
claudeBuckets := payload["claude"]["https://claude.example.com|claude-key"]
82-
if len(claudeBuckets) != 20 {
83-
t.Fatalf("claude buckets len = %d, want 20", len(claudeBuckets))
84+
claudeEntry := payload["claude"]["https://claude.example.com|claude-key"]
85+
if claudeEntry.Success != 1 || claudeEntry.Failed != 0 {
86+
t.Fatalf("claude totals = %d/%d, want 1/0", claudeEntry.Success, claudeEntry.Failed)
87+
}
88+
if len(claudeEntry.RecentRequests) != 20 {
89+
t.Fatalf("claude buckets len = %d, want 20", len(claudeEntry.RecentRequests))
8490
}
85-
claudeSuccess, claudeFailed := sumRecentRequestBuckets(claudeBuckets)
91+
claudeSuccess, claudeFailed := sumRecentRequestBuckets(claudeEntry.RecentRequests)
8692
if claudeSuccess != 1 || claudeFailed != 0 {
8793
t.Fatalf("claude totals = %d/%d, want 1/0", claudeSuccess, claudeFailed)
8894
}

internal/api/handlers/management/auth_files.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,8 @@ func (h *Handler) buildAuthFileEntry(auth *coreauth.Auth) gin.H {
388388
"source": "memory",
389389
"size": int64(0),
390390
}
391+
entry["success"] = auth.Success
392+
entry["failed"] = auth.Failed
391393
entry["recent_requests"] = auth.RecentRequestsSnapshot(time.Now())
392394
if email := authEmail(auth); email != "" {
393395
entry["email"] = email

internal/api/handlers/management/auth_files_recent_requests_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ func TestListAuthFiles_IncludesRecentRequestsBuckets(t *testing.T) {
6262
t.Fatalf("expected file entry object, got %#v", filesRaw[0])
6363
}
6464

65+
if _, ok := fileEntry["success"].(float64); !ok {
66+
t.Fatalf("expected success number, got %#v", fileEntry["success"])
67+
}
68+
if _, ok := fileEntry["failed"].(float64); !ok {
69+
t.Fatalf("expected failed number, got %#v", fileEntry["failed"])
70+
}
71+
6572
recentRaw, ok := fileEntry["recent_requests"].([]any)
6673
if !ok {
6774
t.Fatalf("expected recent_requests array, got %#v", fileEntry["recent_requests"])

sdk/cliproxy/auth/conductor.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,9 @@ func (m *Manager) Update(ctx context.Context, auth *Auth) (*Auth, error) {
11261126
auth.Index = existing.Index
11271127
auth.indexAssigned = existing.indexAssigned
11281128
}
1129+
auth.Success = existing.Success
1130+
auth.Failed = existing.Failed
1131+
auth.recentRequests = existing.recentRequests
11291132
if !existing.Disabled && existing.Status != StatusDisabled && !auth.Disabled && auth.Status != StatusDisabled {
11301133
if len(auth.ModelStates) == 0 && len(existing.ModelStates) > 0 {
11311134
auth.ModelStates = existing.ModelStates
@@ -2022,6 +2025,11 @@ func (m *Manager) MarkResult(ctx context.Context, result Result) {
20222025
if auth, ok := m.auths[result.AuthID]; ok && auth != nil {
20232026
now := time.Now()
20242027
auth.recordRecentRequest(now, result.Success)
2028+
if result.Success {
2029+
auth.Success++
2030+
} else {
2031+
auth.Failed++
2032+
}
20252033

20262034
if result.Success {
20272035
if result.Model != "" {

sdk/cliproxy/auth/conductor_recent_requests_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ func TestManagerMarkResultRecordsRecentRequests(t *testing.T) {
3131
t.Fatalf("GetByID returned ok=%v auth=%v", ok, gotAuth)
3232
}
3333

34+
if gotAuth.Success != 1 || gotAuth.Failed != 1 {
35+
t.Fatalf("auth totals = success=%d failed=%d, want 1/1", gotAuth.Success, gotAuth.Failed)
36+
}
37+
3438
snapshot := gotAuth.RecentRequestsSnapshot(time.Now())
3539
var successTotal int64
3640
var failedTotal int64
@@ -42,3 +46,50 @@ func TestManagerMarkResultRecordsRecentRequests(t *testing.T) {
4246
t.Fatalf("totals = success=%d failed=%d, want 1/1", successTotal, failedTotal)
4347
}
4448
}
49+
50+
func TestManagerUpdatePreservesRecentRequestsAndTotals(t *testing.T) {
51+
mgr := NewManager(nil, nil, nil)
52+
auth := &Auth{
53+
ID: "auth-1",
54+
Provider: "antigravity",
55+
Metadata: map[string]any{
56+
"type": "antigravity",
57+
},
58+
}
59+
if _, err := mgr.Register(WithSkipPersist(context.Background()), auth); err != nil {
60+
t.Fatalf("Register returned error: %v", err)
61+
}
62+
63+
mgr.MarkResult(context.Background(), Result{AuthID: "auth-1", Provider: "antigravity", Model: "gpt-5", Success: true})
64+
65+
updated := &Auth{
66+
ID: "auth-1",
67+
Provider: "antigravity",
68+
Metadata: map[string]any{
69+
"type": "antigravity",
70+
"note": "updated",
71+
},
72+
}
73+
if _, err := mgr.Update(WithSkipPersist(context.Background()), updated); err != nil {
74+
t.Fatalf("Update returned error: %v", err)
75+
}
76+
77+
gotAuth, ok := mgr.GetByID("auth-1")
78+
if !ok || gotAuth == nil {
79+
t.Fatalf("GetByID returned ok=%v auth=%v", ok, gotAuth)
80+
}
81+
if gotAuth.Success != 1 || gotAuth.Failed != 0 {
82+
t.Fatalf("auth totals = success=%d failed=%d, want 1/0", gotAuth.Success, gotAuth.Failed)
83+
}
84+
85+
snapshot := gotAuth.RecentRequestsSnapshot(time.Now())
86+
var successTotal int64
87+
var failedTotal int64
88+
for _, bucket := range snapshot {
89+
successTotal += bucket.Success
90+
failedTotal += bucket.Failed
91+
}
92+
if successTotal != 1 || failedTotal != 0 {
93+
t.Fatalf("bucket totals = success=%d failed=%d, want 1/0", successTotal, failedTotal)
94+
}
95+
}

sdk/cliproxy/auth/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ type Auth struct {
9292
// Runtime carries non-serialisable data used during execution (in-memory only).
9393
Runtime any `json:"-"`
9494

95+
Success int64 `json:"-"`
96+
Failed int64 `json:"-"`
97+
9598
recentRequests recentRequestRing `json:"-"`
9699
indexAssigned bool `json:"-"`
97100
}

0 commit comments

Comments
 (0)