diff --git a/src/anthropic/lib/streaming/_messages.py b/src/anthropic/lib/streaming/_messages.py index b6b5f538..539eae2a 100644 --- a/src/anthropic/lib/streaming/_messages.py +++ b/src/anthropic/lib/streaming/_messages.py @@ -477,7 +477,12 @@ def accumulate_event( json_buf += bytes(event.delta.partial_json, "utf-8") if json_buf: - content.input = from_json(json_buf, partial_mode=True) + try: + content.input = from_json(json_buf, partial_mode=True) + except ValueError as e: + raise ValueError( + f"Unable to parse tool parameter JSON from model. Please retry your request or adjust your prompt. Error: {e}. JSON: {json_buf.decode('utf-8')}" + ) from e setattr(content, JSON_BUF_PROPERTY, json_buf) elif event.delta.type == "citations_delta": diff --git a/tests/lib/streaming/test_partial_json.py b/tests/lib/streaming/test_partial_json.py index 88a6e49f..93a56e13 100644 --- a/tests/lib/streaming/test_partial_json.py +++ b/tests/lib/streaming/test_partial_json.py @@ -147,3 +147,61 @@ def test_partial_json_with_invalid_json(self) -> None: ) except Exception as e: raise AssertionError(f"Unexpected error type: {type(e).__name__} with message: {str(e)}") from e + + def test_partial_json_with_invalid_json_non_beta(self) -> None: + """Test that invalid JSON raises a helpful error on the non-beta path too.""" + from anthropic.types import RawMessageStartEvent, RawContentBlockDeltaEvent, RawContentBlockStartEvent + from anthropic.types.usage import Usage + from anthropic.types.message import Message + from anthropic.types.direct_caller import DirectCaller + from anthropic.types.input_json_delta import InputJSONDelta + from anthropic.lib.streaming._messages import accumulate_event as accumulate_event_non_beta + + msg = Message( + id="msg_123", + type="message", + role="assistant", + content=[], + model="claude-haiku-4-5-20251001", + stop_reason=None, + stop_sequence=None, + usage=Usage(input_tokens=10, output_tokens=1), + ) + snapshot = accumulate_event_non_beta( + event=RawMessageStartEvent(type="message_start", message=msg), + current_snapshot=None, + ) + snapshot = accumulate_event_non_beta( + event=RawContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=ToolUseBlock( + type="tool_use", + id="toolu_01", + name="get_weather", + input={}, + caller=DirectCaller(type="direct"), + ), + ), + current_snapshot=snapshot, + ) + + try: + accumulate_event_non_beta( + event=RawContentBlockDeltaEvent( + type="content_block_delta", + index=0, + delta=InputJSONDelta( + type="input_json_delta", + partial_json='{"city": INVALID_VALUE}', + ), + ), + current_snapshot=snapshot, + ) + raise AssertionError("Expected ValueError for invalid JSON, but no error was raised.") + except ValueError as e: + assert str(e).startswith( + "Unable to parse tool parameter JSON from model. Please retry your request or adjust your prompt." + ) + except Exception as e: + raise AssertionError(f"Unexpected error type: {type(e).__name__} with message: {str(e)}") from e