fix(anthropic): strip trailing assistant messages to prevent prefill error
Anthropic does not support assistant-message prefill and returns a 400 error when the conversation ends with an assistant turn. This commonly happens when heartbeat/system messages accumulate trailing assistant replies in the session history. The _merge_consecutive method already handles same-role merging but did not strip trailing assistant messages. The base provider's _enforce_role_alternation (used by OpenAI-compat) does strip them, but AnthropicProvider uses its own _merge_consecutive instead. Add a trailing-assistant stripping loop to _merge_consecutive, matching the behavior already present in _enforce_role_alternation. Includes 7 new tests covering merge + strip behavior.
This commit is contained in:
@@ -247,7 +247,12 @@ class AnthropicProvider(LLMProvider):
|
||||
|
||||
@staticmethod
|
||||
def _merge_consecutive(msgs: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Anthropic requires alternating user/assistant roles."""
|
||||
"""Anthropic requires alternating user/assistant roles.
|
||||
|
||||
Also strips trailing assistant messages since Anthropic does not
|
||||
support assistant-message prefill and will reject the request with
|
||||
a 400 error if the conversation ends with an assistant turn.
|
||||
"""
|
||||
merged: list[dict[str, Any]] = []
|
||||
for msg in msgs:
|
||||
if merged and merged[-1]["role"] == msg["role"]:
|
||||
@@ -262,6 +267,12 @@ class AnthropicProvider(LLMProvider):
|
||||
merged[-1]["content"] = prev_c
|
||||
else:
|
||||
merged.append(msg)
|
||||
|
||||
# Drop trailing assistant messages to avoid Anthropic's
|
||||
# "does not support assistant message prefill" 400 error.
|
||||
while merged and merged[-1].get("role") == "assistant":
|
||||
merged.pop()
|
||||
|
||||
return merged
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Tests for AnthropicProvider._merge_consecutive."""
|
||||
|
||||
from nanobot.providers.anthropic_provider import AnthropicProvider
|
||||
|
||||
|
||||
class TestMergeConsecutive:
|
||||
"""Verify role alternation and trailing-assistant stripping."""
|
||||
|
||||
def test_basic_alternation(self):
|
||||
msgs = [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "hi"},
|
||||
{"role": "user", "content": "bye"},
|
||||
]
|
||||
result = AnthropicProvider._merge_consecutive(msgs)
|
||||
assert len(result) == 3
|
||||
assert [m["role"] for m in result] == ["user", "assistant", "user"]
|
||||
|
||||
def test_consecutive_same_role_merged(self):
|
||||
msgs = [
|
||||
{"role": "user", "content": "a"},
|
||||
{"role": "user", "content": "b"},
|
||||
{"role": "assistant", "content": "reply"},
|
||||
]
|
||||
result = AnthropicProvider._merge_consecutive(msgs)
|
||||
# Two user messages merged into one, trailing assistant stripped
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
|
||||
def test_trailing_assistant_stripped(self):
|
||||
"""Anthropic rejects prefill — trailing assistant must be removed."""
|
||||
msgs = [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "hi"},
|
||||
]
|
||||
result = AnthropicProvider._merge_consecutive(msgs)
|
||||
assert len(result) == 1
|
||||
assert result[0]["role"] == "user"
|
||||
assert result[0]["content"] == "hello"
|
||||
|
||||
def test_multiple_trailing_assistant_stripped(self):
|
||||
msgs = [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "a"},
|
||||
{"role": "user", "content": "ok"},
|
||||
{"role": "assistant", "content": "b"},
|
||||
{"role": "assistant", "content": "c"},
|
||||
]
|
||||
result = AnthropicProvider._merge_consecutive(msgs)
|
||||
# b+c merged into one assistant, then stripped as trailing
|
||||
assert len(result) == 3
|
||||
assert result[-1]["role"] == "user"
|
||||
assert result[-1]["content"] == "ok"
|
||||
|
||||
def test_empty_messages(self):
|
||||
assert AnthropicProvider._merge_consecutive([]) == []
|
||||
|
||||
def test_single_user_message(self):
|
||||
msgs = [{"role": "user", "content": "hi"}]
|
||||
result = AnthropicProvider._merge_consecutive(msgs)
|
||||
assert len(result) == 1
|
||||
|
||||
def test_single_assistant_stripped(self):
|
||||
msgs = [{"role": "assistant", "content": "hi"}]
|
||||
result = AnthropicProvider._merge_consecutive(msgs)
|
||||
assert len(result) == 0
|
||||
Reference in New Issue
Block a user