Repository navigation
Anthropic provider drops empty assistant turns, breaking role alternation #416
Description
Summary
AnthropicProvider._as_message_params (chatlas/_provider_anthropic.py:884-887) drops assistant turns that have zero content blocks instead of sending a placeholder:
# Drop empty assistant turns to avoid an API error
# (all messages must have non-empty content)
if turn.role == "assistant" and len(content) == 0:
continueIf a turn is dropped this way, it can produce two consecutive user-role messages in the outgoing request, which violates Anthropic's strict requirement that messages alternate user/assistant. This is the same class of bug ellmer just fixed (tidyverse/ellmer#1099, tidyverse/ellmer#1100; see also tidyverse/ellmer#711, tidyverse/ellmer#1070): ellmer now sends a "[empty string]" placeholder instead of dropping the turn, because dropping it both risks an alternation error and "confuses the model."
Note: chatlas already normalizes empty/whitespace text to "[empty string]" at the ContentText level (chatlas/_content.py:288-292), which covers the common case where a model returns an empty or whitespace-only text block (confirmed via the existing Databricks empty-response VCR cassette, tests/_vcr/test_provider_databricks/test_databricks_empty_response.yaml). The bug here is narrower: it's specifically about an AssistantTurn with a genuinely empty content list (contents=[]), which never goes through ContentText's normalization and instead hits the continue branch above.
Repro
from chatlas._provider_anthropic import AnthropicProvider
from chatlas._turn import AssistantTurn, UserTurn
provider = AnthropicProvider(
name="Anthropic",
model="claude-3-5-haiku-latest",
api_key="dummy-key-not-used",
)
turns = [
UserTurn("Respond with only two blank lines"),
AssistantTurn(contents=[], finish_reason="success"),
UserTurn("What's 1+1? Just give me the number"),
]
messages = provider._as_message_params(turns)
print([m["role"] for m in messages])
# ['user', 'user'] <-- two consecutive user messages; Anthropic's API would reject thisOpen question for triage: it's not yet confirmed whether Anthropic's real response-parsing path can ever actually produce an AssistantTurn with contents=[] (as opposed to always synthesizing at least one, possibly-empty, ContentText block first, which would already get the "[empty string]" treatment upstream and never reach this branch). Regardless, the message-construction logic itself is demonstrably unsafe if such a turn is ever constructed (e.g. programmatically, or via a future code path), so the fix is worth making independent of how often it's hit today.
Expected fix
Mirror ellmer's fix: replace the continue with a placeholder content block, e.g. [{"type": "text", "text": "[empty string]"}], instead of dropping the turn.
References
- ellmer PRs: Handle empty responses in AWS and Claude tidyverse/ellmer#1099, Handle empty thinking for AWS provider tidyverse/ellmer#1100 (issues: fix(chat_anthropic): Drop empty assistant turns in
as_json()tidyverse/ellmer#711, chat_aws_bedrock() fails after empty response from claude-sonnet-5 tidyverse/ellmer#1070) - chatlas:
chatlas/_provider_anthropic.py:884-887,chatlas/_content.py:288-292
Activity
fix(anthropic): send placeholder for empty assistant turns instead of…
fix(anthropic): send placeholder for empty assistant turns instead of…