fix(anthropic): default Responses JSON schemas through a helper (#50023) (#50375)

This commit is contained in:
ishaan-j-berri 2026-11-06 18:14:13 +08:00 • committed by GitHub
parent b1d8ee7191
commit a12100b33b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 41 additions and 12 deletions

View file

@ -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")),
}
}

View file

@ -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"}}}

View file

@ -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}},
}