chore: upgrade CLIProxyAPI dependency to v7 across the project

- Updated all references from v6 to v7 for `github.com/router-for-me/CLIProxyAPI`.
- Ensured consistency in imports within core libraries, tests, and integration tests.
- Added missing tests for new features in Redis Protocol integration.
This commit is contained in:
Luis Pater
2026-05-08 11:46:46 +08:00
parent 785b00c312
commit e50cabac4b
317 changed files with 2415 additions and 1035 deletions
+325 -124
View File
@@ -12,17 +12,18 @@ import (
"sync"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor"
"github.com/router-for-me/CLIProxyAPI/v6/internal/watcher"
"github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/usage"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/api"
"github.com/router-for-me/CLIProxyAPI/v7/internal/home"
"github.com/router-for-me/CLIProxyAPI/v7/internal/redisqueue"
"github.com/router-for-me/CLIProxyAPI/v7/internal/registry"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor"
"github.com/router-for-me/CLIProxyAPI/v7/internal/watcher"
"github.com/router-for-me/CLIProxyAPI/v7/internal/wsrelay"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v7/sdk/access"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/config"
log "github.com/sirupsen/logrus"
)
@@ -36,6 +37,9 @@ type Service struct {
// cfgMu protects concurrent access to the configuration.
cfgMu sync.RWMutex
// configUpdateMu serializes config updates across watcher + home.
configUpdateMu sync.Mutex
// configPath is the path to the configuration file.
configPath string
@@ -89,6 +93,9 @@ type Service struct {
// wsGateway manages websocket Gemini providers.
wsGateway *wsrelay.Manager
homeClient *home.Client
homeCancel context.CancelFunc
}
// RegisterUsagePlugin registers a usage plugin on the global usage manager.
@@ -462,6 +469,248 @@ func (s *Service) rebindExecutors() {
}
}
func (s *Service) applyConfigUpdate(newCfg *config.Config) {
if s == nil {
return
}
s.configUpdateMu.Lock()
defer s.configUpdateMu.Unlock()
previousStrategy := ""
var previousSessionAffinity bool
var previousSessionAffinityTTL string
s.cfgMu.RLock()
if s.cfg != nil {
previousStrategy = strings.ToLower(strings.TrimSpace(s.cfg.Routing.Strategy))
previousSessionAffinity = s.cfg.Routing.ClaudeCodeSessionAffinity || s.cfg.Routing.SessionAffinity
previousSessionAffinityTTL = s.cfg.Routing.SessionAffinityTTL
}
s.cfgMu.RUnlock()
if newCfg == nil {
s.cfgMu.RLock()
newCfg = s.cfg
s.cfgMu.RUnlock()
}
if newCfg == nil {
return
}
nextStrategy := strings.ToLower(strings.TrimSpace(newCfg.Routing.Strategy))
normalizeStrategy := func(strategy string) string {
switch strategy {
case "fill-first", "fillfirst", "ff":
return "fill-first"
default:
return "round-robin"
}
}
previousStrategy = normalizeStrategy(previousStrategy)
nextStrategy = normalizeStrategy(nextStrategy)
nextSessionAffinity := newCfg.Routing.ClaudeCodeSessionAffinity || newCfg.Routing.SessionAffinity
nextSessionAffinityTTL := newCfg.Routing.SessionAffinityTTL
selectorChanged := previousStrategy != nextStrategy ||
previousSessionAffinity != nextSessionAffinity ||
previousSessionAffinityTTL != nextSessionAffinityTTL
if s.coreManager != nil && selectorChanged {
var selector coreauth.Selector
switch nextStrategy {
case "fill-first":
selector = &coreauth.FillFirstSelector{}
default:
selector = &coreauth.RoundRobinSelector{}
}
if nextSessionAffinity {
ttl := time.Hour
if ttlStr := strings.TrimSpace(nextSessionAffinityTTL); ttlStr != "" {
if parsed, err := time.ParseDuration(ttlStr); err == nil && parsed > 0 {
ttl = parsed
}
}
selector = coreauth.NewSessionAffinitySelectorWithConfig(coreauth.SessionAffinityConfig{
Fallback: selector,
TTL: ttl,
})
}
s.coreManager.SetSelector(selector)
}
s.applyRetryConfig(newCfg)
s.applyPprofConfig(newCfg)
if s.server != nil {
s.server.UpdateClients(newCfg)
}
s.cfgMu.Lock()
s.cfg = newCfg
s.cfgMu.Unlock()
if s.coreManager != nil {
s.coreManager.SetConfig(newCfg)
s.coreManager.SetOAuthModelAlias(newCfg.OAuthModelAlias)
}
s.rebindExecutors()
}
func forceHomeRuntimeConfig(cfg *config.Config) {
if cfg == nil {
return
}
cfg.APIKeys = nil
cfg.DisableCooling = true
cfg.WebsocketAuth = false
cfg.EnableGeminiCLIEndpoint = false
cfg.RemoteManagement.AllowRemote = false
cfg.RemoteManagement.DisableControlPanel = true
}
func (s *Service) registerHomeExecutors() {
if s == nil || s.coreManager == nil || s.cfg == nil {
return
}
// Register baseline executors so home-dispatched auth entries can execute without
// requiring any local auth-dir credentials.
s.coreManager.RegisterExecutor(executor.NewCodexAutoExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewClaudeExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewGeminiExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewGeminiVertexExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewGeminiCLIExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewAIStudioExecutor(s.cfg, "", s.wsGateway))
s.coreManager.RegisterExecutor(executor.NewAntigravityExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewKimiExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor("openai-compatibility", s.cfg))
}
func (s *Service) applyHomeOverlay(remoteCfg *config.Config) {
if s == nil || remoteCfg == nil {
return
}
s.cfgMu.RLock()
baseCfg := s.cfg
s.cfgMu.RUnlock()
if baseCfg == nil {
return
}
merged := *remoteCfg
merged.Host = baseCfg.Host
merged.Port = baseCfg.Port
merged.TLS = baseCfg.TLS
merged.Home = baseCfg.Home
forceHomeRuntimeConfig(&merged)
s.applyConfigUpdate(&merged)
}
func (s *Service) startHomeUsageForwarder(ctx context.Context, client *home.Client) {
if s == nil || client == nil {
return
}
if ctx == nil {
ctx = context.Background()
}
sleep := func(d time.Duration) bool {
if d <= 0 {
return true
}
timer := time.NewTimer(d)
defer timer.Stop()
select {
case <-ctx.Done():
return false
case <-timer.C:
return true
}
}
go func() {
for {
select {
case <-ctx.Done():
return
default:
}
if !client.HeartbeatOK() {
if !sleep(time.Second) {
return
}
continue
}
items := redisqueue.PopOldest(64)
if len(items) == 0 {
if !sleep(500 * time.Millisecond) {
return
}
continue
}
for i := range items {
if errPush := client.LPushUsage(ctx, items[i]); errPush != nil {
for j := i; j < len(items); j++ {
redisqueue.Enqueue(items[j])
}
if !sleep(time.Second) {
return
}
break
}
}
}
}()
}
func (s *Service) startHomeSubscriber(ctx context.Context) {
if s == nil {
return
}
s.cfgMu.RLock()
cfg := s.cfg
s.cfgMu.RUnlock()
if cfg == nil || !cfg.Home.Enabled {
return
}
if s.homeCancel != nil {
s.homeCancel()
s.homeCancel = nil
}
if s.homeClient != nil {
s.homeClient.Close()
s.homeClient = nil
}
homeCtx := ctx
if homeCtx == nil {
homeCtx = context.Background()
}
homeCtx, cancel := context.WithCancel(homeCtx)
s.homeCancel = cancel
client := home.New(cfg.Home)
s.homeClient = client
home.SetCurrent(client)
go client.StartConfigSubscriber(homeCtx, func(raw []byte) error {
parsed, err := config.ParseConfigBytes(raw)
if err != nil {
log.Warnf("failed to parse home config payload: %v", err)
return err
}
s.applyHomeOverlay(parsed)
return nil
})
s.startHomeUsageForwarder(homeCtx, client)
}
// Run starts the service and blocks until the context is cancelled or the server stops.
// It initializes all components including authentication, file watching, HTTP server,
// and starts processing requests. The method blocks until the context is cancelled.
@@ -480,6 +729,10 @@ func (s *Service) Run(ctx context.Context) error {
}
usage.StartDefault(ctx)
homeEnabled := s.cfg != nil && s.cfg.Home.Enabled
if homeEnabled {
forceHomeRuntimeConfig(s.cfg)
}
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer shutdownCancel()
@@ -489,32 +742,36 @@ func (s *Service) Run(ctx context.Context) error {
}
}()
if err := s.ensureAuthDir(); err != nil {
return err
if !homeEnabled {
if errEnsureAuthDir := s.ensureAuthDir(); errEnsureAuthDir != nil {
return errEnsureAuthDir
}
}
s.applyRetryConfig(s.cfg)
if s.coreManager != nil {
if s.coreManager != nil && !homeEnabled {
if errLoad := s.coreManager.Load(ctx); errLoad != nil {
log.Warnf("failed to load auth store: %v", errLoad)
}
}
tokenResult, err := s.tokenProvider.Load(ctx, s.cfg)
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
if tokenResult == nil {
tokenResult = &TokenClientResult{}
}
if !homeEnabled {
tokenResult, err := s.tokenProvider.Load(ctx, s.cfg)
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
if tokenResult == nil {
tokenResult = &TokenClientResult{}
}
apiKeyResult, err := s.apiKeyProvider.Load(ctx, s.cfg)
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
if apiKeyResult == nil {
apiKeyResult = &APIKeyClientResult{}
apiKeyResult, err := s.apiKeyProvider.Load(ctx, s.cfg)
if err != nil && !errors.Is(err, context.Canceled) {
return err
}
if apiKeyResult == nil {
apiKeyResult = &APIKeyClientResult{}
}
}
// legacy clients removed; no caches to refresh
@@ -526,6 +783,10 @@ func (s *Service) Run(ctx context.Context) error {
s.authManager = newDefaultAuthManager()
}
if homeEnabled {
s.startHomeSubscriber(ctx)
}
s.ensureWebsocketGateway()
if s.server != nil && s.wsGateway != nil {
s.server.AttachWebsocketRoute(s.wsGateway.Path(), s.wsGateway.Handler())
@@ -547,6 +808,12 @@ func (s *Service) Run(ctx context.Context) error {
})
}
if homeEnabled {
s.registerHomeExecutors()
// Home mode does not expose in-process Redis RESP usage output; usage is forwarded to home instead.
redisqueue.SetEnabled(true)
}
if s.hooks.OnBeforeStart != nil {
s.hooks.OnBeforeStart(s.cfg)
}
@@ -607,107 +874,31 @@ func (s *Service) Run(ctx context.Context) error {
s.hooks.OnAfterStart(s)
}
var watcherWrapper *WatcherWrapper
reloadCallback := func(newCfg *config.Config) {
previousStrategy := ""
var previousSessionAffinity bool
var previousSessionAffinityTTL string
s.cfgMu.RLock()
if s.cfg != nil {
previousStrategy = strings.ToLower(strings.TrimSpace(s.cfg.Routing.Strategy))
previousSessionAffinity = s.cfg.Routing.ClaudeCodeSessionAffinity || s.cfg.Routing.SessionAffinity
previousSessionAffinityTTL = s.cfg.Routing.SessionAffinityTTL
if !homeEnabled {
var watcherWrapper *WatcherWrapper
reloadCallback := func(newCfg *config.Config) { s.applyConfigUpdate(newCfg) }
watcherWrapper, errCreate := s.watcherFactory(s.configPath, s.cfg.AuthDir, reloadCallback)
if errCreate != nil {
return fmt.Errorf("cliproxy: failed to create watcher: %w", errCreate)
}
s.cfgMu.RUnlock()
if newCfg == nil {
s.cfgMu.RLock()
newCfg = s.cfg
s.cfgMu.RUnlock()
s.watcher = watcherWrapper
s.ensureAuthUpdateQueue(ctx)
if s.authUpdates != nil {
watcherWrapper.SetAuthUpdateQueue(s.authUpdates)
}
if newCfg == nil {
return
watcherWrapper.SetConfig(s.cfg)
watcherCtx, watcherCancel := context.WithCancel(context.Background())
s.watcherCancel = watcherCancel
if errStart := watcherWrapper.Start(watcherCtx); errStart != nil {
return fmt.Errorf("cliproxy: failed to start watcher: %w", errStart)
}
nextStrategy := strings.ToLower(strings.TrimSpace(newCfg.Routing.Strategy))
normalizeStrategy := func(strategy string) string {
switch strategy {
case "fill-first", "fillfirst", "ff":
return "fill-first"
default:
return "round-robin"
}
}
previousStrategy = normalizeStrategy(previousStrategy)
nextStrategy = normalizeStrategy(nextStrategy)
nextSessionAffinity := newCfg.Routing.ClaudeCodeSessionAffinity || newCfg.Routing.SessionAffinity
nextSessionAffinityTTL := newCfg.Routing.SessionAffinityTTL
selectorChanged := previousStrategy != nextStrategy ||
previousSessionAffinity != nextSessionAffinity ||
previousSessionAffinityTTL != nextSessionAffinityTTL
if s.coreManager != nil && selectorChanged {
var selector coreauth.Selector
switch nextStrategy {
case "fill-first":
selector = &coreauth.FillFirstSelector{}
default:
selector = &coreauth.RoundRobinSelector{}
}
if nextSessionAffinity {
ttl := time.Hour
if ttlStr := strings.TrimSpace(nextSessionAffinityTTL); ttlStr != "" {
if parsed, err := time.ParseDuration(ttlStr); err == nil && parsed > 0 {
ttl = parsed
}
}
selector = coreauth.NewSessionAffinitySelectorWithConfig(coreauth.SessionAffinityConfig{
Fallback: selector,
TTL: ttl,
})
}
s.coreManager.SetSelector(selector)
}
s.applyRetryConfig(newCfg)
s.applyPprofConfig(newCfg)
if s.server != nil {
s.server.UpdateClients(newCfg)
}
s.cfgMu.Lock()
s.cfg = newCfg
s.cfgMu.Unlock()
if s.coreManager != nil {
s.coreManager.SetConfig(newCfg)
s.coreManager.SetOAuthModelAlias(newCfg.OAuthModelAlias)
}
s.rebindExecutors()
log.Info("file watcher started for config and auth directory changes")
}
watcherWrapper, err = s.watcherFactory(s.configPath, s.cfg.AuthDir, reloadCallback)
if err != nil {
return fmt.Errorf("cliproxy: failed to create watcher: %w", err)
}
s.watcher = watcherWrapper
s.ensureAuthUpdateQueue(ctx)
if s.authUpdates != nil {
watcherWrapper.SetAuthUpdateQueue(s.authUpdates)
}
watcherWrapper.SetConfig(s.cfg)
watcherCtx, watcherCancel := context.WithCancel(context.Background())
s.watcherCancel = watcherCancel
if err = watcherWrapper.Start(watcherCtx); err != nil {
return fmt.Errorf("cliproxy: failed to start watcher: %w", err)
}
log.Info("file watcher started for config and auth directory changes")
// Prefer core auth manager auto refresh if available.
if s.coreManager != nil {
if s.coreManager != nil && !homeEnabled {
interval := 15 * time.Minute
s.coreManager.StartAutoRefresh(context.Background(), interval)
log.Infof("core auth auto-refresh started (interval=%s)", interval)
@@ -717,8 +908,8 @@ func (s *Service) Run(ctx context.Context) error {
case <-ctx.Done():
log.Debug("service context cancelled, shutting down...")
return ctx.Err()
case err = <-s.serverErr:
return err
case errServer := <-s.serverErr:
return errServer
}
}
@@ -741,6 +932,16 @@ func (s *Service) Shutdown(ctx context.Context) error {
ctx = context.Background()
}
if s.homeCancel != nil {
s.homeCancel()
s.homeCancel = nil
}
if s.homeClient != nil {
s.homeClient.Close()
s.homeClient = nil
}
home.ClearCurrent()
// legacy refresh loop removed; only stopping core auth manager below
if s.watcherCancel != nil {