mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
feat(providers): classify chatgpt-session failures onto the router contract
This commit is contained in:
85
open-sse/executors/chatgpt-session/errors.ts
Normal file
85
open-sse/executors/chatgpt-session/errors.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Maps every failure this provider can produce onto the HTTP contract the router expects.
|
||||
*
|
||||
* The distinction that matters: a 503 with `connection_cooldown` lets combo routing skip this
|
||||
* connection without opening the provider circuit breaker, while a 400 is terminal and must not
|
||||
* be retried (a changed ChatGPT DOM will not fix itself on a retry).
|
||||
*/
|
||||
|
||||
import { ChatGptSessionInputError } from "./messages.ts";
|
||||
|
||||
export interface ChatGptSessionErrorClass {
|
||||
status: number;
|
||||
code: string;
|
||||
fallbackHint?: "connection_cooldown";
|
||||
}
|
||||
|
||||
interface ErrorLike {
|
||||
message: string;
|
||||
name?: string;
|
||||
status?: number;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
function asErrorLike(error: unknown): ErrorLike {
|
||||
if (error instanceof Error) {
|
||||
const typed = error as Error & { status?: unknown; code?: unknown };
|
||||
return {
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
...(typeof typed.status === "number" ? { status: typed.status } : {}),
|
||||
...(typeof typed.code === "string" ? { code: typed.code } : {}),
|
||||
};
|
||||
}
|
||||
if (error && typeof error === "object") {
|
||||
const typed = error as Record<string, unknown>;
|
||||
return {
|
||||
message: typeof typed.message === "string" ? typed.message : String(error),
|
||||
...(typeof typed.name === "string" ? { name: typed.name } : {}),
|
||||
...(typeof typed.status === "number" ? { status: typed.status } : {}),
|
||||
...(typeof typed.code === "string" ? { code: typed.code } : {}),
|
||||
};
|
||||
}
|
||||
return { message: String(error ?? "") };
|
||||
}
|
||||
|
||||
const BROWSER_UNAVAILABLE =
|
||||
/No supported Chrome|browserType\.launch|Executable doesn't exist|chromium.*not installed/i;
|
||||
const MISSING_CREDENTIALS =
|
||||
/credentials are missing|Cookie or verified browser storage state is required|Cookie header is missing/i;
|
||||
const SESSION_EXPIRED = /not authenticated|storage state is invalid|sign ?in|log ?in|logged out/i;
|
||||
const RATE_LIMITED = /rate limit|usage limit|too many requests|message limit/i;
|
||||
const ROUTE_UNAVAILABLE =
|
||||
/not available for this|not available while the account|is not supported/i;
|
||||
const UI_TIMEOUT = /waitForSelector|Timeout \d+ms exceeded|actionability|interception/i;
|
||||
|
||||
export function classifyChatGptSessionError(error: unknown): ChatGptSessionErrorClass {
|
||||
if (error instanceof ChatGptSessionInputError) {
|
||||
return { status: 400, code: error.code };
|
||||
}
|
||||
|
||||
const like = asErrorLike(error);
|
||||
|
||||
if (BROWSER_UNAVAILABLE.test(like.message)) {
|
||||
return { status: 503, code: "browser_unavailable", fallbackHint: "connection_cooldown" };
|
||||
}
|
||||
if (MISSING_CREDENTIALS.test(like.message)) {
|
||||
return { status: 401, code: "missing_credentials" };
|
||||
}
|
||||
if (SESSION_EXPIRED.test(like.message)) {
|
||||
return { status: 401, code: "session_expired" };
|
||||
}
|
||||
if (RATE_LIMITED.test(like.message)) {
|
||||
return { status: 429, code: "rate_limited" };
|
||||
}
|
||||
if (ROUTE_UNAVAILABLE.test(like.message)) {
|
||||
return { status: 400, code: "route_unavailable" };
|
||||
}
|
||||
if (like.name === "TimeoutError" || UI_TIMEOUT.test(like.message)) {
|
||||
return { status: 400, code: "browser_ui_timeout" };
|
||||
}
|
||||
if (typeof like.status === "number" && like.status >= 400) {
|
||||
return { status: like.status, code: like.code ?? "turn_failed" };
|
||||
}
|
||||
return { status: 502, code: "turn_failed" };
|
||||
}
|
||||
77
tests/unit/chatgpt-session-errors.test.ts
Normal file
77
tests/unit/chatgpt-session-errors.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { classifyChatGptSessionError } from "../../open-sse/executors/chatgpt-session/errors.ts";
|
||||
import { ChatGptSessionInputError } from "../../open-sse/executors/chatgpt-session/messages.ts";
|
||||
|
||||
test("a missing browser is a cooldown-hinted 503", () => {
|
||||
const result = classifyChatGptSessionError(
|
||||
new Error("No supported Chrome or Chromium executable was found")
|
||||
);
|
||||
assert.equal(result.status, 503);
|
||||
assert.equal(result.code, "browser_unavailable");
|
||||
assert.equal(result.fallbackHint, "connection_cooldown");
|
||||
});
|
||||
|
||||
test("a playwright launch failure is also a cooldown-hinted 503", () => {
|
||||
const result = classifyChatGptSessionError(
|
||||
new Error("browserType.launch: Executable doesn't exist at /root/.cache/ms-playwright/x")
|
||||
);
|
||||
assert.equal(result.status, 503);
|
||||
assert.equal(result.fallbackHint, "connection_cooldown");
|
||||
});
|
||||
|
||||
test("missing credentials are a 401", () => {
|
||||
assert.equal(
|
||||
classifyChatGptSessionError(new Error("ChatGPT browser credentials are missing")).status,
|
||||
401
|
||||
);
|
||||
});
|
||||
|
||||
test("an expired session is a 401 session_expired", () => {
|
||||
const result = classifyChatGptSessionError(new Error("ChatGPT page is not authenticated"));
|
||||
assert.equal(result.status, 401);
|
||||
assert.equal(result.code, "session_expired");
|
||||
});
|
||||
|
||||
test("a rate-limit dialog is a 429", () => {
|
||||
const result = classifyChatGptSessionError(new Error("ChatGPT reported a usage limit"));
|
||||
assert.equal(result.status, 429);
|
||||
assert.equal(result.code, "rate_limited");
|
||||
});
|
||||
|
||||
test("an account-capability mismatch is a terminal 400", () => {
|
||||
const result = classifyChatGptSessionError(
|
||||
new Error("pro is not available for this non-Pro connection")
|
||||
);
|
||||
assert.equal(result.status, 400);
|
||||
assert.equal(result.code, "route_unavailable");
|
||||
assert.equal(result.fallbackHint, undefined);
|
||||
});
|
||||
|
||||
test("a DOM timeout is a terminal 400, not a retryable 5xx", () => {
|
||||
const timeout = new Error("locator.waitForSelector: Timeout 30000ms exceeded");
|
||||
timeout.name = "TimeoutError";
|
||||
const result = classifyChatGptSessionError(timeout);
|
||||
assert.equal(result.status, 400);
|
||||
assert.equal(result.code, "browser_ui_timeout");
|
||||
});
|
||||
|
||||
test("input errors map to their own 400 codes", () => {
|
||||
const result = classifyChatGptSessionError(
|
||||
new ChatGptSessionInputError("vision_unsupported", "no images")
|
||||
);
|
||||
assert.equal(result.status, 400);
|
||||
assert.equal(result.code, "vision_unsupported");
|
||||
});
|
||||
|
||||
test("an explicit upstream status wins over message matching", () => {
|
||||
const result = classifyChatGptSessionError({ message: "anything", status: 502, code: "x" });
|
||||
assert.equal(result.status, 502);
|
||||
});
|
||||
|
||||
test("an unrecognised failure is a retryable 502", () => {
|
||||
const result = classifyChatGptSessionError(new Error("something odd happened"));
|
||||
assert.equal(result.status, 502);
|
||||
assert.equal(result.code, "turn_failed");
|
||||
});
|
||||
Reference in New Issue
Block a user