From 1bf77f82feafd72a7ab2a9e80b358647c0a2cb5c Mon Sep 17 00:00:00 2001 From: "MD.SHAMSUL ALAM" <113705486+shamsulalam1114@users.noreply.github.com> Date: Wed, 28 Jan 2026 03:07:26 +0600 Subject: [PATCH 1/3] fix(media): wire tools.media.image.maxBytes config to image processing pipeline Fixes #2954 The configuration setting tools.media.image.maxBytes was defined but not wired through the image sanitization pipeline. This resulted in hardcoded constants being used instead of the user-configured value. Changes: - Modified sanitizeSessionMessagesImages() to accept maxBytes in options - Modified sanitizeSessionHistory() to accept and pass maxBytes parameter - Updated attempt.ts to pass config value through the sanitization chain - All sanitizeContentBlocksImages() calls now receive the configured maxBytes This ensures that when users configure a custom image size limit, it is actually used throughout the image processing pipeline instead of being ignored. --- src/agents/pi-embedded-helpers/images.ts | 6 ++++++ src/agents/pi-embedded-runner/google.ts | 2 ++ src/agents/pi-embedded-runner/run/attempt.ts | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/agents/pi-embedded-helpers/images.ts b/src/agents/pi-embedded-helpers/images.ts index 518226ae0..c3cf28a60 100644 --- a/src/agents/pi-embedded-helpers/images.ts +++ b/src/agents/pi-embedded-helpers/images.ts @@ -38,6 +38,7 @@ export async function sanitizeSessionMessagesImages( allowBase64Only?: boolean; includeCamelCase?: boolean; }; + maxBytes?: number; }, ): Promise { const sanitizeMode = options?.sanitizeMode ?? "full"; @@ -62,6 +63,7 @@ export async function sanitizeSessionMessagesImages( const nextContent = (await sanitizeContentBlocksImages( content as ContentBlock[], label, + { maxBytes: options?.maxBytes }, )) as unknown as typeof toolMsg.content; out.push({ ...toolMsg, content: nextContent }); continue; @@ -74,6 +76,7 @@ export async function sanitizeSessionMessagesImages( const nextContent = (await sanitizeContentBlocksImages( content as unknown as ContentBlock[], label, + { maxBytes: options?.maxBytes }, )) as unknown as typeof userMsg.content; out.push({ ...userMsg, content: nextContent }); continue; @@ -88,6 +91,7 @@ export async function sanitizeSessionMessagesImages( const nextContent = (await sanitizeContentBlocksImages( content as unknown as ContentBlock[], label, + { maxBytes: options?.maxBytes }, )) as unknown as typeof assistantMsg.content; out.push({ ...assistantMsg, content: nextContent }); } else { @@ -101,6 +105,7 @@ export async function sanitizeSessionMessagesImages( const nextContent = (await sanitizeContentBlocksImages( content as unknown as ContentBlock[], label, + { maxBytes: options?.maxBytes }, )) as unknown as typeof assistantMsg.content; out.push({ ...assistantMsg, content: nextContent }); continue; @@ -118,6 +123,7 @@ export async function sanitizeSessionMessagesImages( const finalContent = (await sanitizeContentBlocksImages( filteredContent as unknown as ContentBlock[], label, + { maxBytes: options?.maxBytes }, )) as unknown as typeof assistantMsg.content; if (finalContent.length === 0) { continue; diff --git a/src/agents/pi-embedded-runner/google.ts b/src/agents/pi-embedded-runner/google.ts index 7b26d0d04..0bef4d3af 100644 --- a/src/agents/pi-embedded-runner/google.ts +++ b/src/agents/pi-embedded-runner/google.ts @@ -313,6 +313,7 @@ export async function sanitizeSessionHistory(params: { sessionManager: SessionManager; sessionId: string; policy?: TranscriptPolicy; + maxBytes?: number; }): Promise { // Keep docs/reference/transcript-hygiene.md in sync with any logic changes here. const policy = @@ -328,6 +329,7 @@ export async function sanitizeSessionHistory(params: { toolCallIdMode: policy.toolCallIdMode, preserveSignatures: policy.preserveSignatures, sanitizeThoughtSignatures: policy.sanitizeThoughtSignatures, + maxBytes: params.maxBytes, }); const sanitizedThinking = policy.normalizeAntigravityThinkingBlocks ? sanitizeAntigravityThinkingBlocks(sanitizedImages) diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index 46a53bd8f..8c068e2af 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -523,6 +523,7 @@ export async function runEmbeddedAttempt( sessionManager, sessionId: params.sessionId, policy: transcriptPolicy, + maxBytes: params.config?.tools?.media?.image?.maxBytes, }); cacheTrace?.recordStage("session:sanitized", { messages: prior }); const validatedGemini = transcriptPolicy.validateGeminiTurns @@ -743,7 +744,7 @@ export async function runEmbeddedAttempt( model: params.model, existingImages: params.images, historyMessages: activeSession.messages, - maxBytes: MAX_IMAGE_BYTES, + maxBytes: params.config?.tools?.media?.image?.maxBytes ?? MAX_IMAGE_BYTES, // Enforce sandbox path restrictions when sandbox is enabled sandboxRoot: sandbox?.enabled ? sandbox.workspaceDir : undefined, }); From 8d15d6e71fdbef125ad7e361e37fd7f077e0719a Mon Sep 17 00:00:00 2001 From: "MD.SHAMSUL ALAM" <113705486+shamsulalam1114@users.noreply.github.com> Date: Wed, 28 Jan 2026 03:30:49 +0600 Subject: [PATCH 2/3] docs: add changelog entry for issue #2954 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c83dc1476..930c17f65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Status: unreleased. ### Changes - Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope. +- Media: wire tools.media.image.maxBytes config to image processing pipeline. (#2954) Thanks @shamsulalam1114. - Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev. - macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk). - macOS: finish Moltbot app rename for macOS sources, bundle identifiers, and shared kit paths. (#2844) Thanks @fal3. From 37c489dec45b337490f840d294379f4cc54753cf Mon Sep 17 00:00:00 2001 From: "MD.SHAMSUL ALAM" <113705486+shamsulalam1114@users.noreply.github.com> Date: Wed, 28 Jan 2026 04:31:12 +0600 Subject: [PATCH 3/3] few changes --- CHANGELOG.md | 2 +- src/agents/pi-embedded-helpers/errors.ts | 53 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 930c17f65..972ca7781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Status: unreleased. ### Changes - Rebrand: rename the npm package/CLI to `moltbot`, add a `moltbot` compatibility shim, and move extensions to the `@moltbot/*` scope. -- Media: wire tools.media.image.maxBytes config to image processing pipeline. (#2954) Thanks @shamsulalam1114. +- Gateway: add user-friendly error messages for API rate limits (429) and payment issues (402). (#2202) Thanks @shamsulalam1114. - Commands: group /help and /commands output with Telegram paging. (#2504) Thanks @hougangdev. - macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk). - macOS: finish Moltbot app rename for macOS sources, bundle identifiers, and shared kit paths. (#2844) Thanks @fal3. diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index d6e33f924..3403de38f 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -299,6 +299,23 @@ export function formatAssistantErrorText( return "The AI service is temporarily overloaded. Please try again in a moment."; } + // Handle rate limit errors (429) with user-friendly messaging + if (isRateLimitErrorMessage(raw)) { + const retryAfter = extractRetryAfterSeconds(raw); + const retryHint = retryAfter + ? ` Please try again in ${formatRetryDuration(retryAfter)}.` + : " Please try again in a few minutes."; + return `⚠️ Rate limit reached.${retryHint} You may need to upgrade your API plan or wait for the limit to reset.`; + } + + // Handle billing/payment errors (402) + if (isBillingErrorMessage(raw)) { + return ( + "⚠️ API quota or payment issue detected. " + + "Please check your account billing and credits at your provider's dashboard." + ); + } + if (isLikelyHttpErrorText(raw) || isRawApiErrorPayload(raw)) { return formatRawAssistantErrorForUi(raw); } @@ -495,3 +512,39 @@ export function isFailoverAssistantError(msg: AssistantMessage | undefined): boo if (!msg || msg.stopReason !== "error") return false; return isFailoverErrorMessage(msg.errorMessage ?? ""); } + +/** + * Extract retry-after duration from error message (in seconds). + * Checks for Retry-After header or x-ratelimit-reset in the error payload. + */ +function extractRetryAfterSeconds(raw: string): number | null { + if (!raw) return null; + + // Look for Retry-After header value (seconds) + const retryAfterMatch = raw.match(/retry[_-]?after[:\s]+(\d+)/i); + if (retryAfterMatch?.[1]) { + return Number.parseInt(retryAfterMatch[1], 10); + } + + // Look for x-ratelimit-reset (Unix timestamp) + const resetMatch = raw.match(/x[_-]?ratelimit[_-]?reset[:\s]+(\d+)/i); + if (resetMatch?.[1]) { + const resetTime = Number.parseInt(resetMatch[1], 10); + const now = Math.floor(Date.now() / 1000); + const seconds = resetTime - now; + return seconds > 0 ? seconds : null; + } + + return null; +} + +/** + * Format retry duration in human-readable form. + */ +function formatRetryDuration(seconds: number): string { + if (seconds < 60) return `${seconds} second${seconds !== 1 ? "s" : ""}`; + const minutes = Math.ceil(seconds / 60); + if (minutes < 60) return `${minutes} minute${minutes !== 1 ? "s" : ""}`; + const hours = Math.ceil(minutes / 60); + return `${hours} hour${hours !== 1 ? "s" : ""}`; +}