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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -657,15 +661,16 @@ 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.
If a dict, it can be in OpenAI-style format with "json_schema" key,
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:
Expand Down
68 changes: 68 additions & 0 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Loading