Skip to content

Commit b55a6d5

Browse files
committed
feat(api): 支持旧版标量 API 密钥格式的检测与迁移
1 parent 51b5ccd commit b55a6d5

File tree

3 files changed

+178
-244
lines changed

3 files changed

+178
-244
lines changed

README.md

Lines changed: 1 addition & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -1,175 +1 @@
1-
# CLI Proxy API
2-
3-
English | [中文](README_CN.md)
4-
5-
A proxy server that provides OpenAI/Gemini/Claude/Codex compatible API interfaces for CLI.
6-
7-
It now also supports OpenAI Codex (GPT models) and Claude Code via OAuth.
8-
9-
So you can use local or multi-account CLI access with OpenAI(include Responses)/Gemini/Claude-compatible clients and SDKs.
10-
11-
## Sponsor
12-
13-
[![z.ai](https://assets.router-for.me/english-5.png)](https://z.ai/subscribe?ic=8JVLJQFSKB)
14-
15-
This project is sponsored by Z.ai, supporting us with their GLM CODING PLAN.
16-
17-
GLM CODING PLAN is a subscription service designed for AI coding, starting at just $10/month. It provides access to their flagship GLM-4.7 & (GLM-5 Only Available for Pro Users)model across 10+ popular AI coding tools (Claude Code, Cline, Roo Code, etc.), offering developers top-tier, fast, and stable coding experiences.
18-
19-
Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB
20-
21-
---
22-
23-
<table>
24-
<tbody>
25-
<tr>
26-
<td width="180"><a href="https://www.packyapi.com/register?aff=cliproxyapi"><img src="./assets/packycode.png" alt="PackyCode" width="150"></a></td>
27-
<td>Thanks to PackyCode for sponsoring this project! PackyCode is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. PackyCode provides special discounts for our software users: register using <a href="https://www.packyapi.com/register?aff=cliproxyapi">this link</a> and enter the "cliproxyapi" promo code during recharge to get 10% off.</td>
28-
</tr>
29-
<tr>
30-
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=TJNAIF"><img src="./assets/aicodemirror.png" alt="AICodeMirror" width="150"></a></td>
31-
<td>Thanks to AICodeMirror for sponsoring this project! AICodeMirror provides official high-stability relay services for Claude Code / Codex / Gemini CLI, with enterprise-grade concurrency, fast invoicing, and 24/7 dedicated technical support. Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original price, with extra discounts on top-ups! AICodeMirror offers special benefits for CLIProxyAPI users: register via <a href="https://www.aicodemirror.com/register?invitecode=TJNAIF">this link</a> to enjoy 20% off your first top-up, and enterprise customers can get up to 25% off!</td>
32-
</tr>
33-
</tbody>
34-
</table>
35-
36-
## Overview
37-
38-
- OpenAI/Gemini/Claude compatible API endpoints for CLI models
39-
- OpenAI Codex support (GPT models) via OAuth login
40-
- Claude Code support via OAuth login
41-
- Qwen Code support via OAuth login
42-
- iFlow support via OAuth login
43-
- Amp CLI and IDE extensions support with provider routing
44-
- Streaming and non-streaming responses
45-
- Function calling/tools support
46-
- Multimodal input support (text and images)
47-
- Multiple accounts with round-robin load balancing (Gemini, OpenAI, Claude, Qwen and iFlow)
48-
- Simple CLI authentication flows (Gemini, OpenAI, Claude, Qwen and iFlow)
49-
- Generative Language API Key support
50-
- AI Studio Build multi-account load balancing
51-
- Gemini CLI multi-account load balancing
52-
- Claude Code multi-account load balancing
53-
- Qwen Code multi-account load balancing
54-
- iFlow multi-account load balancing
55-
- OpenAI Codex multi-account load balancing
56-
- OpenAI-compatible upstream providers via config (e.g., OpenRouter)
57-
- Reusable Go SDK for embedding the proxy (see `docs/sdk-usage.md`)
58-
59-
## Getting Started
60-
61-
CLIProxyAPI Guides: [https://help.router-for.me/](https://help.router-for.me/)
62-
63-
## Management API
64-
65-
see [MANAGEMENT_API.md](https://help.router-for.me/management/api)
66-
67-
## Amp CLI Support
68-
69-
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:
70-
71-
- Provider route aliases for Amp's API patterns (`/api/provider/{provider}/v1...`)
72-
- Management proxy for OAuth authentication and account features
73-
- Smart model fallback with automatic routing
74-
- **Model mapping** to route unavailable models to alternatives (e.g., `claude-opus-4.5``claude-sonnet-4`)
75-
- Security-first design with localhost-only management endpoints
76-
77-
**[Complete Amp CLI Integration Guide](https://help.router-for.me/agent-client/amp-cli.html)**
78-
79-
## SDK Docs
80-
81-
- Usage: [docs/sdk-usage.md](docs/sdk-usage.md)
82-
- Advanced (executors & translators): [docs/sdk-advanced.md](docs/sdk-advanced.md)
83-
- Access: [docs/sdk-access.md](docs/sdk-access.md)
84-
- Watcher: [docs/sdk-watcher.md](docs/sdk-watcher.md)
85-
- Custom Provider Example: `examples/custom-provider`
86-
87-
## Contributing
88-
89-
Contributions are welcome! Please feel free to submit a Pull Request.
90-
91-
1. Fork the repository
92-
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
93-
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
94-
4. Push to the branch (`git push origin feature/amazing-feature`)
95-
5. Open a Pull Request
96-
97-
## Who is with us?
98-
99-
Those projects are based on CLIProxyAPI:
100-
101-
### [vibeproxy](https://github.com/automazeio/vibeproxy)
102-
103-
Native macOS menu bar app to use your Claude Code & ChatGPT subscriptions with AI coding tools - no API keys needed
104-
105-
### [Subtitle Translator](https://github.com/VjayC/SRT-Subtitle-Translator-Validator)
106-
107-
Browser-based tool to translate SRT subtitles using your Gemini subscription via CLIProxyAPI with automatic validation/error correction - no API keys needed
108-
109-
### [CCS (Claude Code Switch)](https://github.com/kaitranntt/ccs)
110-
111-
CLI wrapper for instant switching between multiple Claude accounts and alternative models (Gemini, Codex, Antigravity) via CLIProxyAPI OAuth - no API keys needed
112-
113-
### [ProxyPal](https://github.com/heyhuynhgiabuu/proxypal)
114-
115-
Native macOS GUI for managing CLIProxyAPI: configure providers, model mappings, and endpoints via OAuth - no API keys needed.
116-
117-
### [Quotio](https://github.com/nguyenphutrong/quotio)
118-
119-
Native macOS menu bar app that unifies Claude, Gemini, OpenAI, Qwen, and Antigravity subscriptions with real-time quota tracking and smart auto-failover for AI coding tools like Claude Code, OpenCode, and Droid - no API keys needed.
120-
121-
### [CodMate](https://github.com/loocor/CodMate)
122-
123-
Native macOS SwiftUI app for managing CLI AI sessions (Codex, Claude Code, Gemini CLI) with unified provider management, Git review, project organization, global search, and terminal integration. Integrates CLIProxyAPI to provide OAuth authentication for Codex, Claude, Gemini, Antigravity, and Qwen Code, with built-in and third-party provider rerouting through a single proxy endpoint - no API keys needed for OAuth providers.
124-
125-
### [ProxyPilot](https://github.com/Finesssee/ProxyPilot)
126-
127-
Windows-native CLIProxyAPI fork with TUI, system tray, and multi-provider OAuth for AI coding tools - no API keys needed.
128-
129-
### [Claude Proxy VSCode](https://github.com/uzhao/claude-proxy-vscode)
130-
131-
VSCode extension for quick switching between Claude Code models, featuring integrated CLIProxyAPI as its backend with automatic background lifecycle management.
132-
133-
### [ZeroLimit](https://github.com/0xtbug/zero-limit)
134-
135-
Windows desktop app built with Tauri + React for monitoring AI coding assistant quotas via CLIProxyAPI. Track usage across Gemini, Claude, OpenAI Codex, and Antigravity accounts with real-time dashboard, system tray integration, and one-click proxy control - no API keys needed.
136-
137-
### [CPA-XXX Panel](https://github.com/ferretgeek/CPA-X)
138-
139-
A lightweight web admin panel for CLIProxyAPI with health checks, resource monitoring, real-time logs, auto-update, request statistics and pricing display. Supports one-click installation and systemd service.
140-
141-
### [CLIProxyAPI Tray](https://github.com/kitephp/CLIProxyAPI_Tray)
142-
143-
A Windows tray application implemented using PowerShell scripts, without relying on any third-party libraries. The main features include: automatic creation of shortcuts, silent running, password management, channel switching (Main / Plus), and automatic downloading and updating.
144-
145-
### [霖君](https://github.com/wangdabaoqq/LinJun)
146-
147-
霖君 is a cross-platform desktop application for managing AI programming assistants, supporting macOS, Windows, and Linux systems. Unified management of Claude Code, Gemini CLI, OpenAI Codex, Qwen Code, and other AI coding tools, with local proxy for multi-account quota tracking and one-click configuration.
148-
149-
### [CLIProxyAPI Dashboard](https://github.com/itsmylife44/cliproxyapi-dashboard)
150-
151-
A modern web-based management dashboard for CLIProxyAPI built with Next.js, React, and PostgreSQL. Features real-time log streaming, structured configuration editing, API key management, OAuth provider integration for Claude/Gemini/Codex, usage analytics, container management, and config sync with OpenCode via companion plugin - no manual YAML editing needed.
152-
153-
> [!NOTE]
154-
> If you developed a project based on CLIProxyAPI, please open a PR to add it to this list.
155-
156-
## More choices
157-
158-
Those projects are ports of CLIProxyAPI or inspired by it:
159-
160-
### [9Router](https://github.com/decolua/9router)
161-
162-
A Next.js implementation inspired by CLIProxyAPI, easy to install and use, built from scratch with format translation (OpenAI/Claude/Gemini/Ollama), combo system with auto-fallback, multi-account management with exponential backoff, a Next.js web dashboard, and support for CLI tools (Cursor, Claude Code, Cline, RooCode) - no API keys needed.
163-
164-
### [OmniRoute](https://github.com/diegosouzapw/OmniRoute)
165-
166-
Never stop coding. Smart routing to FREE & low-cost AI models with automatic fallback.
167-
168-
OmniRoute is an AI gateway for multi-provider LLMs: an OpenAI-compatible endpoint with smart routing, load balancing, retries, and fallbacks. Add policies, rate limits, caching, and observability for reliable, cost-aware inference.
169-
170-
> [!NOTE]
171-
> If you have developed a port of CLIProxyAPI or a project inspired by it, please open a PR to add it to this list.
172-
173-
## License
174-
175-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
1+
# CLI Proxy API - Aoao

internal/api/handlers/management/config_lists.go

Lines changed: 81 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package management
22

33
import (
4-
"context"
54
"encoding/json"
65
"fmt"
76
"strings"
@@ -10,7 +9,6 @@ import (
109
"github.com/gin-gonic/gin"
1110
"github.com/google/uuid"
1211
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
13-
"github.com/router-for-me/CLIProxyAPI/v6/internal/usagerecord"
1412
)
1513

1614
// Generic helpers for list[string]
@@ -108,58 +106,28 @@ func (h *Handler) deleteFromStringList(c *gin.Context, target *[]string, after f
108106
c.JSON(400, gin.H{"error": "missing index or value"})
109107
}
110108

111-
// apiKeyResponse is a DTO for API key responses (avoids copying mutex from config.ApiKeyEntry)
112-
type apiKeyResponse struct {
113-
ID string `json:"id,omitempty"`
114-
Key string `json:"api-key"`
115-
Name string `json:"name,omitempty"`
116-
IsActive bool `json:"is-active"`
117-
UsageCount int64 `json:"usage-count,omitempty"`
118-
InputTokens int64 `json:"input-tokens,omitempty"`
119-
OutputTokens int64 `json:"output-tokens,omitempty"`
120-
LastUsedAt string `json:"last-used-at,omitempty"`
121-
CreatedAt string `json:"created-at,omitempty"`
109+
func apiKeyEntriesToStrings(entries []config.ApiKeyEntry) []string {
110+
if len(entries) == 0 {
111+
return nil
112+
}
113+
out := make([]string, 0, len(entries))
114+
for i := range entries {
115+
key := strings.TrimSpace(entries[i].Key)
116+
if key == "" {
117+
continue
118+
}
119+
out = append(out, key)
120+
}
121+
if len(out) == 0 {
122+
return nil
123+
}
124+
return out
122125
}
123126

124127
// api-keys
125128
func (h *Handler) GetAPIKeys(c *gin.Context) {
126-
// Get persistent stats from SQLite database
127-
var persistentStats map[string]*usagerecord.APIKeyStats
128-
if store := usagerecord.DefaultStore(); store != nil {
129-
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
130-
defer cancel()
131-
var err error
132-
persistentStats, err = store.GetAPIKeyStats(ctx)
133-
if err != nil {
134-
// Log error but continue with empty stats
135-
c.Error(err)
136-
}
137-
}
138-
139-
// Build response DTOs (avoids copying mutex from config.ApiKeyEntry)
140-
apiKeys := make([]apiKeyResponse, len(h.cfg.APIKeys))
141-
for i, entry := range h.cfg.APIKeys {
142-
apiKeys[i] = apiKeyResponse{
143-
ID: entry.ID,
144-
Key: entry.Key,
145-
Name: entry.Name,
146-
IsActive: entry.IsActive,
147-
CreatedAt: entry.CreatedAt,
148-
LastUsedAt: entry.LastUsedAt,
149-
}
150-
// Merge persistent stats from SQLite if available
151-
if stats, ok := persistentStats[entry.Key]; ok {
152-
// Use persistent stats (from DB) as the source of truth
153-
apiKeys[i].UsageCount = stats.UsageCount
154-
apiKeys[i].InputTokens = stats.InputTokens
155-
apiKeys[i].OutputTokens = stats.OutputTokens
156-
if stats.LastUsedAt != "" {
157-
apiKeys[i].LastUsedAt = stats.LastUsedAt
158-
}
159-
}
160-
}
161-
162-
c.JSON(200, gin.H{"api-keys": apiKeys})
129+
// Align to upstream: return a simple list of api key strings.
130+
c.JSON(200, gin.H{"api-keys": apiKeyEntriesToStrings(h.cfg.APIKeys)})
163131
}
164132

165133
func (h *Handler) PutAPIKeys(c *gin.Context) {
@@ -241,9 +209,10 @@ func (h *Handler) PatchAPIKeys(c *gin.Context) {
241209
// For updating by old/new key value (backward compatible)
242210
Old *string `json:"old"`
243211
New *string `json:"new"`
244-
// For updating by index (deprecated, but kept for compatibility)
245-
Index *int `json:"index"`
246-
Value *config.ApiKeyEntry `json:"value"`
212+
// For updating by index (legacy)
213+
Index *int `json:"index"`
214+
// Value may be either a string (upstream-compatible) or an ApiKeyEntry object (extended).
215+
Value json.RawMessage `json:"value"`
247216
// Partial update fields (use pointers to distinguish nil from zero value)
248217
IsActive *bool `json:"is-active"`
249218
Name *string `json:"name"`
@@ -254,6 +223,53 @@ func (h *Handler) PatchAPIKeys(c *gin.Context) {
254223
return
255224
}
256225

226+
// Update by index with value (string or object) - legacy compatibility.
227+
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.APIKeys) && len(body.Value) > 0 {
228+
// Prefer parsing as string (upstream-compatible TUI expects this).
229+
var asString string
230+
if err := json.Unmarshal(body.Value, &asString); err == nil {
231+
newKey := strings.TrimSpace(asString)
232+
if newKey == "" {
233+
c.JSON(400, gin.H{"error": "key cannot be empty"})
234+
return
235+
}
236+
for i, existing := range h.cfg.APIKeys {
237+
if i != *body.Index && existing.Key == newKey {
238+
c.JSON(409, gin.H{"error": "key already exists"})
239+
return
240+
}
241+
}
242+
h.cfg.APIKeys[*body.Index].Key = newKey
243+
h.persist(c)
244+
return
245+
}
246+
247+
// Fallback: parse as full ApiKeyEntry object.
248+
var entry config.ApiKeyEntry
249+
if err := json.Unmarshal(body.Value, &entry); err == nil {
250+
entry.Key = strings.TrimSpace(entry.Key)
251+
if entry.Key == "" {
252+
c.JSON(400, gin.H{"error": "key cannot be empty"})
253+
return
254+
}
255+
for i, existing := range h.cfg.APIKeys {
256+
if i != *body.Index && existing.Key == entry.Key {
257+
c.JSON(409, gin.H{"error": "key already exists"})
258+
return
259+
}
260+
}
261+
if entry.ID == "" {
262+
entry.ID = h.cfg.APIKeys[*body.Index].ID
263+
}
264+
if entry.CreatedAt == "" {
265+
entry.CreatedAt = h.cfg.APIKeys[*body.Index].CreatedAt
266+
}
267+
h.cfg.APIKeys[*body.Index] = entry
268+
h.persist(c)
269+
return
270+
}
271+
}
272+
257273
// Find target entry by ID or Key
258274
targetIndex := -1
259275
if body.ID != nil && *body.ID != "" {
@@ -315,26 +331,22 @@ func (h *Handler) PatchAPIKeys(c *gin.Context) {
315331
}
316332
}
317333

318-
// Update by index with full value (legacy support)
319-
if body.Index != nil && body.Value != nil && *body.Index >= 0 && *body.Index < len(h.cfg.APIKeys) {
320-
entry := *body.Value
321-
entry.Key = strings.TrimSpace(entry.Key)
322-
if entry.Key == "" {
323-
c.JSON(400, gin.H{"error": "key cannot be empty"})
324-
return
325-
}
326-
// Check for duplicate key (excluding current index)
327-
for i, existing := range h.cfg.APIKeys {
328-
if i != *body.Index && existing.Key == entry.Key {
334+
// Append-only add (compat): allow {"new": "..."} or {"old": null, "new": "..."}.
335+
if body.New != nil && strings.TrimSpace(*body.New) != "" && body.Old == nil {
336+
newKey := strings.TrimSpace(*body.New)
337+
for _, entry := range h.cfg.APIKeys {
338+
if entry.Key == newKey {
329339
c.JSON(409, gin.H{"error": "key already exists"})
330340
return
331341
}
332342
}
333-
// Preserve ID if not provided
334-
if entry.ID == "" {
335-
entry.ID = h.cfg.APIKeys[*body.Index].ID
336-
}
337-
h.cfg.APIKeys[*body.Index] = entry
343+
now := time.Now().UTC().Format(time.RFC3339)
344+
h.cfg.APIKeys = append(h.cfg.APIKeys, config.ApiKeyEntry{
345+
ID: uuid.New().String(),
346+
Key: newKey,
347+
IsActive: true,
348+
CreatedAt: now,
349+
})
338350
h.persist(c)
339351
return
340352
}

0 commit comments

Comments
 (0)