fix(coding-agent): retry explicit provider retry errors

closes #6019
This commit is contained in:
Armin Ronacher
2026-06-24 18:12:25 +02:00
parent c29bbc0958
commit 371adcf371
7 changed files with 178 additions and 18 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed auto-retry for provider stream errors that explicitly tell callers to retry the request ([#6019](https://github.com/earendil-works/pi/issues/6019)).
## [0.80.2] - 2026-06-23
### Changed
@@ -29,6 +29,7 @@ import {
cleanupSessionResources,
getSupportedThinkingLevels,
isContextOverflow,
isRetryableAssistantError,
modelsAreEqual,
resetApiProviders,
streamSimple,
@@ -2482,29 +2483,14 @@ export class AgentSession {
// Auto-Retry
// =========================================================================
private _isNonRetryableProviderLimitError(errorMessage: string): boolean {
return /GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing/i.test(
errorMessage,
);
}
/**
* Check if an error is retryable (overloaded, rate limit, server errors).
* Context overflow errors are NOT retryable (handled by compaction instead).
*/
private _isRetryableError(message: AssistantMessage): boolean {
if (message.stopReason !== "error" || !message.errorMessage) return false;
// Context overflow is handled by compaction, not retry
const contextWindow = this.model?.contextWindow ?? 0;
if (isContextOverflow(message, contextWindow)) return false;
const err = message.errorMessage;
if (this._isNonRetryableProviderLimitError(err)) return false;
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test(
err,
);
// Context overflow is handled by compaction, not retry.
if (isContextOverflow(message, this.model?.contextWindow ?? 0)) return false;
return isRetryableAssistantError(message);
}
/**
@@ -0,0 +1,31 @@
import { fauxAssistantMessage } from "@earendil-works/pi-ai";
import { describe, expect, it } from "vitest";
import { createHarness } from "../harness.ts";
const openAIExplicitRetryMessage =
"An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID req_******** in your message.";
const bedrockExplicitRetryMessage =
'{"message":"The system encountered an unexpected error during processing. Try your request again."}';
describe("regression: issue 6019 explicit provider retry messages", () => {
it.each([
["openai", openAIExplicitRetryMessage],
["bedrock", bedrockExplicitRetryMessage],
])("retries %s explicit retry guidance", async (_provider, errorMessage) => {
const harness = await createHarness({ settings: { retry: { enabled: true, maxRetries: 3, baseDelayMs: 1 } } });
try {
harness.setResponses([
fauxAssistantMessage("", { stopReason: "error", errorMessage }),
fauxAssistantMessage("recovered"),
]);
await harness.session.prompt("test");
expect(harness.faux.state.callCount).toBe(2);
expect(harness.eventsOfType("auto_retry_start").map((event) => event.errorMessage)).toEqual([errorMessage]);
expect(harness.eventsOfType("auto_retry_end").map((event) => event.success)).toEqual([true]);
} finally {
harness.cleanup();
}
});
});