fix(sse): gate Kiro image attachments behind a Claude-capability check (#4763)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-25 22:57:13 -03:00
committed by GitHub
parent 2c8921fe34
commit 0d9029905e
2 changed files with 73 additions and 4 deletions

View File

@@ -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,<data>
@@ -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:")) {

View File

@@ -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)}`
);
}
});