diff --git a/changelog.d/fixes/claude-assistant-prefill.md b/changelog.d/fixes/claude-assistant-prefill.md new file mode 100644 index 0000000000..8b01de5170 --- /dev/null +++ b/changelog.d/fixes/claude-assistant-prefill.md @@ -0,0 +1 @@ +- Strip a trailing text-only assistant turn before official Claude OAuth dispatch. Claude returns 400 `This model does not support assistant message prefill` for that shape; the shared strip only covered Mistral. diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 1f802796a9..78a27ebb4c 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -1,4 +1,5 @@ { + "_rebaseline_2026_09_15_13572_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/executors/base.ts->1754. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.", "_rebaseline_2026_09_15_13445_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/utils/proxyFetch.ts->1296. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.", "_rebaseline_2026_09_15_13643_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/executors/codex.ts->1528. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.", "_rebaseline_2026_09_15_13609_combined_growth": "Combined growth of the 2026-09-15 maxmad64bis uplift batch (each PR rebaselined its own growth; the merged sum is larger): open-sse/services/accountFallback.ts->2507. Every hunk is flag-gated or a verified fix covered by that PR's tests; see the batch report.", @@ -450,7 +451,7 @@ "_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).", "_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.", "open-sse/executors/antigravity.ts": 1665, - "open-sse/executors/base.ts": 1753, + "open-sse/executors/base.ts": 1754, "open-sse/executors/chatgpt-web.ts": 5056, "open-sse/executors/codex.ts": 1528, "open-sse/executors/cursor.ts": 1759, diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 5d07ef73c3..4b559d396e 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -1354,8 +1354,9 @@ export class BaseExecutor { // drop any tool_result orphaned by that strip (discussion #2410). const adjacent = isClaude ? fixToolPairs(fixToolAdjacency(fixed)) : fixed; const stripped = stripTrailingAssistantOrphanToolUse(adjacent); - // Some providers (e.g. Mistral) require the last message to be user - // or tool and reject trailing assistant text messages with 400 (#3396). + // Some providers (Mistral #3396, official Claude OAuth) reject a + // trailing text-only assistant turn with 400. Strip here so combo + // failover does not burn the next account on the same body. tb.messages = stripTrailingAssistantForProvider(stripped, this.provider); } } diff --git a/open-sse/services/contextManager.ts b/open-sse/services/contextManager.ts index 3198a14905..3d4dddec1a 100644 --- a/open-sse/services/contextManager.ts +++ b/open-sse/services/contextManager.ts @@ -937,12 +937,13 @@ export function stripTrailingAssistantOrphanToolUse( } /** - * Providers that strictly require the last message to be `user` or `tool`. - * A trailing `assistant` message with plain text content (no tool_use) is - * valid for Anthropic/OpenAI (signals "continue from here") but rejected by - * Mistral with: "Expected last role User or Tool … but got assistant" (#3396). + * Some providers reject a trailing text-only assistant turn. + * Mistral: "Expected last role User or Tool but got assistant" (#3396). + * Official Claude OAuth: "This model does not support assistant message + * prefill. The conversation must end with a user message." (live 2026-09-13 + * on claude/claude-opus-5). */ -const PROVIDERS_REQUIRING_USER_LAST_MESSAGE = new Set(["mistral"]); +const PROVIDERS_REQUIRING_USER_LAST_MESSAGE = new Set(["mistral", "claude"]); /** * Strip a trailing `assistant` message that contains ONLY plain text (no diff --git a/tests/unit/mistral-trailing-assistant.test.ts b/tests/unit/mistral-trailing-assistant.test.ts index 1ec5769d0d..8d625a7f05 100644 --- a/tests/unit/mistral-trailing-assistant.test.ts +++ b/tests/unit/mistral-trailing-assistant.test.ts @@ -1,16 +1,13 @@ /** - * Regression test for #3396: Mistral returns 400 when the last message is - * `role: "assistant"` with plain text content. + * Regression test for stripTrailingAssistantForProvider. * - * `stripTrailingAssistantOrphanToolUse` only removed tool_use blocks — it left - * trailing text-only assistant messages intact. Mistral (and providers sharing - * the same constraint) reject such requests with: - * "400: Expected last role User or Tool (or Assistant with prefix True) - * for serving but got assistant" + * Mistral (#3396) returns 400 when the last message is role assistant with + * plain text. Official Claude OAuth (claude-opus-5 live 2026-09-13) returns + * 400 "This model does not support assistant message prefill" for the same + * shape. stripTrailingAssistantOrphanToolUse only removes tool_use blocks. * - * The fix adds `stripTrailingAssistantForProvider(messages, provider)` which - * also drops a trailing text-only assistant message for providers that require - * user-last format (e.g. "mistral"). + * stripTrailingAssistantForProvider drops a trailing text-only assistant + * message for providers in PROVIDERS_REQUIRING_USER_LAST_MESSAGE. */ import { describe, it } from "node:test"; import assert from "node:assert/strict"; @@ -56,10 +53,21 @@ describe("stripTrailingAssistantForProvider (#3396)", () => { assert.strictEqual(result.length, 2); }); - it("does NOT strip trailing text assistant for anthropic/claude", () => { + it("strips trailing text-only assistant message for claude", () => { const msgs = [user("hi"), assistant("continue from here")]; const result = stripTrailingAssistantForProvider(msgs, "claude"); - assert.strictEqual(result.length, 2); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].role, "user"); + }); + + it("strips trailing assistant with array-string content for claude", () => { + const msgs = [ + user("hi"), + { role: "assistant", content: [{ type: "text", text: "continue from here" }] }, + ]; + const result = stripTrailingAssistantForProvider(msgs, "claude"); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].role, "user"); }); it("returns messages unchanged when last message is user", () => {