diff --git a/core/llm/utils/retry.test.ts b/core/llm/utils/retry.test.ts index 2f7c3d1bbb..8292bf0506 100644 --- a/core/llm/utils/retry.test.ts +++ b/core/llm/utils/retry.test.ts @@ -205,6 +205,70 @@ describe("Retry Functionality", () => { expect(mockFn).toHaveBeenCalledTimes(2); }); + it("should retry on premature close error", async () => { + const error = new Error("Premature close"); + const mockFn = jest + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue("success"); + + const result = await retryAsync(mockFn, { + maxAttempts: 2, + baseDelay: 10, + }); + + expect(result).toBe("success"); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + + it("should retry on socket hang up error", async () => { + const error = new Error("socket hang up"); + const mockFn = jest + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue("success"); + + const result = await retryAsync(mockFn, { + maxAttempts: 2, + baseDelay: 10, + }); + + expect(result).toBe("success"); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + + it("should retry on premature end error", async () => { + const error = new Error("premature end of stream"); + const mockFn = jest + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue("success"); + + const result = await retryAsync(mockFn, { + maxAttempts: 2, + baseDelay: 10, + }); + + expect(result).toBe("success"); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + + it("should retry on connection reset message error", async () => { + const error = new Error("connection reset by peer"); + const mockFn = jest + .fn() + .mockRejectedValueOnce(error) + .mockResolvedValue("success"); + + const result = await retryAsync(mockFn, { + maxAttempts: 2, + baseDelay: 10, + }); + + expect(result).toBe("success"); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + it("should not retry AbortError", async () => { const error = new Error("Aborted"); error.name = "AbortError"; diff --git a/core/llm/utils/retry.ts b/core/llm/utils/retry.ts index 6d1b94ce26..b6aeda6d33 100644 --- a/core/llm/utils/retry.ts +++ b/core/llm/utils/retry.ts @@ -113,6 +113,17 @@ function defaultShouldRetry(error: any, attempt: number): boolean { return true; } + // Connection/stream errors (e.g. provider drops connection mid-stream) + const connectionPatterns = [ + "premature close", + "premature end", + "connection reset", + "socket hang up", + ]; + if (connectionPatterns.some((p) => lowerMessage.includes(p))) { + return true; + } + // Abort signal errors should not be retried if (isAbortError(error)) { return false; diff --git a/core/util/withExponentialBackoff.ts b/core/util/withExponentialBackoff.ts index bb280911d2..2c38ffb395 100644 --- a/core/util/withExponentialBackoff.ts +++ b/core/util/withExponentialBackoff.ts @@ -19,7 +19,11 @@ const withExponentialBackoff = async ( (error as APIError).response?.status === 429 || /"code"\s*:\s*429/.test(error.message ?? "") || lowerMessage.includes("overloaded") || - lowerMessage.includes("malformed json") + lowerMessage.includes("malformed json") || + lowerMessage.includes("premature close") || + lowerMessage.includes("premature end") || + lowerMessage.includes("connection reset") || + lowerMessage.includes("socket hang up") ) { const retryAfter = (error as APIError).response?.headers.get( RETRY_AFTER_HEADER,