fix(failover): align abort timeout detection and regressions

This commit is contained in:
Sebastian
2026-02-16 20:59:44 -05:00
parent f242246839
commit fbda9a93fd
5 changed files with 40 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
coerceToFailoverError,
describeFailoverError,
isTimeoutError,
resolveFailoverReasonFromError,
} from "./failover-error.js";
@@ -27,6 +28,22 @@ describe("failover-error", () => {
expect(resolveFailoverReasonFromError({ code: "ECONNRESET" })).toBe("timeout");
});
it("infers timeout from abort stop-reason messages", () => {
expect(resolveFailoverReasonFromError({ message: "Unhandled stop reason: abort" })).toBe(
"timeout",
);
expect(resolveFailoverReasonFromError({ message: "stop reason: abort" })).toBe("timeout");
expect(resolveFailoverReasonFromError({ message: "reason: abort" })).toBe("timeout");
});
it("treats AbortError reason=abort as timeout", () => {
const err = Object.assign(new Error("aborted"), {
name: "AbortError",
reason: "reason: abort",
});
expect(isTimeoutError(err)).toBe(true);
});
it("coerces failover-worthy errors into FailoverError with metadata", () => {
const err = coerceToFailoverError("credit balance too low", {
provider: "anthropic",

View File

@@ -1,7 +1,7 @@
import { classifyFailoverReason, type FailoverReason } from "./pi-embedded-helpers.js";
const TIMEOUT_HINT_RE =
/timeout|timed out|deadline exceeded|context deadline exceeded|stop reason:\s*abort|unhandled stop reason:\s*abort/i;
/timeout|timed out|deadline exceeded|context deadline exceeded|stop reason:\s*abort|reason:\s*abort|unhandled stop reason:\s*abort/i;
const ABORT_TIMEOUT_RE = /request was aborted|request aborted/i;
export class FailoverError extends Error {

View File

@@ -400,6 +400,17 @@ describe("runWithModelFallback", () => {
});
});
it("falls back on abort errors with reason: abort", async () => {
await expectFallsBackToHaiku({
provider: "openai",
model: "gpt-4.1-mini",
firstError: Object.assign(new Error("aborted"), {
name: "AbortError",
reason: "reason: abort",
}),
});
});
it("falls back when message says aborted but error is a timeout", async () => {
await expectFallsBackToHaiku({
provider: "openai",

View File

@@ -10,6 +10,7 @@ import {
isFailoverErrorMessage,
isImageDimensionErrorMessage,
isLikelyContextOverflowError,
isTimeoutErrorMessage,
isTransientHttpError,
parseImageDimensionError,
parseImageSizeError,
@@ -286,6 +287,15 @@ describe("isFailoverErrorMessage", () => {
expect(isFailoverErrorMessage(sample)).toBe(true);
}
});
it("matches abort stop-reason timeout variants", () => {
const samples = ["Unhandled stop reason: abort", "stop reason: abort", "reason: abort"];
for (const sample of samples) {
expect(isTimeoutErrorMessage(sample)).toBe(true);
expect(classifyFailoverReason(sample)).toBe("timeout");
expect(isFailoverErrorMessage(sample)).toBe(true);
}
});
});
describe("parseImageSizeError", () => {