mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-16 20:02:45 +03:00
fix(sse): strip trailing assistant prefill on official Claude OAuth (#13572)
Adds `claude` to the providers whose trailing text-only assistant turn is stripped before dispatch — official Claude rejects assistant prefill with `400 This model does not support assistant message prefill`. Maintainer note: the strip applies to the whole `claude` provider family (API key as well as OAuth), matching what the Vertex-hosted Claude path (`open-sse/executors/antigravity.ts`), the Copilot path (`open-sse/executors/github.ts`) and the MITM handler already do unconditionally. Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run. Thanks @HouMinXi!
This commit is contained in:
1
changelog.d/fixes/claude-assistant-prefill.md
Normal file
1
changelog.d/fixes/claude-assistant-prefill.md
Normal file
@@ -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.
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user