diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py index 795f9a4cd09..8fa7bb7e65e 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py @@ -45,6 +45,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: tools: Optional[List[Dict]] = None, top_k: Optional[int] = None, top_p: Optional[float] = None, + output_format: Optional[Dict] = None, extra_kwargs: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Prepare kwargs for litellm.completion/acompletion""" @@ -76,6 +77,8 @@ class LiteLLMMessagesToCompletionTransformationHandler: request_data["top_k"] = top_k if top_p is not None: request_data["top_p"] = top_p + if output_format: + request_data["output_format"] = output_format openai_request = ANTHROPIC_ADAPTER.translate_completion_input_params( request_data @@ -130,6 +133,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: tools: Optional[List[Dict]] = None, top_k: Optional[int] = None, top_p: Optional[float] = None, + output_format: Optional[Dict] = None, **kwargs, ) -> Union[AnthropicMessagesResponse, AsyncIterator]: """Handle non-Anthropic models asynchronously using the adapter""" @@ -148,6 +152,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: tools=tools, top_k=top_k, top_p=top_p, + output_format=output_format, extra_kwargs=kwargs, ) ) @@ -189,6 +194,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: tools: Optional[List[Dict]] = None, top_k: Optional[int] = None, top_p: Optional[float] = None, + output_format: Optional[Dict] = None, _is_async: bool = False, **kwargs, ) -> Union[ @@ -212,6 +218,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: tools=tools, top_k=top_k, top_p=top_p, + output_format=output_format, **kwargs, ) @@ -230,6 +237,7 @@ class LiteLLMMessagesToCompletionTransformationHandler: tools=tools, top_k=top_k, top_p=top_p, + output_format=output_format, extra_kwargs=kwargs, ) ) diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index 877e47a9aea..1706f045f14 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -172,7 +172,7 @@ class LiteLLMAnthropicMessagesAdapter: """ Which anthropic params, we need to translate to the openai format. """ - return ["messages", "metadata", "system", "tool_choice", "tools", "thinking"] + return ["messages", "metadata", "system", "tool_choice", "tools", "thinking", "output_format"] def translate_anthropic_messages_to_openai( # noqa: PLR0915 self, @@ -554,6 +554,42 @@ class LiteLLMAnthropicMessagesAdapter: return new_tools + def translate_anthropic_output_format_to_openai( + self, output_format: Any + ) -> Optional[Dict[str, Any]]: + """ + Translate Anthropic's output_format to OpenAI's response_format. + + Anthropic output_format: {"type": "json_schema", "schema": {...}} + OpenAI response_format: {"type": "json_schema", "json_schema": {"name": "...", "schema": {...}}} + + Args: + output_format: Anthropic output_format dict with 'type' and 'schema' + + Returns: + OpenAI-compatible response_format dict, or None if invalid + """ + if not isinstance(output_format, dict): + return None + + output_type = output_format.get("type") + if output_type != "json_schema": + return None + + schema = output_format.get("schema") + if not schema: + return None + + # Convert to OpenAI response_format structure + return { + "type": "json_schema", + "json_schema": { + "name": "structured_output", + "schema": schema, + "strict": True, + }, + } + def translate_anthropic_to_openai( self, anthropic_message_request: AnthropicMessagesRequest ) -> ChatCompletionRequest: @@ -636,6 +672,16 @@ class LiteLLMAnthropicMessagesAdapter: if reasoning_effort: new_kwargs["reasoning_effort"] = reasoning_effort + ## CONVERT OUTPUT_FORMAT to RESPONSE_FORMAT + if "output_format" in anthropic_message_request: + output_format = anthropic_message_request["output_format"] + if output_format: + response_format = self.translate_anthropic_output_format_to_openai( + output_format=output_format + ) + if response_format: + new_kwargs["response_format"] = response_format + translatable_params = self.translatable_anthropic_params() for k, v in anthropic_message_request.items(): if k not in translatable_params: # pass remaining params as is diff --git a/tests/pass_through_unit_tests/messages_api_structured_output/base_anthropic_messages_structured_output_test.py b/tests/pass_through_unit_tests/messages_api_structured_output/base_anthropic_messages_structured_output_test.py index 9346345eef6..b0a8cf8b96f 100644 --- a/tests/pass_through_unit_tests/messages_api_structured_output/base_anthropic_messages_structured_output_test.py +++ b/tests/pass_through_unit_tests/messages_api_structured_output/base_anthropic_messages_structured_output_test.py @@ -107,14 +107,26 @@ class BaseAnthropicMessagesStructuredOutputTest(ABC): print(f"Response: {response}") - # Validate response structure - assert "content" in response - assert len(response["content"]) > 0 + # Validate response structure - handle both dict and object responses + if isinstance(response, dict): + assert "content" in response + content_list = response["content"] + else: + assert hasattr(response, "content") + content_list = response.content - content = response["content"][0] - assert "text" in content + assert len(content_list) > 0 + + content = content_list[0] + + # Handle both dict and object content blocks + if isinstance(content, dict): + assert "text" in content + response_text = content["text"] + else: + assert hasattr(content, "text") + response_text = content.text - response_text = content["text"] print(f"Response text: {response_text}") # The response should be valid JSON