@@ -116,6 +116,19 @@ func (h *OpenAIResponsesAPIHandler) ResponsesWebsocket(c *gin.Context) {
116116 allowIncrementalInputWithPreviousResponseID = h .websocketUpstreamSupportsIncrementalInputForModel (requestModelName )
117117 }
118118
119+ allowCompactionReplayBypass := false
120+ if pinnedAuthID != "" && h != nil && h .AuthManager != nil {
121+ if pinnedAuth , ok := h .AuthManager .GetByID (pinnedAuthID ); ok && pinnedAuth != nil {
122+ allowCompactionReplayBypass = responsesWebsocketAuthSupportsCompactionReplay (pinnedAuth )
123+ }
124+ } else {
125+ requestModelName := strings .TrimSpace (gjson .GetBytes (payload , "model" ).String ())
126+ if requestModelName == "" {
127+ requestModelName = strings .TrimSpace (gjson .GetBytes (lastRequest , "model" ).String ())
128+ }
129+ allowCompactionReplayBypass = h .websocketUpstreamSupportsCompactionReplayForModel (requestModelName )
130+ }
131+
119132 var requestJSON []byte
120133 var updatedLastRequest []byte
121134 var errMsg * interfaces.ErrorMessage
@@ -124,6 +137,7 @@ func (h *OpenAIResponsesAPIHandler) ResponsesWebsocket(c *gin.Context) {
124137 lastRequest ,
125138 lastResponseOutput ,
126139 allowIncrementalInputWithPreviousResponseID ,
140+ allowCompactionReplayBypass ,
127141 )
128142 if errMsg != nil {
129143 h .LoggingAPIResponseError (context .WithValue (context .Background (), "gin" , c ), errMsg )
@@ -222,21 +236,21 @@ func websocketUpgradeHeaders(req *http.Request) http.Header {
222236}
223237
224238func normalizeResponsesWebsocketRequest (rawJSON []byte , lastRequest []byte , lastResponseOutput []byte ) ([]byte , []byte , * interfaces.ErrorMessage ) {
225- return normalizeResponsesWebsocketRequestWithMode (rawJSON , lastRequest , lastResponseOutput , true )
239+ return normalizeResponsesWebsocketRequestWithMode (rawJSON , lastRequest , lastResponseOutput , true , true )
226240}
227241
228- func normalizeResponsesWebsocketRequestWithMode (rawJSON []byte , lastRequest []byte , lastResponseOutput []byte , allowIncrementalInputWithPreviousResponseID bool ) ([]byte , []byte , * interfaces.ErrorMessage ) {
242+ func normalizeResponsesWebsocketRequestWithMode (rawJSON []byte , lastRequest []byte , lastResponseOutput []byte , allowIncrementalInputWithPreviousResponseID bool , allowCompactionReplayBypass bool ) ([]byte , []byte , * interfaces.ErrorMessage ) {
229243 requestType := strings .TrimSpace (gjson .GetBytes (rawJSON , "type" ).String ())
230244 switch requestType {
231245 case wsRequestTypeCreate :
232246 // log.Infof("responses websocket: response.create request")
233247 if len (lastRequest ) == 0 {
234248 return normalizeResponseCreateRequest (rawJSON )
235249 }
236- return normalizeResponseSubsequentRequest (rawJSON , lastRequest , lastResponseOutput , allowIncrementalInputWithPreviousResponseID )
250+ return normalizeResponseSubsequentRequest (rawJSON , lastRequest , lastResponseOutput , allowIncrementalInputWithPreviousResponseID , allowCompactionReplayBypass )
237251 case wsRequestTypeAppend :
238252 // log.Infof("responses websocket: response.append request")
239- return normalizeResponseSubsequentRequest (rawJSON , lastRequest , lastResponseOutput , allowIncrementalInputWithPreviousResponseID )
253+ return normalizeResponseSubsequentRequest (rawJSON , lastRequest , lastResponseOutput , allowIncrementalInputWithPreviousResponseID , allowCompactionReplayBypass )
240254 default :
241255 return nil , lastRequest , & interfaces.ErrorMessage {
242256 StatusCode : http .StatusBadRequest ,
@@ -265,7 +279,7 @@ func normalizeResponseCreateRequest(rawJSON []byte) ([]byte, []byte, *interfaces
265279 return normalized , bytes .Clone (normalized ), nil
266280}
267281
268- func normalizeResponseSubsequentRequest (rawJSON []byte , lastRequest []byte , lastResponseOutput []byte , allowIncrementalInputWithPreviousResponseID bool ) ([]byte , []byte , * interfaces.ErrorMessage ) {
282+ func normalizeResponseSubsequentRequest (rawJSON []byte , lastRequest []byte , lastResponseOutput []byte , allowIncrementalInputWithPreviousResponseID bool , allowCompactionReplayBypass bool ) ([]byte , []byte , * interfaces.ErrorMessage ) {
269283 if len (lastRequest ) == 0 {
270284 return nil , lastRequest , & interfaces.ErrorMessage {
271285 StatusCode : http .StatusBadRequest ,
@@ -315,16 +329,21 @@ func normalizeResponseSubsequentRequest(rawJSON []byte, lastRequest []byte, last
315329 }
316330 }
317331
318- // When the client sends a full conversation transcript (e.g. after compact),
319- // the input already contains the complete history including assistant messages.
320- // In that case, skip merging with stale lastRequest/lastResponseOutput to avoid
321- // breaking function_call / function_call_output pairings.
332+ // When the client sends a compact replay for a downstream that can consume it
333+ // directly, the input already carries the canonical history. In that case,
334+ // skip merging with stale lastRequest/lastResponseOutput to avoid breaking
335+ // function_call / function_call_output pairings.
322336 // See: https://github.com/router-for-me/CLIProxyAPI/issues/2207
323337 var mergedInput string
324- if inputContainsFullTranscript (nextInput ) {
338+ if allowCompactionReplayBypass && inputContainsFullTranscript (nextInput ) {
325339 log .Infof ("responses websocket: full transcript detected, skipping stale merge (input items=%d)" , len (nextInput .Array ()))
326340 mergedInput = nextInput .Raw
327341 } else {
342+ appendInputRaw := nextInput .Raw
343+ if inputContainsFullTranscript (nextInput ) {
344+ appendInputRaw = inputWithoutCompactionItems (nextInput )
345+ }
346+
328347 existingInput := gjson .GetBytes (lastRequest , "input" )
329348 var errMerge error
330349 mergedInput , errMerge = mergeJSONArrayRaw (existingInput .Raw , normalizeJSONArrayRaw (lastResponseOutput ))
@@ -335,7 +354,7 @@ func normalizeResponseSubsequentRequest(rawJSON []byte, lastRequest []byte, last
335354 }
336355 }
337356
338- mergedInput , errMerge = mergeJSONArrayRaw (mergedInput , nextInput . Raw )
357+ mergedInput , errMerge = mergeJSONArrayRaw (mergedInput , appendInputRaw )
339358 if errMerge != nil {
340359 return nil , lastRequest , & interfaces.ErrorMessage {
341360 StatusCode : http .StatusBadRequest ,
@@ -492,72 +511,104 @@ func websocketUpstreamSupportsIncrementalInput(attributes map[string]string, met
492511}
493512
494513func (h * OpenAIResponsesAPIHandler ) websocketUpstreamSupportsIncrementalInputForModel (modelName string ) bool {
495- if h == nil || h .AuthManager == nil {
514+ auths , _ := h .responsesWebsocketAvailableAuthsForModel (modelName )
515+ for _ , auth := range auths {
516+ if websocketUpstreamSupportsIncrementalInput (auth .Attributes , auth .Metadata ) {
517+ return true
518+ }
519+ }
520+ return false
521+ }
522+
523+ func (h * OpenAIResponsesAPIHandler ) websocketUpstreamSupportsCompactionReplayForModel (modelName string ) bool {
524+ auths , _ := h .responsesWebsocketAvailableAuthsForModel (modelName )
525+ if len (auths ) == 0 {
496526 return false
497527 }
528+ for _ , auth := range auths {
529+ if ! responsesWebsocketAuthSupportsCompactionReplay (auth ) {
530+ return false
531+ }
532+ }
533+ return true
534+ }
498535
499- resolvedModelName := modelName
536+ func (h * OpenAIResponsesAPIHandler ) responsesWebsocketAvailableAuthsForModel (modelName string ) ([]* coreauth.Auth , string ) {
537+ if h == nil || h .AuthManager == nil {
538+ return nil , ""
539+ }
540+ resolvedModelName := responsesWebsocketResolvedModelName (modelName )
541+ providerSet , modelKey := responsesWebsocketProviderSetForModel (resolvedModelName )
542+ if len (providerSet ) == 0 {
543+ return nil , modelKey
544+ }
545+
546+ registryRef := registry .GetGlobalRegistry ()
547+ now := time .Now ()
548+ auths := h .AuthManager .List ()
549+ available := make ([]* coreauth.Auth , 0 , len (auths ))
550+ for _ , auth := range auths {
551+ if ! responsesWebsocketAuthMatchesModel (auth , providerSet , modelKey , registryRef , now ) {
552+ continue
553+ }
554+ available = append (available , auth )
555+ }
556+ return available , modelKey
557+ }
558+
559+ func responsesWebsocketResolvedModelName (modelName string ) string {
500560 initialSuffix := thinking .ParseSuffix (modelName )
501561 if initialSuffix .ModelName == "auto" {
502562 resolvedBase := util .ResolveAutoModel (initialSuffix .ModelName )
503563 if initialSuffix .HasSuffix {
504- resolvedModelName = fmt .Sprintf ("%s(%s)" , resolvedBase , initialSuffix .RawSuffix )
505- } else {
506- resolvedModelName = resolvedBase
564+ return fmt .Sprintf ("%s(%s)" , resolvedBase , initialSuffix .RawSuffix )
507565 }
508- } else {
509- resolvedModelName = util .ResolveAutoModel (modelName )
566+ return resolvedBase
510567 }
568+ return util .ResolveAutoModel (modelName )
569+ }
511570
571+ func responsesWebsocketProviderSetForModel (resolvedModelName string ) (map [string ]struct {}, string ) {
512572 parsed := thinking .ParseSuffix (resolvedModelName )
513573 baseModel := strings .TrimSpace (parsed .ModelName )
514574 providers := util .GetProviderName (baseModel )
515575 if len (providers ) == 0 && baseModel != resolvedModelName {
516576 providers = util .GetProviderName (resolvedModelName )
517577 }
518- if len (providers ) == 0 {
519- return false
520- }
521-
522578 providerSet := make (map [string ]struct {}, len (providers ))
523- for i := 0 ; i < len ( providers ); i ++ {
524- providerKey := strings .TrimSpace (strings .ToLower (providers [ i ] ))
579+ for _ , provider := range providers {
580+ providerKey := strings .TrimSpace (strings .ToLower (provider ))
525581 if providerKey == "" {
526582 continue
527583 }
528584 providerSet [providerKey ] = struct {}{}
529585 }
530- if len (providerSet ) == 0 {
531- return false
532- }
533-
534586 modelKey := baseModel
535587 if modelKey == "" {
536588 modelKey = strings .TrimSpace (resolvedModelName )
537589 }
538- registryRef := registry .GetGlobalRegistry ()
539- now := time .Now ()
540- auths := h .AuthManager .List ()
541- for i := 0 ; i < len (auths ); i ++ {
542- auth := auths [i ]
543- if auth == nil {
544- continue
545- }
546- providerKey := strings .TrimSpace (strings .ToLower (auth .Provider ))
547- if _ , ok := providerSet [providerKey ]; ! ok {
548- continue
549- }
550- if modelKey != "" && registryRef != nil && ! registryRef .ClientSupportsModel (auth .ID , modelKey ) {
551- continue
552- }
553- if ! responsesWebsocketAuthAvailableForModel (auth , modelKey , now ) {
554- continue
555- }
556- if websocketUpstreamSupportsIncrementalInput (auth .Attributes , auth .Metadata ) {
557- return true
558- }
590+ return providerSet , modelKey
591+ }
592+
593+ func responsesWebsocketAuthMatchesModel (auth * coreauth.Auth , providerSet map [string ]struct {}, modelKey string , registryRef * registry.ModelRegistry , now time.Time ) bool {
594+ if auth == nil {
595+ return false
559596 }
560- return false
597+ providerKey := strings .TrimSpace (strings .ToLower (auth .Provider ))
598+ if _ , ok := providerSet [providerKey ]; ! ok {
599+ return false
600+ }
601+ if modelKey != "" && registryRef != nil && ! registryRef .ClientSupportsModel (auth .ID , modelKey ) {
602+ return false
603+ }
604+ return responsesWebsocketAuthAvailableForModel (auth , modelKey , now )
605+ }
606+
607+ func responsesWebsocketAuthSupportsCompactionReplay (auth * coreauth.Auth ) bool {
608+ if auth == nil {
609+ return false
610+ }
611+ return strings .EqualFold (strings .TrimSpace (auth .Provider ), "codex" )
561612}
562613
563614func responsesWebsocketAuthAvailableForModel (auth * coreauth.Auth , modelName string , now time.Time ) bool {
@@ -724,6 +775,21 @@ func inputContainsFullTranscript(input gjson.Result) bool {
724775 return false
725776}
726777
778+ func inputWithoutCompactionItems (input gjson.Result ) string {
779+ if ! input .IsArray () {
780+ return normalizeJSONArrayRaw ([]byte (input .Raw ))
781+ }
782+ filtered := make ([]string , 0 , len (input .Array ()))
783+ for _ , item := range input .Array () {
784+ t := item .Get ("type" ).String ()
785+ if t == "compaction" || t == "compaction_summary" {
786+ continue
787+ }
788+ filtered = append (filtered , item .Raw )
789+ }
790+ return "[" + strings .Join (filtered , "," ) + "]"
791+ }
792+
727793func normalizeJSONArrayRaw (raw []byte ) string {
728794 trimmed := strings .TrimSpace (string (raw ))
729795 if trimmed == "" {
0 commit comments