Merge branch 'main' into fix/issue-2954-image-maxbytes-config

This commit is contained in:
MD. SHAMSUL ALAM 2026-01-28 04:04:56 +06:00 committed by GitHub
commit 840a3962b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 69 additions and 1 deletions

View File

@ -70,6 +70,7 @@ Status: unreleased.
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed). - **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
### Fixes ### 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. - 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. - 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. - macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee.

View File

@ -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", "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(); ).toBeNull();
expect(classifyFailoverReason("image exceeds 5 MB maximum")).toBeNull();
}); });
it("classifies OpenAI usage limit errors as rate_limit", () => { it("classifies OpenAI usage limit errors as rate_limit", () => {
expect(classifyFailoverReason("You have hit your ChatGPT usage limit (plus plan)")).toBe( expect(classifyFailoverReason("You have hit your ChatGPT usage limit (plus plan)")).toBe(

View File

@ -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();
});
});

View File

@ -23,12 +23,14 @@ export {
isFailoverAssistantError, isFailoverAssistantError,
isFailoverErrorMessage, isFailoverErrorMessage,
isImageDimensionErrorMessage, isImageDimensionErrorMessage,
isImageSizeError,
isOverloadedErrorMessage, isOverloadedErrorMessage,
isRawApiErrorPayload, isRawApiErrorPayload,
isRateLimitAssistantError, isRateLimitAssistantError,
isRateLimitErrorMessage, isRateLimitErrorMessage,
isTimeoutErrorMessage, isTimeoutErrorMessage,
parseImageDimensionError, parseImageDimensionError,
parseImageSizeError,
} from "./pi-embedded-helpers/errors.js"; } from "./pi-embedded-helpers/errors.js";
export { isGoogleModelApi, sanitizeGoogleTurnOrdering } from "./pi-embedded-helpers/google.js"; export { isGoogleModelApi, sanitizeGoogleTurnOrdering } from "./pi-embedded-helpers/google.js";

View File

@ -401,6 +401,7 @@ const ERROR_PATTERNS = {
const IMAGE_DIMENSION_ERROR_RE = const IMAGE_DIMENSION_ERROR_RE =
/image dimensions exceed max allowed size for many-image requests:\s*(\d+)\s*pixels/i; /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_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 { function matchesErrorPatterns(raw: string, patterns: readonly ErrorPattern[]): boolean {
if (!raw) return false; if (!raw) return false;
@ -467,6 +468,25 @@ export function isImageDimensionErrorMessage(raw: string): boolean {
return Boolean(parseImageDimensionError(raw)); 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;
return Boolean(parseImageSizeError(errorMessage));
}
export function isCloudCodeAssistFormatError(raw: string): boolean { export function isCloudCodeAssistFormatError(raw: string): boolean {
return !isImageDimensionErrorMessage(raw) && matchesErrorPatterns(raw, ERROR_PATTERNS.format); return !isImageDimensionErrorMessage(raw) && matchesErrorPatterns(raw, ERROR_PATTERNS.format);
} }
@ -478,6 +498,7 @@ export function isAuthAssistantError(msg: AssistantMessage | undefined): boolean
export function classifyFailoverReason(raw: string): FailoverReason | null { export function classifyFailoverReason(raw: string): FailoverReason | null {
if (isImageDimensionErrorMessage(raw)) return null; if (isImageDimensionErrorMessage(raw)) return null;
if (isImageSizeError(raw)) return null;
if (isRateLimitErrorMessage(raw)) return "rate_limit"; if (isRateLimitErrorMessage(raw)) return "rate_limit";
if (isOverloadedErrorMessage(raw)) return "rate_limit"; if (isOverloadedErrorMessage(raw)) return "rate_limit";
if (isCloudCodeAssistFormatError(raw)) return "format"; if (isCloudCodeAssistFormatError(raw)) return "format";

View File

@ -34,6 +34,7 @@ import {
isContextOverflowError, isContextOverflowError,
isFailoverAssistantError, isFailoverAssistantError,
isFailoverErrorMessage, isFailoverErrorMessage,
parseImageSizeError,
parseImageDimensionError, parseImageDimensionError,
isRateLimitAssistantError, isRateLimitAssistantError,
isTimeoutErrorMessage, isTimeoutErrorMessage,
@ -440,6 +441,34 @@ export async function runEmbeddedPiAgent(
}, },
}; };
} }
// Handle image size errors with a user-friendly message (no retry needed)
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${maxBytesHint}. ` +
"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); const promptFailoverReason = classifyFailoverReason(errorText);
if (promptFailoverReason && promptFailoverReason !== "timeout" && lastProfileId) { if (promptFailoverReason && promptFailoverReason !== "timeout" && lastProfileId) {
await markAuthProfileFailure({ await markAuthProfileFailure({

View File

@ -20,7 +20,7 @@ export type EmbeddedPiRunMeta = {
aborted?: boolean; aborted?: boolean;
systemPromptReport?: SessionSystemPromptReport; systemPromptReport?: SessionSystemPromptReport;
error?: { error?: {
kind: "context_overflow" | "compaction_failure" | "role_ordering"; kind: "context_overflow" | "compaction_failure" | "role_ordering" | "image_size";
message: string; message: string;
}; };
/** Stop reason for the agent run (e.g., "completed", "tool_calls"). */ /** Stop reason for the agent run (e.g., "completed", "tool_calls"). */