diff --git a/docs/my-website/docs/anthropic_unified.md b/docs/my-website/docs/anthropic_unified/index.md similarity index 100% rename from docs/my-website/docs/anthropic_unified.md rename to docs/my-website/docs/anthropic_unified/index.md diff --git a/docs/my-website/docs/anthropic_unified/structured_output.md b/docs/my-website/docs/anthropic_unified/structured_output.md new file mode 100644 index 0000000000..433f57537d --- /dev/null +++ b/docs/my-website/docs/anthropic_unified/structured_output.md @@ -0,0 +1,237 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Structured Output /v1/messages + +Use LiteLLM to call Anthropic's structured output feature via the `/v1/messages` endpoint. + +## Supported Providers + +| Provider | Supported | Notes | +|----------|-----------|-------| +| Anthropic | ✅ | Native support | +| Azure AI (Anthropic models) | ✅ | Claude models on Azure AI | +| Bedrock (Converse Anthropic models) | ✅ | Claude models via Bedrock Converse API | + +## Usage + +### LiteLLM Proxy Server + + + + +1. Setup config.yaml + +```yaml +model_list: + - model_name: claude-sonnet + litellm_params: + model: anthropic/claude-sonnet-4-5-20250514 + api_key: os.environ/ANTHROPIC_API_KEY +``` + +2. Start proxy + +```bash +litellm --config /path/to/config.yaml +``` + +3. Test it! + +```bash +curl http://localhost:4000/v1/messages \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $LITELLM_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d '{ + "model": "claude-sonnet", + "max_tokens": 1024, + "messages": [ + { + "role": "user", + "content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm." + } + ], + "output_format": { + "type": "json_schema", + "schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"}, + "plan_interest": {"type": "string"}, + "demo_requested": {"type": "boolean"} + }, + "required": ["name", "email", "plan_interest", "demo_requested"], + "additionalProperties": false + } + } + }' +``` + + + + + +1. Setup config.yaml + +```yaml +model_list: + - model_name: azure-claude-sonnet + litellm_params: + model: azure_ai/claude-sonnet-4-5-20250514 + api_key: os.environ/AZURE_AI_API_KEY + api_base: https://your-endpoint.inference.ai.azure.com +``` + +2. Start proxy + +```bash +litellm --config /path/to/config.yaml +``` + +3. Test it! + +```bash +curl http://localhost:4000/v1/messages \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $LITELLM_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d '{ + "model": "azure-claude-sonnet", + "max_tokens": 1024, + "messages": [ + { + "role": "user", + "content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm." + } + ], + "output_format": { + "type": "json_schema", + "schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"}, + "plan_interest": {"type": "string"}, + "demo_requested": {"type": "boolean"} + }, + "required": ["name", "email", "plan_interest", "demo_requested"], + "additionalProperties": false + } + } + }' +``` + + + + + +1. Setup config.yaml + +```yaml +model_list: + - model_name: bedrock-claude-sonnet + litellm_params: + model: bedrock/anthropic.claude-sonnet-4-5-20250514-v1:0 + aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID + aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY + aws_region_name: us-west-2 +``` + +2. Start proxy + +```bash +litellm --config /path/to/config.yaml +``` + +3. Test it! + +```bash +curl http://localhost:4000/v1/messages \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $LITELLM_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d '{ + "model": "bedrock-claude-sonnet", + "max_tokens": 1024, + "messages": [ + { + "role": "user", + "content": "Extract the key information from this email: John Smith (john@example.com) is interested in our Enterprise plan and wants to schedule a demo for next Tuesday at 2pm." + } + ], + "output_format": { + "type": "json_schema", + "schema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"}, + "plan_interest": {"type": "string"}, + "demo_requested": {"type": "boolean"} + }, + "required": ["name", "email", "plan_interest", "demo_requested"], + "additionalProperties": false + } + } + }' +``` + + + + +## Example Response + +```json +{ + "id": "msg_01XFDUDYJgAACzvnptvVoYEL", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "{\"name\":\"John Smith\",\"email\":\"john@example.com\",\"plan_interest\":\"Enterprise\",\"demo_requested\":true}" + } + ], + "model": "claude-sonnet-4-5-20250514", + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 75, + "output_tokens": 28 + } +} +``` + +## Request Format + +### output_format + +The `output_format` parameter specifies the structured output format. + +```json +{ + "output_format": { + "type": "json_schema", + "schema": { + "type": "object", + "properties": { + "field_name": {"type": "string"}, + "another_field": {"type": "integer"} + }, + "required": ["field_name", "another_field"], + "additionalProperties": false + } + } +} +``` + +#### Fields + +- **type** (string): Must be `"json_schema"` +- **schema** (object): A JSON Schema object defining the expected output structure + - **type** (string): The root type, typically `"object"` + - **properties** (object): Defines the fields and their types + - **required** (array): List of required field names + - **additionalProperties** (boolean): Set to `false` to enforce strict schema adherence diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 9572fc9774..58c79c0274 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -517,7 +517,14 @@ const sidebars = { "mcp_troubleshoot", ] }, - "anthropic_unified", + { + type: "category", + label: "/v1/messages", + items: [ + "anthropic_unified/index", + "anthropic_unified/structured_output", + ] + }, "anthropic_count_tokens", "moderation", "ocr", diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py b/litellm/llms/anthropic/experimental_pass_through/adapters/handler.py index 795f9a4cd0..8fa7bb7e65 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 877e47a9ae..1706f045f1 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/litellm/llms/anthropic/experimental_pass_through/architecture.md b/litellm/llms/anthropic/experimental_pass_through/architecture.md new file mode 100644 index 0000000000..b939723513 --- /dev/null +++ b/litellm/llms/anthropic/experimental_pass_through/architecture.md @@ -0,0 +1,51 @@ +# Anthropic Messages Pass-Through Architecture + +## Request Flow + +```mermaid +flowchart TD + A[litellm.anthropic.messages.acreate] --> B{Provider?} + + B -->|anthropic| C[AnthropicMessagesConfig] + B -->|azure_ai| D[AzureAnthropicMessagesConfig] + B -->|bedrock invoke| E[BedrockAnthropicMessagesConfig] + B -->|vertex_ai| F[VertexAnthropicMessagesConfig] + B -->|Other providers| G[LiteLLMAnthropicMessagesAdapter] + + C --> H[Direct Anthropic API] + D --> I[Azure AI Foundry API] + E --> J[Bedrock Invoke API] + F --> K[Vertex AI API] + + G --> L[translate_anthropic_to_openai] + L --> M[litellm.completion] + M --> N[Provider API] + N --> O[translate_openai_response_to_anthropic] + O --> P[Anthropic Response Format] + + H --> P + I --> P + J --> P + K --> P +``` + +## Adapter Flow (Non-Native Providers) + +```mermaid +sequenceDiagram + participant User + participant Handler as anthropic_messages_handler + participant Adapter as LiteLLMAnthropicMessagesAdapter + participant LiteLLM as litellm.completion + participant Provider as Provider API + + User->>Handler: Anthropic Messages Request + Handler->>Adapter: translate_anthropic_to_openai() + Note over Adapter: messages, tools, thinking,
output_format → response_format + Adapter->>LiteLLM: OpenAI Format Request + LiteLLM->>Provider: Provider-specific Request + Provider->>LiteLLM: Provider Response + LiteLLM->>Adapter: OpenAI Format Response + Adapter->>Handler: translate_openai_response_to_anthropic() + Handler->>User: Anthropic Messages Response +``` diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 7135102db0..308bf367d0 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -42,6 +42,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): "tool_choice", "thinking", "context_management", + "output_format", # TODO: Add Anthropic `metadata` support # "metadata", ] @@ -169,27 +170,32 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): ) -> dict: """ Auto-inject anthropic-beta headers based on features used. - + Handles: - context_management: adds 'context-management-2025-06-27' - tool_search: adds provider-specific tool search header - + - output_format: adds 'structured-outputs-2025-11-13' + Args: headers: Request headers dict - optional_params: Optional parameters including tools, context_management + optional_params: Optional parameters including tools, context_management, output_format custom_llm_provider: Provider name for looking up correct tool search header """ beta_values: set = set() - + # Get existing beta headers if any existing_beta = headers.get("anthropic-beta") if existing_beta: beta_values.update(b.strip() for b in existing_beta.split(",")) - + # Check for context management if optional_params.get("context_management") is not None: beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value) - + + # Check for structured outputs + if optional_params.get("output_format") is not None: + beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.STRUCTURED_OUTPUT_2025_09_25.value) + # Check for tool search tools tools = optional_params.get("tools") if tools: @@ -198,8 +204,8 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): # Use provider-specific tool search header tool_search_header = get_tool_search_beta_header(custom_llm_provider) beta_values.add(tool_search_header) - + if beta_values: headers["anthropic-beta"] = ",".join(sorted(beta_values)) - + return headers diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index 293ee1caaf..81ebb5a360 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -271,8 +271,12 @@ class AmazonAnthropicClaudeMessagesConfig( # 4. Remove `ttl` field from cache_control in messages (Bedrock doesn't support it) self._remove_ttl_from_cache_control(anthropic_messages_request) + + # 5. `output_format` is not supported on Bedrock invoke + if "output_format" in anthropic_messages_request: + anthropic_messages_request.pop("output_format", None) - # 5. AUTO-INJECT beta headers based on features used + # 6. AUTO-INJECT beta headers based on features used anthropic_model_info = AnthropicModelInfo() tools = anthropic_messages_optional_request_params.get("tools") messages_typed = cast(List[AllMessageValues], messages) diff --git a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py index 0bedef3276..fc75376c0c 100644 --- a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py +++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py @@ -117,4 +117,9 @@ class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, Vert anthropic_messages_request.pop( "model", None ) # do not pass model in request body to vertex ai + + anthropic_messages_request.pop( + "output_format", None + ) # do not pass output_format in request body to vertex ai - vertex ai does not support output_format as yet + return anthropic_messages_request diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index 0e687be660..8d18322d37 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -359,6 +359,7 @@ class AnthropicMessagesRequestOptionalParams(TypedDict, total=False): mcp_servers: Optional[List[AnthropicMcpServerTool]] context_management: Optional[Dict[str, Any]] container: Optional[Dict[str, Any]] # Container config with skills for code execution + output_format: Optional[AnthropicOutputSchema] # Structured outputs support class AnthropicMessagesRequest(AnthropicMessagesRequestOptionalParams, total=False): diff --git a/test_anthropic_messages_structured_outputs_minimal.py b/test_anthropic_messages_structured_outputs_minimal.py new file mode 100644 index 0000000000..3fc7dc9a56 --- /dev/null +++ b/test_anthropic_messages_structured_outputs_minimal.py @@ -0,0 +1,74 @@ +""" +Tests for structured outputs support in Anthropic /v1/messages endpoint. +""" +import pytest +from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, +) + + +def test_output_format_supported_and_transforms_correctly(): + """Test that output_format is supported and properly transformed with beta header.""" + config = AnthropicMessagesConfig() + + # 1. Verify it's in supported parameters + supported_params = config.get_supported_anthropic_messages_params("claude-sonnet-4-5") + assert "output_format" in supported_params + + # 2. Verify transformation preserves output_format and adds beta header + output_format = { + "type": "json_schema", + "schema": {"type": "object", "properties": {"result": {"type": "string"}}} + } + + optional_params = {"max_tokens": 1024, "output_format": output_format} + headers = {} + + # Transform request + result = config.transform_anthropic_messages_request( + model="claude-sonnet-4-5", + messages=[{"role": "user", "content": "test"}], + anthropic_messages_optional_request_params=optional_params.copy(), + litellm_params={}, + headers=headers + ) + + # Update headers + headers = config._update_headers_with_anthropic_beta(headers, optional_params) + + # Verify output_format preserved in request body + assert "output_format" in result + assert result["output_format"]["type"] == "json_schema" + + # Verify beta header added + assert "anthropic-beta" in headers + assert "structured-outputs-2025-11-13" in headers["anthropic-beta"] + + +def test_output_format_works_with_bedrock_and_azure(): + """Test that output_format works with Bedrock and Azure Foundry models.""" + config = AnthropicMessagesConfig() + + output_format = {"type": "json_schema", "schema": {"type": "object", "properties": {}}} + optional_params = {"max_tokens": 1024, "output_format": output_format} + messages = [{"role": "user", "content": "test"}] + + # Test Bedrock + bedrock_result = config.transform_anthropic_messages_request( + model="bedrock/anthropic.claude-sonnet-4-5-v2:0", + messages=messages, + anthropic_messages_optional_request_params=optional_params.copy(), + litellm_params={}, + headers={} + ) + assert "output_format" in bedrock_result + + # Test Azure Foundry + azure_result = config.transform_anthropic_messages_request( + model="azure_ai/claude-sonnet-4-5", + messages=messages, + anthropic_messages_optional_request_params=optional_params.copy(), + litellm_params={}, + headers={} + ) + assert "output_format" in azure_result \ No newline at end of file diff --git a/tests/pass_through_unit_tests/messages_api_structured_output/__init__.py b/tests/pass_through_unit_tests/messages_api_structured_output/__init__.py new file mode 100644 index 0000000000..88c85d408a --- /dev/null +++ b/tests/pass_through_unit_tests/messages_api_structured_output/__init__.py @@ -0,0 +1,12 @@ +""" +Anthropic Messages API Structured Outputs Test Suite + +E2E tests for structured outputs functionality across different providers: +- Direct Anthropic API +- Azure AI Foundry Anthropic models +- AWS Bedrock Invoke API +- AWS Bedrock Converse API + +All tests validate that the output_format parameter works correctly +and returns valid JSON instead of Markdown text. +""" \ No newline at end of file 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 new file mode 100644 index 0000000000..b0a8cf8b96 --- /dev/null +++ b/tests/pass_through_unit_tests/messages_api_structured_output/base_anthropic_messages_structured_output_test.py @@ -0,0 +1,138 @@ +""" +Base test class for Anthropic Messages API structured outputs E2E tests. + +Tests that structured outputs work correctly via litellm.anthropic.messages interface +by making actual API calls and validating JSON response format. +""" + +import json +import os +import sys +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional + +sys.path.insert(0, os.path.abspath("../../..")) + +import pytest +import litellm + + +class BaseAnthropicMessagesStructuredOutputTest(ABC): + """ + Base test class for structured outputs E2E tests across different providers. + + Subclasses must implement: + - get_model(): Returns the model string to use for tests + + Subclasses may optionally implement: + - get_api_base(): Returns the API base URL (for Azure, etc.) + - get_api_key(): Returns the API key (for Azure, etc.) + """ + + @abstractmethod + def get_model(self) -> str: + """ + Returns the model string to use for tests. + """ + pass + + def get_api_base(self) -> Optional[str]: + """ + Returns the API base URL. Override for providers like Azure. + """ + return None + + def get_api_key(self) -> Optional[str]: + """ + Returns the API key. Override for providers like Azure. + """ + return None + + def get_output_format_schema(self) -> Dict[str, Any]: + """ + Returns a simple JSON schema for testing structured outputs. + """ + return { + "type": "json_schema", + "schema": { + "type": "object", + "properties": { + "sentiment": { + "type": "string", + "enum": ["positive", "negative", "neutral"] + } + }, + "required": ["sentiment"], + "additionalProperties": False + } + } + + def get_test_messages(self) -> List[Dict[str, Any]]: + """ + Returns test messages for structured output testing. + """ + return [ + { + "role": "user", + "content": "What is the sentiment of this text: 'This product is amazing!' Return only the sentiment." + } + ] + + @pytest.mark.asyncio + async def test_structured_output_e2e(self): + """ + E2E test: Make actual API call with structured output and validate JSON response. + """ + litellm._turn_on_debug() + messages = self.get_test_messages() + output_format = self.get_output_format_schema() + + # Build kwargs with optional api_base and api_key + kwargs: Dict[str, Any] = { + "model": self.get_model(), + "messages": messages, + "max_tokens": 100, + "output_format": output_format, + } + + api_base = self.get_api_base() + if api_base: + kwargs["api_base"] = api_base + + api_key = self.get_api_key() + if api_key: + kwargs["api_key"] = api_key + + response = await litellm.anthropic.messages.acreate(**kwargs) + + print(f"Response: {response}") + + # 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 + + 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 + + print(f"Response text: {response_text}") + + # The response should be valid JSON + parsed_json = json.loads(response_text) + print(f"Parsed JSON: {parsed_json}") + + # Validate the JSON structure + assert "sentiment" in parsed_json + assert parsed_json["sentiment"] in ["positive", "negative", "neutral"] \ No newline at end of file diff --git a/tests/pass_through_unit_tests/messages_api_structured_output/test_anthropic_api_structured_output.py b/tests/pass_through_unit_tests/messages_api_structured_output/test_anthropic_api_structured_output.py new file mode 100644 index 0000000000..c67c60b49f --- /dev/null +++ b/tests/pass_through_unit_tests/messages_api_structured_output/test_anthropic_api_structured_output.py @@ -0,0 +1,29 @@ +""" +E2E Test suite for Anthropic API structured outputs via litellm.anthropic.messages. + +Tests that structured outputs work correctly with direct Anthropic API calls +by making actual API calls and validating JSON response format. + +Requires ANTHROPIC_API_KEY environment variable. +""" + +import os +import sys + +sys.path.insert(0, os.path.abspath("../../../..")) + +from .base_anthropic_messages_structured_output_test import ( + BaseAnthropicMessagesStructuredOutputTest, +) + + +class TestAnthropicAPIStructuredOutput(BaseAnthropicMessagesStructuredOutputTest): + """ + E2E tests for structured outputs with direct Anthropic API. + + Uses Claude Sonnet 4.5 which supports structured outputs with the + 'anthropic-beta: structured-outputs-2025-11-13' header. + """ + + def get_model(self) -> str: + return "claude-sonnet-4-5-20250929" \ No newline at end of file diff --git a/tests/pass_through_unit_tests/messages_api_structured_output/test_azure_anthropic_structured_output.py b/tests/pass_through_unit_tests/messages_api_structured_output/test_azure_anthropic_structured_output.py new file mode 100644 index 0000000000..da46016b35 --- /dev/null +++ b/tests/pass_through_unit_tests/messages_api_structured_output/test_azure_anthropic_structured_output.py @@ -0,0 +1,36 @@ +""" +E2E Test suite for Azure Anthropic structured outputs via litellm.anthropic.messages. + +Tests that structured outputs work correctly with Azure AI Foundry Anthropic models +by making actual API calls and validating JSON response format. + +Requires Azure AI credentials and model deployment. +""" + +import os +import sys +from typing import Optional + +sys.path.insert(0, os.path.abspath("../../../..")) + +from .base_anthropic_messages_structured_output_test import ( + BaseAnthropicMessagesStructuredOutputTest, +) + + +class TestAzureAnthropicStructuredOutput(BaseAnthropicMessagesStructuredOutputTest): + """ + E2E tests for structured outputs with Azure AI Foundry Anthropic models. + + Uses the azure_ai/ prefix which routes through Azure AI Foundry + while maintaining the Anthropic Messages API format. + """ + + def get_model(self) -> str: + return "azure_ai/claude-opus-4-5" + + def get_api_base(self) -> Optional[str]: + return "https://krish-mh44t553-eastus2.services.ai.azure.com/" + + def get_api_key(self) -> Optional[str]: + return os.environ.get("AZURE_ANTHROPIC_API_KEY") \ No newline at end of file diff --git a/tests/pass_through_unit_tests/messages_api_structured_output/test_bedrock_converse_structured_output.py b/tests/pass_through_unit_tests/messages_api_structured_output/test_bedrock_converse_structured_output.py new file mode 100644 index 0000000000..9229677f32 --- /dev/null +++ b/tests/pass_through_unit_tests/messages_api_structured_output/test_bedrock_converse_structured_output.py @@ -0,0 +1,29 @@ +""" +E2E Test suite for Bedrock Converse API structured outputs via litellm.anthropic.messages. + +Tests that structured outputs work correctly with Bedrock Converse API +by making actual API calls and validating JSON response format. + +Requires AWS credentials and Bedrock model access. +""" + +import os +import sys + +sys.path.insert(0, os.path.abspath("../../../..")) + +from .base_anthropic_messages_structured_output_test import ( + BaseAnthropicMessagesStructuredOutputTest, +) + + +class TestBedrockConverseStructuredOutput(BaseAnthropicMessagesStructuredOutputTest): + """ + E2E tests for structured outputs with Bedrock Converse API. + + Uses the bedrock/converse/ prefix which routes through litellm.completion() + and the AmazonConverseConfig transformation. + """ + + def get_model(self) -> str: + return "bedrock/converse/us.anthropic.claude-3-5-sonnet-20241022-v2:0" \ No newline at end of file diff --git a/tests/pass_through_unit_tests/messages_api_structured_output/test_bedrock_invoke_structured_output.py b/tests/pass_through_unit_tests/messages_api_structured_output/test_bedrock_invoke_structured_output.py new file mode 100644 index 0000000000..d41072c46c --- /dev/null +++ b/tests/pass_through_unit_tests/messages_api_structured_output/test_bedrock_invoke_structured_output.py @@ -0,0 +1,32 @@ +""" +E2E Test suite for Bedrock Invoke API structured outputs via litellm.anthropic.messages. + +Tests that structured outputs work correctly with Bedrock Invoke API (native Anthropic format) +by making actual API calls and validating JSON response format. + +Requires AWS credentials and Bedrock model access. +""" + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.abspath("../../../..")) + +from .base_anthropic_messages_structured_output_test import ( + BaseAnthropicMessagesStructuredOutputTest, +) + + +@pytest.mark.skip(reason="Skipping Bedrock Invoke structured output tests") +class TestBedrockInvokeStructuredOutput(BaseAnthropicMessagesStructuredOutputTest): + """ + E2E tests for structured outputs with Bedrock Invoke API. + + Uses the bedrock/invoke/ prefix which routes through the native + Anthropic Messages API format on Bedrock. + """ + + def get_model(self) -> str: + return "bedrock/invoke/us.anthropic.claude-3-5-sonnet-20241022-v2:0" \ No newline at end of file