Skip to content

fix(bedrock): parse streamed Converse tool input like the non-streamed output - #963

Open
alexcastgonz (pcbeingused333) wants to merge 2 commits into
braintrustdata:mainfrom
pcbeingused333:fix/bedrock-converse-stream-tool-input
Open

alexcastgonz (pcbeingused333) wants to merge 2 commits into
braintrustdata:mainfrom
pcbeingused333:fix/bedrock-converse-stream-tool-input

Conversation

@pcbeingused333

Copy link
Copy Markdown

What

_TracedConverseStream joined the toolUse deltas of each block and stored the joined JSON string as the block's input. A converse_stream span therefore logged {"type": "tool_use", "input": "{\"city\":\"Paris\"}"}, while the same call through converse logs input as the dict Bedrock returns. test_wrap_bedrock_converse_normalizes_tool_config_and_output already asserts that shape for the non-streamed path (isinstance(tool_use.get("input"), dict)).

The joined string is now parsed when the stream ends:

  • An empty input (a call without arguments) becomes {}.
  • Input that doesn't parse, such as a call cut off by maxTokens, stays the raw string.

Text, reasoning and usage handling are unchanged.

Checked against real converse_stream captures (Claude Haiku 4.5 / Sonnet 4.6 with thinking, Nova Lite v1, Nova 2 Lite, gpt-oss-20b, all with tool calls). Every tool_use block was a string before and is a dict after. Reasoning text and signature, and usage, were already right.

Tests

  • New test_wrap_bedrock_converse_stream_parses_tool_input, with a cassette recorded from one real converse_stream call on the existing CONVERSE_STREAM_MODEL (us.amazon.nova-lite-v1:0) and _converse_tool_kwargs. It's in cassettes/latest/ and, like the other Bedrock cassettes, copied unchanged to cassettes/1.34.116/. It checks that the span's tool_use.input equals the parsed streamed arguments and is a dict. It fails on main ('{"city":"Paris"}' == {'city': 'Paris'}) and passes with the change.
  • nox -s "test_bedrock_runtime(latest)": 11 passed. nox -s "test_bedrock_runtime(1.34.116)": 10 passed, 1 skipped (the existing document-block test that 1.34.116 can't run). The new test also fails on main in the 1.34.116 session.
  • ruff check / format clean. The cassette was checked for credentials: the authorization header is filtered by the existing vcr_config.

This change was written with Claude Code; the tests above were run on main and on this branch.

🤖 Generated with Claude Code

…d output

converse_stream spans logged a tool_use block's input as the raw JSON
string joined from the toolUse deltas, while converse spans log the dict
Bedrock returns. Parse the joined string once the stream ends ("" becomes
{}, input that doesn't parse, e.g. cut off by maxTokens, stays a string).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T22:56:05.958353Z dd8c819 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dd8c819a33

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "Codex (@codex) address that feedback".

return {}
try:
return json.loads(text)
except ValueError:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve tool inputs beyond the decoder nesting limit

When a Bedrock model emits a valid JSON tool argument nested roughly 1,000 levels on supported Python 3.10/3.11, json.loads raises RecursionError, which bypasses this ValueError handler. _finish invokes this after setting _finished and from the generator's else path, so the exception escapes the final next() and leaves the span unlogged even though the Bedrock stream completed successfully; decoder-limit failures should fall back to the raw string too.

AGENTS.md reference: AGENTS.md:L17-L20

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, thanks. Fixed in 9477af1: _parse_streamed_tool_input now also falls back to the raw string on RecursionError, as harbor/identity.py already does, with a unit test for a 100,000-level nested input.

…epth

json.loads raises RecursionError, not ValueError, for a document nested
deeper than the decoder allows, so such tool input escaped the fallback
and the span was never logged. Fall back to the raw string for it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant