Skip to content
Merged
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 @@ -45,7 +45,7 @@
from anthropic.types.beta.beta_code_execution_tool_result_error import (
BetaCodeExecutionToolResultError,
)
from pydantic import SecretStr, ValidationError
from pydantic import BaseModel, SecretStr, ValidationError

if sys.version_info >= (3, 13):
from typing import TypeVar
Expand All @@ -67,6 +67,7 @@

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"


# region Anthropic Chat Options TypedDict
Expand Down Expand Up @@ -341,7 +342,7 @@ async def _inner_get_response(
# execute
message = await self.anthropic_client.beta.messages.create(**run_options, stream=False)
# process
return self._process_message(message)
return self._process_message(message, options)

@override
async def _inner_get_streaming_response(
Expand Down Expand Up @@ -384,8 +385,10 @@ def _prepare_options(

messages = prepend_instructions_to_messages(list(messages), instructions, role="system")

# Start with a copy of options
run_options: dict[str, Any] = {k: v for k, v in options.items() if v is not None and k not in {"instructions"}}
# Start with a copy of options, excluding keys we handle separately
run_options: dict[str, Any] = {
k: v for k, v in options.items() if v is not None and k not in {"instructions", "response_format"}
}

# Translation between options keys and Anthropic Messages API
for old_key, new_key in OPTION_TRANSLATIONS.items():
Expand Down Expand Up @@ -426,6 +429,13 @@ 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 = 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)

run_options.update(kwargs)
return run_options

Expand All @@ -444,6 +454,41 @@ def _prepare_betas(self, options: dict[str, Any]) -> set[str]:
*options.get("additional_beta_flags", []),
}

def _prepare_response_format(self, response_format: type[BaseModel] | dict[str, Any]) -> dict[str, Any]:
"""Prepare the output_format parameter for structured output.

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.
"""
if isinstance(response_format, dict):
if "json_schema" in response_format:
schema = response_format["json_schema"].get("schema", {})
elif "schema" in response_format:
schema = response_format["schema"]
else:
schema = response_format

if isinstance(schema, dict):
schema["additionalProperties"] = False

return {
"type": "json_schema",
"schema": schema,
}

schema = response_format.model_json_schema()
schema["additionalProperties"] = False

return {
"type": "json_schema",
"schema": schema,
}

def _prepare_messages_for_anthropic(self, messages: MutableSequence[ChatMessage]) -> list[dict[str, Any]]:
"""Prepare a list of ChatMessages for the Anthropic client.

Expand Down Expand Up @@ -606,11 +651,12 @@ def _prepare_tools_for_anthropic(self, options: dict[str, Any]) -> dict[str, Any

# region Response Processing Methods

def _process_message(self, message: BetaMessage) -> ChatResponse:
def _process_message(self, message: BetaMessage, options: dict[str, Any]) -> ChatResponse:
"""Process the response from the Anthropic client.

Args:
message: The message returned by the Anthropic client.
options: The options dict used for the request.

Returns:
A ChatResponse object containing the processed response.
Expand All @@ -627,6 +673,7 @@ def _process_message(self, message: BetaMessage) -> ChatResponse:
usage_details=self._parse_usage_from_anthropic(message.usage),
model_id=message.model,
finish_reason=FINISH_REASON_MAP.get(message.stop_reason) if message.stop_reason else None,
response_format=options.get("response_format"),
raw_representation=message,
)

Expand Down Expand Up @@ -970,7 +1017,7 @@ def _parse_contents_from_anthropic(
# since it triggers on `if content.name:`. The initial tool_use event already
# provides the name, so deltas should only carry incremental arguments.
# This matches OpenAI's behavior where streaming chunks have name="".
call_id, _ = self._last_call_id_name if self._last_call_id_name else ("", "")
call_id, _name = self._last_call_id_name if self._last_call_id_name else ("", "")
contents.append(
Content.from_function_call(
call_id=call_id,
Expand Down
4 changes: 2 additions & 2 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ def test_process_message_basic(mock_anthropic_client: MagicMock) -> None:
mock_message.usage = BetaUsage(input_tokens=10, output_tokens=5)
mock_message.stop_reason = "end_turn"

response = chat_client._process_message(mock_message)
response = chat_client._process_message(mock_message, {})

assert response.response_id == "msg_123"
assert response.model_id == "claude-3-5-sonnet-20241022"
Expand Down Expand Up @@ -528,7 +528,7 @@ def test_process_message_with_tool_use(mock_anthropic_client: MagicMock) -> None
mock_message.usage = BetaUsage(input_tokens=10, output_tokens=5)
mock_message.stop_reason = "tool_use"

response = chat_client._process_message(mock_message)
response = chat_client._process_message(mock_message, {})

assert len(response.messages[0].contents) == 1
assert response.messages[0].contents[0].type == "function_call"
Expand Down
Loading