c3f7e849e5
- Migrated 288 lines of scattered error classification logic from `utils/error-id.ts` into a cohesive `packages/ai/src/error/` module with 13 specialized submodules covering flags, classes, OAuth, providers, rate-limiting, and finalization. - Replaced 100+ generic `Error` throws across 60+ provider and registry files with semantic `AIError.*` classes (e.g., `AIError.MissingApiKeyError`, `AIError.OAuthError`, `AIError.ProviderResponseError`), improving error diagnostics and retry logic. - Consolidated error utility imports from `pi-utils` and scattered classification functions into a single `AIError` namespace, reducing coupling and simplifying error handling across all packages.
95 lines
3.7 KiB
TypeScript
95 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "bun:test";
|
|
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
|
|
import { isContextOverflow } from "@oh-my-pi/pi-ai/error";
|
|
|
|
function createErrorMessage(errorMessage: string): AssistantMessage {
|
|
return {
|
|
role: "assistant",
|
|
content: [{ type: "text", text: "" }],
|
|
api: "anthropic-messages",
|
|
provider: "anthropic",
|
|
model: "claude-sonnet-4-5",
|
|
usage: {
|
|
input: 0,
|
|
output: 0,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 0,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
},
|
|
stopReason: "error",
|
|
errorMessage,
|
|
timestamp: Date.now(),
|
|
};
|
|
}
|
|
|
|
describe("isContextOverflow - model_context_window_exceeded", () => {
|
|
it("detects model_context_window_exceeded in finish_reason error message", () => {
|
|
const message = createErrorMessage("Provider finish_reason: model_context_window_exceeded");
|
|
expect(isContextOverflow(message)).toBe(true);
|
|
});
|
|
|
|
it("detects raw model_context_window_exceeded in error message", () => {
|
|
const message = createErrorMessage("model_context_window_exceeded");
|
|
expect(isContextOverflow(message)).toBe(true);
|
|
});
|
|
it("detects empty Ollama length completion guidance", () => {
|
|
const message = createErrorMessage(
|
|
"Model returned no content: prompt filled the context window; raise Ollama num_ctx or shorten the prompt.",
|
|
);
|
|
expect(isContextOverflow(message)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("isContextOverflow - HTTP 413 variants", () => {
|
|
it("detects generic 413 payload-too-large errors", () => {
|
|
const message = createErrorMessage("413 Request Entity Too Large: payload too large for request body");
|
|
expect(isContextOverflow(message)).toBe(true);
|
|
});
|
|
|
|
it("detects Anthropic request size overflow wording", () => {
|
|
const message = createErrorMessage("Request exceeds the maximum size allowed by this model");
|
|
expect(isContextOverflow(message)).toBe(true);
|
|
});
|
|
|
|
it("does not classify unrelated 413 errors as overflow", () => {
|
|
const message = createErrorMessage("413 Forbidden");
|
|
expect(isContextOverflow(message)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("isContextOverflow - 400/413 no-body (Cerebras, Mistral, proxy wrappers)", () => {
|
|
it("detects bare '400 status code (no body)'", () => {
|
|
expect(isContextOverflow(createErrorMessage("400 status code (no body)"))).toBe(true);
|
|
});
|
|
|
|
it("detects bare '413 status code (no body)'", () => {
|
|
expect(isContextOverflow(createErrorMessage("413 status code (no body)"))).toBe(true);
|
|
});
|
|
|
|
it("detects '400 (no body)' without 'status code' word", () => {
|
|
expect(isContextOverflow(createErrorMessage("400 (no body)"))).toBe(true);
|
|
});
|
|
|
|
// Regression: api.synthetic.new wraps upstream HF 400-no-body in a JSON envelope.
|
|
// finalizeErrorMessage transforms the response to "400 status code: {JSON}" where
|
|
// the JSON value contains the inner "400 status code (no body)" text.
|
|
it('detects wrapped proxy envelope: \'400 status code: {"error":"... 400 status code (no body)"}\'', () => {
|
|
const errorMessage = '400 status code: {"error":"Error from inference backend: 400 status code (no body)"}';
|
|
expect(isContextOverflow(createErrorMessage(errorMessage))).toBe(true);
|
|
});
|
|
|
|
it("detects when status code phrase is embedded deeper in the message", () => {
|
|
const errorMessage = "Upstream rejected request: 400 status code (no body)";
|
|
expect(isContextOverflow(createErrorMessage(errorMessage))).toBe(true);
|
|
});
|
|
|
|
it("does not classify unrelated 400 errors as overflow", () => {
|
|
expect(isContextOverflow(createErrorMessage("400 Bad Request: invalid API key"))).toBe(false);
|
|
});
|
|
|
|
it("does not classify 429 (rate limit) as overflow", () => {
|
|
expect(isContextOverflow(createErrorMessage("429 status code (no body)"))).toBe(false);
|
|
});
|
|
});
|