From 667d33d851448fdec5c2b8f4339f965e91103a3f Mon Sep 17 00:00:00 2001 From: Anas Khan <83116240+anxkhn@users.noreply.github.com> Date: Sun, 5 Jul 2026 09:37:53 +0530 Subject: [PATCH] fix(bedrock): degrade to empty args on malformed streamed tool JSON In the Bedrock streaming path, tool-call arguments are accumulated across contentBlockDelta chunks and parsed with json.loads, but without the JSONDecodeError guard that every sibling provider uses (OpenAI streaming and non-streaming, SAP AI Core streaming). A truncated or malformed streamed tool-argument JSON therefore raises inside the generator and aborts the whole turn as an API_ERROR, instead of degrading to empty args like the other providers and the Bedrock non-streaming path. Wrap the json.loads call in try/except json.JSONDecodeError and fall back to {}, matching the existing behavior across the models package. Add a regression test that streams a truncated toolUse input and asserts the turn completes with empty args rather than an error. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> --- .../src/kagent/adk/models/_bedrock.py | 5 ++- .../tests/unittests/models/test_bedrock.py | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/python/packages/kagent-adk/src/kagent/adk/models/_bedrock.py b/python/packages/kagent-adk/src/kagent/adk/models/_bedrock.py index 5323a7ada8..70ca31b83d 100644 --- a/python/packages/kagent-adk/src/kagent/adk/models/_bedrock.py +++ b/python/packages/kagent-adk/src/kagent/adk/models/_bedrock.py @@ -421,7 +421,10 @@ def _produce(): if aggregated_text: final_parts.append(types.Part.from_text(text=aggregated_text)) for tool_id, tool in tool_uses.items(): - args = json.loads(tool["input_json"]) if tool["input_json"] else {} + try: + args = json.loads(tool["input_json"]) if tool["input_json"] else {} + except json.JSONDecodeError: + args = {} part = types.Part.from_function_call(name=tool["name"], args=args) if part.function_call: part.function_call.id = tool_id diff --git a/python/packages/kagent-adk/tests/unittests/models/test_bedrock.py b/python/packages/kagent-adk/tests/unittests/models/test_bedrock.py index 08cbfad5b5..d9bd9d2169 100644 --- a/python/packages/kagent-adk/tests/unittests/models/test_bedrock.py +++ b/python/packages/kagent-adk/tests/unittests/models/test_bedrock.py @@ -306,6 +306,42 @@ async def fake_to_thread(fn, **kwargs): tool_names = [t["toolSpec"]["name"] for t in call_kwargs["toolConfig"]["tools"]] assert tool_names == ["fetch_get_url"] + @pytest.mark.asyncio + async def test_streaming_malformed_tool_input_degrades_to_empty_args(self): + """A truncated/malformed streamed tool-argument JSON must not abort the + turn: it degrades to empty args, matching the OpenAI and SAP AI Core + streaming paths.""" + llm = KAgentBedrockLlm(model="us.anthropic.claude-sonnet-4-20250514-v1:0") + + stream_events = [ + {"contentBlockStart": {"start": {"toolUse": {"toolUseId": "call-xyz", "name": "get_weather"}}}}, + {"contentBlockDelta": {"delta": {"toolUse": {"input": '{"city": "Paris"'}}}}, + {"messageStop": {"stopReason": "tool_use"}}, + ] + mock_client = mock.MagicMock() + mock_client.converse_stream.return_value = {"stream": stream_events} + + async def fake_to_thread(fn, **kwargs): + return fn(**kwargs) + + request = mock.MagicMock() + request.model = "us.anthropic.claude-sonnet-4-20250514-v1:0" + request.contents = [] + request.config = None + + with ( + mock.patch("kagent.adk.models._bedrock._get_bedrock_client", return_value=mock_client), + mock.patch("kagent.adk.models._bedrock.asyncio.to_thread", side_effect=fake_to_thread), + ): + responses = [r async for r in llm.generate_content_async(request, stream=True)] + + final = responses[-1] + assert final.error_code is None + fc = final.content.parts[0].function_call + assert fc.name == "get_weather" + assert fc.id == "call-xyz" + assert fc.args == {} + def test_create_llm_from_bedrock_model_config(self): from kagent.adk.types import Bedrock, _create_llm_from_model_config