Skip to content

fix(bedrock): read reasoningText on non-streaming Converse responses - #89671

Open
Jiaaqiliu wants to merge 1 commit into
NousResearch:mainfrom
Jiaaqiliu:fix/bedrock-nonstreaming-reasoning-text
Open

fix(bedrock): read reasoningText on non-streaming Converse responses#89671
Jiaaqiliu wants to merge 1 commit into
NousResearch:mainfrom
Jiaaqiliu:fix/bedrock-nonstreaming-reasoning-text

Conversation

@Jiaaqiliu

Copy link
Copy Markdown

What does this PR do?

AWS Converse has two different shapes for reasoning content:

  • non-streaming ReasoningContentBlock — a union whose text member is {"reasoningText": {"text", "signature"}}
  • streaming ReasoningContentBlockDelta — puts "text" at the top level

normalize_converse_response used the streaming shape on the non-streaming response:

elif "reasoningContent" in block:
    reasoning = block["reasoningContent"]
    if isinstance(reasoning, dict):
        thinking_text = reasoning.get("text", "")     # always "" here
        if thinking_text:
            reasoning_parts.append(str(thinking_text))

So reasoning.get("text") was always empty and reasoning_content always None on non-streaming calls. The reasoning box never rendered, reasoning-token accounting saw nothing, and nothing was stored for replay. reasoningText appears nowhere in the tree (grep -rn reasoningText → 0 hits before this PR).

The streaming handler in stream_converse_response is correct for its shape, so this is a mismatch between the two functions rather than a systematic omission.

Reproduction

from agent.bedrock_adapter import normalize_converse_response

resp = normalize_converse_response({
    "output": {"message": {"role": "assistant", "content": [
        {"reasoningContent": {"reasoningText": {"text": "MY REASONING", "signature": "sig"}}},
        {"text": "done"},
    ]}},
    "stopReason": "end_turn",
    "usage": {"inputTokens": 10, "outputTokens": 5},
})

resp.choices[0].message.reasoning_content
# before: None
# after:  'MY REASONING'

Hit by any model that emits reasoningContent on a non-streaming converse() call — us.deepseek.r1-v1:0 emits it unconditionally, and Claude models do with extended thinking enabled via additionalModelRequestFields.

Related Issue

No open issue found. #21202 (merged) added reasoningContent handling to normalized responses but used the delta shape; #36261 is about preserving the reasoning signature on interleaved-thinking replay, which is a separate concern and unaffected by this change.

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • agent/bedrock_adapter.pynormalize_converse_response reads the reasoningText.text union member, falling back to the flat text form so a caller passing an already-flattened block keeps working. A block carrying only redactedContent correctly yields no text.
  • tests/agent/test_bedrock_adapter.py — new TestNonStreamingReasoningContent: union member preserved, flat form still accepted, redacted-only yields None, absent reasoning yields None.

How to Test

pytest tests/agent/test_bedrock_adapter.py -q

70 passed (66 existing + 4 new). Reverting only the source change makes test_reasoning_text_union_member_is_preserved fail with None != 'The user wants the file. I should call read_file.'.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • I've run pytest tests/agent/test_bedrock_adapter.py -q and all tests pass
  • I've added tests for my changes

AWS Converse has two different shapes for reasoning. The non-streaming
`ReasoningContentBlock` is a union whose text member is
`{"reasoningText": {"text", "signature"}}`; only the streaming
`ReasoningContentBlockDelta` puts `text` at the top level.

`normalize_converse_response` used the streaming shape, so
`reasoning.get("text", "")` was always empty on a non-streaming call and the
model's reasoning was dropped entirely: the reasoning box never rendered,
reasoning-token accounting saw nothing, and nothing was stored for replay.
`reasoningText` appeared nowhere in the tree. The streaming handler a few
hundred lines below is correct for its own shape — this is a mismatch between
the two functions, not a systematic omission.

Reproduces with any model that emits reasoningContent on a non-streaming
converse() call (for example us.deepseek.r1-v1:0, or a Claude model with
extended thinking enabled via additionalModelRequestFields).

The flat `{"text": ...}` form is still accepted as a fallback so a caller that
hands the function an already-flattened block keeps working. A block carrying
only `redactedContent` correctly yields no text.
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/bedrock AWS Bedrock (boto3, IAM) P2 Medium — degraded but workaround exists labels Aug 19, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference; please use your judgment.

Correct shape fix with the AWS union documented inline where future readers need it: non-streaming ReasoningContentBlock nests text under reasoningText, and the tolerant fallback keeps already-flattened delta blocks working. Tests cover the union member, the flat legacy shape, redacted-only blocks (no phantom text), and full absence — all four outcomes that matter.

— reviewer-b (automated review)

No blocking issues found.

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

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/bedrock AWS Bedrock (boto3, IAM) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants