fix(anthropic): support output_config (#35036)

This commit is contained in:
ccurme
2026-02-06 11:11:01 -05:00
committed by GitHub
parent f058e45dfb
commit d30e2b88db
5 changed files with 45 additions and 122 deletions

View File

@@ -1167,22 +1167,22 @@ class ChatAnthropic(BaseChatModel):
and "schema" in response_format.get("json_schema", {})
):
response_format = cast(dict, response_format["json_schema"]["schema"])
# Convert OpenAI-style response_format to Anthropic's output_format
payload["output_format"] = _convert_to_anthropic_output_format(
# Convert OpenAI-style response_format to Anthropic's output_config.format
output_config = payload.setdefault("output_config", {})
output_config["format"] = _convert_to_anthropic_output_config_format(
response_format
)
# Handle deprecated output_format parameter for backward compatibility
if "output_format" in payload:
# Native structured output requires the structured outputs beta
if payload["betas"]:
if "structured-outputs-2025-11-13" not in payload["betas"]:
# Merge with existing betas
payload["betas"] = [
*payload["betas"],
"structured-outputs-2025-11-13",
]
else:
payload["betas"] = ["structured-outputs-2025-11-13"]
warnings.warn(
"The 'output_format' parameter is deprecated and will be removed in a "
"future version. Use 'output_config={\"format\": ...}' instead.",
DeprecationWarning,
stacklevel=2,
)
output_config = payload.setdefault("output_config", {})
output_config["format"] = payload.pop("output_format")
if self.reuse_last_container:
# Check for most recent AIMessage with container set in response_metadata
@@ -1197,24 +1197,9 @@ class ChatAnthropic(BaseChatModel):
payload["container"] = container_id
break
# Check if any tools have strict mode enabled
# Note: Beta headers are no longer required for structured outputs
# (output_config.format or strict tool use) as they are now generally available
if "tools" in payload and isinstance(payload["tools"], list):
has_strict_tool = any(
isinstance(tool, dict) and tool.get("strict") is True
for tool in payload["tools"]
)
if has_strict_tool:
# Strict tool use requires the structured outputs beta
if payload["betas"]:
if "structured-outputs-2025-11-13" not in payload["betas"]:
# Merge with existing betas
payload["betas"] = [
*payload["betas"],
"structured-outputs-2025-11-13",
]
else:
payload["betas"] = ["structured-outputs-2025-11-13"]
# Auto-append required betas for specific tool types and input_examples
has_input_examples = False
for tool in payload["tools"]:
@@ -1684,7 +1669,9 @@ class ChatAnthropic(BaseChatModel):
)
elif method == "json_schema":
llm = self.bind(
output_format=_convert_to_anthropic_output_format(schema),
output_config={
"format": _convert_to_anthropic_output_config_format(schema)
},
ls_structured_output_format={
"kwargs": {"method": "json_schema"},
"schema": convert_to_openai_tool(schema),
@@ -1911,10 +1898,16 @@ def _lc_tool_calls_to_anthropic_tool_use_blocks(
]
def _convert_to_anthropic_output_format(schema: dict | type) -> dict[str, Any]:
"""Convert JSON schema, Pydantic model, or `TypedDict` into Claude `output_format`.
def _convert_to_anthropic_output_config_format(schema: dict | type) -> dict[str, Any]:
"""Convert JSON schema, Pydantic model, or `TypedDict` into `output_config.format`.
See Claude docs on [structured outputs](https://platform.claude.com/docs/en/build-with-claude/structured-outputs).
Args:
schema: A JSON schema dict, Pydantic model class, or TypedDict.
Returns:
A dict with `type` and `schema` keys suitable for `output_config.format`.
"""
from anthropic import transform_schema

View File

@@ -727,7 +727,6 @@ class PersonDict(TypedDict):
def test_response_format(schema: dict | type) -> None:
model = ChatAnthropic(
model="claude-sonnet-4-5", # type: ignore[call-arg]
betas=["structured-outputs-2025-11-13"],
)
query = "Chester (a.k.a. Chet) is 100 years old."
@@ -779,7 +778,6 @@ def test_response_format_in_agent() -> None:
def test_strict_tool_use() -> None:
model = ChatAnthropic(
model="claude-sonnet-4-5", # type: ignore[call-arg]
betas=["structured-outputs-2025-11-13"],
)
def get_weather(location: str, unit: Literal["C", "F"]) -> str:

View File

@@ -1663,7 +1663,6 @@ def test_streaming_cache_token_reporting() -> None:
def test_strict_tool_use() -> None:
model = ChatAnthropic(
model=MODEL_NAME, # type: ignore[call-arg]
betas=["structured-outputs-2025-11-13"],
)
def get_weather(location: str, unit: Literal["C", "F"]) -> str:
@@ -1676,8 +1675,8 @@ def test_strict_tool_use() -> None:
assert tool_definition["strict"] is True
def test_beta_merging_with_response_format() -> None:
"""Test that structured-outputs beta is merged with existing betas."""
def test_response_format_with_output_config() -> None:
"""Test that response_format is converted to output_config.format."""
class Person(BaseModel):
"""Person data."""
@@ -1685,114 +1684,47 @@ def test_beta_merging_with_response_format() -> None:
name: str
age: int
# Auto-inject structured-outputs beta with no others specified
# Test that response_format converts to output_config.format
model = ChatAnthropic(model=MODEL_NAME)
payload = model._get_request_payload(
"Test query",
response_format=Person.model_json_schema(),
)
assert payload["betas"] == ["structured-outputs-2025-11-13"]
assert "output_config" in payload
assert "format" in payload["output_config"]
assert payload["output_config"]["format"]["type"] == "json_schema"
assert "schema" in payload["output_config"]["format"]
# Merge structured-outputs beta if other betas are present
model = ChatAnthropic(
model=MODEL_NAME,
betas=["mcp-client-2025-04-04"],
)
payload = model._get_request_payload(
"Test query",
response_format=Person.model_json_schema(),
)
assert payload["betas"] == [
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
]
# Structured-outputs beta already present - don't duplicate
model = ChatAnthropic(
model=MODEL_NAME,
betas=[
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
],
)
payload = model._get_request_payload(
"Test query",
response_format=Person.model_json_schema(),
)
assert payload["betas"] == [
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
]
# No response_format - betas should not be modified
model = ChatAnthropic(
model=MODEL_NAME,
betas=["mcp-client-2025-04-04"],
)
# No response_format - output_config should not have format
model = ChatAnthropic(model=MODEL_NAME)
payload = model._get_request_payload("Test query")
assert payload["betas"] == ["mcp-client-2025-04-04"]
if "output_config" in payload:
assert "format" not in payload["output_config"]
def test_beta_merging_with_strict_tool_use() -> None:
"""Test beta merging for strict tools."""
def test_strict_tool_use_payload() -> None:
"""Test that strict tool use property is correctly passed through to payload."""
def get_weather(location: str) -> str:
"""Get the weather at a location."""
return "Sunny"
# Auto-inject structured-outputs beta with no others specified
# Test that strict=True is correctly passed to payload
model = ChatAnthropic(model=MODEL_NAME) # type: ignore[call-arg]
model_with_tools = model.bind_tools([get_weather], strict=True)
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"What's the weather?",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == ["structured-outputs-2025-11-13"]
assert payload["tools"][0]["strict"] is True
# Merge structured-outputs beta if other betas are present
model = ChatAnthropic(
model=MODEL_NAME, # type: ignore[call-arg]
betas=["mcp-client-2025-04-04"],
)
model_with_tools = model.bind_tools([get_weather], strict=True)
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
# Test that strict=False is correctly passed to payload
model_without_strict = model.bind_tools([get_weather], strict=False)
payload = model_without_strict._get_request_payload( # type: ignore[attr-defined]
"What's the weather?",
**model_with_tools.kwargs, # type: ignore[attr-defined]
**model_without_strict.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == [
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
]
# Structured-outputs beta already present - don't duplicate
model = ChatAnthropic(
model=MODEL_NAME, # type: ignore[call-arg]
betas=[
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
],
)
model_with_tools = model.bind_tools([get_weather], strict=True)
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"What's the weather?",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == [
"mcp-client-2025-04-04",
"structured-outputs-2025-11-13",
]
# No strict tools - betas should not be modified
model = ChatAnthropic(
model=MODEL_NAME, # type: ignore[call-arg]
betas=["mcp-client-2025-04-04"],
)
model_with_tools = model.bind_tools([get_weather], strict=False)
payload = model_with_tools._get_request_payload( # type: ignore[attr-defined]
"What's the weather?",
**model_with_tools.kwargs, # type: ignore[attr-defined]
)
assert payload["betas"] == ["mcp-client-2025-04-04"]
assert payload["tools"][0].get("strict") is False
def test_auto_append_betas_for_tool_types() -> None: