From 6809224df9396eee7b78c26e83ae9baa74fdf68c Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:50:21 -0800 Subject: [PATCH] fix(anthropic): type the decoded JSON mode answer as object and test non-object answers json.loads output now goes through the existing TypeAdapter(object), which returns it unchanged, so no Any reaches the object guard. New stream and non-stream cases check that a list, string or number answer comes back as written. --- litellm/llms/base_llm/base_utils.py | 4 +++- .../chat/test_anthropic_chat_handler.py | 3 +++ .../test_anthropic_chat_transformation.py | 20 ++++++++++++++++++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/litellm/llms/base_llm/base_utils.py b/litellm/llms/base_llm/base_utils.py index c4e077817f5..45f623730a8 100644 --- a/litellm/llms/base_llm/base_utils.py +++ b/litellm/llms/base_llm/base_utils.py @@ -239,7 +239,9 @@ def _is_decoded_json_object( def decoded_answer_object(text: str) -> Mapping[str, AnswerValue] | None: try: - decoded: Final[object] = json.loads(text, parse_float=_source_float, parse_int=_source_int) + decoded: Final = _DECODED_JSON.validate_python( + json.loads(text, parse_float=_source_float, parse_int=_source_int) + ) except (json.JSONDecodeError, RecursionError): return None return decoded if _is_decoded_json_object(decoded) else None diff --git a/tests/unit/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/unit/llms/anthropic/chat/test_anthropic_chat_handler.py index 56a2f461bb4..c11fd58f84e 100644 --- a/tests/unit/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/unit/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -3262,6 +3262,9 @@ _DIGITS_PAST_THE_INT_LIMIT: Final = "1" * (sys.get_int_max_str_digits() + 1) (('{"n": 1e5}',), '{"n": 1e5}'), (('{"values": false}',), "false"), (('{"values": null}',), '{"values": null}'), + (("[1, 2]",), "[1, 2]"), + (('"x"',), '"x"'), + (("5",), "5"), pytest.param( ('{"values": ' + "[" * 300 + "]" * 300 + "}",), "[" * 300 + "]" * 300, diff --git a/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py index d61384e8be5..bf093f1a0d8 100644 --- a/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/unit/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -8584,6 +8584,24 @@ def test_convert_tool_response_to_message_keeps_arguments_too_deep_to_decode(arg assert message.content == arguments +@pytest.mark.parametrize("arguments", ["[1, 2]", '"x"', "5"]) +def test_convert_tool_response_to_message_keeps_a_non_object_answer_as_written(arguments: str) -> None: + message: Final = AnthropicConfig.convert_tool_response_to_message( + tool_calls=[ + ChatCompletionToolCallChunk( + id="toolu_1", + type="function", + function=ChatCompletionToolCallFunctionChunk(name=RESPONSE_FORMAT_TOOL_NAME, arguments=arguments), + index=0, + ) + ], + response_format_schema=None, + ) + + assert message is not None + assert message.content == arguments + + _DEEPLY_NESTED: Final = "[" * 300 + "]" * 300 @@ -8601,7 +8619,7 @@ _DEEPLY_NESTED: Final = "[" * 300 + "]" * 300 ) def test_convert_tool_response_to_message_unwraps_a_deeply_nested_answer( arguments: str, schema: dict[str, object] | None, expected: str -): +) -> None: message = AnthropicConfig.convert_tool_response_to_message( tool_calls=[ ChatCompletionToolCallChunk(