@@ -15,6 +15,7 @@ import {
1515} from "./auth-profiles.js" ;
1616import { DEFAULT_MODEL , DEFAULT_PROVIDER } from "./defaults.js" ;
1717import {
18+ FailoverError ,
1819 coerceToFailoverError ,
1920 describeFailoverError ,
2021 isFailoverError ,
@@ -25,6 +26,7 @@ import {
2526 shouldPreserveTransientCooldownProbeSlot ,
2627 shouldUseTransientCooldownProbeSlot ,
2728} from "./failover-policy.js" ;
29+ import { LiveSessionModelSwitchError } from "./live-model-switch.js" ;
2830import { logModelFallbackDecision } from "./model-fallback-observation.js" ;
2931import type { FallbackAttempt , ModelCandidate } from "./model-fallback.types.js" ;
3032import {
@@ -781,6 +783,48 @@ export async function runWithModelFallback<T>(params: {
781783 model : candidate . model ,
782784 } ) ?? err ;
783785
786+ // LiveSessionModelSwitchError during fallback means the session's
787+ // persisted model conflicts with this fallback candidate. Treat it
788+ // as a known failover so the chain continues to the next candidate
789+ // instead of re-throwing and triggering infinite retry loops in the
790+ // outer runner. (#58466)
791+ if ( err instanceof LiveSessionModelSwitchError ) {
792+ const switchMsg = err . message ;
793+ const switchNormalized = new FailoverError ( switchMsg , {
794+ reason : "overloaded" ,
795+ provider : candidate . provider ,
796+ model : candidate . model ,
797+ } ) ;
798+ lastError = switchNormalized ;
799+ const described = describeFailoverError ( switchNormalized ) ;
800+ attempts . push ( {
801+ provider : candidate . provider ,
802+ model : candidate . model ,
803+ error : described . message ,
804+ reason : described . reason ?? "unknown" ,
805+ status : described . status ,
806+ code : described . code ,
807+ } ) ;
808+ logModelFallbackDecision ( {
809+ decision : "candidate_failed" ,
810+ runId : params . runId ,
811+ requestedProvider : params . provider ,
812+ requestedModel : params . model ,
813+ candidate,
814+ attempt : i + 1 ,
815+ total : candidates . length ,
816+ reason : described . reason ,
817+ status : described . status ,
818+ code : described . code ,
819+ error : described . message ,
820+ nextCandidate : candidates [ i + 1 ] ,
821+ isPrimary,
822+ requestedModelMatched : requestedModel ,
823+ fallbackConfigured : hasFallbackCandidates ,
824+ } ) ;
825+ continue ;
826+ }
827+
784828 // Even unrecognized errors should not abort the fallback loop when
785829 // there are remaining candidates. Only abort/context-overflow errors
786830 // (handled above) are truly non-retryable.
0 commit comments