mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
fix: align mcp transform pipeline with Anthropic Claude Code 2.1.128
Add ImageLimits type and plumb optional limits through the chain: callMCPTool/callMCPToolWithUrlElicitationRetry -> processMCPResult -> transformMCPResult -> transformResultContent -> maybeResizeAndDownsampleImageBuffer. When provided, limits override the module-level defaults (IMAGE_TARGET_RAW_SIZE, IMAGE_MAX_WIDTH, IMAGE_MAX_HEIGHT, API_IMAGE_MAX_BASE64_SIZE) inside maybeResizeAndDownsampleImageBuffer. When undefined, behavior is unchanged for current callers. Add _meta preservation in the text-block case of transformResultContent (only when the caller opts in via includeMeta=true). transformMCPResult passes includeMeta=true on the tool-result path; the prompt-handler call site keeps the default false, preserving prior behavior. Add skipLargeOutput early-return in processMCPResult after the IDE check: when the caller passes skipLargeOutput=true and the content has no images, the function returns content directly without large-output handling. Add unwrap-to-text in processMCPResult for the persisted-content path: when the large-string format gate is enabled (MCP_TRUNCATION_PROMPT_OVERRIDE env var, or tengu_mcp_subagent_prompt Statsig gate), and the content is a single bare text block (no annotations, no _meta), unwrap to raw text and switch the format description to 'Plain text'. Default-off; gate-off behavior is unchanged. Verified structurally against the 2.1.128 binary: function signatures, the IDE check, gate logic, _meta-unwrap pattern, and imageLimits plumbing match this implementation.
This commit is contained in:
@@ -162,6 +162,16 @@ interface CompressedImageResult {
|
||||
originalSize: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-call image processing limits, overriding the module-level defaults.
|
||||
*/
|
||||
export interface ImageLimits {
|
||||
targetRawSize: number
|
||||
maxWidth: number
|
||||
maxHeight: number
|
||||
maxBase64Size: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracted from FileReadTool's readImage function
|
||||
* Resizes image buffer to meet size and dimension constraints
|
||||
@@ -170,7 +180,13 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
imageBuffer: Buffer,
|
||||
originalSize: number,
|
||||
ext: string,
|
||||
limits?: ImageLimits,
|
||||
): Promise<ResizeResult> {
|
||||
const targetRawSize = limits?.targetRawSize ?? IMAGE_TARGET_RAW_SIZE
|
||||
const maxWidth = limits?.maxWidth ?? IMAGE_MAX_WIDTH
|
||||
const maxHeight = limits?.maxHeight ?? IMAGE_MAX_HEIGHT
|
||||
const maxBase64Size = limits?.maxBase64Size ?? API_IMAGE_MAX_BASE64_SIZE
|
||||
|
||||
if (imageBuffer.length === 0) {
|
||||
// Empty buffer would fall through the catch block below (sharp throws
|
||||
// "Unable to determine image format"), and the fallback's size check
|
||||
@@ -189,7 +205,7 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
|
||||
// If dimensions aren't available from metadata
|
||||
if (!metadata.width || !metadata.height) {
|
||||
if (originalSize > IMAGE_TARGET_RAW_SIZE) {
|
||||
if (originalSize > targetRawSize) {
|
||||
// Create fresh sharp instance for compression
|
||||
const compressedBuffer = await sharp(imageBuffer)
|
||||
.jpeg({ quality: 80 })
|
||||
@@ -210,9 +226,9 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
|
||||
// Check if the original file just works
|
||||
if (
|
||||
originalSize <= IMAGE_TARGET_RAW_SIZE &&
|
||||
width <= IMAGE_MAX_WIDTH &&
|
||||
height <= IMAGE_MAX_HEIGHT
|
||||
originalSize <= targetRawSize &&
|
||||
width <= maxWidth &&
|
||||
height <= maxHeight
|
||||
) {
|
||||
return {
|
||||
buffer: imageBuffer,
|
||||
@@ -226,20 +242,19 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
}
|
||||
}
|
||||
|
||||
const needsDimensionResize =
|
||||
width > IMAGE_MAX_WIDTH || height > IMAGE_MAX_HEIGHT
|
||||
const needsDimensionResize = width > maxWidth || height > maxHeight
|
||||
const isPng = normalizedMediaType === 'png'
|
||||
|
||||
// If dimensions are within limits but file is too large, try compression first
|
||||
// This preserves full resolution when possible
|
||||
if (!needsDimensionResize && originalSize > IMAGE_TARGET_RAW_SIZE) {
|
||||
if (!needsDimensionResize && originalSize > targetRawSize) {
|
||||
// For PNGs, try PNG compression first to preserve transparency
|
||||
if (isPng) {
|
||||
// Create fresh sharp instance for each compression attempt
|
||||
const pngCompressed = await sharp(imageBuffer)
|
||||
.png({ compressionLevel: 9, palette: true })
|
||||
.toBuffer()
|
||||
if (pngCompressed.length <= IMAGE_TARGET_RAW_SIZE) {
|
||||
if (pngCompressed.length <= targetRawSize) {
|
||||
return {
|
||||
buffer: pngCompressed,
|
||||
mediaType: 'png',
|
||||
@@ -258,7 +273,7 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
const compressedBuffer = await sharp(imageBuffer)
|
||||
.jpeg({ quality })
|
||||
.toBuffer()
|
||||
if (compressedBuffer.length <= IMAGE_TARGET_RAW_SIZE) {
|
||||
if (compressedBuffer.length <= targetRawSize) {
|
||||
return {
|
||||
buffer: compressedBuffer,
|
||||
mediaType: 'jpeg',
|
||||
@@ -275,14 +290,14 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
}
|
||||
|
||||
// Constrain dimensions if needed
|
||||
if (width > IMAGE_MAX_WIDTH) {
|
||||
height = Math.round((height * IMAGE_MAX_WIDTH) / width)
|
||||
width = IMAGE_MAX_WIDTH
|
||||
if (width > maxWidth) {
|
||||
height = Math.round((height * maxWidth) / width)
|
||||
width = maxWidth
|
||||
}
|
||||
|
||||
if (height > IMAGE_MAX_HEIGHT) {
|
||||
width = Math.round((width * IMAGE_MAX_HEIGHT) / height)
|
||||
height = IMAGE_MAX_HEIGHT
|
||||
if (height > maxHeight) {
|
||||
width = Math.round((width * maxHeight) / height)
|
||||
height = maxHeight
|
||||
}
|
||||
|
||||
// IMPORTANT: Always create fresh sharp(imageBuffer) instances for each operation.
|
||||
@@ -298,7 +313,7 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
.toBuffer()
|
||||
|
||||
// If still too large after resize, try compression
|
||||
if (resizedImageBuffer.length > IMAGE_TARGET_RAW_SIZE) {
|
||||
if (resizedImageBuffer.length > targetRawSize) {
|
||||
// For PNGs, try PNG compression first to preserve transparency
|
||||
if (isPng) {
|
||||
const pngCompressed = await sharp(imageBuffer)
|
||||
@@ -308,7 +323,7 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
})
|
||||
.png({ compressionLevel: 9, palette: true })
|
||||
.toBuffer()
|
||||
if (pngCompressed.length <= IMAGE_TARGET_RAW_SIZE) {
|
||||
if (pngCompressed.length <= targetRawSize) {
|
||||
return {
|
||||
buffer: pngCompressed,
|
||||
mediaType: 'png',
|
||||
@@ -331,7 +346,7 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
})
|
||||
.jpeg({ quality })
|
||||
.toBuffer()
|
||||
if (compressedBuffer.length <= IMAGE_TARGET_RAW_SIZE) {
|
||||
if (compressedBuffer.length <= targetRawSize) {
|
||||
return {
|
||||
buffer: compressedBuffer,
|
||||
mediaType: 'jpeg',
|
||||
@@ -407,11 +422,11 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
imageBuffer[1] === 0x50 &&
|
||||
imageBuffer[2] === 0x4e &&
|
||||
imageBuffer[3] === 0x47 &&
|
||||
(imageBuffer.readUInt32BE(16) > IMAGE_MAX_WIDTH ||
|
||||
imageBuffer.readUInt32BE(20) > IMAGE_MAX_HEIGHT)
|
||||
(imageBuffer.readUInt32BE(16) > maxWidth ||
|
||||
imageBuffer.readUInt32BE(20) > maxHeight)
|
||||
|
||||
// If original image's base64 encoding is within API limit, allow it through uncompressed
|
||||
if (base64Size <= API_IMAGE_MAX_BASE64_SIZE && !overDim) {
|
||||
if (base64Size <= maxBase64Size && !overDim) {
|
||||
logEvent('tengu_image_resize_fallback', {
|
||||
original_size_bytes: originalSize,
|
||||
base64_size_bytes: base64Size,
|
||||
@@ -423,7 +438,7 @@ export async function maybeResizeAndDownsampleImageBuffer(
|
||||
// Image is too large and we failed to compress it - fail with user-friendly error
|
||||
throw new ImageResizeError(
|
||||
overDim
|
||||
? `Unable to resize image — dimensions exceed the ${IMAGE_MAX_WIDTH}x${IMAGE_MAX_HEIGHT}px limit and image processing failed. ` +
|
||||
? `Unable to resize image — dimensions exceed the ${maxWidth}x${maxHeight}px limit and image processing failed. ` +
|
||||
`Please resize the image to reduce its pixel dimensions.`
|
||||
: `Unable to resize image (${formatFileSize(originalSize)} raw, ${formatFileSize(base64Size)} base64). ` +
|
||||
`The image exceeds the 5MB API limit and compression failed. ` +
|
||||
|
||||
Reference in New Issue
Block a user