fix(llms/aws_bedrock): iterate Converse content blocks for Anthropic text (#6369)

This commit is contained in:
Ayaan Gazali
2026-09-25 03:50:09 -07:00
committed by GitHub
parent ccd216cff0
commit fb6d5e1917
2 changed files with 60 additions and 3 deletions
+12 -3
View File
@@ -579,11 +579,20 @@ class AWSBedrockLLM(LLMBase):
# Use converse API for Anthropic models
response = self.client.converse(**converse_params)
# Parse Converse API response
# Parse Converse API response. Claude reasoning models can emit a
# `reasoningContent` block before the `text` block, so iterate to
# find the first block that carries text instead of indexing
# content[0] (same approach as the MiniMax branch below).
if hasattr(response, 'output') and hasattr(response.output, 'message'):
return response.output.message.content[0].text
for block in response.output.message.content:
if hasattr(block, 'text'):
return block.text
return ""
elif 'output' in response and 'message' in response['output']:
return response['output']['message']['content'][0]['text']
for block in response['output']['message']['content']:
if 'text' in block:
return block['text']
return ""
else:
return str(response)
+48
View File
@@ -506,3 +506,51 @@ class TestParseResponseLegacy:
response = {"body": body}
result = llm._parse_response(response, tools=None)
assert result == "hello from ai21"
class TestAnthropicConverseContentParsing:
"""The Anthropic Converse branch must not assume content[0] is the text
block: Claude reasoning models emit a reasoningContent block before the
text block, and some stop conditions produce an empty content array. The
parser iterates for the first block carrying text, like the MiniMax branch.
"""
def test_text_after_reasoning_content_block(self, mock_boto3):
mock_boto3.converse.return_value = {
"output": {
"message": {
"content": [
{"reasoningContent": {"reasoningText": {"text": "step by step..."}}},
{"text": "final answer"},
]
}
}
}
llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3)
assert llm.generate_response(MESSAGES) == "final answer"
def test_empty_content_returns_empty_string(self, mock_boto3):
mock_boto3.converse.return_value = {"output": {"message": {"content": []}}}
llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3)
assert llm.generate_response(MESSAGES) == ""
def test_plain_text_content_still_returned(self, mock_boto3):
mock_boto3.converse.return_value = _converse_response("plain answer")
llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3)
assert llm.generate_response(MESSAGES) == "plain answer"
def test_object_style_response_iterates_blocks(self, mock_boto3):
# Defensive attr-style branch: object wrapper with a reasoning block first.
from types import SimpleNamespace
reasoning_block = SimpleNamespace(reasoningContent={"reasoningText": {"text": "hmm"}})
text_block = SimpleNamespace(text="object answer")
mock_boto3.converse.return_value = SimpleNamespace(
output=SimpleNamespace(message=SimpleNamespace(content=[reasoning_block, text_block]))
)
llm = _make_llm("anthropic.claude-3-5-sonnet-20240620-v1:0", mock_boto3)
assert llm.generate_response(MESSAGES) == "object answer"