Skip to content

Anthropic provider drops empty assistant turns, breaking role alternation #416

Description

@cpsievert

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:
    continue

If 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 this

Open 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Priority: MediumValid bug or well-defined request with moderate impact or a workaround.ai-triage:doneMarks an issue whose AI triage workflow is complete.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions