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
14 changes: 13 additions & 1 deletion agent/bedrock_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,19 @@ def normalize_converse_response(response: Dict) -> SimpleNamespace:
elif "reasoningContent" in block:
reasoning = block["reasoningContent"]
if isinstance(reasoning, dict):
thinking_text = reasoning.get("text", "")
# Converse has two different shapes for this field. The
# NON-streaming ReasoningContentBlock is a union whose text
# member is {"reasoningText": {"text", "signature"}}; only the
# streaming ReasoningContentBlockDelta puts "text" at the top
# level (see the contentBlockDelta handler below). Reading the
# streaming shape here made reasoning_content always empty.
reasoning_text = reasoning.get("reasoningText")
if isinstance(reasoning_text, dict):
thinking_text = reasoning_text.get("text", "")
else:
# Delta shape, tolerated so a caller that hands us an
# already-flattened block still works.
thinking_text = reasoning.get("text", "")
if thinking_text:
reasoning_parts.append(str(thinking_text))
elif "toolUse" in block:
Expand Down
67 changes: 67 additions & 0 deletions tests/agent/test_bedrock_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1212,3 +1212,70 @@ def test_sigv4_claude_still_uses_anthropic_bedrock_sdk(self, monkeypatch):
runtime = self._resolve(monkeypatch, bearer=False)
assert runtime["api_mode"] == "anthropic_messages"
assert runtime.get("bedrock_anthropic") is True


class TestNonStreamingReasoningContent:
"""Non-streaming Converse responses use a different reasoningContent shape.

The AWS `ReasoningContentBlock` (non-streaming) is a union whose text member
is `{"reasoningText": {"text", "signature"}}`. Only the streaming
`ReasoningContentBlockDelta` puts `"text"` at the top level.
`normalize_converse_response` read the streaming shape, so
`reasoning_content` was always None on non-streaming calls and the model's
chain of thought was dropped from display, accounting and replay.
"""

@staticmethod
def _normalize(reasoning_block):
from agent.bedrock_adapter import normalize_converse_response

return normalize_converse_response(
{
"output": {
"message": {
"role": "assistant",
"content": [reasoning_block, {"text": "done"}],
}
},
"stopReason": "end_turn",
"usage": {"inputTokens": 10, "outputTokens": 5},
}
)

def test_reasoning_text_union_member_is_preserved(self):
resp = self._normalize(
{
"reasoningContent": {
"reasoningText": {
"text": "The user wants the file. I should call read_file.",
"signature": "sig-abc",
}
}
}
)
assert (
resp.choices[0].message.reasoning_content
== "The user wants the file. I should call read_file."
)
assert resp.choices[0].message.content == "done"

def test_flat_delta_shape_still_accepted(self):
"""A caller handing us an already-flattened block keeps working."""
resp = self._normalize({"reasoningContent": {"text": "flattened"}})
assert resp.choices[0].message.reasoning_content == "flattened"

def test_redacted_only_block_yields_no_text(self):
resp = self._normalize({"reasoningContent": {"redactedContent": b"\x00\x01"}})
assert resp.choices[0].message.reasoning_content is None

def test_absent_reasoning_is_none(self):
from agent.bedrock_adapter import normalize_converse_response

resp = normalize_converse_response(
{
"output": {"message": {"role": "assistant", "content": [{"text": "hi"}]}},
"stopReason": "end_turn",
"usage": {"inputTokens": 1, "outputTokens": 1},
}
)
assert resp.choices[0].message.reasoning_content is None