From b59ea0e3f37fea52b94fb6fb88f5f192e87bc9d0 Mon Sep 17 00:00:00 2001 From: {Suksham-sharma} Date: Tue, 27 Jan 2026 22:21:51 +0530 Subject: [PATCH 1/3] fix: prevent infinite retry loop for images exceeding 5MB - Change MAX_IMAGE_BYTES from 6MB to 5MB to match Anthropic API limit - Add isImageSizeError() to detect image size errors from API - Handle image size errors with user-friendly message instead of retry - Prevent failover for image size errors (not retriable) Fixes #2271 --- src/agents/pi-embedded-helpers.ts | 1 + src/agents/pi-embedded-helpers/errors.ts | 7 +++++++ src/agents/pi-embedded-runner/run.ts | 24 ++++++++++++++++++++++++ src/agents/pi-embedded-runner/types.ts | 2 +- src/media/constants.ts | 2 +- 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index 6f6bb474f..4aed2d047 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -23,6 +23,7 @@ export { isFailoverAssistantError, isFailoverErrorMessage, isImageDimensionErrorMessage, + isImageSizeError, isOverloadedErrorMessage, isRawApiErrorPayload, isRateLimitAssistantError, diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index d6e33f924..bad476176 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -467,6 +467,12 @@ export function isImageDimensionErrorMessage(raw: string): boolean { return Boolean(parseImageDimensionError(raw)); } +export function isImageSizeError(errorMessage?: string): boolean { + if (!errorMessage) return false; + const lower = errorMessage.toLowerCase(); + return lower.includes("image exceeds") && lower.includes("mb"); +} + export function isCloudCodeAssistFormatError(raw: string): boolean { return !isImageDimensionErrorMessage(raw) && matchesErrorPatterns(raw, ERROR_PATTERNS.format); } @@ -478,6 +484,7 @@ export function isAuthAssistantError(msg: AssistantMessage | undefined): boolean export function classifyFailoverReason(raw: string): FailoverReason | null { if (isImageDimensionErrorMessage(raw)) return null; + if (isImageSizeError(raw)) return null; if (isRateLimitErrorMessage(raw)) return "rate_limit"; if (isOverloadedErrorMessage(raw)) return "rate_limit"; if (isCloudCodeAssistFormatError(raw)) return "format"; diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 69eb1514a..006172e14 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -34,6 +34,7 @@ import { isContextOverflowError, isFailoverAssistantError, isFailoverErrorMessage, + isImageSizeError, parseImageDimensionError, isRateLimitAssistantError, isTimeoutErrorMessage, @@ -440,6 +441,29 @@ export async function runEmbeddedPiAgent( }, }; } + // Handle image size errors with a user-friendly message (no retry needed) + if (isImageSizeError(errorText)) { + return { + payloads: [ + { + text: + "Image too large for the model (max 5MB). " + + "Please compress or resize the image and try again.", + isError: true, + }, + ], + meta: { + durationMs: Date.now() - started, + agentMeta: { + sessionId: sessionIdUsed, + provider, + model: model.id, + }, + systemPromptReport: attempt.systemPromptReport, + error: { kind: "image_size", message: errorText }, + }, + }; + } const promptFailoverReason = classifyFailoverReason(errorText); if (promptFailoverReason && promptFailoverReason !== "timeout" && lastProfileId) { await markAuthProfileFailure({ diff --git a/src/agents/pi-embedded-runner/types.ts b/src/agents/pi-embedded-runner/types.ts index 4be395bce..27ccfa64e 100644 --- a/src/agents/pi-embedded-runner/types.ts +++ b/src/agents/pi-embedded-runner/types.ts @@ -20,7 +20,7 @@ export type EmbeddedPiRunMeta = { aborted?: boolean; systemPromptReport?: SessionSystemPromptReport; error?: { - kind: "context_overflow" | "compaction_failure" | "role_ordering"; + kind: "context_overflow" | "compaction_failure" | "role_ordering" | "image_size"; message: string; }; /** Stop reason for the agent run (e.g., "completed", "tool_calls"). */ diff --git a/src/media/constants.ts b/src/media/constants.ts index e74ac6934..8577b6d20 100644 --- a/src/media/constants.ts +++ b/src/media/constants.ts @@ -1,4 +1,4 @@ -export const MAX_IMAGE_BYTES = 6 * 1024 * 1024; // 6MB +export const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5MB (Anthropic API limit) export const MAX_AUDIO_BYTES = 16 * 1024 * 1024; // 16MB export const MAX_VIDEO_BYTES = 16 * 1024 * 1024; // 16MB export const MAX_DOCUMENT_BYTES = 100 * 1024 * 1024; // 100MB From 20c0d1f2c58a4ca71378deabf3a316bdc0b8acb6 Mon Sep 17 00:00:00 2001 From: Shadow Date: Tue, 27 Jan 2026 15:59:11 -0600 Subject: [PATCH 2/3] fix: avoid global image size regression --- src/agents/pi-embedded-helpers.ts | 1 + src/agents/pi-embedded-helpers/errors.ts | 18 ++++++++++++++++-- src/agents/pi-embedded-runner/run.ts | 11 ++++++++--- src/media/constants.ts | 2 +- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index 4aed2d047..88443756f 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -30,6 +30,7 @@ export { isRateLimitErrorMessage, isTimeoutErrorMessage, parseImageDimensionError, + parseImageSizeError, } from "./pi-embedded-helpers/errors.js"; export { isGoogleModelApi, sanitizeGoogleTurnOrdering } from "./pi-embedded-helpers/google.js"; diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index bad476176..849c4293e 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -401,6 +401,7 @@ const ERROR_PATTERNS = { const IMAGE_DIMENSION_ERROR_RE = /image dimensions exceed max allowed size for many-image requests:\s*(\d+)\s*pixels/i; const IMAGE_DIMENSION_PATH_RE = /messages\.(\d+)\.content\.(\d+)\.image/i; +const IMAGE_SIZE_ERROR_RE = /image exceeds\s*(\d+(?:\.\d+)?)\s*mb/i; function matchesErrorPatterns(raw: string, patterns: readonly ErrorPattern[]): boolean { if (!raw) return false; @@ -467,10 +468,23 @@ export function isImageDimensionErrorMessage(raw: string): boolean { return Boolean(parseImageDimensionError(raw)); } +export function parseImageSizeError(raw: string): { + maxMb?: number; + raw: string; +} | null { + if (!raw) return null; + const lower = raw.toLowerCase(); + if (!lower.includes("image exceeds") || !lower.includes("mb")) return null; + const match = raw.match(IMAGE_SIZE_ERROR_RE); + return { + maxMb: match?.[1] ? Number.parseFloat(match[1]) : undefined, + raw, + }; +} + export function isImageSizeError(errorMessage?: string): boolean { if (!errorMessage) return false; - const lower = errorMessage.toLowerCase(); - return lower.includes("image exceeds") && lower.includes("mb"); + return Boolean(parseImageSizeError(errorMessage)); } export function isCloudCodeAssistFormatError(raw: string): boolean { diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 006172e14..870453f38 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -34,7 +34,7 @@ import { isContextOverflowError, isFailoverAssistantError, isFailoverErrorMessage, - isImageSizeError, + parseImageSizeError, parseImageDimensionError, isRateLimitAssistantError, isTimeoutErrorMessage, @@ -442,12 +442,17 @@ export async function runEmbeddedPiAgent( }; } // Handle image size errors with a user-friendly message (no retry needed) - if (isImageSizeError(errorText)) { + const imageSizeError = parseImageSizeError(errorText); + if (imageSizeError) { + const maxMb = imageSizeError.maxMb; + const maxMbLabel = + typeof maxMb === "number" && Number.isFinite(maxMb) ? `${maxMb}` : null; + const maxBytesHint = maxMbLabel ? ` (max ${maxMbLabel}MB)` : ""; return { payloads: [ { text: - "Image too large for the model (max 5MB). " + + `Image too large for the model${maxBytesHint}. ` + "Please compress or resize the image and try again.", isError: true, }, diff --git a/src/media/constants.ts b/src/media/constants.ts index 8577b6d20..e74ac6934 100644 --- a/src/media/constants.ts +++ b/src/media/constants.ts @@ -1,4 +1,4 @@ -export const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // 5MB (Anthropic API limit) +export const MAX_IMAGE_BYTES = 6 * 1024 * 1024; // 6MB export const MAX_AUDIO_BYTES = 16 * 1024 * 1024; // 16MB export const MAX_VIDEO_BYTES = 16 * 1024 * 1024; // 16MB export const MAX_DOCUMENT_BYTES = 100 * 1024 * 1024; // 100MB From 0b1c8db0ca1cbc9b7411fdb405f2f8c8b6479b5e Mon Sep 17 00:00:00 2001 From: Shadow Date: Tue, 27 Jan 2026 16:01:18 -0600 Subject: [PATCH 3/3] fix: handle image size errors safely (#2871) (thanks @Suksham-sharma) --- CHANGELOG.md | 1 + ...embedded-helpers.classifyfailoverreason.test.ts | 1 + .../pi-embedded-helpers.image-size-error.test.ts | 14 ++++++++++++++ 3 files changed, 16 insertions(+) create mode 100644 src/agents/pi-embedded-helpers.image-size-error.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 61327e5c5..9e39702f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ Status: unreleased. - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). ### Fixes +- Agents: prevent retries on oversized image errors and surface size limits. (#2871) Thanks @Suksham-sharma. - Agents: inherit provider baseUrl/api for inline models. (#2740) Thanks @lploc94. - Memory Search: keep auto provider model defaults and only include remote when configured. (#2576) Thanks @papago2355. - macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee. diff --git a/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts b/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts index bb449a6e4..749a52414 100644 --- a/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts +++ b/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts @@ -31,6 +31,7 @@ describe("classifyFailoverReason", () => { "messages.84.content.1.image.source.base64.data: At least one of the image dimensions exceed max allowed size for many-image requests: 2000 pixels", ), ).toBeNull(); + expect(classifyFailoverReason("image exceeds 5 MB maximum")).toBeNull(); }); it("classifies OpenAI usage limit errors as rate_limit", () => { expect(classifyFailoverReason("You have hit your ChatGPT usage limit (plus plan)")).toBe( diff --git a/src/agents/pi-embedded-helpers.image-size-error.test.ts b/src/agents/pi-embedded-helpers.image-size-error.test.ts new file mode 100644 index 000000000..75b165d8d --- /dev/null +++ b/src/agents/pi-embedded-helpers.image-size-error.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; + +import { parseImageSizeError } from "./pi-embedded-helpers.js"; + +describe("parseImageSizeError", () => { + it("parses max MB values from error text", () => { + expect(parseImageSizeError("image exceeds 5 MB maximum")?.maxMb).toBe(5); + expect(parseImageSizeError("Image exceeds 5.5 MB limit")?.maxMb).toBe(5.5); + }); + + it("returns null for unrelated errors", () => { + expect(parseImageSizeError("context overflow")).toBeNull(); + }); +});