From de162eb71916e4afd3d80d5145e889502eb9c5f9 Mon Sep 17 00:00:00 2001 From: "R.D." Date: Wed, 1 Apr 2026 20:02:31 -0400 Subject: [PATCH] fix: drop assistant prefill for cc bridge --- open-sse/services/claudeCodeCompatible.ts | 25 ++++++++++++++++++++++ tests/unit/cc-compatible-provider.test.mjs | 23 +++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 5ebf6e90..24540d26 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -270,6 +270,31 @@ function buildClaudeCodeCompatibleMessages(messages: MessageLike[]) { merged.push({ role: message.role, content: [...message.content] }); } + // CC-compatible sites we tested reject assistant-prefill shaped requests even + // when Anthropic would normally allow them. Keep assistant/model history, but + // drop trailing assistant turns so the upstream request ends on a user turn. + while (merged.length > 0 && merged[merged.length - 1].role === "assistant") { + merged.pop(); + } + + if (merged.length === 0) { + const fallbackText = converted + .flatMap((message) => message.content) + .map((block) => toNonEmptyString(block.text)) + .filter(Boolean) + .join("\n") + .trim(); + + if (fallbackText) { + return [ + { + role: "user" as const, + content: [{ type: "text", text: fallbackText, cache_control: { type: "ephemeral" } }], + }, + ]; + } + } + for (let i = merged.length - 1; i >= 0; i--) { if (merged[i].role !== "user") continue; const lastBlock = merged[i].content[merged[i].content.length - 1]; diff --git a/tests/unit/cc-compatible-provider.test.mjs b/tests/unit/cc-compatible-provider.test.mjs index e4c487ab..8035af38 100644 --- a/tests/unit/cc-compatible-provider.test.mjs +++ b/tests/unit/cc-compatible-provider.test.mjs @@ -51,7 +51,7 @@ test.after(() => { fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); }); -test("buildClaudeCodeCompatibleRequest keeps order/text while mapping unsupported roles", () => { +test("buildClaudeCodeCompatibleRequest keeps prior role history while dropping trailing assistant prefill", () => { const payload = buildClaudeCodeCompatibleRequest({ sourceBody: { reasoning_effort: "xhigh", @@ -79,6 +79,7 @@ test("buildClaudeCodeCompatibleRequest keeps order/text while mapping unsupporte { role: "user", content: [{ type: "text", text: "u1" }, { type: "image_url" }] }, { role: "model", content: "a1" }, { role: "user", content: [{ type: "text", text: "u2" }, { type: "tool_result" }] }, + { role: "model", content: "prefill" }, ], }, model: "claude-sonnet-4-6", @@ -120,6 +121,26 @@ test("buildClaudeCodeCompatibleRequest keeps order/text while mapping unsupporte assert.equal(JSON.parse(payload.metadata.user_id).session_id, "session-1"); }); +test("buildClaudeCodeCompatibleRequest falls back to a user turn when the source only has assistant/model text", () => { + const payload = buildClaudeCodeCompatibleRequest({ + sourceBody: { + messages: [{ role: "model", content: "draft" }], + }, + normalizedBody: { + messages: [{ role: "model", content: "draft" }], + }, + model: "claude-sonnet-4-6", + sessionId: "session-only-assistant", + }); + + assert.deepEqual(payload.messages, [ + { + role: "user", + content: [{ type: "text", text: "draft", cache_control: { type: "ephemeral" } }], + }, + ]); +}); + test("buildClaudeCodeCompatibleRequest honors token priority fields", () => { const payload = buildClaudeCodeCompatibleRequest({ sourceBody: { max_completion_tokens: 321 },