From d5f44298947fd607df8981e5f1ec465588d76069 Mon Sep 17 00:00:00 2001 From: Ehsan Barkhordar Date: Mon, 7 Sep 2026 12:03:40 +0000 Subject: [PATCH 1/2] fix(bedrock): send a placeholder for empty assistant turns on Converse --- CHANGELOG.md | 2 ++ chatlas/_provider_bedrock_converse.py | 6 ++++++ tests/test_provider_bedrock_converse.py | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f23f7167..573b8ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug fixes +* `ChatBedrock()` (with the default `api="converse"`) no longer sends assistant turns with an empty `content` array, which Converse rejects. This happens when a response carries no content blocks, for example when a guardrail intervenes before the model produces any. A `"[empty string]"` placeholder is sent instead, matching how empty text content is already normalized. (#426) + * `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) * `ChatGoogle()` no longer raises `ValueError: Unknown content type: ContentThinking` on the second and later turns when `reasoning` is enabled; thinking content is now replayed to the model as thought parts, and the `thought_signature` on thought parts is preserved (previously only tool-call parts kept it). (#403) diff --git a/chatlas/_provider_bedrock_converse.py b/chatlas/_provider_bedrock_converse.py index 38698e8a..00e61f41 100644 --- a/chatlas/_provider_bedrock_converse.py +++ b/chatlas/_provider_bedrock_converse.py @@ -324,6 +324,12 @@ def as_converse_messages(turns: list[Turn]) -> list[MessageUnionTypeDef]: for c in turn.contents: content.append(as_converse_content(c, document_index=index)) index += 1 + + # Converse requires non-empty content, and dropping the turn instead + # would leave two consecutive user messages. + if role == "assistant" and not content: + content = [{"text": "[empty string]"}] + messages.append({"role": role, "content": content}) return messages diff --git a/tests/test_provider_bedrock_converse.py b/tests/test_provider_bedrock_converse.py index 4d5e0f71..6164d764 100644 --- a/tests/test_provider_bedrock_converse.py +++ b/tests/test_provider_bedrock_converse.py @@ -452,6 +452,28 @@ def test_unknown_turn_role_raises(self): with pytest.raises(ValueError, match="Unknown role"): as_converse_messages([turn]) + def test_empty_assistant_turn_gets_a_placeholder(self): + from chatlas._provider_bedrock_converse import as_converse_messages + from chatlas._turn import AssistantTurn, UserTurn + + turns = [ + UserTurn("Don't say anything"), + AssistantTurn([]), + UserTurn("What did I just say?"), + ] + messages = as_converse_messages(turns) + + assert [m["role"] for m in messages] == ["user", "assistant", "user"] + assert messages[1]["content"] == [{"text": "[empty string]"}] + + def test_empty_user_turn_is_left_alone(self): + from chatlas._provider_bedrock_converse import as_converse_messages + from chatlas._turn import UserTurn + + messages = as_converse_messages([UserTurn([])]) + + assert messages[0]["content"] == [] + class TestRequestTransport: def binary_request(self) -> "ConverseRequestTypeDef": From 0de559e31b8350ede99afc75222e377df179da63 Mon Sep 17 00:00:00 2001 From: Ehsan Barkhordar Date: Tue, 8 Sep 2026 00:14:22 +0000 Subject: [PATCH 2/2] docs(changelog): move the bedrock entry under Unreleased The entry landed under the 0.23.0 heading, which shipped on 2026-09-04. Release commit c21e245 renames Unreleased to the version at release time, so a new section is where a post-release fix belongs. --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 573b8ca5..27c82e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --> +## [Unreleased] + +### Bug fixes + +* `ChatBedrock()` (with the default `api="converse"`) no longer sends assistant turns with an empty `content` array, which Converse rejects. This happens when a response carries no content blocks, for example when a guardrail intervenes before the model produces any. A `"[empty string]"` placeholder is sent instead, matching how empty text content is already normalized. (#426) + + ## [0.23.0] - 2026-09-04 ### New features @@ -25,8 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug fixes -* `ChatBedrock()` (with the default `api="converse"`) no longer sends assistant turns with an empty `content` array, which Converse rejects. This happens when a response carries no content blocks, for example when a guardrail intervenes before the model produces any. A `"[empty string]"` placeholder is sent instead, matching how empty text content is already normalized. (#426) - * `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) * `ChatGoogle()` no longer raises `ValueError: Unknown content type: ContentThinking` on the second and later turns when `reasoning` is enabled; thinking content is now replayed to the model as thought parts, and the `thought_signature` on thought parts is preserved (previously only tool-call parts kept it). (#403)