From 0d9029905e0c5a7f5dc48263b48fed94a5115bfa Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 25 Jun 2026 22:57:13 -0300 Subject: [PATCH] fix(sse): gate Kiro image attachments behind a Claude-capability check (#4763) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green. --- open-sse/translator/request/openai-to-kiro.ts | 15 +++-- tests/unit/translator-openai-to-kiro.test.ts | 62 +++++++++++++++++++ 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/open-sse/translator/request/openai-to-kiro.ts b/open-sse/translator/request/openai-to-kiro.ts index 3b6052dbcb..eec8324152 100644 --- a/open-sse/translator/request/openai-to-kiro.ts +++ b/open-sse/translator/request/openai-to-kiro.ts @@ -128,6 +128,12 @@ function convertMessages(messages, tools, model) { let currentRole = null; let toolsAttached = false; + // Only Claude models support images in Kiro. Kiro also routes non-Claude + // models (deepseek, minimax, glm, qwen3-coder-next, auto-kiro) that do not + // accept image attachments — gate image extraction behind a Claude check so + // we never attach images those models would reject. + const supportsImages = typeof model === "string" && model.toLowerCase().includes("claude"); + const flushPending = () => { if (currentRole === "user") { const content = pendingUserContent.join("\n\n").trim() || "(empty)"; @@ -250,9 +256,10 @@ function convertMessages(messages, tools, model) { .map((c) => c.text || ""); content = textParts.join("\n"); - // Extract images (OpenAI image_url and Anthropic image formats) + // Extract images (OpenAI image_url and Anthropic image formats). + // Skip entirely for models that do not support images — see supportsImages. for (const block of msg.content) { - if (block.type === "image_url") { + if (supportsImages && block.type === "image_url") { const url: string = block.image_url?.url || ""; if (url.startsWith("data:")) { // data:image/jpeg;base64, @@ -261,11 +268,11 @@ function convertMessages(messages, tools, model) { const format = mediaType.split("/")[1] || "jpeg"; if (bytes) pendingImages.push({ format, source: { bytes } }); } - } else if (block.type === "image" && block.source?.type === "base64") { + } else if (supportsImages && block.type === "image" && block.source?.type === "base64") { const format = (block.source.media_type || "image/jpeg").split("/")[1] || "jpeg"; if (block.source.data) pendingImages.push({ format, source: { bytes: block.source.data } }); - } else if (block.type === "image" && typeof block.image === "string") { + } else if (supportsImages && block.type === "image" && typeof block.image === "string") { // AI SDK-style image part: { type: "image", image: "data:...;base64,..." } (#1330) const url = block.image; if (url.startsWith("data:")) { diff --git a/tests/unit/translator-openai-to-kiro.test.ts b/tests/unit/translator-openai-to-kiro.test.ts index 4ed7067f3c..a2faddcb03 100644 --- a/tests/unit/translator-openai-to-kiro.test.ts +++ b/tests/unit/translator-openai-to-kiro.test.ts @@ -915,3 +915,65 @@ test("OpenAI -> Kiro serializes non-string role:tool content to non-empty text ( assert.notEqual(text, "", "non-string tool content must not collapse to empty string"); assert.match(text, /entry A/, "serialized content preserves the structured text blocks"); }); + +// Only Claude models support images in Kiro. Non-Claude Kiro models +// (deepseek-3.2, minimax-m2.5, glm-5, qwen3-coder-next, auto-kiro) must NOT +// receive image attachments — attaching them is wrong for those models. +const PNG_DATA_URL = "data:image/png;base64,aGVsbG8="; + +function buildImageRequest(model: string) { + return buildKiroPayload( + model, + { + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Describe this picture" }, + { type: "image_url", image_url: { url: PNG_DATA_URL } }, + { type: "image", source: { type: "base64", media_type: "image/png", data: "aGk=" } }, + { type: "image", image: PNG_DATA_URL }, + ], + }, + ], + }, + false, + null + ); +} + +test("OpenAI -> Kiro attaches images for Claude models", () => { + const result = buildImageRequest("claude-sonnet-4.6"); + const images = result.conversationState.currentMessage.userInputMessage.images; + assert.ok(Array.isArray(images), "Claude models must keep image attachments"); + // Three image blocks (image_url + Anthropic base64 + AI SDK-style) → 3 entries + assert.equal(images.length, 3, "all three supported image part shapes are attached"); + assert.equal(images[0].format, "png"); + assert.ok(images[0].source.bytes, "image bytes are preserved for Claude"); +}); + +test("OpenAI -> Kiro drops images for non-Claude models (deepseek)", () => { + const result = buildImageRequest("deepseek-3.2"); + const images = result.conversationState.currentMessage.userInputMessage.images; + assert.ok( + images === undefined || images.length === 0, + `non-Claude Kiro models must NOT receive image attachments, got: ${JSON.stringify(images)}` + ); + // The accompanying text must still survive. + assert.match( + result.conversationState.currentMessage.userInputMessage.content, + /Describe this picture/, + "text content is preserved even when images are dropped" + ); +}); + +test("OpenAI -> Kiro drops images for non-Claude models (glm / auto-kiro)", () => { + for (const model of ["glm-5", "minimax-m2.5", "qwen3-coder-next", "auto-kiro"]) { + const result = buildImageRequest(model); + const images = result.conversationState.currentMessage.userInputMessage.images; + assert.ok( + images === undefined || images.length === 0, + `${model} must NOT receive image attachments, got: ${JSON.stringify(images)}` + ); + } +});