From 316661ef5ae58c449697cf6329c8497d7d4610bf Mon Sep 17 00:00:00 2001 From: Atharva Jaiswal <92455570+AtharvaJaiswal005@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:10:03 +0530 Subject: [PATCH] Fix Pydantic numeric constraints failing with Anthropic tool-based structured output The output_format path (newer models) already filters unsupported JSON schema constraints via filter_anthropic_output_schema(). However the tool-based path used by older models (map_response_format_to_anthropic_tool) was not applying this filter, causing Anthropic to reject schemas with minimum/maximum/exclusiveMinimum/exclusiveMaximum properties. Added filter_anthropic_output_schema() call in the tool-based path so both code paths consistently strip unsupported constraints. Fixes #21016 --- litellm/llms/anthropic/chat/transformation.py | 9 +- .../test_anthropic_structured_output.py | 132 ++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index fe57046f808..e08efa226a7 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -800,11 +800,18 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): ) if json_schema is None: return None + + # Filter unsupported constraints (minimum, maximum, etc.) from the schema. + # The output_format path (newer models) already does this via + # map_response_format_to_anthropic_output_format, but the tool-based + # path for older models was missing it. See #21016. + json_schema = self.filter_anthropic_output_schema(json_schema) + """ When using tools in this way: - https://docs.anthropic.com/en/docs/build-with-claude/tool-use#json-mode - You usually want to provide a single tool - You should set tool_choice (see Forcing tool use) to instruct the model to explicitly use that tool - - Remember that the model will pass the input to the tool, so the name of the tool and description should be from the model’s perspective. + - Remember that the model will pass the input to the tool, so the name of the tool and description should be from the model's perspective. """ _tool = self._create_json_tool_call_for_response_format( diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_structured_output.py b/tests/test_litellm/llms/anthropic/test_anthropic_structured_output.py index 705edeaf69c..1694808bcd9 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_structured_output.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_structured_output.py @@ -174,3 +174,135 @@ class TestAnthropicStructuredOutput: assert "description" in age_schema assert "minimum value: 0" in age_schema["description"] assert "maximum value: 150" in age_schema["description"] + + +class TestAnthropicToolBasedStructuredOutput: + """ + Test that structured output via the tool-based path (older models) also + filters unsupported JSON schema constraints. + + The output_format path (newer models like Sonnet 4.5+) already filters + constraints via filter_anthropic_output_schema(). The tool-based path + for older models was missing this filtering, causing Anthropic API errors + like: "For 'integer' type, properties maximum, minimum are not supported" + + Related issue: https://github.com/BerriAI/litellm/issues/21016 + """ + + def test_numeric_constraints_filtered_in_tool_path(self): + """ + Test that ge/le/gt/lt constraints on numeric fields are stripped + when using the tool-based response_format path (older models). + """ + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + class Rating(BaseModel): + score: int = Field(ge=1, le=10, description="Rating score") + confidence: float = Field(gt=0.0, lt=1.0, description="Confidence") + + config = AnthropicConfig() + json_schema = config.get_json_schema_from_pydantic_object(Rating) + + response_format = { + "type": "json_schema", + "json_schema": json_schema["json_schema"], + } + + tool = config.map_response_format_to_anthropic_tool( + value=response_format, optional_params={}, is_thinking_enabled=False + ) + + assert tool is not None + tool_schema = tool["input_schema"] + + # Numeric constraints should be stripped + score_schema = tool_schema["properties"]["score"] + assert "minimum" not in score_schema + assert "maximum" not in score_schema + + confidence_schema = tool_schema["properties"]["confidence"] + assert "exclusiveMinimum" not in confidence_schema + assert "exclusiveMaximum" not in confidence_schema + + def test_numeric_constraints_moved_to_description_in_tool_path(self): + """ + Test that stripped numeric constraints are added to the description + in the tool-based path, matching the output_format path behavior. + """ + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + class Rating(BaseModel): + score: int = Field(ge=1, le=10, description="Rating score") + + config = AnthropicConfig() + json_schema = config.get_json_schema_from_pydantic_object(Rating) + + response_format = { + "type": "json_schema", + "json_schema": json_schema["json_schema"], + } + + tool = config.map_response_format_to_anthropic_tool( + value=response_format, optional_params={}, is_thinking_enabled=False + ) + + assert tool is not None + score_schema = tool["input_schema"]["properties"]["score"] + assert "minimum value: 1" in score_schema["description"] + assert "maximum value: 10" in score_schema["description"] + + def test_string_constraints_filtered_in_tool_path(self): + """ + Test that minLength/maxLength constraints on string fields are + stripped in the tool-based path. + """ + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + class UserInput(BaseModel): + name: str = Field(min_length=1, max_length=100, description="User name") + + config = AnthropicConfig() + json_schema = config.get_json_schema_from_pydantic_object(UserInput) + + response_format = { + "type": "json_schema", + "json_schema": json_schema["json_schema"], + } + + tool = config.map_response_format_to_anthropic_tool( + value=response_format, optional_params={}, is_thinking_enabled=False + ) + + assert tool is not None + name_schema = tool["input_schema"]["properties"]["name"] + assert "minLength" not in name_schema + assert "maxLength" not in name_schema + assert "minimum length: 1" in name_schema["description"] + assert "maximum length: 100" in name_schema["description"] + + def test_array_constraints_filtered_in_tool_path(self): + """ + Test that minItems/maxItems constraints on list fields are + stripped in the tool-based path. + """ + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + class ItemList(BaseModel): + items: List[str] = Field(min_length=1, max_length=10, description="Items") + + config = AnthropicConfig() + json_schema = config.get_json_schema_from_pydantic_object(ItemList) + + response_format = { + "type": "json_schema", + "json_schema": json_schema["json_schema"], + } + + tool = config.map_response_format_to_anthropic_tool( + value=response_format, optional_params={}, is_thinking_enabled=False + ) + + assert tool is not None + items_schema = tool["input_schema"]["properties"]["items"] + assert "minItems" not in items_schema + assert "maxItems" not in items_schema