Skip to content

Commit 8b1d674

Browse files
author
ccs-upstream-sync[bot]
committed
Merge remote-tracking branch 'upstream/main' into upstream-sync/20260502-0544
2 parents 08295a4 + 79579c3 commit 8b1d674

28 files changed

Lines changed: 257 additions & 1499 deletions

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ CLIProxyAPI Guides: [https://help.router-for.me/](https://help.router-for.me/)
7474

7575
see [MANAGEMENT_API.md](https://help.router-for.me/management/api)
7676

77+
## Usage Statistics
78+
79+
Since v6.10.0, CLIProxyAPI and [CPAMC](https://github.com/router-for-me/Cli-Proxy-API-Management-Center) no longer ship built-in usage statistics. If you need usage statistics, use:
80+
81+
### [CPA Usage Keeper](https://github.com/Willxup/cpa-usage-keeper)
82+
83+
Standalone persistence and visualization service for CLIProxyAPI, with periodic data sync, SQLite storage, aggregate APIs, and a built-in dashboard for usage and statistics.
84+
7785
## Amp CLI Support
7886

7987
CLIProxyAPI includes integrated support for [Amp CLI](https://ampcode.com) and Amp IDE extensions, enabling you to use your Google/ChatGPT/Claude OAuth subscriptions with Amp's coding tools:
@@ -183,10 +191,6 @@ Cross-platform desktop app (macOS, Windows, Linux) wrapping CLIProxyAPI with a n
183191

184192
Ready-to-use cross-platform quota inspector for CLIProxyAPI, supporting per-account codex 5h/7d quota windows, plan-based sorting, status coloring, and multi-account summary analytics.
185193

186-
### [CPA Usage Keeper](https://github.com/Willxup/cpa-usage-keeper)
187-
188-
Standalone persistence and visualization service for CLIProxyAPI, with periodic data sync, SQLite storage, aggregate APIs, and a built-in dashboard for usage and statistics.
189-
190194
### [CodexCliPlus](https://github.com/C4AL/CodexCliPlus)
191195

192196
Windows-focused, local-first desktop management platform for Codex CLI built on CLIProxyAPI, focused on simplifying local setup, account and runtime management, and providing a more complete Codex CLI experience for local users.

README_CN.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ CLIProxyAPI 用户手册: [https://help.router-for.me/](https://help.router-fo
7474

7575
请参见 [MANAGEMENT_API_CN.md](https://help.router-for.me/cn/management/api)
7676

77+
## 使用量统计
78+
79+
自v6.10.0版本以后,CLIProxyAPI及 [CPAMC](https://github.com/router-for-me/Cli-Proxy-API-Management-Center) 项目不再预置数据统计功能,如果有数据统计需求的请使用以下项目:
80+
81+
### [CPA Usage Keeper](https://github.com/Willxup/cpa-usage-keeper)
82+
83+
独立的 CLIProxyAPI 使用量持久化与可视化服务,定期同步 CLIProxyAPI 数据,存储到 SQLite,提供聚合 API,并内置使用量分析与统计仪表盘。
84+
7785
## Amp CLI 支持
7886

7987
CLIProxyAPI 已内置对 [Amp CLI](https://ampcode.com) 和 Amp IDE 扩展的支持,可让你使用自己的 Google/ChatGPT/Claude OAuth 订阅来配合 Amp 编码工具:
@@ -179,10 +187,6 @@ Shadow AI 是一款专为受限环境设计的 AI 辅助工具。提供无窗口
179187

180188
上手即用的面向 CLIProxyAPI 跨平台配额查询工具,支持按账号展示 codex 5h/7d 配额窗口、按计划排序、状态着色及多账号汇总分析。
181189

182-
### [CPA Usage Keeper](https://github.com/Willxup/cpa-usage-keeper)
183-
184-
独立的 CLIProxyAPI 使用量持久化与可视化服务,定期同步 CPA 数据,存储到 SQLite,提供聚合 API,并内置使用量分析与统计仪表盘。
185-
186190
### [CodexCliPlus](https://github.com/C4AL/CodexCliPlus)
187191

188192
基于 CLIProxyAPI 的 Windows Codex CLI 本地优先桌面管理平台,聚焦简化本机配置、账号与运行状态管理,并为本地用户提供更完整的 Codex CLI 使用体验。

README_JA.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ CLIProxyAPIガイド:[https://help.router-for.me/](https://help.router-for.me/
7272

7373
[MANAGEMENT_API.md](https://help.router-for.me/management/api)を参照
7474

75+
## 使用量統計
76+
77+
v6.10.0以降、CLIProxyAPIおよび [CPAMC](https://github.com/router-for-me/Cli-Proxy-API-Management-Center) プロジェクトには使用量統計機能がプリセットされなくなりました。使用量統計が必要な場合は、次のプロジェクトをご利用ください:
78+
79+
### [CPA Usage Keeper](https://github.com/Willxup/cpa-usage-keeper)
80+
81+
CLIProxyAPI向けの独立した使用量永続化・可視化サービス。CLIProxyAPIデータを定期同期してSQLiteに保存し、集計APIと、使用量や各種統計を確認できる組み込みダッシュボードを提供します。
82+
7583
## Amp CLIサポート
7684

7785
CLIProxyAPIは[Amp CLI](https://ampcode.com)およびAmp IDE拡張機能の統合サポートを含んでおり、Google/ChatGPT/ClaudeのOAuthサブスクリプションをAmpのコーディングツールで使用できます:
@@ -178,10 +186,6 @@ CLIProxyAPIをネイティブGUIでラップしたクロスプラットフォー
178186

179187
CLIProxyAPI向けのすぐに使えるクロスプラットフォームのクォータ確認ツール。アカウントごとの codex 5h/7d クォータ表示、プラン別ソート、ステータス色分け、複数アカウントの集計分析に対応。
180188

181-
### [CPA Usage Keeper](https://github.com/Willxup/cpa-usage-keeper)
182-
183-
CLIProxyAPI向けの独立した使用量永続化・可視化サービス。CPAデータを定期同期してSQLiteに保存し、集計APIと、使用量や各種統計を確認できる組み込みダッシュボードを提供します。
184-
185189
### [CodexCliPlus](https://github.com/C4AL/CodexCliPlus)
186190

187191
CLIProxyAPIを基盤にしたWindows向けのローカル優先Codex CLIデスクトップ管理プラットフォーム。ローカル設定、アカウント、実行状態の管理を簡素化し、ローカルユーザーにより包括的なCodex CLI体験を提供します。

cmd/server/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ import (
2525
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
2626
"github.com/router-for-me/CLIProxyAPI/v6/internal/managementasset"
2727
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
28+
"github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue"
2829
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
2930
"github.com/router-for-me/CLIProxyAPI/v6/internal/store"
3031
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator"
3132
"github.com/router-for-me/CLIProxyAPI/v6/internal/tui"
32-
"github.com/router-for-me/CLIProxyAPI/v6/internal/usage"
3333
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
3434
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
3535
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
@@ -469,7 +469,7 @@ func main() {
469469
configFileExists = true
470470
}
471471
}
472-
usage.SetStatisticsEnabled(cfg.UsageStatisticsEnabled)
472+
redisqueue.SetUsageStatisticsEnabled(cfg.UsageStatisticsEnabled)
473473
coreauth.SetQuotaCooldownDisabled(cfg.DisableCooling)
474474

475475
if err = logging.ConfigureLogOutput(cfg); err != nil {

docker-build.sh

Lines changed: 5 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -5,123 +5,13 @@
55
# This script automates the process of building and running the Docker container
66
# with version information dynamically injected at build time.
77

8-
# Hidden feature: Preserve usage statistics across rebuilds
9-
# Usage: ./docker-build.sh --with-usage
10-
# First run prompts for management API key, saved to temp/stats/.api_secret
11-
128
set -euo pipefail
139

14-
STATS_DIR="temp/stats"
15-
STATS_FILE="${STATS_DIR}/.usage_backup.json"
16-
SECRET_FILE="${STATS_DIR}/.api_secret"
17-
WITH_USAGE=false
18-
19-
get_port() {
20-
if [[ -f "config.yaml" ]]; then
21-
grep -E "^port:" config.yaml | sed -E 's/^port: *["'"'"']?([0-9]+)["'"'"']?.*$/\1/'
22-
else
23-
echo "8317"
24-
fi
25-
}
26-
27-
export_stats_api_secret() {
28-
if [[ -f "${SECRET_FILE}" ]]; then
29-
API_SECRET=$(cat "${SECRET_FILE}")
30-
else
31-
if [[ ! -d "${STATS_DIR}" ]]; then
32-
mkdir -p "${STATS_DIR}"
33-
fi
34-
echo "First time using --with-usage. Management API key required."
35-
read -r -p "Enter management key: " -s API_SECRET
36-
echo
37-
echo "${API_SECRET}" > "${SECRET_FILE}"
38-
chmod 600 "${SECRET_FILE}"
39-
fi
40-
}
41-
42-
check_container_running() {
43-
local port
44-
port=$(get_port)
45-
46-
if ! curl -s -o /dev/null -w "%{http_code}" "http://localhost:${port}/" | grep -q "200"; then
47-
echo "Error: cli-proxy-api service is not responding at localhost:${port}"
48-
echo "Please start the container first or use without --with-usage flag."
49-
exit 1
50-
fi
51-
}
52-
53-
export_stats() {
54-
local port
55-
port=$(get_port)
56-
57-
if [[ ! -d "${STATS_DIR}" ]]; then
58-
mkdir -p "${STATS_DIR}"
59-
fi
60-
check_container_running
61-
echo "Exporting usage statistics..."
62-
EXPORT_RESPONSE=$(curl -s -w "\n%{http_code}" -H "X-Management-Key: ${API_SECRET}" \
63-
"http://localhost:${port}/v0/management/usage/export")
64-
HTTP_CODE=$(echo "${EXPORT_RESPONSE}" | tail -n1)
65-
RESPONSE_BODY=$(echo "${EXPORT_RESPONSE}" | sed '$d')
66-
67-
if [[ "${HTTP_CODE}" != "200" ]]; then
68-
echo "Export failed (HTTP ${HTTP_CODE}): ${RESPONSE_BODY}"
69-
exit 1
70-
fi
71-
72-
echo "${RESPONSE_BODY}" > "${STATS_FILE}"
73-
echo "Statistics exported to ${STATS_FILE}"
74-
}
75-
76-
import_stats() {
77-
local port
78-
port=$(get_port)
79-
80-
echo "Importing usage statistics..."
81-
IMPORT_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
82-
-H "X-Management-Key: ${API_SECRET}" \
83-
-H "Content-Type: application/json" \
84-
-d @"${STATS_FILE}" \
85-
"http://localhost:${port}/v0/management/usage/import")
86-
IMPORT_CODE=$(echo "${IMPORT_RESPONSE}" | tail -n1)
87-
IMPORT_BODY=$(echo "${IMPORT_RESPONSE}" | sed '$d')
88-
89-
if [[ "${IMPORT_CODE}" == "200" ]]; then
90-
echo "Statistics imported successfully"
91-
else
92-
echo "Import failed (HTTP ${IMPORT_CODE}): ${IMPORT_BODY}"
93-
fi
94-
95-
rm -f "${STATS_FILE}"
96-
}
97-
98-
wait_for_service() {
99-
local port
100-
port=$(get_port)
101-
102-
echo "Waiting for service to be ready..."
103-
for i in {1..30}; do
104-
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${port}/" | grep -q "200"; then
105-
break
106-
fi
107-
sleep 1
108-
done
109-
sleep 2
110-
}
111-
112-
case "${1:-}" in
113-
"")
114-
;;
115-
"--with-usage")
116-
WITH_USAGE=true
117-
export_stats_api_secret
118-
;;
119-
*)
120-
echo "Error: unknown option '${1}'. Did you mean '--with-usage'?"
121-
echo "Usage: ./docker-build.sh [--with-usage]"
122-
exit 1
123-
;;
124-
esac
10+
if [[ "${1:-}" != "" ]]; then
11+
echo "Error: unknown option '${1}'."
12+
echo "Usage: ./docker-build.sh"
13+
exit 1
14+
fi
12515

12616
# --- Step 1: Choose Environment ---
12717
echo "Please select an option:"
@@ -133,14 +23,7 @@ read -r -p "Enter choice [1-2]: " choice
13323
case "$choice" in
13424
1)
13525
echo "--- Running with Pre-built Image ---"
136-
if [[ "${WITH_USAGE}" == "true" ]]; then
137-
export_stats
138-
fi
13926
docker compose up -d --remove-orphans --no-build
140-
if [[ "${WITH_USAGE}" == "true" ]]; then
141-
wait_for_service
142-
import_stats
143-
fi
14427
echo "Services are starting from remote image."
14528
echo "Run 'docker compose logs -f' to see the logs."
14629
;;
@@ -167,18 +50,9 @@ case "$choice" in
16750
--build-arg COMMIT="${COMMIT}" \
16851
--build-arg BUILD_DATE="${BUILD_DATE}"
16952

170-
if [[ "${WITH_USAGE}" == "true" ]]; then
171-
export_stats
172-
fi
173-
17453
echo "Starting the services..."
17554
docker compose up -d --remove-orphans --pull never
17655

177-
if [[ "${WITH_USAGE}" == "true" ]]; then
178-
wait_for_service
179-
import_stats
180-
fi
181-
18256
echo "Build complete. Services are starting."
18357
echo "Run 'docker compose logs -f' to see the logs."
18458
;;

internal/api/handlers/management/api_key_usage.go

Lines changed: 27 additions & 6 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
@@ -35,7 +41,7 @@ func mergeRecentRequestBuckets(dst, src []coreauth.RecentRequestBucket) []coreau
3541
}
3642

3743
// GetAPIKeyUsage returns recent request buckets for all in-memory api_key auths,
38-
// grouped by provider and keyed by the raw api-key value.
44+
// grouped by provider and keyed by "base_url|api_key".
3945
func (h *Handler) GetAPIKeyUsage(c *gin.Context) {
4046
if h == nil {
4147
c.JSON(http.StatusInternalServerError, gin.H{"error": "handler not initialized"})
@@ -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
@@ -64,6 +70,14 @@ func (h *Handler) GetAPIKeyUsage(c *gin.Context) {
6470
if apiKey == "" {
6571
continue
6672
}
73+
baseURL := ""
74+
if auth.Attributes != nil {
75+
baseURL = strings.TrimSpace(auth.Attributes["base_url"])
76+
if baseURL == "" {
77+
baseURL = strings.TrimSpace(auth.Attributes["base-url"])
78+
}
79+
}
80+
compositeKey := baseURL + "|" + apiKey
6781
provider := strings.ToLower(strings.TrimSpace(auth.Provider))
6882
if provider == "" {
6983
provider = "unknown"
@@ -72,14 +86,21 @@ func (h *Handler) GetAPIKeyUsage(c *gin.Context) {
7286
recent := auth.RecentRequestsSnapshot(now)
7387
providerBucket, ok := out[provider]
7488
if !ok {
75-
providerBucket = make(map[string][]coreauth.RecentRequestBucket)
89+
providerBucket = make(map[string]apiKeyUsageEntry)
7690
out[provider] = providerBucket
7791
}
78-
if existing, exists := providerBucket[apiKey]; exists {
79-
providerBucket[apiKey] = mergeRecentRequestBuckets(existing, recent)
92+
if existing, exists := providerBucket[compositeKey]; exists {
93+
existing.Success += auth.Success
94+
existing.Failed += auth.Failed
95+
existing.RecentRequests = mergeRecentRequestBuckets(existing.RecentRequests, recent)
96+
providerBucket[compositeKey] = existing
8097
continue
8198
}
82-
providerBucket[apiKey] = recent
99+
providerBucket[compositeKey] = apiKeyUsageEntry{
100+
Success: auth.Success,
101+
Failed: auth.Failed,
102+
RecentRequests: recent,
103+
}
83104
}
84105

85106
c.JSON(http.StatusOK, out)

internal/api/handlers/management/api_key_usage_test.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ func TestGetAPIKeyUsage_GroupsByProviderAndAPIKey(t *testing.T) {
3131
ID: "codex-auth",
3232
Provider: "codex",
3333
Attributes: map[string]string{
34-
"api_key": "codex-key",
34+
"api_key": "codex-key",
35+
"base_url": "https://codex.example.com",
3536
},
3637
}); err != nil {
3738
t.Fatalf("register codex auth: %v", err)
@@ -40,7 +41,8 @@ func TestGetAPIKeyUsage_GroupsByProviderAndAPIKey(t *testing.T) {
4041
ID: "claude-auth",
4142
Provider: "claude",
4243
Attributes: map[string]string{
43-
"api_key": "claude-key",
44+
"api_key": "claude-key",
45+
"base_url": "https://claude.example.com",
4446
},
4547
}); err != nil {
4648
t.Fatalf("register claude auth: %v", err)
@@ -62,25 +64,31 @@ func TestGetAPIKeyUsage_GroupsByProviderAndAPIKey(t *testing.T) {
6264
t.Fatalf("status = %d, want %d body=%s", rec.Code, http.StatusOK, rec.Body.String())
6365
}
6466

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

70-
codexBuckets := payload["codex"]["codex-key"]
71-
if len(codexBuckets) != 20 {
72-
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)
7375
}
74-
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)
7580
if codexSuccess != 1 || codexFailed != 1 {
7681
t.Fatalf("codex totals = %d/%d, want 1/1", codexSuccess, codexFailed)
7782
}
7883

79-
claudeBuckets := payload["claude"]["claude-key"]
80-
if len(claudeBuckets) != 20 {
81-
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))
8290
}
83-
claudeSuccess, claudeFailed := sumRecentRequestBuckets(claudeBuckets)
91+
claudeSuccess, claudeFailed := sumRecentRequestBuckets(claudeEntry.RecentRequests)
8492
if claudeSuccess != 1 || claudeFailed != 0 {
8593
t.Fatalf("claude totals = %d/%d, want 1/0", claudeSuccess, claudeFailed)
8694
}

0 commit comments

Comments
 (0)