@@ -557,8 +557,20 @@ export class ReviewOrchestrator {
557557 if ( batchFailures > 0 ) {
558558 if ( batchSuccesses === 0 ) {
559559 const failedNames = mergedResults . filter ( r => r . status !== 'success' ) . map ( r => r . name ) . join ( ', ' ) ;
560- logger . error ( `All LLM batches failed (${ batchFailures } /${ batches . length } ): ${ failedNames } . Continuing with static analysis only.` ) ;
560+ const providerFailureSummary = this . formatProviderFailureSummary ( mergedResults ) ;
561+ const failOnProviderFailure = process . env . FAIL_ON_NO_HEALTHY_PROVIDERS === 'true' ;
562+ logger . error (
563+ `All LLM batches failed (${ batchFailures } /${ batches . length } ): ${ failedNames } . ` +
564+ ( failOnProviderFailure
565+ ? 'Failing because FAIL_ON_NO_HEALTHY_PROVIDERS=true.'
566+ : 'Continuing with static analysis only.' )
567+ ) ;
561568 await progressTracker ?. updateProgress ( 'llm' , 'failed' , `All batches failed: ${ failedNames } ` ) ;
569+ if ( failOnProviderFailure ) {
570+ throw new Error (
571+ `All LLM providers failed during review; failing because FAIL_ON_NO_HEALTHY_PROVIDERS=true. ${ providerFailureSummary } `
572+ ) ;
573+ }
562574 } else {
563575 logger . warn ( `Partial batch failure: ${ batchFailures } failed, ${ batchSuccesses } succeeded. Using successful results.` ) ;
564576 await progressTracker ?. updateProgress ( 'llm' , 'completed' , `Batches: ${ batchSuccesses } /${ batches . length } succeeded` ) ;
@@ -889,6 +901,33 @@ export class ReviewOrchestrator {
889901 }
890902 }
891903
904+ private formatProviderFailureSummary ( results : ProviderResult [ ] ) : string {
905+ const failures = results
906+ . filter ( result => result . status !== 'success' )
907+ . map ( result => {
908+ const reason = result . error ?. message || result . status ;
909+ return `${ result . name } : ${ this . redactProviderFailureReason ( reason ) } ` ;
910+ } ) ;
911+
912+ if ( failures . length === 0 ) {
913+ return 'No provider error details were reported.' ;
914+ }
915+
916+ const summary = failures . join ( '; ' ) ;
917+ return summary . length > 1000 ? `${ summary . slice ( 0 , 1000 ) } ...` : summary ;
918+ }
919+
920+ private redactProviderFailureReason ( reason : string ) : string {
921+ return reason
922+ . replace ( / s k - [ A - Z a - z 0 - 9 _ - ] { 16 , } / g, 'sk-***' )
923+ . replace ( / g h [ p o u s r ] _ [ A - Z a - z 0 - 9 _ ] { 16 , } / g, 'gh*-***' )
924+ . replace ( / g i t h u b _ p a t _ [ A - Z a - z 0 - 9 _ ] + / g, 'github_pat_***' )
925+ . replace ( / ( r e f r e s h _ t o k e n [ " ' \s : = ] + ) [ ^ " ' , \s } ] + / gi, '$1***' )
926+ . replace ( / ( a u t h o r i z a t i o n : \s * b e a r e r \s + ) [ ^ \s ] + / gi, '$1***' )
927+ . replace ( / ( O P E N A I _ A P I _ K E Y [ " ' \s : = ] + ) [ ^ " ' , \s } ] + / gi, '$1***' )
928+ . replace ( / ( O P E N R O U T E R _ A P I _ K E Y [ " ' \s : = ] + ) [ ^ " ' , \s } ] + / gi, '$1***' ) ;
929+ }
930+
892931 /**
893932 * Run all static analysis operations in parallel
894933 */
0 commit comments