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:
+325
-124
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user