mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-11-02 12:12:17 +00:00
`saveBase64Image` recorded the media type declared in the incoming data URL
while persisting bytes that sharp had re-encoded. Those are not always the same
format: sharp rasterizes SVG to PNG, so an image arriving as `image/svg+xml` was
stored as `<id>-<name>.svg`, typed `image/svg+xml`, holding PNG bytes.
That type is not cosmetic. `encode.js` hands `file.type` to providers verbatim —
`media_type` for Anthropic, `inlineData.mimeType` for Google, and the `data:`
prefix for OpenAI — so re-attaching such an image sends PNG bytes under a media
type the provider does not accept for images, and the request fails.
A declared type is also only ever a claim. The same mismatch arises whenever a
producer mislabels what it sends, which for MCP tool output means any connected
server can decide what the record says about bytes it did not have to match.
`resizeImageBuffer` already read the encoded output back to measure it, so it now
resolves that metadata to a media type and returns it, and `saveBase64Image`
records it in place of the declared one, falling back to the declared type only
when sharp reports a format that has no media type of its own. The upload paths
were already correct — they record `image/${imageOutputType}`, the format they
convert to — so this brings the tool-output path in line with them.
`resolveImageMimeType` lives in `packages/api` because AVIF and HEIC share the
heif container and are told apart by its compression, which is worth stating once
with tests rather than open-coding at each call site.
120 lines
5 KiB
JavaScript
120 lines
5 KiB
JavaScript
const sharp = require('sharp');
|
|
const { resolveImageMimeType } = require('@librechat/api');
|
|
const { EModelEndpoint } = require('librechat-data-provider');
|
|
|
|
/**
|
|
* Resizes an image from a given buffer based on the specified resolution.
|
|
*
|
|
* @param {Buffer} inputBuffer - The buffer of the image to be resized.
|
|
* @param {'low' | 'high' | {percentage?: number, px?: number}} resolution - The resolution to resize the image to.
|
|
* 'low' for a maximum of 512x512 resolution,
|
|
* 'high' for a maximum of 768x2000 resolution,
|
|
* or a custom object with percentage or px values.
|
|
* @param {EModelEndpoint} endpoint - Identifier for specific endpoint handling
|
|
* @returns {Promise<{buffer: Buffer, bytes: number, width: number, height: number, type: string | undefined}>}
|
|
* The resized image buffer, its dimensions, and the media type of the re-encoded bytes.
|
|
* `type` is `undefined` when sharp reports a format with no media type of its own.
|
|
* @throws Will throw an error if the resolution parameter is invalid.
|
|
*/
|
|
async function resizeImageBuffer(inputBuffer, resolution, endpoint) {
|
|
const maxLowRes = 512;
|
|
const maxShortSideHighRes = 768;
|
|
const maxLongSideHighRes = endpoint === EModelEndpoint.anthropic ? 1568 : 2000;
|
|
|
|
let customPercent, customPx;
|
|
if (resolution && typeof resolution === 'object') {
|
|
if (typeof resolution.percentage === 'number') {
|
|
customPercent = resolution.percentage;
|
|
} else if (typeof resolution.px === 'number') {
|
|
customPx = resolution.px;
|
|
}
|
|
}
|
|
|
|
let newWidth, newHeight;
|
|
let resizeOptions = { fit: 'inside', withoutEnlargement: true };
|
|
|
|
if (customPercent != null || customPx != null) {
|
|
// percentage-based resize
|
|
const metadata = await sharp(inputBuffer).metadata();
|
|
if (customPercent != null) {
|
|
newWidth = Math.round(metadata.width * (customPercent / 100));
|
|
newHeight = Math.round(metadata.height * (customPercent / 100));
|
|
} else {
|
|
// pixel max on both sides
|
|
newWidth = Math.min(metadata.width, customPx);
|
|
newHeight = Math.min(metadata.height, customPx);
|
|
}
|
|
resizeOptions.width = newWidth;
|
|
resizeOptions.height = newHeight;
|
|
} else if (resolution === 'low') {
|
|
resizeOptions.width = maxLowRes;
|
|
resizeOptions.height = maxLowRes;
|
|
} else if (resolution === 'high') {
|
|
const metadata = await sharp(inputBuffer).metadata();
|
|
const isWidthShorter = metadata.width < metadata.height;
|
|
|
|
if (isWidthShorter) {
|
|
// Width is the shorter side
|
|
newWidth = Math.min(metadata.width, maxShortSideHighRes);
|
|
// Calculate new height to maintain aspect ratio
|
|
newHeight = Math.round((metadata.height / metadata.width) * newWidth);
|
|
// Ensure the long side does not exceed the maximum allowed
|
|
if (newHeight > maxLongSideHighRes) {
|
|
newHeight = maxLongSideHighRes;
|
|
newWidth = Math.round((metadata.width / metadata.height) * newHeight);
|
|
}
|
|
} else {
|
|
// Height is the shorter side
|
|
newHeight = Math.min(metadata.height, maxShortSideHighRes);
|
|
// Calculate new width to maintain aspect ratio
|
|
newWidth = Math.round((metadata.width / metadata.height) * newHeight);
|
|
// Ensure the long side does not exceed the maximum allowed
|
|
if (newWidth > maxLongSideHighRes) {
|
|
newWidth = maxLongSideHighRes;
|
|
newHeight = Math.round((metadata.height / metadata.width) * newWidth);
|
|
}
|
|
}
|
|
|
|
resizeOptions.width = newWidth;
|
|
resizeOptions.height = newHeight;
|
|
} else {
|
|
throw new Error('Invalid resolution parameter');
|
|
}
|
|
|
|
const resizedBuffer = await sharp(inputBuffer).rotate().resize(resizeOptions).toBuffer();
|
|
|
|
const resizedMetadata = await sharp(resizedBuffer).metadata();
|
|
return {
|
|
buffer: resizedBuffer,
|
|
bytes: resizedMetadata.size,
|
|
width: resizedMetadata.width,
|
|
height: resizedMetadata.height,
|
|
type: resolveImageMimeType(resizedMetadata),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resizes an image buffer to a specified format and width.
|
|
*
|
|
* @param {Object} options - The options for resizing and converting the image.
|
|
* @param {Buffer} options.inputBuffer - The buffer of the image to be resized.
|
|
* @param {string} options.desiredFormat - The desired output format of the image.
|
|
* @param {number} [options.width=150] - The desired width of the image. Defaults to 150 pixels.
|
|
* @returns {Promise<{ buffer: Buffer, width: number, height: number, bytes: number }>} An object containing the resized image buffer, its size, and dimensions.
|
|
* @throws Will throw an error if the resolution or format parameters are invalid.
|
|
*/
|
|
async function resizeAndConvert({ inputBuffer, desiredFormat, width = 150 }) {
|
|
const resizedBuffer = await sharp(inputBuffer)
|
|
.resize({ width })
|
|
.toFormat(desiredFormat)
|
|
.toBuffer();
|
|
const resizedMetadata = await sharp(resizedBuffer).metadata();
|
|
return {
|
|
buffer: resizedBuffer,
|
|
width: resizedMetadata.width,
|
|
height: resizedMetadata.height,
|
|
bytes: Buffer.byteLength(resizedBuffer),
|
|
};
|
|
}
|
|
|
|
module.exports = { resizeImageBuffer, resizeAndConvert };
|