mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-04 05:20:52 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1548c567ab | ||
|
|
5b23fc570c | ||
|
|
04e1c7a05a | ||
|
|
9181e72204 | ||
|
|
4939865f6d | ||
|
|
3da7f7482e | ||
|
|
9072b029b2 | ||
|
|
c296cfb8c0 | ||
|
|
2707377fcb | ||
|
|
259f586ff7 | ||
|
|
d885b81f23 | ||
|
|
fe6bffd080 | ||
|
|
250f212fa3 | ||
|
|
a275db3fdb |
@@ -30,6 +30,10 @@ Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB
|
||||
<td width="180"><a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa"><img src="./assets/cubence.png" alt="Cubence" width="150"></a></td>
|
||||
<td>Thanks to Cubence for sponsoring this project! Cubence is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. Cubence provides special discounts for our software users: register using <a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa">this link</a> and enter the "CLIPROXYAPI" promo code during recharge to get 10% off.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=TJNAIF"><img src="./assets/aicodemirror.png" alt="AICodeMirror" width="150"></a></td>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -142,6 +146,10 @@ A lightweight web admin panel for CLIProxyAPI with health checks, resource monit
|
||||
|
||||
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.
|
||||
|
||||
### [霖君](https://github.com/wangdabaoqq/LinJun)
|
||||
|
||||
霖君 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.
|
||||
|
||||
> [!NOTE]
|
||||
> If you developed a project based on CLIProxyAPI, please open a PR to add it to this list.
|
||||
|
||||
|
||||
16
README_CN.md
16
README_CN.md
@@ -30,6 +30,10 @@ GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元
|
||||
<td width="180"><a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa"><img src="./assets/cubence.png" alt="Cubence" width="150"></a></td>
|
||||
<td>感谢 Cubence 对本项目的赞助!Cubence 是一家可靠高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种服务的中转。Cubence 为本软件用户提供了特别优惠:使用<a href="https://cubence.com/signup?code=CLIPROXYAPI&source=cpa">此链接</a>注册,并在充值时输入 "CLIPROXYAPI" 优惠码即可享受九折优惠。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="180"><a href="https://www.aicodemirror.com/register?invitecode=TJNAIF"><img src="./assets/aicodemirror.png" alt="AICodeMirror" width="150"></a></td>
|
||||
<td>感谢 AICodeMirror 赞助了本项目!AICodeMirror 提供 Claude Code / Codex / Gemini CLI 官方高稳定中转服务,支持企业级高并发、极速开票、7×24 专属技术支持。 Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更有折上折!AICodeMirror 为 CLIProxyAPI 的用户提供了特别福利,通过<a href="https://www.aicodemirror.com/register?invitecode=TJNAIF">此链接</a>注册的用户,可享受首充8折,企业客户最高可享 7.5 折!</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -137,6 +141,14 @@ Windows 桌面应用,基于 Tauri + React 构建,用于通过 CLIProxyAPI
|
||||
|
||||
面向 CLIProxyAPI 的 Web 管理面板,提供健康检查、资源监控、日志查看、自动更新、请求统计与定价展示,支持一键安装与 systemd 服务。
|
||||
|
||||
### [CLIProxyAPI Tray](https://github.com/kitephp/CLIProxyAPI_Tray)
|
||||
|
||||
Windows 托盘应用,基于 PowerShell 脚本实现,不依赖任何第三方库。主要功能包括:自动创建快捷方式、静默运行、密码管理、通道切换(Main / Plus)以及自动下载与更新。
|
||||
|
||||
### [霖君](https://github.com/wangdabaoqq/LinJun)
|
||||
|
||||
霖君是一款用于管理AI编程助手的跨平台桌面应用,支持macOS、Windows、Linux系统。统一管理Claude Code、Gemini CLI、OpenAI Codex、Qwen Code等AI编程工具,本地代理实现多账户配额跟踪和一键配置。
|
||||
|
||||
> [!NOTE]
|
||||
> 如果你开发了基于 CLIProxyAPI 的项目,请提交一个 PR(拉取请求)将其添加到此列表中。
|
||||
|
||||
@@ -148,10 +160,6 @@ Windows 桌面应用,基于 Tauri + React 构建,用于通过 CLIProxyAPI
|
||||
|
||||
基于 Next.js 的实现,灵感来自 CLIProxyAPI,易于安装使用;自研格式转换(OpenAI/Claude/Gemini/Ollama)、组合系统与自动回退、多账户管理(指数退避)、Next.js Web 控制台,并支持 Cursor、Claude Code、Cline、RooCode 等 CLI 工具,无需 API 密钥。
|
||||
|
||||
### [CLIProxyAPI Tray](https://github.com/kitephp/CLIProxyAPI_Tray)
|
||||
|
||||
Windows 托盘应用,基于 PowerShell 脚本实现,不依赖任何第三方库。主要功能包括:自动创建快捷方式、静默运行、密码管理、通道切换(Main / Plus)以及自动下载与更新。
|
||||
|
||||
> [!NOTE]
|
||||
> 如果你开发了 CLIProxyAPI 的移植或衍生项目,请提交 PR 将其添加到此列表中。
|
||||
|
||||
|
||||
BIN
assets/aicodemirror.png
Normal file
BIN
assets/aicodemirror.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
@@ -40,6 +40,11 @@ api-keys:
|
||||
# Enable debug logging
|
||||
debug: false
|
||||
|
||||
# Enable pprof HTTP debug server (host:port). Keep it bound to localhost for safety.
|
||||
pprof:
|
||||
enable: false
|
||||
addr: "127.0.0.1:8316"
|
||||
|
||||
# When true, disable high-overhead HTTP middleware features to reduce per-request memory usage under high concurrency.
|
||||
commercial-mode: false
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const DefaultPanelGitHubRepository = "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"
|
||||
const (
|
||||
DefaultPanelGitHubRepository = "https://github.com/router-for-me/Cli-Proxy-API-Management-Center"
|
||||
DefaultPprofAddr = "127.0.0.1:8316"
|
||||
)
|
||||
|
||||
// Config represents the application's configuration, loaded from a YAML file.
|
||||
type Config struct {
|
||||
@@ -41,6 +44,9 @@ type Config struct {
|
||||
// Debug enables or disables debug-level logging and other debug features.
|
||||
Debug bool `yaml:"debug" json:"debug"`
|
||||
|
||||
// Pprof config controls the optional pprof HTTP debug server.
|
||||
Pprof PprofConfig `yaml:"pprof" json:"pprof"`
|
||||
|
||||
// CommercialMode disables high-overhead HTTP middleware features to minimize per-request memory usage.
|
||||
CommercialMode bool `yaml:"commercial-mode" json:"commercial-mode"`
|
||||
|
||||
@@ -121,6 +127,14 @@ type TLSConfig struct {
|
||||
Key string `yaml:"key" json:"key"`
|
||||
}
|
||||
|
||||
// PprofConfig holds pprof HTTP server settings.
|
||||
type PprofConfig struct {
|
||||
// Enable toggles the pprof HTTP debug server.
|
||||
Enable bool `yaml:"enable" json:"enable"`
|
||||
// Addr is the host:port address for the pprof HTTP server.
|
||||
Addr string `yaml:"addr" json:"addr"`
|
||||
}
|
||||
|
||||
// RemoteManagement holds management API configuration under 'remote-management'.
|
||||
type RemoteManagement struct {
|
||||
// AllowRemote toggles remote (non-localhost) access to management API.
|
||||
@@ -514,6 +528,8 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
||||
cfg.ErrorLogsMaxFiles = 10
|
||||
cfg.UsageStatisticsEnabled = false
|
||||
cfg.DisableCooling = false
|
||||
cfg.Pprof.Enable = false
|
||||
cfg.Pprof.Addr = DefaultPprofAddr
|
||||
cfg.AmpCode.RestrictManagementToLocalhost = false // Default to false: API key auth is sufficient
|
||||
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
||||
if err = yaml.Unmarshal(data, &cfg); err != nil {
|
||||
@@ -556,6 +572,11 @@ func LoadConfigOptional(configFile string, optional bool) (*Config, error) {
|
||||
cfg.RemoteManagement.PanelGitHubRepository = DefaultPanelGitHubRepository
|
||||
}
|
||||
|
||||
cfg.Pprof.Addr = strings.TrimSpace(cfg.Pprof.Addr)
|
||||
if cfg.Pprof.Addr == "" {
|
||||
cfg.Pprof.Addr = DefaultPprofAddr
|
||||
}
|
||||
|
||||
if cfg.LogsMaxTotalSizeMB < 0 {
|
||||
cfg.LogsMaxTotalSizeMB = 0
|
||||
}
|
||||
|
||||
@@ -131,7 +131,10 @@ func ResolveLogDirectory(cfg *config.Config) string {
|
||||
return logDir
|
||||
}
|
||||
if !isDirWritable(logDir) {
|
||||
authDir := strings.TrimSpace(cfg.AuthDir)
|
||||
authDir, err := util.ResolveAuthDir(cfg.AuthDir)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to resolve auth-dir %q for log directory: %v", cfg.AuthDir, err)
|
||||
}
|
||||
if authDir != "" {
|
||||
logDir = filepath.Join(authDir, "logs")
|
||||
}
|
||||
|
||||
@@ -1003,6 +1003,8 @@ func vertexBaseURL(location string) string {
|
||||
loc := strings.TrimSpace(location)
|
||||
if loc == "" {
|
||||
loc = "us-central1"
|
||||
} else if loc == "global" {
|
||||
return "https://aiplatform.googleapis.com"
|
||||
}
|
||||
return fmt.Sprintf("https://%s-aiplatform.googleapis.com", loc)
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
||||
if signatureResult.Exists() && signatureResult.String() != "" {
|
||||
arrayClientSignatures := strings.SplitN(signatureResult.String(), "#", 2)
|
||||
if len(arrayClientSignatures) == 2 {
|
||||
if modelName == arrayClientSignatures[0] {
|
||||
if cache.GetModelGroup(modelName) == arrayClientSignatures[0] {
|
||||
clientSignature = arrayClientSignatures[1]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ import (
|
||||
func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||
rawJSON := bytes.Clone(inputRawJSON)
|
||||
|
||||
inputResult := gjson.GetBytes(rawJSON, "input")
|
||||
if inputResult.Type == gjson.String {
|
||||
input, _ := sjson.Set(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`, "0.content.0.text", inputResult.String())
|
||||
rawJSON, _ = sjson.SetRawBytes(rawJSON, "input", []byte(input))
|
||||
}
|
||||
|
||||
rawJSON, _ = sjson.SetBytes(rawJSON, "stream", true)
|
||||
rawJSON, _ = sjson.SetBytes(rawJSON, "store", false)
|
||||
rawJSON, _ = sjson.SetBytes(rawJSON, "parallel_tool_calls", true)
|
||||
|
||||
@@ -68,6 +68,9 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
||||
case "message", "":
|
||||
// Handle regular message conversion
|
||||
role := item.Get("role").String()
|
||||
if role == "developer" {
|
||||
role = "user"
|
||||
}
|
||||
message := `{"role":"","content":""}`
|
||||
message, _ = sjson.Set(message, "role", role)
|
||||
|
||||
@@ -167,7 +170,8 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
||||
// Only function tools need structural conversion because Chat Completions nests details under "function".
|
||||
toolType := tool.Get("type").String()
|
||||
if toolType != "" && toolType != "function" && tool.IsObject() {
|
||||
chatCompletionsTools = append(chatCompletionsTools, tool.Value())
|
||||
// Almost all providers lack built-in tools, so we just ignore them.
|
||||
// chatCompletionsTools = append(chatCompletionsTools, tool.Value())
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@ func BuildConfigChangeDetails(oldCfg, newCfg *config.Config) []string {
|
||||
if oldCfg.Debug != newCfg.Debug {
|
||||
changes = append(changes, fmt.Sprintf("debug: %t -> %t", oldCfg.Debug, newCfg.Debug))
|
||||
}
|
||||
if oldCfg.Pprof.Enable != newCfg.Pprof.Enable {
|
||||
changes = append(changes, fmt.Sprintf("pprof.enable: %t -> %t", oldCfg.Pprof.Enable, newCfg.Pprof.Enable))
|
||||
}
|
||||
if strings.TrimSpace(oldCfg.Pprof.Addr) != strings.TrimSpace(newCfg.Pprof.Addr) {
|
||||
changes = append(changes, fmt.Sprintf("pprof.addr: %s -> %s", strings.TrimSpace(oldCfg.Pprof.Addr), strings.TrimSpace(newCfg.Pprof.Addr)))
|
||||
}
|
||||
if oldCfg.LoggingToFile != newCfg.LoggingToFile {
|
||||
changes = append(changes, fmt.Sprintf("logging-to-file: %t -> %t", oldCfg.LoggingToFile, newCfg.LoggingToFile))
|
||||
}
|
||||
|
||||
163
sdk/cliproxy/pprof_server.go
Normal file
163
sdk/cliproxy/pprof_server.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package cliproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type pprofServer struct {
|
||||
mu sync.Mutex
|
||||
server *http.Server
|
||||
addr string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
func newPprofServer() *pprofServer {
|
||||
return &pprofServer{}
|
||||
}
|
||||
|
||||
func (s *Service) applyPprofConfig(cfg *config.Config) {
|
||||
if s == nil || cfg == nil {
|
||||
return
|
||||
}
|
||||
if s.pprofServer == nil {
|
||||
s.pprofServer = newPprofServer()
|
||||
}
|
||||
s.pprofServer.Apply(cfg)
|
||||
}
|
||||
|
||||
func (s *Service) shutdownPprof(ctx context.Context) error {
|
||||
if s == nil || s.pprofServer == nil {
|
||||
return nil
|
||||
}
|
||||
return s.pprofServer.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (p *pprofServer) Apply(cfg *config.Config) {
|
||||
if p == nil || cfg == nil {
|
||||
return
|
||||
}
|
||||
addr := strings.TrimSpace(cfg.Pprof.Addr)
|
||||
if addr == "" {
|
||||
addr = config.DefaultPprofAddr
|
||||
}
|
||||
enabled := cfg.Pprof.Enable
|
||||
|
||||
p.mu.Lock()
|
||||
currentServer := p.server
|
||||
currentAddr := p.addr
|
||||
p.addr = addr
|
||||
p.enabled = enabled
|
||||
if !enabled {
|
||||
p.server = nil
|
||||
p.mu.Unlock()
|
||||
if currentServer != nil {
|
||||
p.stopServer(currentServer, currentAddr, "disabled")
|
||||
}
|
||||
return
|
||||
}
|
||||
if currentServer != nil && currentAddr == addr {
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
p.server = nil
|
||||
p.mu.Unlock()
|
||||
|
||||
if currentServer != nil {
|
||||
p.stopServer(currentServer, currentAddr, "restarted")
|
||||
}
|
||||
|
||||
p.startServer(addr)
|
||||
}
|
||||
|
||||
func (p *pprofServer) Shutdown(ctx context.Context) error {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
p.mu.Lock()
|
||||
currentServer := p.server
|
||||
currentAddr := p.addr
|
||||
p.server = nil
|
||||
p.enabled = false
|
||||
p.mu.Unlock()
|
||||
|
||||
if currentServer == nil {
|
||||
return nil
|
||||
}
|
||||
return p.stopServerWithContext(ctx, currentServer, currentAddr, "shutdown")
|
||||
}
|
||||
|
||||
func (p *pprofServer) startServer(addr string) {
|
||||
mux := newPprofMux()
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
if !p.enabled || p.addr != addr || p.server != nil {
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
p.server = server
|
||||
p.mu.Unlock()
|
||||
|
||||
log.Infof("pprof server starting on %s", addr)
|
||||
go func() {
|
||||
if errServe := server.ListenAndServe(); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {
|
||||
log.Errorf("pprof server failed on %s: %v", addr, errServe)
|
||||
p.mu.Lock()
|
||||
if p.server == server {
|
||||
p.server = nil
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *pprofServer) stopServer(server *http.Server, addr string, reason string) {
|
||||
_ = p.stopServerWithContext(context.Background(), server, addr, reason)
|
||||
}
|
||||
|
||||
func (p *pprofServer) stopServerWithContext(ctx context.Context, server *http.Server, addr string, reason string) error {
|
||||
if server == nil {
|
||||
return nil
|
||||
}
|
||||
stopCtx := ctx
|
||||
if stopCtx == nil {
|
||||
stopCtx = context.Background()
|
||||
}
|
||||
stopCtx, cancel := context.WithTimeout(stopCtx, 5*time.Second)
|
||||
defer cancel()
|
||||
if errStop := server.Shutdown(stopCtx); errStop != nil {
|
||||
log.Errorf("pprof server stop failed on %s: %v", addr, errStop)
|
||||
return errStop
|
||||
}
|
||||
log.Infof("pprof server stopped on %s (%s)", addr, reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
func newPprofMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
mux.Handle("/debug/pprof/allocs", pprof.Handler("allocs"))
|
||||
mux.Handle("/debug/pprof/block", pprof.Handler("block"))
|
||||
mux.Handle("/debug/pprof/goroutine", pprof.Handler("goroutine"))
|
||||
mux.Handle("/debug/pprof/heap", pprof.Handler("heap"))
|
||||
mux.Handle("/debug/pprof/mutex", pprof.Handler("mutex"))
|
||||
mux.Handle("/debug/pprof/threadcreate", pprof.Handler("threadcreate"))
|
||||
return mux
|
||||
}
|
||||
@@ -57,6 +57,9 @@ type Service struct {
|
||||
// server is the HTTP API server instance.
|
||||
server *api.Server
|
||||
|
||||
// pprofServer manages the optional pprof HTTP debug server.
|
||||
pprofServer *pprofServer
|
||||
|
||||
// serverErr channel for server startup/shutdown errors.
|
||||
serverErr chan error
|
||||
|
||||
@@ -501,6 +504,8 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
fmt.Printf("API server started successfully on: %s:%d\n", s.cfg.Host, s.cfg.Port)
|
||||
|
||||
s.applyPprofConfig(s.cfg)
|
||||
|
||||
if s.hooks.OnAfterStart != nil {
|
||||
s.hooks.OnAfterStart(s)
|
||||
}
|
||||
@@ -546,6 +551,7 @@ func (s *Service) Run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
s.applyRetryConfig(newCfg)
|
||||
s.applyPprofConfig(newCfg)
|
||||
if s.server != nil {
|
||||
s.server.UpdateClients(newCfg)
|
||||
}
|
||||
@@ -639,6 +645,13 @@ func (s *Service) Shutdown(ctx context.Context) error {
|
||||
s.authQueueStop = nil
|
||||
}
|
||||
|
||||
if errShutdownPprof := s.shutdownPprof(ctx); errShutdownPprof != nil {
|
||||
log.Errorf("failed to stop pprof server: %v", errShutdownPprof)
|
||||
if shutdownErr == nil {
|
||||
shutdownErr = errShutdownPprof
|
||||
}
|
||||
}
|
||||
|
||||
// no legacy clients to persist
|
||||
|
||||
if s.server != nil {
|
||||
|
||||
Reference in New Issue
Block a user