diff --git a/open-sse/translator/request/openai-to-claude.js b/open-sse/translator/request/openai-to-claude.js index 580debfe..65a99b8c 100644 --- a/open-sse/translator/request/openai-to-claude.js +++ b/open-sse/translator/request/openai-to-claude.js @@ -96,6 +96,40 @@ export function openaiToClaudeRequest(model, body, stream) { flushCurrentMessage(); + // GUARD: some Claude auth channels (OAuth/Claude Code) reject requests + // that end on an assistant turn ("assistant message prefill" 400). + // Agentic loops (e.g. Hermes) sometimes resend their own last output as + // the new final message to request a continuation, with no new user + // turn in between. Normalize by appending a synthetic turn so the + // request always ends on `user`, regardless of auth channel or model. + // If the trailing assistant message has unresolved tool_use blocks, + // Anthropic separately requires a matching tool_result for each one + // (not just any user turn), so synthesize those instead of plain text. + { + const lastMsg = result.messages[result.messages.length - 1]; + if (lastMsg && lastMsg.role === ROLE.ASSISTANT) { + const unresolvedToolUseIds = Array.isArray(lastMsg.content) + ? lastMsg.content.filter(b => b.type === CLAUDE_BLOCK.TOOL_USE).map(b => b.id) + : []; + + if (unresolvedToolUseIds.length > 0) { + result.messages.push({ + role: ROLE.USER, + content: unresolvedToolUseIds.map(id => ({ + type: CLAUDE_BLOCK.TOOL_RESULT, + tool_use_id: id, + content: "Continuing." + })) + }); + } else { + result.messages.push({ + role: ROLE.USER, + content: [{ type: CLAUDE_BLOCK.TEXT, text: "Continue." }] + }); + } + } + } + // Add cache_control to last assistant message for (let i = result.messages.length - 1; i >= 0; i--) { const message = result.messages[i];