fix(translator): don't drop image-only user messages in prepareClaudeRequest

hasValidContent() only treated text/tool_use/tool_result blocks as valid
content, so a user message containing only an image block was filtered
out as empty. When it was the only non-system message, this left an
empty messages array and Anthropic rejected the request.
This commit is contained in:
cardinusantara
2026-08-05 13:19:55 +07:00
committed by decolua
parent 646b3b9ba3
commit a7941ddab4
2 changed files with 24 additions and 1 deletions

View File

@@ -16,7 +16,9 @@ export function hasValidContent(msg) {
return msg.content.some(block =>
(block.type === CLAUDE_BLOCK.TEXT && block.text?.trim()) ||
block.type === CLAUDE_BLOCK.TOOL_USE ||
block.type === CLAUDE_BLOCK.TOOL_RESULT
block.type === CLAUDE_BLOCK.TOOL_RESULT ||
block.type === CLAUDE_BLOCK.IMAGE ||
block.type === CLAUDE_BLOCK.DOCUMENT
);
}
return false;

View File

@@ -67,6 +67,27 @@ describe("OpenAI → Claude context mapping", () => {
expect(JSON.stringify(out), "remote image dropped").toContain("pic.png");
});
// claude.js hasValidContent() — a user message whose content is only an
// image (no text block) was filtered out by prepareClaudeRequest's
// "drop empty messages" pass, so an image-only turn (e.g. a vision
// describe request with no accompanying prompt text in the user message)
// produced an empty `messages` array and Anthropic rejected the request
// with "messages: at least one message is required".
it("user message with only an image is not dropped as empty", () => {
const out = T({
messages: [
{ role: "system", content: "Describe the image." },
{ role: "user", content: [
{ type: "image_url", image_url: { url: "data:image/png;base64,AAAA" } },
] },
],
});
expect(out.messages.length, "image-only user message was dropped").toBeGreaterThan(0);
expect(out.messages[0].content).toEqual(
expect.arrayContaining([expect.objectContaining({ type: "image" })])
);
});
// prepareClaudeRequest reconciles max_tokens vs thinking.budget_tokens.
// applyThinking runs after adjustMaxTokens caps max_tokens, so a claude-budget
// model at "max" effort (budget 128000) can exceed the clamped max_tokens and