/** * Vision / multimodal support tests for the Command Code executor. * * Verifies that vision-capable models (MiniMax M3, MiMo V2.5, Kimi K2, Qwen 3.x, GPT-5, Claude 3/4, Fable 5, Gemini 3.x, Stepfun, Fugu, etc.) * receive image parts in Command Code CLI format, while text-only * models strip images as before (no regression). */ import test from "node:test"; import assert from "node:assert/strict"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-cmd-code-vision-")); process.env.DATA_DIR = TEST_DATA_DIR; const { getExecutor } = await import("../../open-sse/executors/index.ts"); const core = await import("../../src/lib/db/core.ts"); const originalFetch = globalThis.fetch; function commandCodeStream(lines: unknown[]) { const text = lines.map((l) => JSON.stringify(l)).join("\n") + "\n"; return new Response(text, { status: 200, headers: { "Content-Type": "application/x-ndjson" } }); } test.after(() => { globalThis.fetch = originalFetch; core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); test.afterEach(() => { globalThis.fetch = originalFetch; }); // ── helpers ──────────────────────────────────────────────────────────── type FetchCall = { url: string; init: Record; body: Record }; function captureFetch(response: Response) { const calls: FetchCall[] = []; globalThis.fetch = async (url, init: RequestInit = {}) => { calls.push({ url: String(url), init: init as Record, body: JSON.parse(String(init.body)), }); return response; }; return calls; } function userContent(calls: FetchCall[]): unknown { return ( (calls[0].body.params as Record).messages as Record[] )[0].content; } // ── vision models: image parts preserved in CC CLI format ───────────── test("vision model minimax-m3 preserves image_url part as CC CLI {type:image}", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "MiniMaxAI/MiniMax-M3", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "text", text: "What's in this?" }, { type: "image_url", image_url: { url: "data:image/png;base64,iVBORw0KGgo=" }, }, ], }, ], }, }); const content = userContent(calls); assert.ok(Array.isArray(content), "vision model user content must be an array"); const parts = content as Record[]; assert.equal(parts.length, 2); // Text part preserved assert.equal(parts[0].type, "text"); assert.equal(parts[0].text, "What's in this?"); // Image part converted to CC CLI format assert.equal(parts[1].type, "image"); assert.equal(parts[1].image, "data:image/png;base64,iVBORw0KGgo="); }); test("vision model minimax-m3 preserves image_url with HTTP URL", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "minimax-m3", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "text", text: "Describe" }, { type: "image_url", image_url: { url: "https://example.com/photo.jpg" }, }, ], }, ], }, }); const content = userContent(calls); assert.ok(Array.isArray(content)); const parts = content as Record[]; assert.equal(parts.length, 2); assert.equal(parts[1].type, "image"); assert.equal(parts[1].image, "https://example.com/photo.jpg"); }); test("vision model mimo-v2.5 preserves image parts", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "mimo-v2.5", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "text", text: "Analyze" }, { type: "image_url", image_url: { url: "https://example.com/img.png" }, }, ], }, ], }, }); const content = userContent(calls); assert.ok(Array.isArray(content)); const parts = content as Record[]; assert.equal(parts.length, 2); assert.equal(parts[1].type, "image"); }); test("vision model mimo-v2.5-pro is text-only (no image parts)", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "mimo-v2.5-pro", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "text", text: "Hi" }, { type: "image_url", image_url: { url: "https://example.com/img.png" }, }, ], }, ], }, }); const content = userContent(calls); // mimo-v2.5-pro is text-only — content must be flattened to a plain string assert.equal(typeof content, "string"); assert.equal(content, "Hi"); }); test("vision model mimo-v2-omni preserves image parts", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "mimo-v2-omni", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "text", text: "Check" }, { type: "image_url", image_url: { url: "data:image/jpeg;base64,/9j/4AAQ=" }, }, ], }, ], }, }); const content = userContent(calls); assert.ok(Array.isArray(content)); const parts = content as Record[]; assert.equal(parts.length, 2); assert.equal(parts[1].type, "image"); assert.equal(parts[1].image, "data:image/jpeg;base64,/9j/4AAQ="); }); // ── non-vision models: images still stripped (no regression) ────────── test("text-only model deepseek-v4-pro strips image_url parts", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "deepseek/deepseek-v4-pro", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "text", text: "Hello" }, { type: "image_url", image_url: { url: "https://example.com/img.png" }, }, ], }, ], }, }); const content = userContent(calls); // Non-vision model: content is a plain string, images stripped assert.equal(typeof content, "string"); assert.equal(content, "Hello"); }); test("text-only model deepseek-v4-flash strips image_url parts (no regression)", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "deepseek/deepseek-v4-flash", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "text", text: "Text only" }, { type: "image_url", image_url: { url: "data:image/png;base64,AAA=" }, }, ], }, ], }, }); const content = userContent(calls); assert.equal(typeof content, "string"); assert.equal(content, "Text only"); }); // ── edge cases ──────────────────────────────────────────────────────── test("vision model with only image content emits empty text fallback", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "minimax-m3", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "image_url", image_url: { url: "data:image/png;base64,iVBOR=" }, }, ], }, ], }, }); const content = userContent(calls); assert.ok(Array.isArray(content)); const parts = content as Record[]; // Single image part preserved — no empty text injected because // the image itself keeps content non-empty. assert.equal(parts.length, 1); assert.equal(parts[0].type, "image"); assert.equal(parts[0].image, "data:image/png;base64,iVBOR="); }); test("vision model passes plain string content through unchanged", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "minimax-m3", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [{ role: "user", content: "Plain string message" }], }, }); const content = userContent(calls); assert.equal(typeof content, "string"); assert.equal(content, "Plain string message"); }); test("vision model honors body.model rewrite for vision detection", async () => { // #5166 scenario: body.model overwrites the execute model arg. // Vision detection must use the rewritten model id. const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); // execute() gets a non-vision combo model, body.model rewrites to a vision model await getExecutor("command-code").execute({ model: "gpt-5.4-mini", stream: false, credentials: { apiKey: "cc_test_key" }, body: { model: "MiniMaxAI/MiniMax-M3", messages: [ { role: "user", content: [ { type: "text", text: "Describe" }, { type: "image_url", image_url: { url: "https://example.com/img.png" }, }, ], }, ], }, }); const content = userContent(calls); // body.model = MiniMax-M3 (vision) → images preserved assert.ok(Array.isArray(content)); const parts = content as Record[]; assert.equal(parts.length, 2); assert.equal(parts[1].type, "image"); }); test("vision model with multiple image parts preserves all of them", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "minimax-m3", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "text", text: "Compare" }, { type: "image_url", image_url: { url: "https://example.com/a.jpg" }, }, { type: "image_url", image_url: { url: "https://example.com/b.jpg" }, }, ], }, ], }, }); const content = userContent(calls); assert.ok(Array.isArray(content)); const parts = content as Record[]; assert.equal(parts.length, 3); assert.equal(parts[0].type, "text"); assert.equal(parts[1].type, "image"); assert.equal(parts[1].image, "https://example.com/a.jpg"); assert.equal(parts[2].type, "image"); assert.equal(parts[2].image, "https://example.com/b.jpg"); }); test("vision model with image_url as plain string (no object wrapper) still works", async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model: "minimax-m3", stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "text", text: "Look" }, { type: "image_url", image_url: "https://example.com/img.png", }, ], }, ], }, }); const content = userContent(calls); assert.ok(Array.isArray(content)); const parts = content as Record[]; assert.equal(parts.length, 2); assert.equal(parts[1].type, "image"); assert.equal(parts[1].image, "https://example.com/img.png"); }); // ── CC vision models (Command Code docs registry) ────────────────── const VISION_CASES = [ ["Kimi K2.6", "moonshotai/Kimi-K2.6"], ["Kimi K2.7 Code", "moonshotai/Kimi-K2.7-Code"], ["Kimi K2.5", "moonshotai/Kimi-K2.5"], ["Qwen 3.6 Plus", "Qwen/Qwen3.6-Plus"], ["Qwen 3.7 Plus", "Qwen/Qwen3.7-Plus"], ["Step 3.7 Flash", "stepfun/Step-3.7-Flash"], ["GPT-5.5", "gpt-5.5"], ["GPT-5.4", "gpt-5.4"], ["GPT-5.3 Codex", "gpt-5.3-codex"], ["GPT-5.4 Mini", "gpt-5.4-mini"], ["Claude Fable 5", "claude-fable-5"], ["Sakana Fugu Ultra", "sakana/fugu-ultra"], ["Claude Opus 4.7 (isVisionModelId)", "claude-opus-4-7"], ["Claude Sonnet 4.6 (isVisionModelId)", "claude-sonnet-4-6"], ["Gemini 3.5 Flash (isVisionModelId)", "google/gemini-3.5-flash"], ]; for (const [name, model] of VISION_CASES) { test(`vision model ${name} preserves image parts`, async () => { const calls = captureFetch( commandCodeStream([{ type: "text-delta", text: "ok" }, { type: "finish" }]) ); await getExecutor("command-code").execute({ model, stream: false, credentials: { apiKey: "cc_test_key" }, body: { messages: [ { role: "user", content: [ { type: "text", text: "Check" }, { type: "image_url", image_url: { url: "https://example.com/img.png" }, }, ], }, ], }, }); const content = userContent(calls); assert.ok(Array.isArray(content), `${name} user content must be an array`); const parts = content; assert.equal(parts.length, 2); assert.equal(parts[1].type, "image"); }); }