Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `ChatBedrock()` now defaults `base_url` to the official AWS SDKs' endpoint override environment variables when set: `AWS_ENDPOINT_URL_BEDROCK_RUNTIME` for `api="converse"`, and `AWS_ENDPOINT_URL_BEDROCK_MANTLE` for `api="messages"` and `api="responses"`. Similarly, `ChatAnthropic()` respects the `ANTHROPIC_BASE_URL` environment variable (via the anthropic SDK). Setting these variables is enough to route requests through a proxy or gateway, so you don't have to pass `base_url` on every call.

### Bug fixes
* `ChatAnthropic()` no longer drops assistant turns that have no content; doing so could produce two consecutive user messages, violating the API's user/assistant alternation requirement. A `"[empty string]"` placeholder is sent instead, matching how empty text content is already normalized. (#416)

* `ChatDatabricks()` no longer drops the assistant's reply from the conversation when a GPT-OSS endpoint streams typed content. The typed part array was merged into the accumulated completion before it was normalized, so every later text delta was appended to it one character at a time and the finished turn came back empty. (#409)
* `.to_solver()` no longer corrupts the system prompt or the prior turns it reads out of Inspect AI's message state. The system prompt was being set to the `repr()` of the `ChatMessageSystem` object rather than its text, and message content arriving in Inspect AI's `str` form (rather than as a list of `Content`) was iterated one character at a time. (#407)
Expand Down
13 changes: 9 additions & 4 deletions chatlas/_provider_anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,17 +884,22 @@ def _as_message_params(self, turns: Sequence[Turn]) -> list["MessageParam"]:
if not isinstance(turn, (UserTurn, AssistantTurn)):
raise ValueError(f"Unknown role {turn.role}")

content = [
content: list[ContentBlockParam] = [
self._as_content_block(c)
for c in turn.contents
if not isinstance(c, PROVIDER_ANNOTATION_TYPES)
or anthropic_replayable(c)
]

# Drop empty assistant turns to avoid an API error
# (all messages must have non-empty content)
# An assistant turn with no content can't simply be dropped:
# doing so could produce two consecutive user messages,
# violating the API's user/assistant alternation requirement.
# Send a placeholder instead (the API also requires all
# messages to have non-empty content).
if turn.role == "assistant" and len(content) == 0:
continue
content = [
cast("TextBlockParam", {"type": "text", "text": "[empty string]"})
]

# Add cache control to the last content block in the last turn
# https://docs.claude.com/en/docs/build-with-claude/prompt-caching#how-automatic-prefix-checking-works
Expand Down
18 changes: 12 additions & 6 deletions tests/test_provider_anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,24 +928,30 @@ def test_anthropic_list_models():
assert_list_models(chat_func)


def test_anthropic_removes_empty_assistant_turns():
"""Test that empty assistant turns are dropped to avoid API errors."""
def test_anthropic_empty_assistant_turn_placeholder():
"""Empty assistant turns get a placeholder instead of being dropped (#416).

Dropping the turn could produce two consecutive user messages, violating
the API's user/assistant alternation requirement.
"""
chat = chat_func()
chat.set_turns(
[
UserTurn("Don't say anything"),
AssistantTurn([]),
UserTurn("What did I just say?"),
]
)

# Get the message params that would be sent to the API
provider = cast(AnthropicProvider, chat.provider)
turns_json = provider._as_message_params(chat.get_turns())

# Should only have the user turn, not the empty assistant turn
assert len(turns_json) == 1
assert turns_json[0]["role"] == "user"
assert turns_json[0]["content"][0]["text"] == "Don't say anything" # type: ignore
# The empty assistant turn is kept (with placeholder content), so
# user/assistant roles still alternate as the API requires
assert [m["role"] for m in turns_json] == ["user", "assistant", "user"]
assert turns_json[1]["content"] == [{"type": "text", "text": "[empty string]"}]
assert turns_json[2]["content"][0]["text"] == "What did I just say?" # type: ignore


@pytest.mark.vcr
Expand Down
Loading