diff --git a/litellm/llms/anthropic/messages/responses_adapters/transformation.py b/litellm/llms/anthropic/messages/responses_adapters/transformation.py index 110e9fc40ff..b4455f1a4e2 100644 --- a/litellm/llms/anthropic/messages/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/messages/responses_adapters/transformation.py @@ -584,6 +584,12 @@ class LiteLLMAnthropicToResponsesAPIAdapter: if block.get("type") == "text" and isinstance(text := block.get("text"), str) and text ) + @staticmethod + def _resolve_output_format_strict(strict: bool | None) -> bool: + if strict is None: + return True + return strict + def translate_request( self, anthropic_request: AnthropicMessagesRequest, @@ -693,7 +699,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter: "type": "json_schema", "name": "structured_output", "schema": schema, - "strict": output_format.get("strict", False), + "strict": self._resolve_output_format_strict(output_format.get("strict")), } } diff --git a/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py b/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py index dbb8c925fa0..660f00cd722 100644 --- a/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py +++ b/tests/unit/llms/anthropic/messages/responses_adapters/test_responses_adapters_transformation.py @@ -5,7 +5,8 @@ Tests for LiteLLMAnthropicToResponsesAPIAdapter import json import os -from typing import Any, Dict, Final, List +from collections.abc import Mapping +from typing import Any, Dict, Final, List, Literal from unittest.mock import MagicMock import pytest @@ -24,6 +25,9 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import ( from litellm.litellm_core_utils.prompt_templates.reasoning_utils import ( encrypted_reasoning_signature, ) +from litellm.llms.anthropic.messages.adapters.transformation import ( + LiteLLMAnthropicMessagesAdapter, +) from litellm.llms.anthropic.messages.responses_adapters.transformation import ( LiteLLMAnthropicToResponsesAPIAdapter, ) @@ -141,21 +145,23 @@ class TestOutputConfigStructuredOutput: } def test_output_config_format_json_schema_converted(self): - """output_config.format.json_schema is converted to OpenAI text.format, defaulting strict to False.""" + """output_config.format.json_schema becomes strict OpenAI text.format.""" req = _make_request(output_config={"format": {"type": "json_schema", "schema": self._SCHEMA}}) kwargs = _ADAPTER.translate_request(req) assert "text" in kwargs fmt = kwargs["text"]["format"] assert fmt["type"] == "json_schema" assert fmt["schema"] == self._SCHEMA - assert fmt["strict"] is False + assert fmt["strict"] is True assert fmt["name"] == "structured_output" - def test_output_config_format_explicit_strict_true_is_preserved(self): - """Nested output_config.format with explicit strict=True is preserved.""" - req = _make_request(output_config={"format": {"type": "json_schema", "schema": self._SCHEMA, "strict": True}}) - kwargs = _ADAPTER.translate_request(req) - assert kwargs["text"]["format"]["strict"] is True + @pytest.mark.parametrize("strict", (True, False)) + def test_output_config_format_explicit_strict_is_preserved(self, strict: bool) -> None: + req: Final = _make_request( + output_config={"format": {"type": "json_schema", "schema": self._SCHEMA, "strict": strict}} + ) + kwargs: Final = _ADAPTER.translate_request(req) + assert kwargs["text"]["format"]["strict"] is strict def test_output_config_without_format_does_not_set_text(self): """output_config with only non-format keys doesn't produce text.format.""" @@ -164,12 +170,12 @@ class TestOutputConfigStructuredOutput: assert "text" not in kwargs def test_output_format_still_works(self): - """The original output_format field still takes precedence when present, defaulting strict to False.""" + """The original output_format field still defaults to strict mode.""" req = _make_request(output_format={"type": "json_schema", "schema": self._SCHEMA}) kwargs = _ADAPTER.translate_request(req) assert "text" in kwargs assert kwargs["text"]["format"]["type"] == "json_schema" - assert kwargs["text"]["format"]["strict"] is False + assert kwargs["text"]["format"]["strict"] is True def test_output_format_explicit_strict_false_is_preserved(self): """output_format with an explicit strict=False is preserved as False.""" @@ -183,6 +189,23 @@ class TestOutputConfigStructuredOutput: kwargs = _ADAPTER.translate_request(req) assert kwargs["text"]["format"]["strict"] is True + @pytest.mark.parametrize("format_field", ("output_format", "output_config")) + @pytest.mark.parametrize("strict_options", ({}, {"strict": None})) + def test_default_strict_matches_messages_to_chat_adapter( + self, + format_field: Literal["output_format", "output_config"], + strict_options: Mapping[str, None], + ) -> None: + output_format: Final = {"type": "json_schema", "schema": self._SCHEMA, **strict_options} + request: Final = _make_request( + **{format_field: output_format if format_field == "output_format" else {"format": output_format}} + ) + chat, _ = LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai(request) + responses: Final = _ADAPTER.translate_request(request) + + assert chat["response_format"]["json_schema"]["strict"] is True + assert responses["text"]["format"]["strict"] is chat["response_format"]["json_schema"]["strict"] + def test_output_format_takes_precedence_over_output_config(self): """output_format takes precedence over output_config.format, for both schema and strict.""" other_schema = {"type": "object", "properties": {"id": {"type": "integer"}}} diff --git a/tests/unit/llms/openai/responses/count_tokens/test_token_counter.py b/tests/unit/llms/openai/responses/count_tokens/test_token_counter.py index 29c25c5fdbf..35b544be31f 100644 --- a/tests/unit/llms/openai/responses/count_tokens/test_token_counter.py +++ b/tests/unit/llms/openai/responses/count_tokens/test_token_counter.py @@ -301,7 +301,7 @@ async def test_anthropic_format_counts_the_structured_output_schema(monkeypatch: assert sent == { "model": "gpt-4o", "input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "Answer in JSON."}]}], - "text": {"format": {"type": "json_schema", "name": "structured_output", "schema": schema, "strict": False}}, + "text": {"format": {"type": "json_schema", "name": "structured_output", "schema": schema, "strict": True}}, }