Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1432,7 +1432,7 @@ def map_openai_params(
):
_output_format = self.map_response_format_to_anthropic_output_format(value)
if _output_format is not None:
optional_params["output_format"] = _output_format
optional_params.setdefault("output_config", {})["format"] = _output_format
else:
_tool = self.map_response_format_to_anthropic_tool(value, optional_params, is_thinking_enabled)
if _tool is None:
Expand Down Expand Up @@ -1713,10 +1713,8 @@ def update_headers_with_optional_anthropic_beta(self, headers: dict, optional_pa
)
if optional_params.get("context_management") is not None:
self._ensure_context_management_beta_header(headers, optional_params["context_management"])
output_config = optional_params.get("output_config")
if optional_params.get("output_format") is not None or (
isinstance(output_config, dict) and output_config.get("format") is not None
):
# Legacy output_format passthrough still needs the beta header
if optional_params.get("output_format") is not None:
self._ensure_beta_header(headers, ANTHROPIC_BETA_HEADER_VALUES.STRUCTURED_OUTPUT_2025_09_25.value)
if optional_params.get("speed") == "fast":
self._ensure_beta_header(headers, ANTHROPIC_BETA_HEADER_VALUES.FAST_MODE_2026_02_01.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,11 @@ def test_anthropic_chat_transform_request_includes_context_management():
assert result["context_management"] == _sample_context_management_payload()


def test_anthropic_structured_output_beta_header():
def test_anthropic_structured_output_no_beta_header_for_ga():
"""
Test that GA models using output_config.format do NOT include the
structured-outputs beta header (it's only needed for legacy output_format).
"""
from litellm.types.utils import CallTypes
from litellm.utils import return_raw_request

Expand Down Expand Up @@ -945,12 +949,15 @@ def test_anthropic_structured_output_beta_header():
)

assert response is not None
print(f"response: {response}")
print(f"raw_request_headers: {response['raw_request_headers']}")
assert (
"structured-outputs-2025-11-13"
in response["raw_request_headers"]["anthropic-beta"]
# GA models should use output_config.format, no beta header needed
beta_header = response["raw_request_headers"].get("anthropic-beta", "")
assert "structured-outputs-2025-11-13" not in beta_header, (
f"GA models should not include structured-outputs beta header, got: {beta_header}"
)
# Verify the request body uses output_config.format
assert "output_config" in response["raw_request_body"]
assert "format" in response["raw_request_body"]["output_config"]
assert "output_format" not in response["raw_request_body"]


@pytest.mark.parametrize(
Expand All @@ -965,7 +972,7 @@ def test_anthropic_structured_output_beta_header():
def test_opus_uses_native_structured_output(model_name):
"""
Test that Opus 4.5 and 4.6 models use native Anthropic structured outputs
(output_format) rather than the tool-based workaround.
(output_config.format GA API) rather than the tool-based workaround.
"""
config = AnthropicConfig()

Expand All @@ -992,9 +999,13 @@ def test_opus_uses_native_structured_output(model_name):
drop_params=False,
)

# Should use output_format (native structured outputs)
assert "output_format" in optional_params
assert optional_params["output_format"]["type"] == "json_schema"
# Should use output_config.format (GA structured outputs API)
assert "output_config" in optional_params
assert "format" in optional_params["output_config"]
assert optional_params["output_config"]["format"]["type"] == "json_schema"

# Should NOT use legacy output_format
assert "output_format" not in optional_params

# Should NOT create a tool-based workaround
assert "tools" not in optional_params
Expand Down Expand Up @@ -1031,14 +1042,100 @@ def test_non_structured_output_model_uses_tool_workaround():
drop_params=False,
)

# Should NOT use output_format
# Should NOT use output_config.format or legacy output_format
assert "output_format" not in optional_params
assert "output_config" not in optional_params

# Should use tool-based workaround
assert "tools" in optional_params
assert "tool_choice" in optional_params


def test_ga_structured_output_in_transform_request():
"""
Test that transform_request produces output_config.format (GA API)
instead of output_format (beta API) for supported models.
"""
config = AnthropicConfig()

response_format = {
"type": "json_schema",
"json_schema": {
"name": "test_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
},
"required": ["name", "age"],
"additionalProperties": False,
},
},
}

optional_params = config.map_openai_params(
non_default_params={"response_format": response_format},
optional_params={"max_tokens": 1024},
model="claude-sonnet-4-5-20250514",
drop_params=False,
)

headers = {}
result = config.transform_request(
model="claude-sonnet-4-5-20250514",
messages=[{"role": "user", "content": "Hello"}],
optional_params=optional_params,
litellm_params={},
headers=headers,
)

# GA API: should use output_config.format
assert "output_config" in result
assert "format" in result["output_config"]
assert result["output_config"]["format"]["type"] == "json_schema"

# Should NOT use legacy output_format
assert "output_format" not in result

# No structured-outputs beta header needed for GA
beta_header = headers.get("anthropic-beta", "")
assert "structured-outputs-2025-11-13" not in beta_header


def test_ga_structured_output_with_existing_output_config_effort():
"""
Test that output_config.format and output_config.effort can coexist
when both response_format and output_config effort are provided.
"""
config = AnthropicConfig()

response_format = {
"type": "json_schema",
"json_schema": {
"name": "test_schema",
"schema": {
"type": "object",
"properties": {"result": {"type": "string"}},
"required": ["result"],
"additionalProperties": False,
},
},
}

optional_params = config.map_openai_params(
non_default_params={"response_format": response_format},
optional_params={"max_tokens": 1024, "output_config": {"effort": "high"}},
model="claude-sonnet-4-5-20250514",
drop_params=False,
)

# Both format and effort should be in output_config
assert "output_config" in optional_params
assert optional_params["output_config"]["format"]["type"] == "json_schema"
assert optional_params["output_config"]["effort"] == "high"


# ============ Tool Search Tests ============


Expand Down Expand Up @@ -1629,8 +1726,8 @@ def test_effort_output_config_preservation():
assert result["output_config"]["effort"] == "medium"


def test_output_config_format_preservation_and_beta_header():
"""Test that output_config.format is preserved and treated as structured output."""
def test_output_config_format_preservation_without_beta_header():
"""Test that GA output_config.format is preserved without a beta header."""
config = AnthropicConfig()
output_format = {
"type": "json_schema",
Expand All @@ -1649,7 +1746,7 @@ def test_output_config_format_preservation_and_beta_header():

assert result["output_config"]["format"] == output_format
assert result["output_config"]["effort"] == "xhigh"
assert "structured-outputs-2025-11-13" in headers["anthropic-beta"]
assert "anthropic-beta" not in headers


def test_effort_beta_header_injection():
Expand Down
Loading