Skip to content

Commit b697733

Browse files
committed
fix: fail when all providers fail during review
1 parent 9bae41c commit b697733

4 files changed

Lines changed: 129 additions & 4 deletions

File tree

__tests__/unit/core/orchestrator.health.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,67 @@ describe('ReviewOrchestrator health check guard rails', () => {
198198
expect(review).toBeTruthy();
199199
expect(execute).toHaveBeenCalledWith([provider], expect.any(String), expect.any(Number));
200200
});
201+
202+
it('fails when every LLM provider fails and provider failure is configured as blocking', async () => {
203+
const previousFailOnNoHealthy = process.env.FAIL_ON_NO_HEALTHY_PROVIDERS;
204+
process.env.FAIL_ON_NO_HEALTHY_PROVIDERS = 'true';
205+
206+
const provider = {
207+
name: 'codex/gpt-5.5',
208+
review: jest.fn(),
209+
healthCheck: jest.fn(),
210+
} as unknown as Provider;
211+
const execute = jest.fn().mockResolvedValue([
212+
{
213+
name: 'codex/gpt-5.5',
214+
status: 'error',
215+
error: new Error('Codex CLI failed: OPENAI_API_KEY=sk-1234567890abcdef refresh_token=secret-value'),
216+
durationSeconds: 0,
217+
} as ProviderResult,
218+
]);
219+
220+
try {
221+
const orchestrator = makeOrchestrator({
222+
config: {
223+
...DEFAULT_CONFIG,
224+
dryRun: true,
225+
enableCaching: false,
226+
analyticsEnabled: false,
227+
graphEnabled: false,
228+
providers: ['codex/gpt-5.5'],
229+
fallbackProviders: [],
230+
providerLimit: 1,
231+
},
232+
providerRegistry: {
233+
createProviders: jest.fn().mockResolvedValue([provider]),
234+
discoverAdditionalFreeProviders: jest.fn().mockResolvedValue([]),
235+
} as any,
236+
llmExecutor: {
237+
filterHealthyProviders: jest.fn().mockResolvedValue({
238+
healthy: [provider],
239+
healthCheckResults: [],
240+
}),
241+
execute,
242+
} as any,
243+
});
244+
245+
const pr = makePR([{ filename: 'a.ts', status: 'modified', additions: 1, deletions: 0, changes: 1 }]);
246+
247+
let thrown: Error | undefined;
248+
try {
249+
await orchestrator.executeReview(pr);
250+
} catch (error) {
251+
thrown = error as Error;
252+
}
253+
254+
expect(thrown?.message).toMatch(/All LLM providers failed during review/);
255+
expect(thrown?.message).not.toMatch(/sk-1234567890abcdef|secret-value/);
256+
} finally {
257+
if (previousFailOnNoHealthy === undefined) {
258+
delete process.env.FAIL_ON_NO_HEALTHY_PROVIDERS;
259+
} else {
260+
process.env.FAIL_ON_NO_HEALTHY_PROVIDERS = previousFailOnNoHealthy;
261+
}
262+
}
263+
});
201264
});

dist/index.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26615,8 +26615,17 @@ var ReviewOrchestrator = class {
2661526615
if (batchFailures > 0) {
2661626616
if (batchSuccesses === 0) {
2661726617
const failedNames = mergedResults.filter((r) => r.status !== "success").map((r) => r.name).join(", ");
26618-
logger.error(`All LLM batches failed (${batchFailures}/${batches.length}): ${failedNames}. Continuing with static analysis only.`);
26618+
const providerFailureSummary = this.formatProviderFailureSummary(mergedResults);
26619+
const failOnProviderFailure = process.env.FAIL_ON_NO_HEALTHY_PROVIDERS === "true";
26620+
logger.error(
26621+
`All LLM batches failed (${batchFailures}/${batches.length}): ${failedNames}. ` + (failOnProviderFailure ? "Failing because FAIL_ON_NO_HEALTHY_PROVIDERS=true." : "Continuing with static analysis only.")
26622+
);
2661926623
await progressTracker?.updateProgress("llm", "failed", `All batches failed: ${failedNames}`);
26624+
if (failOnProviderFailure) {
26625+
throw new Error(
26626+
`All LLM providers failed during review; failing because FAIL_ON_NO_HEALTHY_PROVIDERS=true. ${providerFailureSummary}`
26627+
);
26628+
}
2662026629
} else {
2662126630
logger.warn(`Partial batch failure: ${batchFailures} failed, ${batchSuccesses} succeeded. Using successful results.`);
2662226631
await progressTracker?.updateProgress("llm", "completed", `Batches: ${batchSuccesses}/${batches.length} succeeded`);
@@ -26869,6 +26878,20 @@ var ReviewOrchestrator = class {
2686926878
logger.warn("Failed to update PR description summary", error2);
2687026879
}
2687126880
}
26881+
formatProviderFailureSummary(results) {
26882+
const failures = results.filter((result) => result.status !== "success").map((result) => {
26883+
const reason = result.error?.message || result.status;
26884+
return `${result.name}: ${this.redactProviderFailureReason(reason)}`;
26885+
});
26886+
if (failures.length === 0) {
26887+
return "No provider error details were reported.";
26888+
}
26889+
const summary = failures.join("; ");
26890+
return summary.length > 1e3 ? `${summary.slice(0, 1e3)}...` : summary;
26891+
}
26892+
redactProviderFailureReason(reason) {
26893+
return reason.replace(/sk-[A-Za-z0-9_-]{16,}/g, "sk-***").replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, "gh*-***").replace(/github_pat_[A-Za-z0-9_]+/g, "github_pat_***").replace(/(refresh_token["'\s:=]+)[^"',\s}]+/gi, "$1***").replace(/(authorization:\s*bearer\s+)[^\s]+/gi, "$1***").replace(/(OPENAI_API_KEY["'\s:=]+)[^"',\s}]+/gi, "$1***").replace(/(OPENROUTER_API_KEY["'\s:=]+)[^"',\s}]+/gi, "$1***");
26894+
}
2687226895
/**
2687326896
* Run all static analysis operations in parallel
2687426897
*/

dist/index.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/orchestrator.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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(/sk-[A-Za-z0-9_-]{16,}/g, 'sk-***')
923+
.replace(/gh[pousr]_[A-Za-z0-9_]{16,}/g, 'gh*-***')
924+
.replace(/github_pat_[A-Za-z0-9_]+/g, 'github_pat_***')
925+
.replace(/(refresh_token["'\s:=]+)[^"',\s}]+/gi, '$1***')
926+
.replace(/(authorization:\s*bearer\s+)[^\s]+/gi, '$1***')
927+
.replace(/(OPENAI_API_KEY["'\s:=]+)[^"',\s}]+/gi, '$1***')
928+
.replace(/(OPENROUTER_API_KEY["'\s:=]+)[^"',\s}]+/gi, '$1***');
929+
}
930+
892931
/**
893932
* Run all static analysis operations in parallel
894933
*/

0 commit comments

Comments
 (0)