Pick selects an auth with session affinity when possible. Priority for session ID extraction: 1. metadata.user_id (Claude Code format with _session_{uuid}) - highest priority 2. X-Session-ID header 3. Session_id header (Codex) 4. X-Client-Request-Id header (PI) 5. metadata.user_id (non-Claude Code f
(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, auths []*Auth)
| 415 | // a session uses multiple models (e.g., gemini-2.5-pro and gemini-3-flash-preview) |
| 416 | // that may be supported by different auth credentials, and to avoid cross-provider conflicts. |
| 417 | func (s *SessionAffinitySelector) Pick(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, auths []*Auth) (*Auth, error) { |
| 418 | entry := selectorLogEntry(ctx) |
| 419 | primaryID, fallbackID := extractSessionIDs(opts.Headers, opts.OriginalRequest, opts.Metadata) |
| 420 | if primaryID == "" { |
| 421 | entry.Debugf("session-affinity: no session ID extracted, falling back to default selector | provider=%s model=%s", provider, model) |
| 422 | return s.fallback.Pick(ctx, provider, model, opts, auths) |
| 423 | } |
| 424 | |
| 425 | now := time.Now() |
| 426 | available, err := getAvailableAuths(auths, provider, model, now) |
| 427 | if err != nil { |
| 428 | return nil, err |
| 429 | } |
| 430 | |
| 431 | cacheKey := provider + "::" + primaryID + "::" + model |
| 432 | |
| 433 | if cachedAuthID, ok := s.cache.GetAndRefresh(cacheKey); ok { |
| 434 | for _, auth := range available { |
| 435 | if auth.ID == cachedAuthID { |
| 436 | entry.Infof("session-affinity: cache hit | session=%s auth=%s provider=%s model=%s", truncateSessionID(primaryID), auth.ID, provider, model) |
| 437 | return auth, nil |
| 438 | } |
| 439 | } |
| 440 | // Cached auth not available, reselect via fallback selector for even distribution |
| 441 | auth, err := s.fallback.Pick(ctx, provider, model, opts, auths) |
| 442 | if err != nil { |
| 443 | return nil, err |
| 444 | } |
| 445 | s.cache.Set(cacheKey, auth.ID) |
| 446 | entry.Infof("session-affinity: cache hit but auth unavailable, reselected | session=%s auth=%s provider=%s model=%s", truncateSessionID(primaryID), auth.ID, provider, model) |
| 447 | return auth, nil |
| 448 | } |
| 449 | |
| 450 | if fallbackID != "" && fallbackID != primaryID { |
| 451 | fallbackKey := provider + "::" + fallbackID + "::" + model |
| 452 | if cachedAuthID, ok := s.cache.Get(fallbackKey); ok { |
| 453 | for _, auth := range available { |
| 454 | if auth.ID == cachedAuthID { |
| 455 | s.cache.Set(cacheKey, auth.ID) |
| 456 | entry.Infof("session-affinity: fallback cache hit | session=%s fallback=%s auth=%s provider=%s model=%s", truncateSessionID(primaryID), truncateSessionID(fallbackID), auth.ID, provider, model) |
| 457 | return auth, nil |
| 458 | } |
| 459 | } |
| 460 | } |
| 461 | } |
| 462 | |
| 463 | auth, err := s.fallback.Pick(ctx, provider, model, opts, auths) |
| 464 | if err != nil { |
| 465 | return nil, err |
| 466 | } |
| 467 | s.cache.Set(cacheKey, auth.ID) |
| 468 | entry.Infof("session-affinity: cache miss, new binding | session=%s auth=%s provider=%s model=%s", truncateSessionID(primaryID), auth.ID, provider, model) |
| 469 | return auth, nil |
| 470 | } |
| 471 | |
| 472 | func selectorLogEntry(ctx context.Context) *log.Entry { |
| 473 | if ctx == nil { |