Skip to content

Commit 8553b88

Browse files
author
ccs-upstream-sync[bot]
committed
Merge remote-tracking branch 'upstream/main' into upstream-sync/20260501-1801
2 parents 69aa302 + e37f3be commit 8553b88

5 files changed

Lines changed: 177 additions & 1 deletion

File tree

.goreleaser.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ builds:
1919
archives:
2020
- id: "cli-proxy-api-plus"
2121
format: tar.gz
22+
name_template: >-
23+
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{- if eq .Arch "arm64" -}}aarch64{{- else -}}{{ .Arch }}{{- end -}}
2224
format_overrides:
2325
- goos: windows
2426
format: zip
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package management
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
"time"
7+
8+
"github.com/gin-gonic/gin"
9+
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
10+
)
11+
12+
func mergeRecentRequestBuckets(dst, src []coreauth.RecentRequestBucket) []coreauth.RecentRequestBucket {
13+
if len(dst) == 0 {
14+
return src
15+
}
16+
if len(src) == 0 {
17+
return dst
18+
}
19+
if len(dst) != len(src) {
20+
n := len(dst)
21+
if len(src) < n {
22+
n = len(src)
23+
}
24+
for i := 0; i < n; i++ {
25+
dst[i].Success += src[i].Success
26+
dst[i].Failed += src[i].Failed
27+
}
28+
return dst
29+
}
30+
for i := range dst {
31+
dst[i].Success += src[i].Success
32+
dst[i].Failed += src[i].Failed
33+
}
34+
return dst
35+
}
36+
37+
// GetAPIKeyUsage returns recent request buckets for all in-memory api_key auths,
38+
// grouped by provider and keyed by the raw api-key value.
39+
func (h *Handler) GetAPIKeyUsage(c *gin.Context) {
40+
if h == nil {
41+
c.JSON(http.StatusInternalServerError, gin.H{"error": "handler not initialized"})
42+
return
43+
}
44+
45+
h.mu.Lock()
46+
manager := h.authManager
47+
h.mu.Unlock()
48+
if manager == nil {
49+
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "core auth manager unavailable"})
50+
return
51+
}
52+
53+
now := time.Now()
54+
out := make(map[string]map[string][]coreauth.RecentRequestBucket)
55+
for _, auth := range manager.List() {
56+
if auth == nil {
57+
continue
58+
}
59+
kind, apiKey := auth.AccountInfo()
60+
if !strings.EqualFold(strings.TrimSpace(kind), "api_key") {
61+
continue
62+
}
63+
apiKey = strings.TrimSpace(apiKey)
64+
if apiKey == "" {
65+
continue
66+
}
67+
provider := strings.ToLower(strings.TrimSpace(auth.Provider))
68+
if provider == "" {
69+
provider = "unknown"
70+
}
71+
72+
recent := auth.RecentRequestsSnapshot(now)
73+
providerBucket, ok := out[provider]
74+
if !ok {
75+
providerBucket = make(map[string][]coreauth.RecentRequestBucket)
76+
out[provider] = providerBucket
77+
}
78+
if existing, exists := providerBucket[apiKey]; exists {
79+
providerBucket[apiKey] = mergeRecentRequestBuckets(existing, recent)
80+
continue
81+
}
82+
providerBucket[apiKey] = recent
83+
}
84+
85+
c.JSON(http.StatusOK, out)
86+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package management
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/gin-gonic/gin"
11+
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
12+
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
13+
)
14+
15+
func sumRecentRequestBuckets(buckets []coreauth.RecentRequestBucket) (int64, int64) {
16+
var success int64
17+
var failed int64
18+
for _, bucket := range buckets {
19+
success += bucket.Success
20+
failed += bucket.Failed
21+
}
22+
return success, failed
23+
}
24+
25+
func TestGetAPIKeyUsage_GroupsByProviderAndAPIKey(t *testing.T) {
26+
t.Setenv("MANAGEMENT_PASSWORD", "")
27+
gin.SetMode(gin.TestMode)
28+
29+
manager := coreauth.NewManager(nil, nil, nil)
30+
if _, err := manager.Register(context.Background(), &coreauth.Auth{
31+
ID: "codex-auth",
32+
Provider: "codex",
33+
Attributes: map[string]string{
34+
"api_key": "codex-key",
35+
},
36+
}); err != nil {
37+
t.Fatalf("register codex auth: %v", err)
38+
}
39+
if _, err := manager.Register(context.Background(), &coreauth.Auth{
40+
ID: "claude-auth",
41+
Provider: "claude",
42+
Attributes: map[string]string{
43+
"api_key": "claude-key",
44+
},
45+
}); err != nil {
46+
t.Fatalf("register claude auth: %v", err)
47+
}
48+
49+
manager.MarkResult(context.Background(), coreauth.Result{AuthID: "codex-auth", Provider: "codex", Model: "gpt-5", Success: true})
50+
manager.MarkResult(context.Background(), coreauth.Result{AuthID: "codex-auth", Provider: "codex", Model: "gpt-5", Success: false})
51+
manager.MarkResult(context.Background(), coreauth.Result{AuthID: "claude-auth", Provider: "claude", Model: "claude-4", Success: true})
52+
53+
h := NewHandlerWithoutConfigFilePath(&config.Config{AuthDir: t.TempDir()}, manager)
54+
55+
rec := httptest.NewRecorder()
56+
ginCtx, _ := gin.CreateTestContext(rec)
57+
req := httptest.NewRequest(http.MethodGet, "/v0/management/api-key-usage", nil)
58+
ginCtx.Request = req
59+
h.GetAPIKeyUsage(ginCtx)
60+
61+
if rec.Code != http.StatusOK {
62+
t.Fatalf("status = %d, want %d body=%s", rec.Code, http.StatusOK, rec.Body.String())
63+
}
64+
65+
var payload map[string]map[string][]coreauth.RecentRequestBucket
66+
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
67+
t.Fatalf("decode payload: %v", err)
68+
}
69+
70+
codexBuckets := payload["codex"]["codex-key"]
71+
if len(codexBuckets) != 20 {
72+
t.Fatalf("codex buckets len = %d, want 20", len(codexBuckets))
73+
}
74+
codexSuccess, codexFailed := sumRecentRequestBuckets(codexBuckets)
75+
if codexSuccess != 1 || codexFailed != 1 {
76+
t.Fatalf("codex totals = %d/%d, want 1/1", codexSuccess, codexFailed)
77+
}
78+
79+
claudeBuckets := payload["claude"]["claude-key"]
80+
if len(claudeBuckets) != 20 {
81+
t.Fatalf("claude buckets len = %d, want 20", len(claudeBuckets))
82+
}
83+
claudeSuccess, claudeFailed := sumRecentRequestBuckets(claudeBuckets)
84+
if claudeSuccess != 1 || claudeFailed != 0 {
85+
t.Fatalf("claude totals = %d/%d, want 1/0", claudeSuccess, claudeFailed)
86+
}
87+
}

internal/api/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@ func (s *Server) registerManagementRoutes() {
596596
mgmt.PUT("/api-keys", s.mgmt.PutAPIKeys)
597597
mgmt.PATCH("/api-keys", s.mgmt.PatchAPIKeys)
598598
mgmt.DELETE("/api-keys", s.mgmt.DeleteAPIKeys)
599+
mgmt.GET("/api-key-usage", s.mgmt.GetAPIKeyUsage)
599600

600601
mgmt.GET("/gemini-api-key", s.mgmt.GetGeminiKeys)
601602
mgmt.PUT("/gemini-api-key", s.mgmt.PutGeminiKeys)

sdk/cliproxy/auth/types.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func recentRequestBucketIndex(bucketID int64) int {
164164

165165
func formatRecentRequestBucketLabel(bucketID int64) string {
166166
start := time.Unix(bucketID*recentRequestBucketSeconds, 0).In(time.Local)
167-
end := start.Add(10 * time.Minute)
167+
end := start.Add(time.Duration(recentRequestBucketSeconds) * time.Second)
168168
return start.Format("15:04") + "-" + end.Format("15:04")
169169
}
170170

0 commit comments

Comments
 (0)