diff --git a/litellm/litellm_core_utils/prompt_templates/common_utils.py b/litellm/litellm_core_utils/prompt_templates/common_utils.py index 2d26b5dd1e2..07d5e6314dd 100644 --- a/litellm/litellm_core_utils/prompt_templates/common_utils.py +++ b/litellm/litellm_core_utils/prompt_templates/common_utils.py @@ -1004,7 +1004,7 @@ def _has_legacy_defs(schema: object) -> bool: return "definitions" in schema or (isinstance(components, dict) and isinstance(components.get("schemas"), dict)) -# Schema-bomb budget for ``unpack_legacy_defs``: cap the cumulative JSON-byte +# Schema-bomb budget for ``$ref`` inlining: cap the cumulative JSON-byte # size of every inlined target. A byte cap is the universal measure of # expansion -- it simultaneously bounds ref-count fan-out, node-count # amplification, and scalar-byte amplification (large ``description`` / @@ -1012,14 +1012,14 @@ def _has_legacy_defs(schema: object) -> bool: # inline well under 1MB; 10MB sits two orders of magnitude above that, well # below memory-pressure territory, and rejects request-supplied bombs before # the proxy materialises them. -_LEGACY_DEFS_MAX_INLINED_BYTES: Final = 10_000_000 +DEFS_MAX_INLINED_BYTES: Final = 10_000_000 def unpack_legacy_defs( schema: dict, *, copy: bool = False, - max_inlined_bytes: int = _LEGACY_DEFS_MAX_INLINED_BYTES, + max_inlined_bytes: int = DEFS_MAX_INLINED_BYTES, ) -> dict: """Inline ``$ref``s backed by draft-04 ``definitions`` / OpenAPI ``components.schemas``. ``$defs`` is left untouched. diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 31713f8f085..ef4ad7011c5 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1267,13 +1267,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): import copy from litellm.litellm_core_utils.prompt_templates.common_utils import ( + DEFS_MAX_INLINED_BYTES, unpack_defs, ) json_schema = copy.deepcopy(json_schema) defs: Final = json_schema.pop("$defs", json_schema.pop("definitions", {})) if defs: - unpack_defs(json_schema, defs) + unpack_defs(json_schema, defs, max_inlined_bytes=DEFS_MAX_INLINED_BYTES) # Filter out unsupported fields for Anthropic's output_format API filtered_schema: Final = self.filter_anthropic_output_schema(json_schema) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index db147c17c4f..e6c6cab0631 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -12011,6 +12011,7 @@ "output_cost_per_token": 5e-06, "supports_assistant_prefill": true, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_computer_use": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -12033,6 +12034,7 @@ "output_cost_per_token": 5e-06, "supports_assistant_prefill": true, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_computer_use": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -12254,6 +12256,7 @@ "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index db147c17c4f..e6c6cab0631 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -12011,6 +12011,7 @@ "output_cost_per_token": 5e-06, "supports_assistant_prefill": true, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_computer_use": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -12033,6 +12034,7 @@ "output_cost_per_token": 5e-06, "supports_assistant_prefill": true, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_computer_use": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -12254,6 +12256,7 @@ "supports_assistant_prefill": false, "supports_computer_use": true, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, 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 2701991c01c..2f66a7259d3 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_structured_output.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_structured_output.py @@ -46,9 +46,7 @@ class TestAnthropicStructuredOutput: "json_schema": json_schema["json_schema"], } - output_format = config.map_response_format_to_anthropic_output_format( - response_format - ) + output_format = config.map_response_format_to_anthropic_output_format(response_format) # Verify that maxItems is filtered out for Anthropic assert output_format is not None @@ -82,9 +80,7 @@ class TestAnthropicStructuredOutput: "json_schema": json_schema["json_schema"], } - output_format = config.map_response_format_to_anthropic_output_format( - response_format - ) + output_format = config.map_response_format_to_anthropic_output_format(response_format) assert output_format is not None transformed_schema = output_format["schema"] @@ -112,9 +108,7 @@ class TestAnthropicStructuredOutput: "json_schema": json_schema["json_schema"], } - output_format = config.map_response_format_to_anthropic_output_format( - response_format - ) + output_format = config.map_response_format_to_anthropic_output_format(response_format) assert output_format is not None transformed_schema = output_format["schema"] @@ -125,10 +119,7 @@ class TestAnthropicStructuredOutput: # Nested maxItems should also be removed if "$defs" in transformed_schema: nested_item_schema = transformed_schema["$defs"].get("NestedItem", {}) - if ( - "properties" in nested_item_schema - and "tags" in nested_item_schema["properties"] - ): + if "properties" in nested_item_schema and "tags" in nested_item_schema["properties"]: assert "maxItems" not in nested_item_schema["properties"]["tags"] def test_other_constraints_preserved(self): @@ -153,9 +144,7 @@ class TestAnthropicStructuredOutput: "json_schema": json_schema["json_schema"], } - output_format = config.map_response_format_to_anthropic_output_format( - response_format - ) + output_format = config.map_response_format_to_anthropic_output_format(response_format) assert output_format is not None transformed_schema = output_format["schema"] @@ -177,3 +166,41 @@ class TestAnthropicStructuredOutput: assert "description" in age_schema assert "minimum value: 0" in age_schema["description"] assert "maximum value: 150" in age_schema["description"] + + +class TestAnthropicOutputFormatSchemaBudget: + """The $defs inlining in map_response_format_to_anthropic_output_format is byte-bounded.""" + + @staticmethod + def _response_format(schema: dict) -> dict: + return {"type": "json_schema", "json_schema": {"name": "out", "schema": schema}} + + def test_schema_bomb_rejected(self): + """A compact request whose $defs expand past the byte budget raises instead of materialising.""" + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + big = {"type": "string", "description": "x" * 200_000} + schema = { + "type": "object", + "$defs": {"Big": big}, + "properties": {f"p{i}": {"$ref": "#/$defs/Big"} for i in range(60)}, + } + + with pytest.raises(ValueError, match="budget"): + AnthropicConfig().map_response_format_to_anthropic_output_format(self._response_format(schema)) + + def test_normal_defs_still_resolve(self): + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + schema = { + "type": "object", + "$defs": {"Item": {"type": "string", "description": "an item"}}, + "properties": {"a": {"$ref": "#/$defs/Item"}, "b": {"$ref": "#/$defs/Item"}}, + } + + output_format = AnthropicConfig().map_response_format_to_anthropic_output_format(self._response_format(schema)) + assert output_format is not None + resolved = output_format["schema"]["properties"] + assert resolved["a"]["type"] == "string" + assert resolved["b"]["type"] == "string" + assert "$ref" not in str(resolved)