fix(auth): scope affinity by provider

Keep sticky auth affinity limited to matching providers and stop persisting execution-session IDs as long-lived affinity keys so provider switching and normal streaming traffic do not create incorrect pins or stale affinity state.
This commit is contained in:
VooDisss
2026-03-27 18:52:58 +02:00
parent 4c4cbd44da
commit 6962e09dd9
3 changed files with 50 additions and 16 deletions

View File

@@ -210,7 +210,6 @@ func requestExecutionMetadata(ctx context.Context) map[string]any {
} }
if executionSessionID := executionSessionIDFromContext(ctx); executionSessionID != "" { if executionSessionID := executionSessionIDFromContext(ctx); executionSessionID != "" {
meta[coreexecutor.ExecutionSessionMetadataKey] = executionSessionID meta[coreexecutor.ExecutionSessionMetadataKey] = executionSessionID
meta[authAffinityMetadataKey] = executionSessionID
} else if ctx != nil { } else if ctx != nil {
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil { if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil {
if apiKey, exists := ginCtx.Get("apiKey"); exists && apiKey != nil { if apiKey, exists := ginCtx.Get("apiKey"); exists && apiKey != nil {

View File

@@ -2246,8 +2246,17 @@ func authAffinityKeyFromMetadata(meta map[string]any) string {
} }
} }
func (m *Manager) AuthAffinity(key string) string { func scopedAuthAffinityKey(provider, key string) string {
provider = strings.TrimSpace(strings.ToLower(provider))
key = strings.TrimSpace(key) key = strings.TrimSpace(key)
if provider == "" || key == "" {
return ""
}
return provider + "|" + key
}
func (m *Manager) AuthAffinity(provider, key string) string {
key = scopedAuthAffinityKey(provider, key)
if m == nil || key == "" { if m == nil || key == "" {
return "" return ""
} }
@@ -2256,12 +2265,12 @@ func (m *Manager) AuthAffinity(key string) string {
return strings.TrimSpace(m.affinity[key]) return strings.TrimSpace(m.affinity[key])
} }
func (m *Manager) applyAuthAffinity(opts *cliproxyexecutor.Options) { func (m *Manager) applyAuthAffinity(provider string, opts *cliproxyexecutor.Options) {
if m == nil || opts == nil || pinnedAuthIDFromMetadata(opts.Metadata) != "" { if m == nil || opts == nil || pinnedAuthIDFromMetadata(opts.Metadata) != "" {
return return
} }
if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" {
if affinityAuthID := m.AuthAffinity(affinityKey); affinityAuthID != "" { if affinityAuthID := m.AuthAffinity(provider, affinityKey); affinityAuthID != "" {
if opts.Metadata == nil { if opts.Metadata == nil {
opts.Metadata = make(map[string]any) opts.Metadata = make(map[string]any)
} }
@@ -2275,15 +2284,15 @@ func (m *Manager) persistAuthAffinity(entry *log.Entry, opts cliproxyexecutor.Op
return return
} }
if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" { if affinityKey := authAffinityKeyFromMetadata(opts.Metadata); affinityKey != "" {
m.SetAuthAffinity(affinityKey, authID) m.SetAuthAffinity(provider, affinityKey, authID)
if entry != nil && log.IsLevelEnabled(log.DebugLevel) { if entry != nil && log.IsLevelEnabled(log.DebugLevel) {
entry.Debugf("auth affinity pinned auth_id=%s provider=%s model=%s", authID, provider, model) entry.Debugf("auth affinity pinned auth_id=%s provider=%s model=%s", authID, provider, model)
} }
} }
} }
func (m *Manager) SetAuthAffinity(key, authID string) { func (m *Manager) SetAuthAffinity(provider, key, authID string) {
key = strings.TrimSpace(key) key = scopedAuthAffinityKey(provider, key)
authID = strings.TrimSpace(authID) authID = strings.TrimSpace(authID)
if m == nil || key == "" || authID == "" { if m == nil || key == "" || authID == "" {
return return
@@ -2296,8 +2305,8 @@ func (m *Manager) SetAuthAffinity(key, authID string) {
m.affinityMu.Unlock() m.affinityMu.Unlock()
} }
func (m *Manager) ClearAuthAffinity(key string) { func (m *Manager) ClearAuthAffinity(provider, key string) {
key = strings.TrimSpace(key) key = scopedAuthAffinityKey(provider, key)
if m == nil || key == "" { if m == nil || key == "" {
return return
} }
@@ -2389,7 +2398,7 @@ func (m *Manager) pickNextLegacy(ctx context.Context, provider, model string, op
} }
func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, error) { func (m *Manager) pickNext(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, error) {
m.applyAuthAffinity(&opts) m.applyAuthAffinity(provider, &opts)
if !m.useSchedulerFastPath() { if !m.useSchedulerFastPath() {
return m.pickNextLegacy(ctx, provider, model, opts, tried) return m.pickNextLegacy(ctx, provider, model, opts, tried)
} }
@@ -2504,7 +2513,18 @@ func (m *Manager) pickNextMixedLegacy(ctx context.Context, providers []string, m
} }
func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) { func (m *Manager) pickNextMixed(ctx context.Context, providers []string, model string, opts cliproxyexecutor.Options, tried map[string]struct{}) (*Auth, ProviderExecutor, string, error) {
m.applyAuthAffinity(&opts) if pinnedAuthIDFromMetadata(opts.Metadata) == "" {
for _, provider := range providers {
providerKey := strings.TrimSpace(strings.ToLower(provider))
if providerKey == "" {
continue
}
m.applyAuthAffinity(providerKey, &opts)
if pinnedAuthIDFromMetadata(opts.Metadata) != "" {
break
}
}
}
if !m.useSchedulerFastPath() { if !m.useSchedulerFastPath() {
return m.pickNextMixedLegacy(ctx, providers, model, opts, tried) return m.pickNextMixedLegacy(ctx, providers, model, opts, tried)
} }

View File

@@ -52,7 +52,7 @@ func TestManagerPickNextMixedUsesAuthAffinity(t *testing.T) {
t.Fatalf("Register(codex-b) error = %v", errRegister) t.Fatalf("Register(codex-b) error = %v", errRegister)
} }
manager.SetAuthAffinity("idem-1", "codex-b") manager.SetAuthAffinity("codex", "idem-1", "codex-b")
opts := cliproxyexecutor.Options{Metadata: map[string]any{"auth_affinity_key": "idem-1"}} opts := cliproxyexecutor.Options{Metadata: map[string]any{"auth_affinity_key": "idem-1"}}
got, _, provider, errPick := manager.pickNextMixed(context.Background(), []string{"codex"}, "gpt-5.4", opts, map[string]struct{}{}) got, _, provider, errPick := manager.pickNextMixed(context.Background(), []string{"codex"}, "gpt-5.4", opts, map[string]struct{}{})
@@ -74,12 +74,27 @@ func TestManagerAuthAffinityRoundTrip(t *testing.T) {
t.Parallel() t.Parallel()
manager := NewManager(nil, nil, nil) manager := NewManager(nil, nil, nil)
manager.SetAuthAffinity("idem-2", "auth-1") manager.SetAuthAffinity("codex", "idem-2", "auth-1")
if got := manager.AuthAffinity("idem-2"); got != "auth-1" { if got := manager.AuthAffinity("codex", "idem-2"); got != "auth-1" {
t.Fatalf("AuthAffinity = %q, want %q", got, "auth-1") t.Fatalf("AuthAffinity = %q, want %q", got, "auth-1")
} }
manager.ClearAuthAffinity("idem-2") manager.ClearAuthAffinity("codex", "idem-2")
if got := manager.AuthAffinity("idem-2"); got != "" { if got := manager.AuthAffinity("codex", "idem-2"); got != "" {
t.Fatalf("AuthAffinity after clear = %q, want empty", got) t.Fatalf("AuthAffinity after clear = %q, want empty", got)
} }
} }
func TestManagerAuthAffinityScopedByProvider(t *testing.T) {
t.Parallel()
manager := NewManager(nil, nil, nil)
manager.SetAuthAffinity("codex", "shared-key", "codex-auth")
manager.SetAuthAffinity("gemini", "shared-key", "gemini-auth")
if got := manager.AuthAffinity("codex", "shared-key"); got != "codex-auth" {
t.Fatalf("codex affinity = %q, want %q", got, "codex-auth")
}
if got := manager.AuthAffinity("gemini", "shared-key"); got != "gemini-auth" {
t.Fatalf("gemini affinity = %q, want %q", got, "gemini-auth")
}
}