co-mono/packages/ai/test/image-limits.test.ts
Mario Zechner e1ce9c1f49 Fix image limits test to use realistic payload sizes
Previous test used compressed 8k images (0.01MB) which was meaningless.
Now tests with actual large noise images that don't compress.

Realistic payload limits discovered:
- Anthropic: 6 x 3MB = ~18MB total (not 32MB as documented)
- OpenAI: 2 x 15MB = ~30MB total
- Gemini: 10 x 20MB = ~200MB total (very permissive)
- Mistral: 4 x 10MB = ~40MB total
- xAI: 1 x 20MB (strict request size limit)
- Groq: 5 x 5760px images (5 image + pixel limit)
- zAI: 2 x 15MB = ~30MB (50MB request limit)
- OpenRouter: 2 x 5MB = ~10MB total

Also fixed GEMINI_API_KEY env var (was GOOGLE_API_KEY).

Related to #120
2025-12-16 23:48:59 +01:00

1120 lines
39 KiB
TypeScript

/**
* Image limits test suite
*
* Tests provider-specific image limitations:
* - Maximum number of images in a context (with small 100x100 images)
* - Maximum image size (bytes)
* - Maximum image dimensions
* - Maximum payload (realistic large images stress test)
*
* ============================================================================
* DISCOVERED LIMITS (Dec 2025):
* ============================================================================
*
* BASIC LIMITS (small images):
* | Provider | Model | Max Images | Max Size | Max Dim |
* |-------------|--------------------|------------|----------|----------|
* | Anthropic | claude-3-5-haiku | 100 | 5MB | 8000px |
* | OpenAI | gpt-4o-mini | 500 | ≥25MB | ≥20000px |
* | Gemini | gemini-2.5-flash | ~2000* | ≥40MB | 8000px |
* | Mistral | pixtral-12b | 8 | ~15MB | 8000px |
* | xAI | grok-2-vision | ≥100 | 25MB | 8000px |
* | Groq | llama-4-scout-17b | 5 | ~5MB | ~5760px**|
* | zAI | glm-4.5v | *** | ≥20MB | 8000px |
* | OpenRouter | z-ai/glm-4.5v | *** | ~10MB | ≥20000px |
*
* REALISTIC PAYLOAD LIMITS (large images):
* | Provider | Image Size | Max Count | Total Payload | Limit Hit |
* |-------------|------------|-----------|---------------|---------------------|
* | Anthropic | ~3MB | 6 | ~18MB | Request too large |
* | OpenAI | ~15MB | 2 | ~30MB | Generic error |
* | Gemini | ~20MB | 10 | ~200MB | String length |
* | Mistral | ~10MB | 4 | ~40MB | 413 Payload too large|
* | xAI | ~20MB | 1 | ~20MB | 413 Entity too large|
* | Groq | 5760px | 5 | N/A | 5 image limit |
* | zAI | ~15MB | 2 | ~30MB | 50MB request limit |
* | OpenRouter | ~5MB | 2 | ~10MB | Provider error |
*
* Notes:
* - Anthropic: 100 image hard limit, 5MB per image, but ~18MB total request
* limit in practice (32MB documented but hit limit at ~24MB).
* - OpenAI: 500 image limit but total payload limited to ~30-45MB.
* - Gemini: * Very permissive. 10 x 20MB = 200MB worked!
* - Mistral: 8 images max, ~40MB total payload.
* - xAI: 25MB per image but strict request size limit (~20MB total).
* - Groq: ** Most restrictive. 5 images max, 33177600 pixels max (≈5760x5760).
* - zAI: 50MB request limit (explicit in error message).
* - OpenRouter: *** Context-window limited (65536 tokens).
*
* ============================================================================
* PRACTICAL RECOMMENDATIONS FOR CODING AGENTS:
* ============================================================================
*
* Conservative cross-provider safe limits:
* - Max 2 images per request at ~5MB each (~10MB total)
* - Max 5760px dimension (for Groq pixel limit)
*
* If excluding Groq:
* - Max 4 images per request at ~5MB each (~20MB total)
* - Max 8000px dimension
*
* For Anthropic-only (most common case):
* - Max 6 images at ~3MB each OR 100 images at <200KB each
* - Max 5MB per image
* - Max 8000px dimension
* - Stay under ~18MB total request size
*
* ============================================================================
*/
import { execSync } from "node:child_process";
import { mkdirSync, rmSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { getModel } from "../src/models.js";
import { complete } from "../src/stream.js";
import type { Api, Context, ImageContent, Model, OptionsForApi, UserMessage } from "../src/types.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Temp directory for generated images
const TEMP_DIR = join(__dirname, ".temp-images");
/**
* Generate a valid PNG image of specified dimensions using ImageMagick
*/
function generateImage(width: number, height: number, filename: string): string {
const filepath = join(TEMP_DIR, filename);
execSync(`magick -size ${width}x${height} xc:red "${filepath}"`, { stdio: "ignore" });
const buffer = require("fs").readFileSync(filepath);
return buffer.toString("base64");
}
/**
* Generate a valid PNG image of approximately the specified size in bytes
*/
function generateImageWithSize(targetBytes: number, filename: string): string {
const filepath = join(TEMP_DIR, filename);
// Use uncompressed PNG to get predictable sizes
// Each pixel is 3 bytes (RGB), plus PNG overhead (~100 bytes)
// For a square image: side = sqrt(targetBytes / 3)
const side = Math.ceil(Math.sqrt(targetBytes / 3));
// Use noise pattern to prevent compression from shrinking the file
execSync(`magick -size ${side}x${side} xc: +noise Random -depth 8 PNG24:"${filepath}"`, { stdio: "ignore" });
// Check actual size and adjust if needed
const stats = require("fs").statSync(filepath);
if (stats.size < targetBytes * 0.8) {
// If too small, increase dimensions
const newSide = Math.ceil(side * Math.sqrt(targetBytes / stats.size));
execSync(`magick -size ${newSide}x${newSide} xc: +noise Random -depth 8 PNG24:"${filepath}"`, {
stdio: "ignore",
});
}
const buffer = require("fs").readFileSync(filepath);
return buffer.toString("base64");
}
/**
* Create a user message with multiple images
*/
function createMultiImageMessage(imageCount: number, imageBase64: string): UserMessage {
const content: (ImageContent | { type: "text"; text: string })[] = [
{ type: "text", text: `I am sending you ${imageCount} images. Just reply with "received ${imageCount}".` },
];
for (let i = 0; i < imageCount; i++) {
content.push({
type: "image",
data: imageBase64,
mimeType: "image/png",
});
}
return {
role: "user",
content,
timestamp: Date.now(),
};
}
/**
* Test sending a specific number of images to a model
*/
async function testImageCount<TApi extends Api>(
model: Model<TApi>,
imageCount: number,
imageBase64: string,
options?: OptionsForApi<TApi>,
): Promise<{ success: boolean; error?: string }> {
const context: Context = {
messages: [createMultiImageMessage(imageCount, imageBase64)],
};
try {
const response = await complete(model, context, options);
if (response.stopReason === "error") {
return { success: false, error: response.errorMessage };
}
return { success: true };
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : String(e) };
}
}
/**
* Test sending an image of a specific size
*/
async function testImageSize<TApi extends Api>(
model: Model<TApi>,
imageBase64: string,
options?: OptionsForApi<TApi>,
): Promise<{ success: boolean; error?: string }> {
const context: Context = {
messages: [
{
role: "user",
content: [
{ type: "text", text: "I am sending you an image. Just reply with 'received'." },
{ type: "image", data: imageBase64, mimeType: "image/png" },
],
timestamp: Date.now(),
},
],
};
try {
const response = await complete(model, context, options);
if (response.stopReason === "error") {
return { success: false, error: response.errorMessage };
}
return { success: true };
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : String(e) };
}
}
/**
* Test sending an image with specific dimensions
*/
async function testImageDimensions<TApi extends Api>(
model: Model<TApi>,
imageBase64: string,
options?: OptionsForApi<TApi>,
): Promise<{ success: boolean; error?: string }> {
const context: Context = {
messages: [
{
role: "user",
content: [
{ type: "text", text: "I am sending you an image. Just reply with 'received'." },
{ type: "image", data: imageBase64, mimeType: "image/png" },
],
timestamp: Date.now(),
},
],
};
try {
const response = await complete(model, context, options);
if (response.stopReason === "error") {
return { success: false, error: response.errorMessage };
}
return { success: true };
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : String(e) };
}
}
/**
* Find the maximum value that succeeds using linear search
*/
async function findLimit(
testFn: (value: number) => Promise<{ success: boolean; error?: string }>,
min: number,
max: number,
step: number,
): Promise<{ limit: number; lastError?: string }> {
let lastSuccess = min;
let lastError: string | undefined;
for (let value = min; value <= max; value += step) {
console.log(` Testing value: ${value}...`);
const result = await testFn(value);
if (result.success) {
lastSuccess = value;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
return { limit: lastSuccess, lastError };
}
// =============================================================================
// Provider-specific test suites
// =============================================================================
describe("Image Limits E2E Tests", () => {
let smallImage: string; // 100x100 for count tests
beforeAll(() => {
// Create temp directory
mkdirSync(TEMP_DIR, { recursive: true });
// Generate small test image for count tests
smallImage = generateImage(100, 100, "small.png");
});
afterAll(() => {
// Clean up temp directory
rmSync(TEMP_DIR, { recursive: true, force: true });
});
// -------------------------------------------------------------------------
// Anthropic (claude-3-5-haiku-20241022)
// Limits: 100 images, 5MB per image, 8000px max dimension
// -------------------------------------------------------------------------
describe.skipIf(!process.env.ANTHROPIC_API_KEY)("Anthropic (claude-3-5-haiku-20241022)", () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
it("should accept a small number of images (5)", async () => {
const result = await testImageCount(model, 5, smallImage);
expect(result.success, result.error).toBe(true);
});
it("should find maximum image count limit", { timeout: 600000 }, async () => {
// Known limit: 100 images
const { limit, lastError } = await findLimit((count) => testImageCount(model, count, smallImage), 20, 120, 20);
console.log(`\n Anthropic max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(80);
expect(limit).toBeLessThanOrEqual(100);
});
it("should find maximum image size limit", { timeout: 600000 }, async () => {
const MB = 1024 * 1024;
// Known limit: 5MB per image
const sizes = [1, 2, 3, 4, 5, 6];
let lastSuccess = 0;
let lastError: string | undefined;
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n Anthropic max image size: ~${lastSuccess}MB (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(1);
});
it("should find maximum image dimension limit", { timeout: 600000 }, async () => {
// Known limit: 8000px
const dimensions = [1000, 2000, 4000, 6000, 8000, 10000];
let lastSuccess = 0;
let lastError: string | undefined;
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n Anthropic max dimension: ~${lastSuccess}px (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(6000);
expect(lastSuccess).toBeLessThanOrEqual(8000);
});
});
// -------------------------------------------------------------------------
// OpenAI (gpt-4o-mini via openai-completions)
// Limits: 500 images, ~20MB per image (documented)
// -------------------------------------------------------------------------
describe.skipIf(!process.env.OPENAI_API_KEY)("OpenAI (gpt-4o-mini)", () => {
const model: Model<"openai-completions"> = { ...getModel("openai", "gpt-4o-mini"), api: "openai-completions" };
it("should accept a small number of images (5)", async () => {
const result = await testImageCount(model, 5, smallImage);
expect(result.success, result.error).toBe(true);
});
it("should find maximum image count limit", { timeout: 600000 }, async () => {
// Known limit: 500 images
const { limit, lastError } = await findLimit(
(count) => testImageCount(model, count, smallImage),
100,
600,
100,
);
console.log(`\n OpenAI max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(400);
expect(limit).toBeLessThanOrEqual(500);
});
it("should find maximum image size limit", { timeout: 600000 }, async () => {
const MB = 1024 * 1024;
// Documented limit: 20MB
const sizes = [5, 10, 15, 20, 25];
let lastSuccess = 0;
let lastError: string | undefined;
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n OpenAI max image size: ~${lastSuccess}MB (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(15);
});
it("should find maximum image dimension limit", { timeout: 600000 }, async () => {
const dimensions = [2000, 4000, 8000, 16000, 20000];
let lastSuccess = 0;
let lastError: string | undefined;
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n OpenAI max dimension: ~${lastSuccess}px (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(2000);
});
});
// -------------------------------------------------------------------------
// Google Gemini (gemini-2.5-flash)
// Limits: Very high (~2500 images), large size support
// -------------------------------------------------------------------------
describe.skipIf(!process.env.GEMINI_API_KEY)("Gemini (gemini-2.5-flash)", () => {
const model = getModel("google", "gemini-2.5-flash");
it("should accept a small number of images (5)", async () => {
const result = await testImageCount(model, 5, smallImage);
expect(result.success, result.error).toBe(true);
});
it("should find maximum image count limit", { timeout: 900000 }, async () => {
// Known to work up to ~2500, hits errors around 3000
const { limit, lastError } = await findLimit(
(count) => testImageCount(model, count, smallImage),
500,
3000,
500,
);
console.log(`\n Gemini max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(500);
});
it("should find maximum image size limit", { timeout: 600000 }, async () => {
const MB = 1024 * 1024;
// Very permissive, tested up to 60MB successfully
const sizes = [10, 20, 30, 40];
let lastSuccess = 0;
let lastError: string | undefined;
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n Gemini max image size: ~${lastSuccess}MB (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(20);
});
it("should find maximum image dimension limit", { timeout: 600000 }, async () => {
const dimensions = [2000, 4000, 8000, 16000, 20000];
let lastSuccess = 0;
let lastError: string | undefined;
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n Gemini max dimension: ~${lastSuccess}px (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(2000);
});
});
// -------------------------------------------------------------------------
// Mistral (pixtral-12b)
// Limits: ~8 images, ~15MB per image
// -------------------------------------------------------------------------
describe.skipIf(!process.env.MISTRAL_API_KEY)("Mistral (pixtral-12b)", () => {
const model = getModel("mistral", "pixtral-12b");
it("should accept a small number of images (5)", async () => {
const result = await testImageCount(model, 5, smallImage);
expect(result.success, result.error).toBe(true);
});
it("should find maximum image count limit", { timeout: 600000 }, async () => {
// Known to fail around 9 images
const { limit, lastError } = await findLimit((count) => testImageCount(model, count, smallImage), 5, 15, 1);
console.log(`\n Mistral max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(5);
});
it("should find maximum image size limit", { timeout: 600000 }, async () => {
const MB = 1024 * 1024;
const sizes = [5, 10, 15, 20];
let lastSuccess = 0;
let lastError: string | undefined;
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n Mistral max image size: ~${lastSuccess}MB (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(5);
});
it("should find maximum image dimension limit", { timeout: 600000 }, async () => {
const dimensions = [2000, 4000, 8000, 16000, 20000];
let lastSuccess = 0;
let lastError: string | undefined;
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n Mistral max dimension: ~${lastSuccess}px (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(2000);
});
});
// -------------------------------------------------------------------------
// OpenRouter (z-ai/glm-4.5v)
// Limits: Context-window limited (~45 images at 100x100), ~15MB per image
// -------------------------------------------------------------------------
describe.skipIf(!process.env.OPENROUTER_API_KEY)("OpenRouter (z-ai/glm-4.5v)", () => {
const model = getModel("openrouter", "z-ai/glm-4.5v");
it("should accept a small number of images (5)", async () => {
const result = await testImageCount(model, 5, smallImage);
expect(result.success, result.error).toBe(true);
});
it("should find maximum image count limit", { timeout: 600000 }, async () => {
// Limited by context window, not explicit image limit
const { limit, lastError } = await findLimit((count) => testImageCount(model, count, smallImage), 10, 60, 10);
console.log(`\n OpenRouter max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(10);
});
it("should find maximum image size limit", { timeout: 600000 }, async () => {
const MB = 1024 * 1024;
const sizes = [5, 10, 15, 20];
let lastSuccess = 0;
let lastError: string | undefined;
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n OpenRouter max image size: ~${lastSuccess}MB (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(5);
});
it("should find maximum image dimension limit", { timeout: 600000 }, async () => {
const dimensions = [2000, 4000, 8000, 16000, 20000];
let lastSuccess = 0;
let lastError: string | undefined;
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n OpenRouter max dimension: ~${lastSuccess}px (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(2000);
});
});
// -------------------------------------------------------------------------
// xAI (grok-2-vision)
// -------------------------------------------------------------------------
describe.skipIf(!process.env.XAI_API_KEY)("xAI (grok-2-vision)", () => {
const model = getModel("xai", "grok-2-vision");
it("should accept a small number of images (5)", async () => {
const result = await testImageCount(model, 5, smallImage);
expect(result.success, result.error).toBe(true);
});
it("should find maximum image count limit", { timeout: 600000 }, async () => {
const { limit, lastError } = await findLimit((count) => testImageCount(model, count, smallImage), 10, 100, 10);
console.log(`\n xAI max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(5);
});
it("should find maximum image size limit", { timeout: 600000 }, async () => {
const MB = 1024 * 1024;
const sizes = [5, 10, 15, 20, 25];
let lastSuccess = 0;
let lastError: string | undefined;
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n xAI max image size: ~${lastSuccess}MB (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(5);
});
it("should find maximum image dimension limit", { timeout: 600000 }, async () => {
const dimensions = [2000, 4000, 8000, 16000, 20000];
let lastSuccess = 0;
let lastError: string | undefined;
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n xAI max dimension: ~${lastSuccess}px (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(2000);
});
});
// -------------------------------------------------------------------------
// Groq (llama-4-scout-17b)
// -------------------------------------------------------------------------
describe.skipIf(!process.env.GROQ_API_KEY)("Groq (llama-4-scout-17b)", () => {
const model = getModel("groq", "meta-llama/llama-4-scout-17b-16e-instruct");
it("should accept a small number of images (5)", async () => {
const result = await testImageCount(model, 5, smallImage);
expect(result.success, result.error).toBe(true);
});
it("should find maximum image count limit", { timeout: 600000 }, async () => {
const { limit, lastError } = await findLimit((count) => testImageCount(model, count, smallImage), 5, 50, 5);
console.log(`\n Groq max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(5);
});
it("should find maximum image size limit", { timeout: 600000 }, async () => {
const MB = 1024 * 1024;
const sizes = [1, 5, 10, 15, 20];
let lastSuccess = 0;
let lastError: string | undefined;
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n Groq max image size: ~${lastSuccess}MB (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(1);
});
it("should find maximum image dimension limit", { timeout: 600000 }, async () => {
const dimensions = [2000, 4000, 8000, 16000, 20000];
let lastSuccess = 0;
let lastError: string | undefined;
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n Groq max dimension: ~${lastSuccess}px (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(2000);
});
});
// -------------------------------------------------------------------------
// zAI (glm-4.5v)
// -------------------------------------------------------------------------
describe.skipIf(!process.env.ZAI_API_KEY)("zAI (glm-4.5v)", () => {
const model = getModel("zai", "glm-4.5v");
it("should accept a small number of images (5)", async () => {
const result = await testImageCount(model, 5, smallImage);
expect(result.success, result.error).toBe(true);
});
it("should find maximum image count limit", { timeout: 600000 }, async () => {
const { limit, lastError } = await findLimit((count) => testImageCount(model, count, smallImage), 10, 100, 10);
console.log(`\n zAI max images: ~${limit} (last error: ${lastError})`);
expect(limit).toBeGreaterThanOrEqual(5);
});
it("should find maximum image size limit", { timeout: 600000 }, async () => {
const MB = 1024 * 1024;
const sizes = [5, 10, 15, 20];
let lastSuccess = 0;
let lastError: string | undefined;
for (const sizeMB of sizes) {
console.log(` Testing size: ${sizeMB}MB...`);
const imageBase64 = generateImageWithSize(sizeMB * MB, `size-${sizeMB}mb.png`);
const result = await testImageSize(model, imageBase64);
if (result.success) {
lastSuccess = sizeMB;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n zAI max image size: ~${lastSuccess}MB (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(5);
});
it("should find maximum image dimension limit", { timeout: 600000 }, async () => {
const dimensions = [2000, 4000, 8000, 16000, 20000];
let lastSuccess = 0;
let lastError: string | undefined;
for (const dim of dimensions) {
console.log(` Testing dimension: ${dim}x${dim}...`);
const imageBase64 = generateImage(dim, dim, `dim-${dim}.png`);
const result = await testImageDimensions(model, imageBase64);
if (result.success) {
lastSuccess = dim;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 100)}`);
break;
}
}
console.log(`\n zAI max dimension: ~${lastSuccess}px (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(2000);
});
});
// =========================================================================
// MAX SIZE IMAGES TEST
// =========================================================================
// Tests how many images at (or near) max allowed size each provider can handle.
// This tests realistic payload limits, not just image count with tiny files.
//
// Note: A real 8kx8k noise PNG is ~183MB (exceeds all provider limits).
// So we test with images sized near each provider's actual size limit.
// =========================================================================
describe("Max Size Images (realistic payload stress test)", () => {
// Generate images at specific sizes for each provider's limit
const imageCache: Map<number, string> = new Map();
function getImageAtSize(targetMB: number): string {
if (imageCache.has(targetMB)) {
return imageCache.get(targetMB)!;
}
console.log(` Generating ~${targetMB}MB noise image...`);
const imageBase64 = generateImageWithSize(targetMB * 1024 * 1024, `stress-${targetMB}mb.png`);
const actualSize = Buffer.from(imageBase64, "base64").length;
console.log(` Actual size: ${(actualSize / 1024 / 1024).toFixed(2)}MB`);
imageCache.set(targetMB, imageBase64);
return imageBase64;
}
// Anthropic - 5MB per image limit, 32MB total request, 100 image count
// Using 3MB to stay under 5MB limit (generateImageWithSize has overhead)
it.skipIf(!process.env.ANTHROPIC_API_KEY)(
"Anthropic: max ~3MB images before rejection",
{ timeout: 900000 },
async () => {
const model = getModel("anthropic", "claude-3-5-haiku-20241022");
const image3mb = getImageAtSize(3);
// 32MB total limit / ~4MB actual = ~8 images
const counts = [1, 2, 4, 6, 8, 10, 12];
let lastSuccess = 0;
let lastError: string | undefined;
for (const count of counts) {
console.log(` Testing ${count} x ~3MB images...`);
const result = await testImageCount(model, count, image3mb);
if (result.success) {
lastSuccess = count;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 150)}`);
break;
}
}
console.log(`\n Anthropic max ~3MB images: ${lastSuccess} (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(1);
},
);
// OpenAI - 20MB per image documented, we found ≥25MB works
// Test with 15MB images to stay safely under limit
it.skipIf(!process.env.OPENAI_API_KEY)(
"OpenAI: max ~15MB images before rejection",
{ timeout: 1800000 },
async () => {
const model = getModel("openai", "gpt-4o-mini");
const image15mb = getImageAtSize(15);
// Test progressively
const counts = [1, 2, 5, 10, 20];
let lastSuccess = 0;
let lastError: string | undefined;
for (const count of counts) {
console.log(` Testing ${count} x ~15MB images...`);
const result = await testImageCount(model, count, image15mb);
if (result.success) {
lastSuccess = count;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 150)}`);
break;
}
}
console.log(`\n OpenAI max ~15MB images: ${lastSuccess} (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(1);
},
);
// Gemini - very permissive, ≥40MB per image works
// Test with 20MB images
it.skipIf(!process.env.GEMINI_API_KEY)(
"Gemini: max ~20MB images before rejection",
{ timeout: 1800000 },
async () => {
const model = getModel("google", "gemini-2.5-flash");
const image20mb = getImageAtSize(20);
// Test progressively
const counts = [1, 2, 5, 10, 20, 50];
let lastSuccess = 0;
let lastError: string | undefined;
for (const count of counts) {
console.log(` Testing ${count} x ~20MB images...`);
const result = await testImageCount(model, count, image20mb);
if (result.success) {
lastSuccess = count;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 150)}`);
break;
}
}
console.log(`\n Gemini max ~20MB images: ${lastSuccess} (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(1);
},
);
// Mistral - 8 image limit, ~15MB per image
// Test with 10MB images (safely under limit)
it.skipIf(!process.env.MISTRAL_API_KEY)(
"Mistral: max ~10MB images before rejection",
{ timeout: 600000 },
async () => {
const model = getModel("mistral", "pixtral-12b");
const image10mb = getImageAtSize(10);
// Known limit is 8 images
const counts = [1, 2, 4, 6, 8, 9];
let lastSuccess = 0;
let lastError: string | undefined;
for (const count of counts) {
console.log(` Testing ${count} x ~10MB images...`);
const result = await testImageCount(model, count, image10mb);
if (result.success) {
lastSuccess = count;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 150)}`);
break;
}
}
console.log(`\n Mistral max ~10MB images: ${lastSuccess} (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(1);
},
);
// xAI - 25MB per image limit (26214400 bytes exact)
// Test with 20MB images (safely under limit)
it.skipIf(!process.env.XAI_API_KEY)("xAI: max ~20MB images before rejection", { timeout: 1200000 }, async () => {
const model = getModel("xai", "grok-2-vision");
const image20mb = getImageAtSize(20);
// Test progressively
const counts = [1, 2, 5, 10, 20];
let lastSuccess = 0;
let lastError: string | undefined;
for (const count of counts) {
console.log(` Testing ${count} x ~20MB images...`);
const result = await testImageCount(model, count, image20mb);
if (result.success) {
lastSuccess = count;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 150)}`);
break;
}
}
console.log(`\n xAI max ~20MB images: ${lastSuccess} (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(1);
});
// Groq - very limited (5 images, ~5760px max due to 33M pixel limit)
// 8k images (64M pixels) exceed limit, so test with 5760px images instead
it.skipIf(!process.env.GROQ_API_KEY)(
"Groq: max 5760px images before rejection",
{ timeout: 600000 },
async () => {
const model = getModel("groq", "meta-llama/llama-4-scout-17b-16e-instruct");
// Generate 5760x5760 image (33177600 pixels = Groq's limit)
console.log(" Generating 5760x5760 test image for Groq...");
const image5760 = generateImage(5760, 5760, "stress-5760.png");
// Known limit is 5 images
const counts = [1, 2, 3, 4, 5, 6];
let lastSuccess = 0;
let lastError: string | undefined;
for (const count of counts) {
console.log(` Testing ${count} x 5760px images...`);
const result = await testImageCount(model, count, image5760);
if (result.success) {
lastSuccess = count;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 150)}`);
break;
}
}
console.log(`\n Groq max 5760px images: ${lastSuccess} (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(1);
},
);
// zAI - ≥20MB per image, context-window limited (65k tokens)
// Test with 15MB images
it.skipIf(!process.env.ZAI_API_KEY)("zAI: max ~15MB images before rejection", { timeout: 1200000 }, async () => {
const model = getModel("zai", "glm-4.5v");
const image15mb = getImageAtSize(15);
// Context-limited, test progressively
const counts = [1, 2, 5, 10, 20];
let lastSuccess = 0;
let lastError: string | undefined;
for (const count of counts) {
console.log(` Testing ${count} x ~15MB images...`);
const result = await testImageCount(model, count, image15mb);
if (result.success) {
lastSuccess = count;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 150)}`);
break;
}
}
console.log(`\n zAI max ~15MB images: ${lastSuccess} (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(1);
});
// OpenRouter - ~10MB per image, context-window limited (65k tokens)
// Test with 5MB images (safer size)
it.skipIf(!process.env.OPENROUTER_API_KEY)(
"OpenRouter: max ~5MB images before rejection",
{ timeout: 900000 },
async () => {
const model = getModel("openrouter", "z-ai/glm-4.5v");
const image5mb = getImageAtSize(5);
// Context-limited, test progressively
const counts = [1, 2, 5, 10, 20];
let lastSuccess = 0;
let lastError: string | undefined;
for (const count of counts) {
console.log(` Testing ${count} x ~5MB images...`);
const result = await testImageCount(model, count, image5mb);
if (result.success) {
lastSuccess = count;
console.log(` SUCCESS`);
} else {
lastError = result.error;
console.log(` FAILED: ${result.error?.substring(0, 150)}`);
break;
}
}
console.log(`\n OpenRouter max ~5MB images: ${lastSuccess} (last error: ${lastError})`);
expect(lastSuccess).toBeGreaterThanOrEqual(1);
},
);
});
});