diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index 8c4fdfce5cb..a23a05463e7 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -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 diff --git a/libs/partners/anthropic/tests/cassettes/test_response_format_in_agent.yaml.gz b/libs/partners/anthropic/tests/cassettes/test_response_format_in_agent.yaml.gz index db85f4f3411..24576faa70e 100644 Binary files a/libs/partners/anthropic/tests/cassettes/test_response_format_in_agent.yaml.gz and b/libs/partners/anthropic/tests/cassettes/test_response_format_in_agent.yaml.gz differ diff --git a/libs/partners/anthropic/tests/cassettes/test_strict_tool_use.yaml.gz b/libs/partners/anthropic/tests/cassettes/test_strict_tool_use.yaml.gz index 8d9cb98abb0..2a193d2f1f0 100644 Binary files a/libs/partners/anthropic/tests/cassettes/test_strict_tool_use.yaml.gz and b/libs/partners/anthropic/tests/cassettes/test_strict_tool_use.yaml.gz differ diff --git a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py index 7fb53d4f5ab..b92f3a54aa0 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py @@ -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: diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index ca252e25013..42bb97e2944 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -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: