diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 98c181f152..65f23af44f 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -76,7 +76,6 @@ ANTHROPIC_DEFAULT_MAX_TOKENS: Final[int] = 1024 BETA_FLAGS: Final[list[str]] = ["mcp-client-2025-04-04", "code-execution-2025-08-25"] -STRUCTURED_OUTPUTS_BETA_FLAG: Final[str] = "structured-outputs-2025-11-13" ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None) AnthropicAsyncClient = AsyncAnthropic | AsyncAnthropicBedrock | AsyncAnthropicFoundry | AsyncAnthropicVertex @@ -632,12 +631,17 @@ def _prepare_options( if tools_config := self._prepare_tools_for_anthropic(options): run_options.update(tools_config) - # response_format - use native output_format for structured outputs + # response_format - emit Anthropic's GA ``output_config.format`` shape. + # The deprecated ``output_format`` parameter (gated by the + # ``structured-outputs-2025-11-13`` beta flag) produced concatenated / + # malformed JSON when combined with tools — the GA path does not. + # Merge into any caller-supplied ``output_config`` so e.g. the + # adaptive-thinking ``effort`` setting survives the transformation. response_format = options.get("response_format") if response_format is not None: - run_options["output_format"] = self._prepare_response_format(response_format) - # Add the structured outputs beta flag - run_options["betas"].add(STRUCTURED_OUTPUTS_BETA_FLAG) + output_config = dict(run_options.get("output_config") or {}) + output_config["format"] = self._prepare_response_format(response_format) + run_options["output_config"] = output_config return run_options @@ -657,7 +661,7 @@ def _prepare_betas(self, options: Mapping[str, Any]) -> set[str]: } def _prepare_response_format(self, response_format: type[BaseModel] | dict[str, Any]) -> dict[str, Any]: - """Prepare the output_format parameter for structured output. + """Build the ``output_config.format`` payload for Anthropic structured outputs. Args: response_format: Either a Pydantic model class or a dict with the schema specification. @@ -665,7 +669,8 @@ def _prepare_response_format(self, response_format: type[BaseModel] | dict[str, or direct format with "schema" key, or the raw schema dict itself. Returns: - A dictionary representing the output_format for Anthropic's structured outputs. + A ``{"type": "json_schema", "schema": ...}`` dict — the value placed + under ``output_config["format"]`` on the GA structured-outputs path. """ if isinstance(response_format, dict): if "json_schema" in response_format: diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 0cfec3423c..fcdedd4700 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -1800,6 +1800,74 @@ class TestModel(BaseModel): assert "properties" in result["schema"] +async def test_prepare_options_uses_output_config_for_response_format( + mock_anthropic_client: MagicMock, +) -> None: + """``response_format`` is forwarded as GA ``output_config.format`` (not the deprecated ``output_format``). + + The deprecated ``output_format`` parameter, gated by the + ``structured-outputs-2025-11-13`` beta flag, produced concatenated / + malformed JSON when combined with tools. The GA ``output_config`` shape + works correctly with tools, so we emit that and no longer set the beta + flag. + """ + + class StructuredOut(BaseModel): + answer: str + + client = create_test_anthropic_client(mock_anthropic_client) + messages = [Message(role="user", contents=["Hello"])] + chat_options = ChatOptions(max_tokens=100, response_format=StructuredOut) + + run_options = client._prepare_options(messages, chat_options) + + assert "output_format" not in run_options + assert "output_config" in run_options + fmt = run_options["output_config"]["format"] + assert fmt["type"] == "json_schema" + assert fmt["schema"]["additionalProperties"] is False + assert "answer" in fmt["schema"]["properties"] + # The deprecated structured-outputs beta flag is no longer needed on the + # GA path and must not leak into ``betas``. + assert "structured-outputs-2025-11-13" not in run_options["betas"] + + +async def test_prepare_options_preserves_caller_supplied_output_config_effort( + mock_anthropic_client: MagicMock, +) -> None: + """A caller-supplied ``output_config.effort`` (e.g. adaptive thinking) survives the format merge.""" + + class StructuredOut(BaseModel): + answer: str + + client = create_test_anthropic_client(mock_anthropic_client) + messages = [Message(role="user", contents=["Hello"])] + # ``output_config`` is provider-specific; pass it through additional kwargs + # the way a caller would when configuring adaptive thinking. + run_options = client._prepare_options( + messages, + ChatOptions(max_tokens=100, response_format=StructuredOut), + output_config={"effort": "high"}, + ) + + output_config = run_options["output_config"] + assert output_config["effort"] == "high" + assert output_config["format"]["type"] == "json_schema" + assert "answer" in output_config["format"]["schema"]["properties"] + + +async def test_prepare_options_no_response_format_omits_output_config( + mock_anthropic_client: MagicMock, +) -> None: + """Without ``response_format``, no ``output_config`` is added implicitly.""" + client = create_test_anthropic_client(mock_anthropic_client) + messages = [Message(role="user", contents=["Hello"])] + run_options = client._prepare_options(messages, ChatOptions(max_tokens=100)) + + assert "output_config" not in run_options + assert "output_format" not in run_options + + # Message Preparation Tests