feat(runtime): improve executor binding logic and add config-based executor handling

- Added `UsesConfig` to `XAIAutoExecutor` for determining config-based binding.
- Refactored `Service` to support thread-safe executor registration, using a new mutex (`executorRegistrationMu`).
- Improved executor rebind logic to prevent unnecessary replacements unless forced or required by config updates.
- Updated handling of config updates to correctly replace stale XAI executors.
- Enhanced the test suite for edge cases in executor binding and replacement for multiple providers.

Closes: #4567
This commit is contained in:
Luis Pater
2026-07-26 00:16:48 +08:00
parent 95d5b2485f
commit 27fc3169bb
4 changed files with 206 additions and 30 deletions

View File

@@ -1594,6 +1594,11 @@ func NewXAIAutoExecutor(cfg *config.Config) *XAIAutoExecutor {
func (e *XAIAutoExecutor) Identifier() string { return "xai" }
// UsesConfig reports whether the executor was created for cfg.
func (e *XAIAutoExecutor) UsesConfig(cfg *config.Config) bool {
return e != nil && e.httpExec != nil && e.httpExec.cfg == cfg
}
func (e *XAIAutoExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error {
if e == nil || e.httpExec == nil {
return nil

View File

@@ -53,9 +53,10 @@ type Service struct {
configUpdateMu sync.Mutex
// configRuntimeMu orders side-effecting runtime application after config commits.
configRuntimeMu sync.Mutex
configSequence uint64
appliedRoutingState *routingRuntimeState
configRuntimeMu sync.Mutex
executorRegistrationMu sync.Mutex
configSequence uint64
appliedRoutingState *routingRuntimeState
// configPath is the path to the configuration file.
configPath string
@@ -348,10 +349,13 @@ func (s *Service) syncPluginModelRuntime(ctx context.Context) {
if ctx.Err() != nil {
return
}
s.cfgMu.RLock()
homeEnabled := s.cfg != nil && s.cfg.Home.Enabled
s.cfgMu.RUnlock()
s.registerAvailableExecutors(ctx, executorRegistrationOptions{
includeBaseline: s.cfg != nil && s.cfg.Home.Enabled,
includeBaseline: homeEnabled,
includePlugins: true,
forceReplaceAuths: true,
forceReplaceAuths: false,
auths: s.coreManager.List(),
})
s.refreshPluginModelRegistrations(ctx)
@@ -1036,7 +1040,7 @@ func (c *openAICompatibilityRegistrationCache) lookup(compatName string) (*openA
return entry, ok
}
func (s *Service) hasNativeOpenAICompatExecutorConfig(a *coreauth.Auth, providerKey string) bool {
func (s *Service) hasNativeOpenAICompatExecutorConfig(a *coreauth.Auth, providerKey string, cfg *config.Config) bool {
if a == nil {
return false
}
@@ -1052,7 +1056,7 @@ func (s *Service) hasNativeOpenAICompatExecutorConfig(a *coreauth.Auth, provider
if strings.EqualFold(strings.TrimSpace(a.Provider), "openai-compatibility") {
return true
}
if s == nil || s.cfg == nil {
if s == nil || cfg == nil {
return false
}
@@ -1069,8 +1073,8 @@ func (s *Service) hasNativeOpenAICompatExecutorConfig(a *coreauth.Auth, provider
candidates = append(candidates, strings.ToLower(provider))
}
for i := range s.cfg.OpenAICompatibility {
compat := &s.cfg.OpenAICompatibility[i]
for i := range cfg.OpenAICompatibility {
compat := &cfg.OpenAICompatibility[i]
if compat.Disabled {
continue
}
@@ -1130,10 +1134,15 @@ func (s *Service) registerAvailableExecutors(ctx context.Context, opts executorR
if ctx == nil {
ctx = context.Background()
}
s.executorRegistrationMu.Lock()
defer s.executorRegistrationMu.Unlock()
if ctx.Err() != nil {
return
}
// Keep all Service-owned executor registration paths here so native, Home,
// auth-derived, and plugin executors stay in the same binding order.
if opts.includeBaseline {
s.registerExecutorsForAuths(baselineExecutorAuths(), true)
s.registerExecutorsForAuths(baselineExecutorAuths(), opts.forceReplaceAuths)
}
if len(opts.auths) > 0 {
s.registerExecutorsForAuths(opts.auths, opts.forceReplaceAuths)
@@ -1187,6 +1196,9 @@ func (s *Service) registerExecutorForAuth(a *coreauth.Auth, forceReplace bool) {
if s == nil || s.coreManager == nil || a == nil {
return
}
s.cfgMu.RLock()
cfg := s.cfg
s.cfgMu.RUnlock()
if strings.EqualFold(strings.TrimSpace(a.Provider), "codex") {
if !forceReplace {
existingExecutor, hasExecutor := s.coreManager.Executor("codex")
@@ -1197,7 +1209,7 @@ func (s *Service) registerExecutorForAuth(a *coreauth.Auth, forceReplace bool) {
}
}
}
s.coreManager.RegisterExecutor(executor.NewCodexAutoExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewCodexAutoExecutor(cfg))
return
}
// Skip disabled auth entries when (re)binding executors.
@@ -1220,29 +1232,38 @@ func (s *Service) registerExecutorForAuth(a *coreauth.Auth, forceReplace bool) {
}
}
}
s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor(compatProviderKey, s.cfg))
s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor(compatProviderKey, cfg))
return
}
switch strings.ToLower(a.Provider) {
case constant.Gemini:
s.coreManager.RegisterExecutor(executor.NewGeminiExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewGeminiExecutor(cfg))
case constant.GeminiInteractions:
s.coreManager.RegisterExecutor(executor.NewGeminiInteractionsExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewGeminiInteractionsExecutor(cfg))
case "vertex":
s.coreManager.RegisterExecutor(executor.NewGeminiVertexExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewGeminiVertexExecutor(cfg))
case "aistudio":
if s.wsGateway != nil {
s.coreManager.RegisterExecutor(executor.NewAIStudioExecutor(s.cfg, a.ID, s.wsGateway))
s.coreManager.RegisterExecutor(executor.NewAIStudioExecutor(cfg, a.ID, s.wsGateway))
}
return
case "antigravity":
s.coreManager.RegisterExecutor(executor.NewAntigravityExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewAntigravityExecutor(cfg))
case "claude":
s.coreManager.RegisterExecutor(executor.NewClaudeExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewClaudeExecutor(cfg))
case "kimi":
s.coreManager.RegisterExecutor(executor.NewKimiExecutor(s.cfg))
s.coreManager.RegisterExecutor(executor.NewKimiExecutor(cfg))
case "xai":
s.coreManager.RegisterExecutor(executor.NewXAIAutoExecutor(s.cfg))
if !forceReplace {
existingExecutor, hasExecutor := s.coreManager.Executor("xai")
if hasExecutor {
existingXAIAutoExecutor, isXAIAutoExecutor := existingExecutor.(*executor.XAIAutoExecutor)
if isXAIAutoExecutor && existingXAIAutoExecutor.UsesConfig(cfg) {
return
}
}
}
s.coreManager.RegisterExecutor(executor.NewXAIAutoExecutor(cfg))
default:
providerKey := strings.ToLower(strings.TrimSpace(a.Provider))
if providerKey == "" {
@@ -1250,7 +1271,7 @@ func (s *Service) registerExecutorForAuth(a *coreauth.Auth, forceReplace bool) {
}
if s.pluginHost != nil &&
s.pluginHost.HasExecutorCandidateProvider(providerKey) &&
!s.hasNativeOpenAICompatExecutorConfig(a, providerKey) {
!s.hasNativeOpenAICompatExecutorConfig(a, providerKey, cfg) {
s.unregisterOpenAICompatExecutor(providerKey)
return
}
@@ -1261,7 +1282,7 @@ func (s *Service) registerExecutorForAuth(a *coreauth.Auth, forceReplace bool) {
}
}
}
s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor(providerKey, s.cfg))
s.coreManager.RegisterExecutor(executor.NewOpenAICompatExecutor(providerKey, cfg))
}
}

View File

@@ -1,11 +1,16 @@
package cliproxy
import (
"context"
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/pluginhost"
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor"
"github.com/router-for-me/CLIProxyAPI/v7/internal/watcher"
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/config"
sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator"
)
func TestEnsureExecutorsForAuth_CodexDoesNotReplaceInNormalMode(t *testing.T) {
@@ -64,7 +69,77 @@ func TestEnsureExecutorsForAuthWithMode_CodexForceReplace(t *testing.T) {
}
}
func TestEnsureExecutorsForAuth_XAIBindsAutoExecutor(t *testing.T) {
func TestSyncPluginModelRuntime_UnrelatedAuthDoesNotReplaceWebsocketExecutor(t *testing.T) {
testCases := []struct {
name string
provider string
homeEnabled bool
}{
{name: "codex standard mode", provider: "codex"},
{name: "codex home mode", provider: "codex", homeEnabled: true},
{name: "xai standard mode", provider: "xai"},
{name: "xai home mode", provider: "xai", homeEnabled: true},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
cfg := &config.Config{}
cfg.Home.Enabled = tt.homeEnabled
service := &Service{
cfg: cfg,
coreManager: coreauth.NewManager(nil, nil, nil),
pluginHost: pluginhost.New(),
}
providerAuth := &coreauth.Auth{
ID: tt.provider + "-auth",
Provider: tt.provider,
Status: coreauth.StatusActive,
}
unrelatedAuth := &coreauth.Auth{
ID: "unrelated-auth",
Provider: "claude",
Status: coreauth.StatusActive,
}
t.Cleanup(func() {
GlobalModelRegistry().UnregisterClient(providerAuth.ID)
GlobalModelRegistry().UnregisterClient(unrelatedAuth.ID)
sdkAuth.RegisterPluginAuthParser(nil)
sdktranslator.SetPluginHooks(nil)
})
if _, errRegister := service.coreManager.Register(ctx, providerAuth); errRegister != nil {
t.Fatalf("register %s auth: %v", tt.provider, errRegister)
}
if _, errRegister := service.coreManager.Register(ctx, unrelatedAuth); errRegister != nil {
t.Fatalf("register unrelated auth: %v", errRegister)
}
service.ensureExecutorsForAuth(providerAuth)
firstExecutor, okFirst := service.coreManager.Executor(tt.provider)
if !okFirst || firstExecutor == nil {
t.Fatalf("expected %s executor before plugin model sync", tt.provider)
}
updatedAuth := unrelatedAuth.Clone()
updatedAuth.Label = "updated unrelated auth"
service.handleAuthUpdate(ctx, watcher.AuthUpdate{
Action: watcher.AuthUpdateActionModify,
ID: updatedAuth.ID,
Auth: updatedAuth,
})
secondExecutor, okSecond := service.coreManager.Executor(tt.provider)
if !okSecond || secondExecutor == nil {
t.Fatalf("expected %s executor after plugin model sync", tt.provider)
}
if firstExecutor != secondExecutor {
t.Fatalf("expected unrelated auth sync to preserve the %s executor", tt.provider)
}
})
}
}
func TestEnsureExecutorsForAuth_XAIDoesNotReplaceInNormalMode(t *testing.T) {
service := &Service{
cfg: &config.Config{},
coreManager: coreauth.NewManager(nil, nil, nil),
@@ -76,12 +151,87 @@ func TestEnsureExecutorsForAuth_XAIBindsAutoExecutor(t *testing.T) {
}
service.ensureExecutorsForAuth(auth)
gotExecutor, ok := service.coreManager.Executor("xai")
if !ok || gotExecutor == nil {
t.Fatal("expected xai executor after bind")
firstExecutor, okFirst := service.coreManager.Executor("xai")
if !okFirst || firstExecutor == nil {
t.Fatal("expected xai executor after first bind")
}
if _, ok := gotExecutor.(*executor.XAIAutoExecutor); !ok {
t.Fatalf("xai executor type = %T, want *executor.XAIAutoExecutor", gotExecutor)
if _, isXAIAutoExecutor := firstExecutor.(*executor.XAIAutoExecutor); !isXAIAutoExecutor {
t.Fatalf("xai executor type = %T, want *executor.XAIAutoExecutor", firstExecutor)
}
service.ensureExecutorsForAuth(auth)
secondExecutor, okSecond := service.coreManager.Executor("xai")
if !okSecond || secondExecutor == nil {
t.Fatal("expected xai executor after second bind")
}
if firstExecutor != secondExecutor {
t.Fatal("expected xai executor to stay unchanged in normal mode")
}
}
func TestEnsureExecutorsForAuthWithMode_XAIForceReplace(t *testing.T) {
service := &Service{
cfg: &config.Config{},
coreManager: coreauth.NewManager(nil, nil, nil),
}
auth := &coreauth.Auth{
ID: "xai-auth-2",
Provider: "xai",
Status: coreauth.StatusActive,
}
service.ensureExecutorsForAuth(auth)
firstExecutor, okFirst := service.coreManager.Executor("xai")
if !okFirst || firstExecutor == nil {
t.Fatal("expected xai executor after first bind")
}
service.ensureExecutorsForAuthWithMode(auth, true)
secondExecutor, okSecond := service.coreManager.Executor("xai")
if !okSecond || secondExecutor == nil {
t.Fatal("expected xai executor after forced rebind")
}
if firstExecutor == secondExecutor {
t.Fatal("expected xai executor replacement in force mode")
}
if _, isXAIAutoExecutor := secondExecutor.(*executor.XAIAutoExecutor); !isXAIAutoExecutor {
t.Fatalf("xai executor type = %T, want *executor.XAIAutoExecutor", secondExecutor)
}
}
func TestEnsureExecutorsForAuth_XAIReplacesExecutorAfterConfigUpdate(t *testing.T) {
service := &Service{
cfg: &config.Config{},
coreManager: coreauth.NewManager(nil, nil, nil),
pluginHost: pluginhost.New(),
}
t.Cleanup(func() {
sdkAuth.RegisterPluginAuthParser(nil)
sdktranslator.SetPluginHooks(nil)
})
auth := &coreauth.Auth{
ID: "xai-auth-config-update",
Provider: "xai",
Status: coreauth.StatusActive,
}
service.ensureExecutorsForAuth(auth)
firstExecutor, okFirst := service.coreManager.Executor("xai")
if !okFirst || firstExecutor == nil {
t.Fatal("expected xai executor before config update")
}
service.applyWatcherConfigUpdate(&config.Config{})
service.ensureExecutorsForAuth(auth)
secondExecutor, okSecond := service.coreManager.Executor("xai")
if !okSecond || secondExecutor == nil {
t.Fatal("expected xai executor after config update")
}
if firstExecutor == secondExecutor {
t.Fatal("expected stale xai executor replacement after config update")
}
if _, isXAIAutoExecutor := secondExecutor.(*executor.XAIAutoExecutor); !isXAIAutoExecutor {
t.Fatalf("xai executor type = %T, want *executor.XAIAutoExecutor", secondExecutor)
}
}

View File

@@ -50,7 +50,7 @@ func TestHasNativeOpenAICompatExecutorConfig(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := service.hasNativeOpenAICompatExecutorConfig(tt.auth, tt.providerKey)
got := service.hasNativeOpenAICompatExecutorConfig(tt.auth, tt.providerKey, service.cfg)
if got != tt.want {
t.Fatalf("hasNativeOpenAICompatExecutorConfig() = %v, want %v", got, tt.want)
}