fix(translator): keep tool-result images, restore Kiro tool names, preserve thinking display

Forward images inside tool_result to OpenAI and Kiro upstreams via following user messages, restore original client tool names on Kiro responses via _toolNameMap, and preserve thinking display settings across translations.
This commit is contained in:
Manan Santoki
2026-09-17 18:11:20 +07:00
parent 0c6ab4f99b
commit f4f06f290c
12 changed files with 328 additions and 23 deletions

View File

@@ -127,7 +127,7 @@ export class BaseExecutor {
for (let urlIndex = 0; urlIndex < fallbackCount; urlIndex++) {
const url = this.buildUrl(model, stream, urlIndex, credentials);
const transformedBody = this.transformRequest(model, body, stream, credentials);
const headers = this.buildHeaders(credentials, stream, url, model);
const headers = this.buildHeaders(credentials, stream, url, model, transformedBody);
if (!retryAttemptsByUrl[urlIndex]) retryAttemptsByUrl[urlIndex] = 0;

View File

@@ -146,7 +146,7 @@ export class DefaultExecutor extends BaseExecutor {
return BEARER;
}
buildHeaders(credentials, stream = true, url, model) {
buildHeaders(credentials, stream = true, url, model, body = null) {
const rt = credentials?.runtimeTransport;
const headers = { "Content-Type": "application/json", ...(rt ? rt.headers : this.config.headers) };
const desc = rt?.auth || AUTH_DESCRIPTORS[this.provider] || this.resolveAuthDescriptor();
@@ -166,7 +166,7 @@ export class DefaultExecutor extends BaseExecutor {
const isClaudeModel = typeof model === "string" && /^claude-/.test(model);
if (model && (this.provider === "claude"
|| (this.provider?.startsWith?.("anthropic-compatible-") && isClaudeModel))) {
headers["Anthropic-Beta"] = selectAnthropicBeta(model);
headers["Anthropic-Beta"] = selectAnthropicBeta(model, body);
}
// Strip first-party Claude Code identity headers for non-Anthropic anthropic-compatible upstreams

View File

@@ -62,8 +62,17 @@ const ANTHROPIC_BETA_BASE = [
const ANTHROPIC_BETA_HEAVY_AGENT = ["advanced-tool-use-2025-11-20", "effort-2025-11-24"];
// Heavy-agent beta flags are gated to opus/sonnet — cheaper models don't need them.
export function selectAnthropicBeta(model = "") {
const flags = [...ANTHROPIC_BETA_BASE];
// `redact-thinking` asks Anthropic to return signature-only thinking blocks, which
// is right for clients that never render thinking but blanks the summaries a
// client explicitly requested with `thinking.display: "summarized"`.
const ANTHROPIC_BETA_REDACT_THINKING = "redact-thinking-2026-02-12";
export function wantsThinkingSummaries(body) {
return body?.thinking?.display === "summarized";
}
export function selectAnthropicBeta(model = "", body = null) {
const flags = ANTHROPIC_BETA_BASE.filter((flag) => flag !== ANTHROPIC_BETA_REDACT_THINKING || !wantsThinkingSummaries(body));
if (/^claude-(opus|sonnet)/.test(model)) flags.push(...ANTHROPIC_BETA_HEAVY_AGENT);
return flags.join(",");
}

View File

@@ -232,7 +232,7 @@ function stripAll(body) {
}
// Apply unified thinking config to body in the resolved provider-native format.
function applyFormat(fmt, body, cfg, caps, supportedLevels) {
function applyFormat(fmt, body, cfg, caps, supportedLevels, display) {
const none = cfg.mode === "none";
const canDisable = caps.thinkingCanDisable !== false;
// Model cannot disable thinking → clamp "none" to minimal effort instead.
@@ -249,7 +249,7 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels) {
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
// Models that can disable thinking need the explicit adaptive switch.
// Permanently adaptive models such as Fable 5.1 accept effort directly.
if (canDisable) body.thinking = { type: "adaptive" };
if (canDisable) body.thinking = { type: "adaptive", ...(display ? { display } : {}) };
else delete body.thinking;
const level = toLevel(eff);
body.output_config = { effort: level === "xhigh" || level === "auto" ? "high" : level };
@@ -258,7 +258,7 @@ function applyFormat(fmt, body, cfg, caps, supportedLevels) {
case "claude-budget": {
if (none && canDisable) { body.thinking = { type: "disabled" }; break; }
const budget = toBudget(eff, caps.thinkingRange);
body.thinking = budget === -1 ? { type: "enabled" } : { type: "enabled", budget_tokens: budget || 8192 };
body.thinking = budget === -1 ? { type: "enabled", ...(display ? { display } : {}) } : { type: "enabled", budget_tokens: budget || 8192, ...(display ? { display } : {}) };
break;
}
case "gemini-level": {
@@ -378,7 +378,10 @@ export function applyThinking(targetFormat, model, body, provider = null, intent
const fmt = resolveFormat(targetFormat, cleanModel, provider);
const supportedLevels = getThinkingLevels(provider, cleanModel);
// Anthropic's `display` (summarized | omitted) decides whether thinking text
// comes back at all; keep what the client asked for instead of resetting it.
const display = typeof body.thinking?.display === "string" ? body.thinking.display : undefined;
stripAll(body);
applyFormat(fmt, body, cfg, caps, supportedLevels);
applyFormat(fmt, body, cfg, caps, supportedLevels, display);
return body;
}

View File

@@ -415,6 +415,28 @@ export function anchorClaudeCache(body) {
// - Add thinking block for Anthropic endpoint (provider === "claude")
// - Fix tool_use/tool_result ordering
// - Apply cloaking (billing header + fake user ID) for OAuth tokens
export function hoistToolResultImages(body) {
if (!Array.isArray(body?.messages)) return body;
let touched = false;
const messages = body.messages.map((msg) => {
if (msg?.role !== ROLE.USER || !Array.isArray(msg.content)) return msg;
const hoisted = [];
const content = msg.content.map((block) => {
if (block?.type !== CLAUDE_BLOCK.TOOL_RESULT || !Array.isArray(block.content)) return block;
const images = block.content.filter((c) => c?.type === CLAUDE_BLOCK.IMAGE);
if (!images.length) return block;
const rest = block.content.filter((c) => c?.type !== CLAUDE_BLOCK.IMAGE);
hoisted.push({ type: CLAUDE_BLOCK.TEXT, text: `[Image from tool result ${block.tool_use_id}]` }, ...images);
return { ...block, content: rest.length ? rest : [{ type: CLAUDE_BLOCK.TEXT, text: "(image attached below)" }] };
});
if (!hoisted.length) return msg;
touched = true;
// tool_result blocks must lead a user message; the hoisted image follows them.
return { ...msg, content: [...content, ...hoisted] };
});
return touched ? { ...body, messages } : body;
}
export function prepareClaudeRequest(body, provider = null, apiKey = null, connectionId = null, rawHeaders = null, sessionId = null) {
// quirk: MiniMax's Claude-compatible endpoint rejects Anthropic's output_config (400 invalid params)
if (PROVIDERS[provider]?.quirks?.dropOutputConfig) {
@@ -608,6 +630,14 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne
}
}
// Anthropic itself reads images inside tool_result; other Anthropic-compatible
// endpoints (OpenCode Go, Kimi, DeepSeek, GLM, MiniMax) accept image blocks
// only as user content and silently drop them inside a tool result. Move a
// tool's screenshot out of the result and into the same user turn.
if (provider !== "claude" && !provider?.startsWith("anthropic-compatible")) {
body = hoistToolResultImages(body);
}
// Apply cloaking for OAuth tokens (billing header + fake user ID)
// session_id in user_id must match X-Claude-Code-Session-Id for fingerprint consistency
if ((provider === "claude" || provider?.startsWith("anthropic-compatible")) && apiKey) {

View File

@@ -97,11 +97,21 @@ function convertClaudeMessagesToKiro(messages, model) {
if (typeof block.content === "string") {
resultContent = block.content;
} else if (Array.isArray(block.content)) {
// Images a tool returned (screenshots) ride along as user images;
// Kiro tool results are text-only.
let hasImage = false;
for (const c of block.content) {
if (c?.type === CLAUDE_BLOCK.IMAGE && c.source?.type === "base64") {
hasImage = true;
const imageType = c.source.media_type || DEFAULT_IMAGE_MIME;
pendingImages.push({ format: imageType.split("/")[1] || imageType, source: { bytes: c.source.data } });
}
}
resultContent =
block.content
.filter((c) => c.type === CLAUDE_BLOCK.TEXT)
.map((c) => c.text)
.join("\n") || JSON.stringify(block.content);
.join("\n") || (hasImage ? "(image attached)" : JSON.stringify(block.content));
} else if (block.content) {
resultContent = JSON.stringify(block.content);
}
@@ -341,6 +351,13 @@ export function claudeToKiroRequest(model, body, stream, credentials) {
enumerable: false,
});
// Kiro tool specs get sanitized names (`mcp__a__b` → `mcp_a_b`); keep the
// reverse map so tool calls stream back under the client's own names.
const restoredToolNames = new Map();
for (const [original, sanitized] of nameMap) {
if (original !== sanitized) restoredToolNames.set(sanitized, original);
}
if (restoredToolNames.size) payload._toolNameMap = restoredToolNames;
return payload;
}

View File

@@ -196,25 +196,41 @@ function convertClaudeMessage(msg) {
});
break;
case CLAUDE_BLOCK.TOOL_RESULT:
case CLAUDE_BLOCK.TOOL_RESULT: {
let resultContent = "";
const resultImages = [];
if (typeof block.content === "string") {
resultContent = block.content;
} else if (Array.isArray(block.content)) {
resultContent = block.content
.filter(c => c.type === CLAUDE_BLOCK.TEXT)
.map(c => c.text)
.join("\n") || JSON.stringify(block.content);
for (const c of block.content) {
if (c?.type === CLAUDE_BLOCK.IMAGE && c.source?.type === "base64") {
resultImages.push({
type: OPENAI_BLOCK.IMAGE_URL,
image_url: { url: encodeDataUri(c.source.media_type, c.source.data) }
});
}
}
const textOnly = block.content.filter(c => c?.type === CLAUDE_BLOCK.TEXT);
resultContent = textOnly.map(c => c.text).join("\n")
|| (resultImages.length ? "" : JSON.stringify(block.content));
} else if (block.content) {
resultContent = JSON.stringify(block.content);
}
toolResults.push({
role: ROLE.TOOL,
tool_call_id: block.tool_use_id,
content: resultContent
});
// The OpenAI tool role is text-only, so a screenshot or any other image a
// tool returned would otherwise vanish. Hand it to the model in the user
// turn that follows the tool messages, tagged with the call it came from.
if (resultImages.length) {
parts.push({ type: OPENAI_BLOCK.TEXT, text: `[Image from tool result ${block.tool_use_id}]` });
parts.push(...resultImages);
}
break;
}
}
}

View File

@@ -42,16 +42,19 @@ function toNativeImageBlock(part) {
type: OPENAI_BLOCK.IMAGE,
image: encodeDataUri(parsed.mimeType, parsed.base64),
mimeType: parsed.mimeType,
mediaType: parsed.mimeType,
};
}
if (part.type === OPENAI_BLOCK.IMAGE || part.type === CLAUDE_BLOCK.IMAGE) {
if (typeof part.image === "string" && part.image.startsWith("data:")) {
const parsed = parseDataUri(part.image);
const mime = part.mimeType || parsed?.mimeType || "image/png";
return {
type: OPENAI_BLOCK.IMAGE,
image: part.image,
mimeType: part.mimeType || parsed?.mimeType || "image/png",
mimeType: mime,
mediaType: mime,
};
}
const source = part.source;
@@ -61,6 +64,7 @@ function toNativeImageBlock(part) {
type: OPENAI_BLOCK.IMAGE,
image: encodeDataUri(mime, source.data),
mimeType: mime,
mediaType: mime,
};
}
}

View File

@@ -434,6 +434,13 @@ export function openaiToKiroRequest(model, body, stream, credentials) {
enumerable: false
});
// Kiro tool specs get sanitized names (`mcp__a__b` → `mcp_a_b`); keep the
// reverse map so tool calls stream back under the client's own names.
const restoredToolNames = new Map();
for (const [original, sanitized] of nameMap) {
if (original !== sanitized) restoredToolNames.set(sanitized, original);
}
if (restoredToolNames.size) payload._toolNameMap = restoredToolNames;
return payload;
}

View File

@@ -46,6 +46,14 @@ function convertFinishReason(reason) {
* Convert one OpenAI-format chunk (from KiroExecutor) into Claude SSE events.
* Returns an array of Claude events, or null when the chunk yields nothing.
*/
// Kiro only accepts sanitized tool names; the request translator leaves the
// reverse map on the stream state so calls come back under the client's names.
function restoreToolName(state, name) {
const raw = name || "";
const map = state?.toolNameMap;
return map && typeof map.get === "function" && map.has(raw) ? map.get(raw) : raw;
}
export function kiroToClaudeResponse(chunk, state) {
// KiroExecutor emits chat.completion.chunk objects; tolerate string chunks
// by attempting a parse (defensive — the direct path is always objects).
@@ -161,7 +169,7 @@ export function kiroToClaudeResponse(chunk, state) {
const toolBlockIndex = state.nextBlockIndex++;
state.toolCalls.set(idx, {
id: tc.id,
name: tc.function?.name || "",
name: restoreToolName(state, tc.function?.name),
blockIndex: toolBlockIndex,
});
results.push({
@@ -170,7 +178,7 @@ export function kiroToClaudeResponse(chunk, state) {
content_block: {
type: "tool_use",
id: tc.id,
name: tc.function?.name || "",
name: restoreToolName(state, tc.function?.name),
input: {},
},
});
@@ -246,7 +254,7 @@ export function kiroToClaudeNonStreaming(data) {
content.push({
type: "tool_use",
id: tc.id || `toolu_${Date.now()}`,
name: tc.function?.name || "",
name: restoreToolName(state, tc.function?.name),
input,
});
}

View File

@@ -20,13 +20,38 @@ function chunkMeta(state) {
* Parse Kiro SSE event and convert to OpenAI format
* Kiro events: assistantResponseEvent, codeEvent, supplementaryWebLinksEvent, etc.
*/
// Kiro only accepts sanitized tool names; the request translator leaves the
// reverse map on the stream state so calls come back under the client's names.
function restoreToolName(state, name) {
const raw = name || "";
const map = state?.toolNameMap;
return map && typeof map.get === "function" && map.has(raw) ? map.get(raw) : raw;
}
export function kiroToOpenAIResponse(chunk, state) {
if (!chunk) return null;
// If chunk is already in OpenAI format (from executor transform), return as-is
// If chunk is already in OpenAI format (from executor transform), return it
// with the client's tool names restored.
if (chunk.object === "chat.completion.chunk" && chunk.choices) {
return chunk;
if (!state?.toolNameMap?.size) return chunk;
return {
...chunk,
choices: chunk.choices.map((choice) => {
const calls = choice?.delta?.tool_calls;
if (!Array.isArray(calls)) return choice;
return {
...choice,
delta: {
...choice.delta,
tool_calls: calls.map((tc) => tc?.function?.name
? { ...tc, function: { ...tc.function, name: restoreToolName(state, tc.function.name) } }
: tc),
},
};
}),
};
}
// Handle string chunk (raw SSE data)
@@ -109,7 +134,7 @@ export function kiroToOpenAIResponse(chunk, state) {
state.hadToolUse = true;
const toolUse = data.toolUseEvent || data;
const toolCallId = toolUse.toolUseId || fallbackToolCallId();
const toolName = toolUse.name || "";
const toolName = restoreToolName(state, toolUse.name);
const toolInput = toolUse.input || {};
const openaiChunk = buildChunk(chunkMeta(state), {

View File

@@ -0,0 +1,186 @@
// Fixes for agent clients (Claude Code) driving non-Anthropic upstreams:
// - tool-result images survive the Claude → OpenAI / Kiro request translation
// - Kiro tool calls stream back under the client's own (unsanitized) names
// - the client's thinking `display` is kept on Claude-format upstreams
import { describe, it, expect } from "vitest";
import "./registerAll.js";
import { translateRequest, translateResponse, initState } from "../../open-sse/translator/index.js";
import { FORMATS } from "../../open-sse/translator/formats.js";
import { applyThinking } from "../../open-sse/translator/concerns/thinkingUnified.js";
import { kiroToClaudeResponse } from "../../open-sse/translator/response/kiro-to-claude.js";
import { kiroToOpenAIResponse } from "../../open-sse/translator/response/kiro-to-openai.js";
import { selectAnthropicBeta } from "../../open-sse/providers/shared.js";
import { hoistToolResultImages } from "../../open-sse/translator/formats/claude.js";
import { openaiToCommandCodeRequest } from "../../open-sse/translator/request/openai-to-commandcode.js";
const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
const screenshotTurn = (extraTools = []) => ({
tools: [
{ name: "mcp__browser__computer", description: "browser", input_schema: { type: "object", properties: {} } },
...extraTools,
],
messages: [
{ role: "user", content: "take a screenshot" },
{ role: "assistant", content: [{ type: "tool_use", id: "toolu_1", name: "mcp__browser__computer", input: { action: "screenshot" } }] },
{
role: "user",
content: [{
type: "tool_result",
tool_use_id: "toolu_1",
content: [
{ type: "text", text: "Successfully captured screenshot (1x1, png)" },
{ type: "image", source: { type: "base64", media_type: "image/png", data: PNG } },
],
}],
},
],
});
describe("tool-result images reach OpenAI-format upstreams", () => {
it("emits the tool message text and a follow-up user message carrying the image", () => {
const out = translateRequest(FORMATS.CLAUDE, FORMATS.OPENAI, "gpt-x", screenshotTurn(), true, null, "openai");
const toolMsg = out.messages.find((m) => m.role === "tool");
expect(toolMsg.tool_call_id).toBe("toolu_1");
expect(toolMsg.content).toBe("Successfully captured screenshot (1x1, png)");
expect(toolMsg.content).not.toContain(PNG);
const follow = out.messages[out.messages.indexOf(toolMsg) + 1];
expect(follow.role).toBe("user");
const image = follow.content.find((p) => p.type === "image_url");
expect(image.image_url.url).toBe(`data:image/png;base64,${PNG}`);
expect(follow.content.find((p) => p.type === "text").text).toContain("toolu_1");
});
it("does not dump base64 into a tool message that had no text", () => {
const body = screenshotTurn();
body.messages[2].content[0].content = [{ type: "image", source: { type: "base64", media_type: "image/png", data: PNG } }];
const out = translateRequest(FORMATS.CLAUDE, FORMATS.OPENAI, "gpt-x", body, true, null, "openai");
const toolMsg = out.messages.find((m) => m.role === "tool");
expect(toolMsg.content).toBe("");
expect(out.messages.some((m) => Array.isArray(m.content) && m.content.some((p) => p.type === "image_url"))).toBe(true);
});
it("leaves text-only tool results exactly as before", () => {
const body = screenshotTurn();
body.messages[2].content[0].content = "plain result";
const out = translateRequest(FORMATS.CLAUDE, FORMATS.OPENAI, "gpt-x", body, true, null, "openai");
const toolMsg = out.messages.find((m) => m.role === "tool");
expect(toolMsg.content).toBe("plain result");
expect(out.messages[out.messages.length - 1]).toBe(toolMsg);
});
it("forwards tool-result images to Kiro as user images", () => {
const out = translateRequest(FORMATS.CLAUDE, FORMATS.KIRO, "claude-sonnet-4.5", screenshotTurn(), true, null, "kiro");
const json = JSON.stringify(out.conversationState);
expect(json).toContain(PNG);
expect(json).toContain("Successfully captured screenshot");
});
});
describe("Kiro tool names round-trip", () => {
it("returns the sanitized→original map on the translated body", () => {
const out = translateRequest(FORMATS.CLAUDE, FORMATS.KIRO, "claude-sonnet-4.5", screenshotTurn(), true, null, "kiro");
expect(out._toolNameMap).toBeInstanceOf(Map);
expect(out._toolNameMap.get("mcp_browser_computer")).toBe("mcp__browser__computer");
const wire = JSON.parse(JSON.stringify(out.conversationState));
expect(JSON.stringify(wire)).toContain("mcp_browser_computer");
expect(JSON.stringify(wire)).not.toContain("mcp__browser__computer");
});
it("omits the map when no name changed", () => {
const body = screenshotTurn();
body.tools = [{ name: "plain_tool", description: "x", input_schema: { type: "object", properties: {} } }];
body.messages[1].content[0].name = "plain_tool";
const out = translateRequest(FORMATS.CLAUDE, FORMATS.KIRO, "claude-sonnet-4.5", body, true, null, "kiro");
expect(out._toolNameMap).toBeUndefined();
});
it("restores the client name on streamed Claude tool_use blocks", () => {
const state = { ...initState(FORMATS.CLAUDE), toolNameMap: new Map([["mcp_browser_computer", "mcp__browser__computer"]]) };
const chunk = {
id: "c1", object: "chat.completion.chunk", created: 1, model: "claude-sonnet-4.5",
choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: "call_1", type: "function", function: { name: "mcp_browser_computer", arguments: "" } }] }, finish_reason: null }],
};
const events = kiroToClaudeResponse(chunk, state);
const start = events.find((e) => e.type === "content_block_start" && e.content_block?.type === "tool_use");
expect(start.content_block.name).toBe("mcp__browser__computer");
});
it("passes unknown names through untouched", () => {
const state = { ...initState(FORMATS.CLAUDE), toolNameMap: new Map([["mcp_browser_computer", "mcp__browser__computer"]]) };
const chunk = {
id: "c1", object: "chat.completion.chunk", created: 1, model: "claude-sonnet-4.5",
choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: "call_2", type: "function", function: { name: "other_tool", arguments: "" } }] }, finish_reason: null }],
};
const events = kiroToClaudeResponse(chunk, state);
const start = events.find((e) => e.type === "content_block_start" && e.content_block?.type === "tool_use");
expect(start.content_block.name).toBe("other_tool");
});
it("restores the client name on OpenAI chunks passed through kiro-to-openai", () => {
const state = { ...initState(FORMATS.OPENAI), toolNameMap: new Map([["mcp_browser_computer", "mcp__browser__computer"]]) };
const chunk = {
id: "c1", object: "chat.completion.chunk", created: 1, model: "claude-sonnet-4.5",
choices: [{ index: 0, delta: { tool_calls: [{ index: 0, id: "call_1", type: "function", function: { name: "mcp_browser_computer", arguments: "{}" } }] }, finish_reason: null }],
};
const out = kiroToOpenAIResponse(chunk, state);
expect(out.choices[0].delta.tool_calls[0].function.name).toBe("mcp__browser__computer");
expect(kiroToOpenAIResponse(chunk, initState(FORMATS.OPENAI))).toBe(chunk);
});
});
describe("thinking display is preserved for Claude-format upstreams", () => {
it("keeps display on adaptive thinking", () => {
const body = { model: "claude-sonnet-5", thinking: { type: "adaptive", display: "summarized" }, output_config: { effort: "high" }, messages: [] };
applyThinking(FORMATS.CLAUDE, "claude-sonnet-5", body, "claude");
expect(body.thinking).toEqual({ type: "adaptive", display: "summarized" });
expect(body.output_config).toEqual({ effort: "high" });
});
it("keeps display on budget thinking and omits it when the client sent none", () => {
const withDisplay = { model: "claude-haiku-4-5-20251001", thinking: { type: "adaptive", display: "omitted" }, output_config: { effort: "low" }, messages: [] };
applyThinking(FORMATS.CLAUDE, "claude-haiku-4-5-20251001", withDisplay, "claude");
expect(withDisplay.thinking.type).toBe("enabled");
expect(withDisplay.thinking.display).toBe("omitted");
const without = { model: "claude-sonnet-5", thinking: { type: "adaptive" }, output_config: { effort: "high" }, messages: [] };
applyThinking(FORMATS.CLAUDE, "claude-sonnet-5", without, "claude");
expect(without.thinking).toEqual({ type: "adaptive" });
});
});
describe("redact-thinking beta follows the client's display request", () => {
it("keeps redact-thinking by default and drops it for summarized display", () => {
expect(selectAnthropicBeta("claude-sonnet-5")).toContain("redact-thinking-2026-02-12");
expect(selectAnthropicBeta("claude-sonnet-5", { thinking: { type: "adaptive", display: "omitted" } })).toContain("redact-thinking-2026-02-12");
const summarized = selectAnthropicBeta("claude-sonnet-5", { thinking: { type: "adaptive", display: "summarized" } });
expect(summarized).not.toContain("redact-thinking-2026-02-12");
expect(summarized).toContain("interleaved-thinking-2025-05-14");
expect(summarized).toContain("effort-2025-11-24");
});
});
describe("tool-result images reach Anthropic-compatible and Command Code upstreams", () => {
it("hoists a tool_result image into the same user turn after the results", () => {
const body = screenshotTurn();
const out = hoistToolResultImages(body);
const user = out.messages[2];
expect(user.content[0].type).toBe("tool_result");
expect(user.content[0].content.every((c) => c.type !== "image")).toBe(true);
expect(user.content.some((c) => c.type === "image" && c.source?.data === PNG)).toBe(true);
expect(user.content.find((c) => c.type === "text" && /toolu_1/.test(c.text))).toBeTruthy();
// No image: untouched object identity.
const plain = { messages: [{ role: "user", content: [{ type: "tool_result", tool_use_id: "x", content: "ok" }] }] };
expect(hoistToolResultImages(plain)).toBe(plain);
});
it("sends an image block to Command Code instead of a placeholder", () => {
const openaiBody = translateRequest(FORMATS.CLAUDE, FORMATS.OPENAI, "muse-spark", screenshotTurn(), true, null, "commandcode");
const out = openaiToCommandCodeRequest("muse-spark", openaiBody, true);
const json = JSON.stringify(out);
expect(json).not.toContain("[image omitted]");
expect(json).toContain(`"type":"image"`);
expect(json).toContain(`data:image/png;base64,${PNG}`);
expect(json).toContain(`"mediaType":"image/png"`);
});
});