diff --git a/open-sse/translator/formats/claude.js b/open-sse/translator/formats/claude.js index adbd3ce8..128917ec 100644 --- a/open-sse/translator/formats/claude.js +++ b/open-sse/translator/formats/claude.js @@ -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; diff --git a/tests/translator/bugs-toClaude-context.test.js b/tests/translator/bugs-toClaude-context.test.js index 96b704e8..45707177 100644 --- a/tests/translator/bugs-toClaude-context.test.js +++ b/tests/translator/bugs-toClaude-context.test.js @@ -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