diff --git a/packages/agent/src/adapters/error-classification.test.ts b/packages/agent/src/adapters/error-classification.test.ts index d15bb4851f..af20ac5b3f 100644 --- a/packages/agent/src/adapters/error-classification.test.ts +++ b/packages/agent/src/adapters/error-classification.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { classifyAgentError } from "./error-classification"; +import { + classifyAgentError, + isPromptTooLongError, +} from "./error-classification"; describe("classifyAgentError", () => { it.each([ @@ -22,6 +25,8 @@ describe("classifyAgentError", () => { ["API Error: 429 rate limited", "upstream_provider_failure"], ["API Error: 529 overloaded", "upstream_provider_failure"], ["API Error: 400 invalid request", "agent_error"], + // 413 is a hard client rejection, never a transient upstream failure. + ["API Error: 413 Payload Too Large", "agent_error"], [ "Connection closed mid-response without the API Error prefix", "agent_error", @@ -32,3 +37,40 @@ describe("classifyAgentError", () => { expect(classifyAgentError(message)).toBe(expected); }); }); + +describe("isPromptTooLongError", () => { + it.each([ + [ + 'API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long: 214431 tokens > 204698 maximum"}}', + true, + ], + [ + 'API Error: 413 {"error":{"message":"litellm.ContextWindowExceededError: The estimated number of input and maximum output tokens (262334) exceeded this model context window limit (262144)","code":"5021"}}', + true, + ], + // Must match without the "API Error: 413" prefix. + [ + "litellm.ContextWindowExceededError: The estimated number of input and maximum output tokens (262334) exceeded this model context window limit (262144)", + true, + ], + // The ACP-wrapped shape the agent-server catch actually sees. + [ + 'Internal error: API Error: 413 {"error":{"message":"exceeded this model context window limit (262144)"}}', + true, + ], + // Any gateway 413 means an oversized payload, whatever the body text. + ["API Error: 413 Payload Too Large", true], + // Pins the 413 matcher's i flag. + ["api error: 413 payload too large", true], + ["API Error: 429 rate limited", false], + ["API Error: 400 invalid request", false], + ["some unrelated failure", false], + ] as const)("detects %j as %s", (message, expected) => { + expect(isPromptTooLongError(new Error(message))).toBe(expected); + }); + + it("handles non-Error inputs", () => { + expect(isPromptTooLongError({ message: "prompt is too long" })).toBe(true); + expect(isPromptTooLongError(undefined)).toBe(false); + }); +}); diff --git a/packages/agent/src/adapters/error-classification.ts b/packages/agent/src/adapters/error-classification.ts index 83a88f78fe..56e5268585 100644 --- a/packages/agent/src/adapters/error-classification.ts +++ b/packages/agent/src/adapters/error-classification.ts @@ -48,7 +48,16 @@ export function classifyAgentError( return "agent_error"; } -/** Hard API rejection: the assembled prompt exceeds the model's context window. */ +/** + * Hard API rejection: the prompt exceeds the model's context window + * (Anthropic phrasing, or the LLM gateway's HTTP 413). Retrying the same + * transcript can never succeed; callers must shrink the prompt. + */ export function isPromptTooLongError(error: unknown): boolean { - return /prompt is too long/i.test(getErrorMessage(error)); + const message = getErrorMessage(error); + return ( + /prompt is too long/i.test(message) || + /exceeded this model context window limit/i.test(message) || + /API Error:\s*413\b/i.test(message) + ); } diff --git a/packages/agent/src/server/agent-server.test.ts b/packages/agent/src/server/agent-server.test.ts index 49cd5ca9d2..171e953ba0 100644 --- a/packages/agent/src/server/agent-server.test.ts +++ b/packages/agent/src/server/agent-server.test.ts @@ -3406,11 +3406,27 @@ describe("AgentServer HTTP Mode", () => { }); it.each([ - { retryOutcome: "succeeds", retryFails: false }, - { retryOutcome: "fails", retryFails: true }, + { + retryOutcome: "succeeds", + retryFails: false, + oversizedError: "Internal error: Prompt is too long", + }, + { + retryOutcome: "fails", + retryFails: true, + oversizedError: "Internal error: Prompt is too long", + }, + // The LLM gateway phrases oversized rejections as HTTP 413; the + // fresh-session retry must trigger on that shape too. + { + retryOutcome: "succeeds after a gateway 413", + retryFails: false, + oversizedError: + 'Internal error: API Error: 413 {"error":{"message":"litellm.ContextWindowExceededError: The estimated number of input and maximum output tokens (262334) exceeded this model context window limit (262144)","code":"5021"}}', + }, ])( "clears resume state when the fresh-session retry $retryOutcome", - async ({ retryFails }) => { + async ({ retryFails, oversizedError }) => { const s = createServer(); await s.start(); @@ -3418,7 +3434,7 @@ describe("AgentServer HTTP Mode", () => { const prompt = vi.fn(async (params: { prompt: ContentBlock[] }) => { prompts.push(params.prompt); if (prompts.length === 1) { - throw new Error("Internal error: Prompt is too long"); + throw new Error(oversizedError); } if (retryFails) { throw new Error("Fresh-session retry failed");