mirror of
https://github.com/hwchase17/langchain.git
synced 2026-02-21 14:43:07 +00:00
fix(anthropic): support output_config (#35036)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user