@@ -479,7 +479,7 @@ func serveChatCompletions(w http.ResponseWriter, r *http.Request, options Option
479479 writeJSON (w , http .StatusBadRequest , map [string ]any {"detail" : err .Error ()})
480480 return
481481 }
482- candidates , err := listCandidates (r .Context (), options )
482+ candidates , err := listCandidatesForRequest (r .Context (), options , chatReq , r . Header )
483483 if err != nil {
484484 writeJSON (w , http .StatusInternalServerError , map [string ]any {"detail" : err .Error ()})
485485 return
@@ -763,7 +763,7 @@ func serveMessages(w http.ResponseWriter, r *http.Request, options Options) {
763763 }
764764 allowedTools := allowedAnthropicToolNames (body )
765765 chatReq := proxy.ChatRequest {Model : model , Stream : false , Raw : body }
766- candidates , err := listCandidates (r .Context (), options )
766+ candidates , err := listCandidatesForRequest (r .Context (), options , chatReq , r . Header )
767767 if err != nil {
768768 writeAnthropicError (w , http .StatusInternalServerError , err .Error (), "api_error" )
769769 return
@@ -960,15 +960,22 @@ func serveResponses(w http.ResponseWriter, r *http.Request, options Options) {
960960 writeOpenAIError (w , http .StatusBadRequest , "input must contain at least one message" , "invalid_request_error" )
961961 return
962962 }
963- candidates , err := listCandidates (r .Context (), options )
963+ chatReq := proxy.ChatRequest {Model : model , Stream : stream , Raw : body }
964+ // Preserve client prompt_cache_key on chatReq for sticky fingerprint (stripped later for upstream).
965+ if pck := strings .TrimSpace (stringValue (raw ["prompt_cache_key" ])); pck != "" {
966+ if chatReq .Raw == nil {
967+ chatReq .Raw = map [string ]any {}
968+ }
969+ chatReq .Raw ["prompt_cache_key" ] = pck
970+ }
971+ candidates , err := listCandidatesForRequest (r .Context (), options , chatReq , r .Header )
964972 if err != nil {
965973 writeOpenAIError (w , http .StatusInternalServerError , err .Error (), "server_error" )
966974 return
967975 }
968976 service := proxy.ChatService {Catalog : modelCatalog (options ), Client : upstreamClient (options ), PickObserver : options .PickObserver , AffinityStore : options .AffinityStore }
969977 started := time .Now ()
970978 responseID := responses .NewResponseID ()
971- chatReq := proxy.ChatRequest {Model : model , Stream : stream , Raw : body }
972979 respPolicy := historycompact .ResolveOutboundToolPolicy (
973980 "openai_responses" ,
974981 r .UserAgent (),
@@ -2433,6 +2440,25 @@ func upstreamClient(options Options) *grok.Client {
24332440 return & grok.Client {BaseURL : options .Config .UpstreamBase }
24342441}
24352442
2443+ func listCandidatesForRequest (ctx context.Context , options Options , chatReq proxy.ChatRequest , headers http.Header ) ([]pool.Candidate , error ) {
2444+ candidates , err := listCandidates (ctx , options )
2445+ if err != nil {
2446+ return nil , err
2447+ }
2448+ fp := proxy .ChatFingerprint (chatReq )
2449+ if fp == "" {
2450+ fp = proxy .ChatFingerprintFromHeaders (headers , chatReq .Model )
2451+ }
2452+ // If still empty, try raw prompt_cache_key alone.
2453+ if fp == "" && chatReq .Raw != nil {
2454+ if pck , _ := chatReq .Raw ["prompt_cache_key" ].(string ); strings .TrimSpace (pck ) != "" {
2455+ fp = "chat:" + strings .TrimSpace (chatReq .Model ) + ":prompt_cache_key:" + strings .TrimSpace (pck )
2456+ }
2457+ }
2458+ sticky := stickyAccountID (ctx , options , fp )
2459+ return ensureStickyCandidate (ctx , options , candidates , sticky ), nil
2460+ }
2461+
24362462func listCandidates (ctx context.Context , options Options ) ([]pool.Candidate , error ) {
24372463 if len (options .Candidates ) > 0 {
24382464 out := make ([]pool.Candidate , len (options .Candidates ))
@@ -2445,6 +2471,42 @@ func listCandidates(ctx context.Context, options Options) ([]pool.Candidate, err
24452471 return options .Store .ListPoolCandidates (ctx )
24462472}
24472473
2474+ // ensureStickyCandidate injects an affinity-bound account into the pick window
2475+ // even when it is outside the top-N least_used scan. Critical for prompt-cache
2476+ // multi-turn (Codex prompt_cache_key / X-Grok-Conv-Id) high hit rates.
2477+ func ensureStickyCandidate (ctx context.Context , options Options , candidates []pool.Candidate , stickyID string ) []pool.Candidate {
2478+ stickyID = strings .TrimSpace (stickyID )
2479+ if stickyID == "" || options .Store == nil {
2480+ return candidates
2481+ }
2482+ for _ , c := range candidates {
2483+ if c .ID == stickyID {
2484+ return candidates
2485+ }
2486+ }
2487+ extra , err := options .Store .GetPoolCandidate (ctx , stickyID )
2488+ if err != nil || extra == nil {
2489+ return candidates
2490+ }
2491+ // Put sticky first so prepareChain affinity boost is redundant but safe.
2492+ out := make ([]pool.Candidate , 0 , len (candidates )+ 1 )
2493+ out = append (out , * extra )
2494+ out = append (out , candidates ... )
2495+ return out
2496+ }
2497+
2498+ func stickyAccountID (ctx context.Context , options Options , fingerprint string ) string {
2499+ fingerprint = strings .TrimSpace (fingerprint )
2500+ if fingerprint == "" || options .AffinityStore == nil {
2501+ return ""
2502+ }
2503+ id , err := options .AffinityStore .GetAffinity (ctx , fingerprint )
2504+ if err != nil {
2505+ return ""
2506+ }
2507+ return strings .TrimSpace (id )
2508+ }
2509+
24482510func modelCatalog (options Options ) * models.Catalog {
24492511 if options .Models != nil {
24502512 return options .Models
0 commit comments