diff --git a/docs/my-website/docs/providers/aws_polly.md b/docs/my-website/docs/providers/aws_polly.md
new file mode 100644
index 00000000000..21b0fa679bf
--- /dev/null
+++ b/docs/my-website/docs/providers/aws_polly.md
@@ -0,0 +1,364 @@
+# AWS Polly Text to Speech (tts)
+
+## Overview
+
+| Property | Details |
+|-------|-------|
+| Description | Convert text to natural-sounding speech using AWS Polly's neural and standard TTS engines |
+| Provider Route on LiteLLM | `aws_polly/` |
+| Supported Operations | `/audio/speech` |
+| Link to Provider Doc | [AWS Polly SynthesizeSpeech ↗](https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html) |
+
+## Quick Start
+
+### **LiteLLM SDK**
+
+```python showLineNumbers title="SDK Usage"
+import litellm
+from pathlib import Path
+import os
+
+# Set environment variables
+os.environ["AWS_ACCESS_KEY_ID"] = ""
+os.environ["AWS_SECRET_ACCESS_KEY"] = ""
+os.environ["AWS_REGION_NAME"] = "us-east-1"
+
+# AWS Polly call
+speech_file_path = Path(__file__).parent / "speech.mp3"
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input="the quick brown fox jumped over the lazy dogs",
+)
+response.stream_to_file(speech_file_path)
+```
+
+### **LiteLLM PROXY**
+
+```yaml showLineNumbers title="proxy_config.yaml"
+model_list:
+ - model_name: polly-neural
+ litellm_params:
+ model: aws_polly/neural
+ aws_access_key_id: "os.environ/AWS_ACCESS_KEY_ID"
+ aws_secret_access_key: "os.environ/AWS_SECRET_ACCESS_KEY"
+ aws_region_name: "us-east-1"
+```
+
+## Polly Engines
+
+AWS Polly supports different speech synthesis engines. Specify the engine in the model name:
+
+| Model | Engine | Cost (per 1M chars) | Description |
+|-------|--------|---------------------|-------------|
+| `aws_polly/standard` | Standard | $4.00 | Original Polly voices, faster and lowest cost |
+| `aws_polly/neural` | Neural | $16.00 | More natural, human-like speech (recommended) |
+| `aws_polly/generative` | Generative | $30.00 | Most expressive, highest quality (limited voices) |
+| `aws_polly/long-form` | Long-form | $100.00 | Optimized for long content like articles |
+
+### **LiteLLM SDK**
+
+```python showLineNumbers title="Using Different Engines"
+import litellm
+
+# Neural engine (recommended)
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input="Hello world",
+)
+
+# Standard engine (lower cost)
+response = litellm.speech(
+ model="aws_polly/standard",
+ voice="Joanna",
+ input="Hello world",
+)
+
+# Generative engine (highest quality)
+response = litellm.speech(
+ model="aws_polly/generative",
+ voice="Matthew",
+ input="Hello world",
+)
+```
+
+### **LiteLLM PROXY**
+
+```yaml showLineNumbers title="proxy_config.yaml"
+model_list:
+ - model_name: polly-neural
+ litellm_params:
+ model: aws_polly/neural
+ aws_region_name: "us-east-1"
+ - model_name: polly-standard
+ litellm_params:
+ model: aws_polly/standard
+ aws_region_name: "us-east-1"
+ - model_name: polly-generative
+ litellm_params:
+ model: aws_polly/generative
+ aws_region_name: "us-east-1"
+```
+
+## Available Voices
+
+### Native Polly Voices
+
+AWS Polly has many voices across different languages. Here are popular US English voices:
+
+| Voice | Gender | Engine Support |
+|-------|--------|----------------|
+| `Joanna` | Female | Neural, Standard |
+| `Matthew` | Male | Neural, Standard, Generative |
+| `Ivy` | Female (child) | Neural, Standard |
+| `Kendra` | Female | Neural, Standard |
+| `Amy` | Female (British) | Neural, Standard |
+| `Brian` | Male (British) | Neural, Standard |
+
+### **LiteLLM SDK**
+
+```python showLineNumbers title="Using Native Polly Voices"
+import litellm
+
+# US English female
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input="Hello from Joanna",
+)
+
+# US English male
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Matthew",
+ input="Hello from Matthew",
+)
+
+# British English female
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Amy",
+ input="Hello from Amy",
+)
+```
+
+### **LiteLLM PROXY**
+
+```yaml showLineNumbers title="proxy_config.yaml"
+model_list:
+ - model_name: polly-joanna
+ litellm_params:
+ model: aws_polly/neural
+ voice: "Joanna"
+ aws_region_name: "us-east-1"
+ - model_name: polly-matthew
+ litellm_params:
+ model: aws_polly/neural
+ voice: "Matthew"
+ aws_region_name: "us-east-1"
+```
+
+### OpenAI Voice Mappings
+
+LiteLLM also supports OpenAI voice names, which are automatically mapped to Polly voices:
+
+| OpenAI Voice | Maps to Polly Voice |
+|--------------|---------------------|
+| `alloy` | Joanna |
+| `echo` | Matthew |
+| `fable` | Amy |
+| `onyx` | Brian |
+| `nova` | Ivy |
+| `shimmer` | Kendra |
+
+### **LiteLLM SDK**
+
+```python showLineNumbers title="Using OpenAI Voice Names"
+import litellm
+
+# These are equivalent
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="alloy", # Maps to Joanna
+ input="Hello world",
+)
+
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna", # Native Polly voice
+ input="Hello world",
+)
+```
+
+## SSML Support
+
+AWS Polly supports SSML (Speech Synthesis Markup Language) for advanced control over speech output. LiteLLM automatically detects SSML input.
+
+### **LiteLLM SDK**
+
+```python showLineNumbers title="SSML Example"
+import litellm
+
+ssml_input = """
+
+ Hello,
+ this is a test with emphasis
+ and slower speech.
+
+"""
+
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input=ssml_input,
+)
+```
+
+### **LiteLLM PROXY**
+
+```bash showLineNumbers title="cURL Request with SSML"
+curl -X POST http://localhost:4000/v1/audio/speech \
+ -H "Authorization: Bearer sk-1234" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "polly-neural",
+ "voice": "Joanna",
+ "input": "Hello world"
+ }' \
+ --output speech.mp3
+```
+
+## Supported Parameters
+
+```python showLineNumbers title="All Parameters"
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna", # Required: Voice selection
+ input="text to convert", # Required: Input text (or SSML)
+ response_format="mp3", # Optional: mp3, ogg_vorbis, pcm
+
+ # AWS-specific parameters
+ language_code="en-US", # Optional: Language code
+ sample_rate="22050", # Optional: Sample rate in Hz
+)
+```
+
+## Response Formats
+
+| Format | Description |
+|--------|-------------|
+| `mp3` | MP3 audio (default) |
+| `ogg_vorbis` | Ogg Vorbis audio |
+| `pcm` | Raw PCM audio |
+
+### **LiteLLM SDK**
+
+```python showLineNumbers title="Different Response Formats"
+import litellm
+
+# MP3 (default)
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input="Hello",
+ response_format="mp3",
+)
+
+# Ogg Vorbis
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input="Hello",
+ response_format="ogg_vorbis",
+)
+```
+
+## AWS Authentication
+
+LiteLLM supports multiple AWS authentication methods.
+
+### **LiteLLM SDK**
+
+```python showLineNumbers title="Authentication Options"
+import litellm
+import os
+
+# Option 1: Environment variables (recommended)
+os.environ["AWS_ACCESS_KEY_ID"] = "your-access-key"
+os.environ["AWS_SECRET_ACCESS_KEY"] = "your-secret-key"
+os.environ["AWS_REGION_NAME"] = "us-east-1"
+
+response = litellm.speech(model="aws_polly/neural", voice="Joanna", input="Hello")
+
+# Option 2: Pass credentials directly
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input="Hello",
+ aws_access_key_id="your-access-key",
+ aws_secret_access_key="your-secret-key",
+ aws_region_name="us-east-1",
+)
+
+# Option 3: IAM Role (when running on AWS)
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input="Hello",
+ aws_region_name="us-east-1",
+)
+
+# Option 4: AWS Profile
+response = litellm.speech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input="Hello",
+ aws_profile_name="my-profile",
+)
+```
+
+### **LiteLLM PROXY**
+
+```yaml showLineNumbers title="proxy_config.yaml"
+model_list:
+ # Using environment variables
+ - model_name: polly-neural
+ litellm_params:
+ model: aws_polly/neural
+ aws_access_key_id: "os.environ/AWS_ACCESS_KEY_ID"
+ aws_secret_access_key: "os.environ/AWS_SECRET_ACCESS_KEY"
+ aws_region_name: "us-east-1"
+
+ # Using IAM Role (when proxy runs on AWS)
+ - model_name: polly-neural-iam
+ litellm_params:
+ model: aws_polly/neural
+ aws_region_name: "us-east-1"
+
+ # Using AWS Profile
+ - model_name: polly-neural-profile
+ litellm_params:
+ model: aws_polly/neural
+ aws_profile_name: "my-profile"
+```
+
+## Async Support
+
+```python showLineNumbers title="Async Usage"
+import litellm
+import asyncio
+
+async def main():
+ response = await litellm.aspeech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input="Hello from async AWS Polly",
+ aws_region_name="us-east-1",
+ )
+
+ with open("output.mp3", "wb") as f:
+ f.write(response.content)
+
+asyncio.run(main())
+```
diff --git a/docs/my-website/docs/text_to_speech.md b/docs/my-website/docs/text_to_speech.md
index ea2a9c2eff3..ce298b538df 100644
--- a/docs/my-website/docs/text_to_speech.md
+++ b/docs/my-website/docs/text_to_speech.md
@@ -14,7 +14,7 @@ import TabItem from '@theme/TabItem';
| Fallbacks | ✅ | Works between supported models |
| Loadbalancing | ✅ | Works between supported models |
| Guardrails | ✅ | Applies to input text (non-streaming only) |
-| Supported Providers | OpenAI, Azure OpenAI, Vertex AI | |
+| Supported Providers | OpenAI, Azure OpenAI, Vertex AI, AWS Polly, ElevenLabs | |
## **LiteLLM Python SDK Usage**
### Quick Start
@@ -101,6 +101,7 @@ litellm --config /path/to/config.yaml
| OpenAI | [Usage](#quick-start) |
| Azure OpenAI| [Usage](../docs/providers/azure#azure-text-to-speech-tts) |
| Azure AI Speech Service (AVA)| [Usage](../docs/providers/azure_ai_speech) |
+| AWS Polly | [Usage](#aws-polly-text-to-speech) |
| Vertex AI | [Usage](../docs/providers/vertex#text-to-speech-apis) |
| Gemini | [Usage](#gemini-text-to-speech) |
| ElevenLabs | [Usage](../docs/providers/elevenlabs#text-to-speech-tts) |
@@ -246,6 +247,12 @@ curl http://0.0.0.0:4000/v1/audio/speech \
--output vertex_speech.mp3
```
+### AWS Polly Text-to-Speech
+
+AWS Polly provides neural and standard text-to-speech engines with support for multiple voices and languages.
+
+See the [AWS Polly provider documentation](../docs/providers/aws_polly) for detailed usage examples.
+
## ✨ Enterprise LiteLLM Proxy - Set Max Request File Size
Use this when you want to limit the file size for requests sent to `audio/transcriptions`
diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js
index fbaebf19366..b6b8fe1223d 100644
--- a/docs/my-website/sidebars.js
+++ b/docs/my-website/sidebars.js
@@ -664,6 +664,7 @@ const sidebars = {
"providers/bedrock_agents",
"providers/bedrock_writer",
"providers/bedrock_batches",
+ "providers/aws_polly",
"providers/bedrock_vector_store",
]
},
diff --git a/litellm/llms/aws_polly/__init__.py b/litellm/llms/aws_polly/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/litellm/llms/aws_polly/text_to_speech/__init__.py b/litellm/llms/aws_polly/text_to_speech/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/litellm/llms/aws_polly/text_to_speech/transformation.py b/litellm/llms/aws_polly/text_to_speech/transformation.py
new file mode 100644
index 00000000000..dc6c40000f1
--- /dev/null
+++ b/litellm/llms/aws_polly/text_to_speech/transformation.py
@@ -0,0 +1,391 @@
+"""
+AWS Polly Text-to-Speech transformation
+
+Maps OpenAI TTS spec to AWS Polly SynthesizeSpeech API
+Reference: https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html
+"""
+
+import json
+from typing import TYPE_CHECKING, Any, Coroutine, Dict, Optional, Tuple, Union
+
+import httpx
+
+from litellm.llms.base_llm.text_to_speech.transformation import (
+ BaseTextToSpeechConfig,
+ TextToSpeechRequestData,
+)
+from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
+
+if TYPE_CHECKING:
+ from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
+ from litellm.types.llms.openai import HttpxBinaryResponseContent
+else:
+ LiteLLMLoggingObj = Any
+ HttpxBinaryResponseContent = Any
+
+
+class AWSPollyTextToSpeechConfig(BaseTextToSpeechConfig, BaseAWSLLM):
+ """
+ Configuration for AWS Polly Text-to-Speech
+
+ Reference: https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html
+ """
+
+ def __init__(self):
+ BaseTextToSpeechConfig.__init__(self)
+ BaseAWSLLM.__init__(self)
+
+ # Default settings
+ DEFAULT_VOICE = "Joanna"
+ DEFAULT_ENGINE = "neural"
+ DEFAULT_OUTPUT_FORMAT = "mp3"
+ DEFAULT_REGION = "us-east-1"
+
+ # Voice name mappings from OpenAI voices to Polly voices
+ VOICE_MAPPINGS = {
+ "alloy": "Joanna", # US English female
+ "echo": "Matthew", # US English male
+ "fable": "Amy", # British English female
+ "onyx": "Brian", # British English male
+ "nova": "Ivy", # US English female (child)
+ "shimmer": "Kendra", # US English female
+ }
+
+ # Response format mappings from OpenAI to Polly
+ FORMAT_MAPPINGS = {
+ "mp3": "mp3",
+ "opus": "ogg_vorbis",
+ "aac": "mp3", # Polly doesn't support AAC, use MP3
+ "flac": "mp3", # Polly doesn't support FLAC, use MP3
+ "wav": "pcm",
+ "pcm": "pcm",
+ }
+
+ # Valid Polly engines
+ VALID_ENGINES = {"standard", "neural", "long-form", "generative"}
+
+ def dispatch_text_to_speech(
+ self,
+ model: str,
+ input: str,
+ voice: Optional[Union[str, Dict]],
+ optional_params: Dict,
+ litellm_params_dict: Dict,
+ logging_obj: "LiteLLMLoggingObj",
+ timeout: Union[float, httpx.Timeout],
+ extra_headers: Optional[Dict[str, Any]],
+ base_llm_http_handler: Any,
+ aspeech: bool,
+ api_base: Optional[str],
+ api_key: Optional[str],
+ **kwargs: Any,
+ ) -> Union[
+ "HttpxBinaryResponseContent",
+ Coroutine[Any, Any, "HttpxBinaryResponseContent"],
+ ]:
+ """
+ Dispatch method to handle AWS Polly TTS requests
+
+ This method encapsulates AWS-specific credential resolution and parameter handling
+
+ Args:
+ base_llm_http_handler: The BaseLLMHTTPHandler instance from main.py
+ """
+ # Get AWS region from kwargs or environment
+ aws_region_name = kwargs.get("aws_region_name") or self._get_aws_region_name_for_polly(
+ optional_params=optional_params
+ )
+
+ # Convert voice to string if it's a dict
+ voice_str: Optional[str] = None
+ if isinstance(voice, str):
+ voice_str = voice
+ elif isinstance(voice, dict):
+ voice_str = voice.get("name") if voice else None
+
+ # Update litellm_params with resolved values
+ # Note: AWS credentials (aws_access_key_id, aws_secret_access_key, etc.)
+ # are already in litellm_params_dict via get_litellm_params() in main.py
+ litellm_params_dict["aws_region_name"] = aws_region_name
+ litellm_params_dict["api_base"] = api_base
+ litellm_params_dict["api_key"] = api_key
+
+ # Call the text_to_speech_handler
+ response = base_llm_http_handler.text_to_speech_handler(
+ model=model,
+ input=input,
+ voice=voice_str,
+ text_to_speech_provider_config=self,
+ text_to_speech_optional_params=optional_params,
+ custom_llm_provider="aws_polly",
+ litellm_params=litellm_params_dict,
+ logging_obj=logging_obj,
+ timeout=timeout,
+ extra_headers=extra_headers,
+ client=None,
+ _is_async=aspeech,
+ )
+
+ return response
+
+ def _get_aws_region_name_for_polly(self, optional_params: Dict) -> str:
+ """Get AWS region name for Polly API calls."""
+ aws_region_name = optional_params.get("aws_region_name")
+ if aws_region_name is None:
+ aws_region_name = self.get_aws_region_name_for_non_llm_api_calls()
+ return aws_region_name
+
+ def get_supported_openai_params(self, model: str) -> list:
+ """
+ AWS Polly TTS supports these OpenAI parameters
+ """
+ return ["voice", "response_format", "speed"]
+
+ def map_openai_params(
+ self,
+ model: str,
+ optional_params: Dict,
+ voice: Optional[Union[str, Dict]] = None,
+ drop_params: bool = False,
+ kwargs: Dict = {},
+ ) -> Tuple[Optional[str], Dict]:
+ """
+ Map OpenAI parameters to AWS Polly parameters
+ """
+ mapped_params = {}
+
+ # Map voice - support both native Polly voices and OpenAI voice mappings
+ mapped_voice: Optional[str] = None
+ if isinstance(voice, str):
+ if voice in self.VOICE_MAPPINGS:
+ # OpenAI voice -> Polly voice
+ mapped_voice = self.VOICE_MAPPINGS[voice]
+ else:
+ # Assume it's already a Polly voice name
+ mapped_voice = voice
+
+ # Map response format
+ if "response_format" in optional_params:
+ format_name = optional_params["response_format"]
+ if format_name in self.FORMAT_MAPPINGS:
+ mapped_params["output_format"] = self.FORMAT_MAPPINGS[format_name]
+ else:
+ mapped_params["output_format"] = format_name
+ else:
+ mapped_params["output_format"] = self.DEFAULT_OUTPUT_FORMAT
+
+ # Extract engine from model name (e.g., "aws_polly/neural" -> "neural")
+ engine = self._extract_engine_from_model(model)
+ mapped_params["engine"] = engine
+
+ # Pass through Polly-specific parameters (use AWS API casing)
+ if "language_code" in kwargs:
+ mapped_params["LanguageCode"] = kwargs["language_code"]
+ if "lexicon_names" in kwargs:
+ mapped_params["LexiconNames"] = kwargs["lexicon_names"]
+ if "sample_rate" in kwargs:
+ mapped_params["SampleRate"] = kwargs["sample_rate"]
+
+ return mapped_voice, mapped_params
+
+ def _extract_engine_from_model(self, model: str) -> str:
+ """
+ Extract engine from model name.
+
+ Examples:
+ - aws_polly/neural -> neural
+ - aws_polly/standard -> standard
+ - aws_polly/long-form -> long-form
+ - aws_polly -> neural (default)
+ """
+ if "/" in model:
+ parts = model.split("/")
+ if len(parts) >= 2:
+ engine = parts[1].lower()
+ if engine in self.VALID_ENGINES:
+ return engine
+ return self.DEFAULT_ENGINE
+
+ def validate_environment(
+ self,
+ headers: dict,
+ model: str,
+ api_key: Optional[str] = None,
+ api_base: Optional[str] = None,
+ ) -> dict:
+ """
+ Validate AWS environment and set up headers.
+ AWS SigV4 signing will be done in transform_text_to_speech_request.
+ """
+ validated_headers = headers.copy()
+ validated_headers["Content-Type"] = "application/json"
+ return validated_headers
+
+ def get_complete_url(
+ self,
+ model: str,
+ api_base: Optional[str],
+ litellm_params: dict,
+ ) -> str:
+ """
+ Get the complete URL for AWS Polly SynthesizeSpeech request
+
+ Polly endpoint format:
+ https://polly.{region}.amazonaws.com/v1/speech
+ """
+ if api_base is not None:
+ return api_base.rstrip("/") + "/v1/speech"
+
+ aws_region_name = litellm_params.get("aws_region_name", self.DEFAULT_REGION)
+ return f"https://polly.{aws_region_name}.amazonaws.com/v1/speech"
+
+ def is_ssml_input(self, input: str) -> bool:
+ """
+ Returns True if input is SSML, False otherwise.
+
+ Based on AWS Polly SSML requirements - must contain tag.
+ """
+ return "" in input or " Tuple[Dict[str, str], str]:
+ """
+ Sign the AWS Polly request using SigV4.
+
+ Returns:
+ Tuple of (signed_headers, json_body_string)
+ """
+ try:
+ from botocore.auth import SigV4Auth
+ from botocore.awsrequest import AWSRequest
+ except ImportError:
+ raise ImportError("Missing boto3 to call AWS Polly. Run 'pip install boto3'.")
+
+ # Get AWS region
+ aws_region_name = litellm_params.get("aws_region_name", self.DEFAULT_REGION)
+
+ # Get AWS credentials
+ credentials = self.get_credentials(
+ aws_access_key_id=litellm_params.get("aws_access_key_id"),
+ aws_secret_access_key=litellm_params.get("aws_secret_access_key"),
+ aws_session_token=litellm_params.get("aws_session_token"),
+ aws_region_name=aws_region_name,
+ aws_session_name=litellm_params.get("aws_session_name"),
+ aws_profile_name=litellm_params.get("aws_profile_name"),
+ aws_role_name=litellm_params.get("aws_role_name"),
+ aws_web_identity_token=litellm_params.get("aws_web_identity_token"),
+ aws_sts_endpoint=litellm_params.get("aws_sts_endpoint"),
+ aws_external_id=litellm_params.get("aws_external_id"),
+ )
+
+ # Serialize request body to JSON
+ json_body = json.dumps(request_body)
+
+ # Create headers for signing
+ headers = {
+ "Content-Type": "application/json",
+ }
+
+ # Create AWS request for signing
+ aws_request = AWSRequest(
+ method="POST",
+ url=endpoint_url,
+ data=json_body,
+ headers=headers,
+ )
+
+ # Sign the request
+ SigV4Auth(credentials, "polly", aws_region_name).add_auth(aws_request)
+
+ # Return signed headers and body
+ return dict(aws_request.headers), json_body
+
+ def transform_text_to_speech_request(
+ self,
+ model: str,
+ input: str,
+ voice: Optional[str],
+ optional_params: Dict,
+ litellm_params: Dict,
+ headers: dict,
+ ) -> TextToSpeechRequestData:
+ """
+ Transform OpenAI TTS request to AWS Polly SynthesizeSpeech format.
+
+ Supports:
+ - Native Polly voices (Joanna, Matthew, etc.)
+ - OpenAI voice mapping (alloy, echo, etc.)
+ - SSML input (auto-detected via tag)
+ - Multiple engines (neural, standard, long-form, generative)
+
+ Returns:
+ TextToSpeechRequestData: Contains signed request for Polly API
+ """
+ # Get voice (already mapped in main.py, or use default)
+ polly_voice = voice or self.DEFAULT_VOICE
+
+ # Get output format
+ output_format = optional_params.get("output_format", self.DEFAULT_OUTPUT_FORMAT)
+
+ # Get engine
+ engine = optional_params.get("engine", self.DEFAULT_ENGINE)
+
+ # Build request body
+ request_body: Dict[str, Any] = {
+ "Engine": engine,
+ "OutputFormat": output_format,
+ "Text": input,
+ "VoiceId": polly_voice,
+ }
+
+ # Auto-detect SSML
+ if self.is_ssml_input(input):
+ request_body["TextType"] = "ssml"
+ else:
+ request_body["TextType"] = "text"
+
+ # Add optional Polly parameters (already in AWS casing from map_openai_params)
+ for key in ["LanguageCode", "LexiconNames", "SampleRate"]:
+ if key in optional_params:
+ request_body[key] = optional_params[key]
+
+ # Get endpoint URL
+ endpoint_url = self.get_complete_url(
+ model=model,
+ api_base=litellm_params.get("api_base"),
+ litellm_params=litellm_params,
+ )
+
+ # Sign the request with AWS SigV4
+ signed_headers, json_body = self._sign_polly_request(
+ request_body=request_body,
+ endpoint_url=endpoint_url,
+ litellm_params=litellm_params,
+ )
+
+ # Return as ssml_body so the handler uses data= instead of json=
+ # This preserves the exact JSON string that was signed
+ return TextToSpeechRequestData(
+ ssml_body=json_body,
+ headers=signed_headers,
+ )
+
+ def transform_text_to_speech_response(
+ self,
+ model: str,
+ raw_response: httpx.Response,
+ logging_obj: "LiteLLMLoggingObj",
+ ) -> "HttpxBinaryResponseContent":
+ """
+ Transform AWS Polly response to standard format.
+
+ Polly returns the audio data directly in the response body.
+ """
+ from litellm.types.llms.openai import HttpxBinaryResponseContent
+
+ return HttpxBinaryResponseContent(raw_response)
+
diff --git a/litellm/main.py b/litellm/main.py
index 5550d098985..60fe3eb2dec 100644
--- a/litellm/main.py
+++ b/litellm/main.py
@@ -105,10 +105,22 @@ from litellm.llms.vertex_ai.common_utils import (
from litellm.realtime_api.main import _realtime_health_check
from litellm.secret_managers.main import get_secret_bool, get_secret_str
from litellm.types.router import GenericLiteLLMParams
-from litellm.types.utils import RawRequestTypedDict, StreamingChoices
+from litellm.types.utils import (
+ ModelResponseStream,
+ RawRequestTypedDict,
+ StreamingChoices,
+)
from litellm.utils import (
+ Choices,
CustomStreamWrapper,
+ EmbeddingResponse,
+ Message,
+ ModelResponse,
ProviderConfigManager,
+ TextChoices,
+ TextCompletionResponse,
+ TextCompletionStreamWrapper,
+ TranscriptionResponse,
Usage,
_get_model_info_helper,
add_provider_specific_params_to_optional_params,
@@ -166,8 +178,8 @@ from .llms.azure_ai.anthropic.handler import AzureAnthropicChatCompletion
from .llms.azure_ai.embed import AzureAIEmbedding
from .llms.bedrock.chat import BedrockConverseLLM, BedrockLLM
from .llms.bedrock.embed.embedding import BedrockEmbedding
-from .llms.bedrock.image_generation.image_handler import BedrockImageGeneration
from .llms.bedrock.image_edit.handler import BedrockImageEdit
+from .llms.bedrock.image_generation.image_handler import BedrockImageGeneration
from .llms.bytez.chat.transformation import BytezChatConfig
from .llms.clarifai.chat.transformation import ClarifaiConfig
from .llms.codestral.completion.handler import CodestralTextCompletion
@@ -240,18 +252,6 @@ from .types.utils import (
all_litellm_params,
)
-from litellm.types.utils import ModelResponseStream
-from litellm.utils import (
- Choices,
- EmbeddingResponse,
- Message,
- ModelResponse,
- TextChoices,
- TextCompletionResponse,
- TextCompletionStreamWrapper,
- TranscriptionResponse,
-)
-
####### ENVIRONMENT VARIABLES ###################
openai_chat_completions = OpenAIChatCompletion()
openai_text_completions = OpenAITextCompletion()
@@ -6471,6 +6471,35 @@ def speech( # noqa: PLR0915
api_key=api_key,
**kwargs,
)
+ elif custom_llm_provider == "aws_polly":
+ from litellm.llms.aws_polly.text_to_speech.transformation import (
+ AWSPollyTextToSpeechConfig,
+ )
+
+ # AWS Polly Text-to-Speech
+ if text_to_speech_provider_config is None:
+ text_to_speech_provider_config = AWSPollyTextToSpeechConfig()
+
+ # Cast to specific AWS Polly config type to access dispatch method
+ aws_polly_config = cast(
+ AWSPollyTextToSpeechConfig, text_to_speech_provider_config
+ )
+
+ response = aws_polly_config.dispatch_text_to_speech(
+ model=model,
+ input=input,
+ voice=voice,
+ optional_params=optional_params,
+ litellm_params_dict=litellm_params_dict,
+ logging_obj=logging_obj,
+ timeout=timeout,
+ extra_headers=extra_headers,
+ base_llm_http_handler=base_llm_http_handler,
+ aspeech=aspeech or False,
+ api_base=api_base,
+ api_key=api_key,
+ **kwargs,
+ )
if response is None:
raise Exception(
@@ -6905,6 +6934,7 @@ def _get_encoding():
global _encoding_cache
if _encoding_cache is None:
import sys
+
# Access via module to trigger __getattr__ if not cached
_encoding_cache = sys.modules[__name__].encoding
return _encoding_cache
diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json
index e973c43f60c..f4b42d1fd6e 100644
--- a/litellm/model_prices_and_context_window_backup.json
+++ b/litellm/model_prices_and_context_window_backup.json
@@ -25423,6 +25423,42 @@
"/v1/audio/speech"
]
},
+ "aws_polly/standard": {
+ "input_cost_per_character": 4e-06,
+ "litellm_provider": "aws_polly",
+ "mode": "audio_speech",
+ "supported_endpoints": [
+ "/v1/audio/speech"
+ ],
+ "source": "https://aws.amazon.com/polly/pricing/"
+ },
+ "aws_polly/neural": {
+ "input_cost_per_character": 1.6e-05,
+ "litellm_provider": "aws_polly",
+ "mode": "audio_speech",
+ "supported_endpoints": [
+ "/v1/audio/speech"
+ ],
+ "source": "https://aws.amazon.com/polly/pricing/"
+ },
+ "aws_polly/long-form": {
+ "input_cost_per_character": 1e-04,
+ "litellm_provider": "aws_polly",
+ "mode": "audio_speech",
+ "supported_endpoints": [
+ "/v1/audio/speech"
+ ],
+ "source": "https://aws.amazon.com/polly/pricing/"
+ },
+ "aws_polly/generative": {
+ "input_cost_per_character": 3e-05,
+ "litellm_provider": "aws_polly",
+ "mode": "audio_speech",
+ "supported_endpoints": [
+ "/v1/audio/speech"
+ ],
+ "source": "https://aws.amazon.com/polly/pricing/"
+ },
"us.amazon.nova-lite-v1:0": {
"input_cost_per_token": 6e-08,
"litellm_provider": "bedrock_converse",
diff --git a/litellm/types/utils.py b/litellm/types/utils.py
index f39afbfc2e0..3416459bc28 100644
--- a/litellm/types/utils.py
+++ b/litellm/types/utils.py
@@ -2915,6 +2915,7 @@ class LlmProviders(str, Enum):
BYTEZ = "bytez"
REPLICATE = "replicate"
RUNWAYML = "runwayml"
+ AWS_POLLY = "aws_polly"
HUGGINGFACE = "huggingface"
TOGETHER_AI = "together_ai"
OPENROUTER = "openrouter"
diff --git a/litellm/utils.py b/litellm/utils.py
index 8aa5eb6561b..805fbafcfce 100644
--- a/litellm/utils.py
+++ b/litellm/utils.py
@@ -8096,6 +8096,12 @@ class ProviderConfigManager:
)
return VertexAITextToSpeechConfig()
+ elif litellm.LlmProviders.AWS_POLLY == provider:
+ from litellm.llms.aws_polly.text_to_speech.transformation import (
+ AWSPollyTextToSpeechConfig,
+ )
+
+ return AWSPollyTextToSpeechConfig()
return None
@staticmethod
diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json
index e973c43f60c..f4b42d1fd6e 100644
--- a/model_prices_and_context_window.json
+++ b/model_prices_and_context_window.json
@@ -25423,6 +25423,42 @@
"/v1/audio/speech"
]
},
+ "aws_polly/standard": {
+ "input_cost_per_character": 4e-06,
+ "litellm_provider": "aws_polly",
+ "mode": "audio_speech",
+ "supported_endpoints": [
+ "/v1/audio/speech"
+ ],
+ "source": "https://aws.amazon.com/polly/pricing/"
+ },
+ "aws_polly/neural": {
+ "input_cost_per_character": 1.6e-05,
+ "litellm_provider": "aws_polly",
+ "mode": "audio_speech",
+ "supported_endpoints": [
+ "/v1/audio/speech"
+ ],
+ "source": "https://aws.amazon.com/polly/pricing/"
+ },
+ "aws_polly/long-form": {
+ "input_cost_per_character": 1e-04,
+ "litellm_provider": "aws_polly",
+ "mode": "audio_speech",
+ "supported_endpoints": [
+ "/v1/audio/speech"
+ ],
+ "source": "https://aws.amazon.com/polly/pricing/"
+ },
+ "aws_polly/generative": {
+ "input_cost_per_character": 3e-05,
+ "litellm_provider": "aws_polly",
+ "mode": "audio_speech",
+ "supported_endpoints": [
+ "/v1/audio/speech"
+ ],
+ "source": "https://aws.amazon.com/polly/pricing/"
+ },
"us.amazon.nova-lite-v1:0": {
"input_cost_per_token": 6e-08,
"litellm_provider": "bedrock_converse",
diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json
index 72e3bbbe1fd..152b3df52e6 100644
--- a/provider_endpoints_support.json
+++ b/provider_endpoints_support.json
@@ -205,6 +205,22 @@
"a2a": true
}
},
+ "aws_polly": {
+ "display_name": "AWS - Polly (`aws_polly`)",
+ "url": "https://docs.litellm.ai/docs/providers/aws_polly",
+ "endpoints": {
+ "chat_completions": false,
+ "messages": false,
+ "responses": false,
+ "embeddings": false,
+ "image_generations": false,
+ "audio_transcriptions": false,
+ "audio_speech": true,
+ "moderations": false,
+ "batches": false,
+ "rerank": false
+ }
+ },
"azure": {
"display_name": "Azure (`azure`)",
"url": "https://docs.litellm.ai/docs/providers/azure",
diff --git a/tests/audio_tests/aws_polly_speech.mp3 b/tests/audio_tests/aws_polly_speech.mp3
new file mode 100644
index 00000000000..68d22cd383e
Binary files /dev/null and b/tests/audio_tests/aws_polly_speech.mp3 differ
diff --git a/tests/audio_tests/aws_polly_speech_generative.mp3 b/tests/audio_tests/aws_polly_speech_generative.mp3
new file mode 100644
index 00000000000..68d22cd383e
Binary files /dev/null and b/tests/audio_tests/aws_polly_speech_generative.mp3 differ
diff --git a/tests/audio_tests/test_audio_speech.py b/tests/audio_tests/test_audio_speech.py
index fa71326e11b..67e0dbffa61 100644
--- a/tests/audio_tests/test_audio_speech.py
+++ b/tests/audio_tests/test_audio_speech.py
@@ -522,3 +522,165 @@ async def test_azure_ava_tts_fable_voice_mapping():
assert "Testing voice mapping" in ssml_body
assert " Joanna).
+ Verifies that OpenAI voices are correctly mapped to Polly voices.
+ """
+ import json
+ from unittest.mock import MagicMock, patch
+ import httpx
+
+ mock_response_content = b"fake_audio_data"
+ mock_httpx_response = MagicMock(spec=httpx.Response)
+ mock_httpx_response.content = mock_response_content
+ mock_httpx_response.status_code = 200
+ mock_httpx_response.headers = {"content-type": "audio/mpeg"}
+
+ with patch("litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post") as mock_post:
+ mock_post.return_value = mock_httpx_response
+
+ response = await litellm.aspeech(
+ model="aws_polly/neural",
+ voice="alloy",
+ input="Testing OpenAI voice mapping",
+ aws_region_name="us-east-1",
+ )
+
+ assert mock_post.called
+
+ call_args = mock_post.call_args
+ request_data = call_args.kwargs.get("data")
+
+ # Parse the JSON body
+ assert request_data is not None
+ request_body = json.loads(request_data)
+
+ # Verify alloy was mapped to Joanna
+ assert request_body["VoiceId"] == "Joanna"
+ assert request_body["Text"] == "Testing OpenAI voice mapping"
+
+
+@pytest.mark.asyncio
+async def test_aws_polly_tts_with_ssml():
+ """
+ Test AWS Polly TTS with SSML input.
+ Verifies that SSML is detected and TextType is set correctly.
+ """
+ import json
+ from unittest.mock import MagicMock, patch
+ import httpx
+
+ mock_response_content = b"fake_audio_data"
+ mock_httpx_response = MagicMock(spec=httpx.Response)
+ mock_httpx_response.content = mock_response_content
+ mock_httpx_response.status_code = 200
+ mock_httpx_response.headers = {"content-type": "audio/mpeg"}
+
+ ssml_input = 'Hello, this is SSML.'
+
+ with patch("litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post") as mock_post:
+ mock_post.return_value = mock_httpx_response
+
+ response = await litellm.aspeech(
+ model="aws_polly/neural",
+ voice="Joanna",
+ input=ssml_input,
+ aws_region_name="us-east-1",
+ )
+
+ assert mock_post.called
+
+ call_args = mock_post.call_args
+ request_data = call_args.kwargs.get("data")
+
+ # Parse the JSON body
+ assert request_data is not None
+ request_body = json.loads(request_data)
+
+ # Verify SSML is detected and TextType is set to ssml
+ assert request_body["Text"] == ssml_input
+ assert request_body["TextType"] == "ssml"
+ assert request_body["VoiceId"] == "Joanna"
+
+
+@pytest.mark.asyncio
+async def test_aws_polly_tts_real_api():
+ """
+ Test AWS Polly TTS with real API request.
+ Requires AWS credentials to be configured.
+ """
+ speech_file_path = Path(__file__).parent / "aws_polly_speech_generative.mp3"
+
+ response = await litellm.aspeech(
+ model="aws_polly/generative",
+ voice="Joanna",
+ input="Hello, this is a test of AWS Polly text to speech integration with LiteLLM.",
+ aws_region_name="us-east-1",
+ )
+
+ from litellm.types.llms.openai import HttpxBinaryResponseContent
+
+ assert isinstance(response, HttpxBinaryResponseContent)
+
+ binary_content = response.content
+ assert len(binary_content) > 0
+
+ # MP3 files start with ID3 tag or MPEG sync word
+ assert binary_content[:3] == b"ID3" or binary_content[:2] == b"\xff\xfb" or binary_content[:2] == b"\xff\xf3"
+
+ response.stream_to_file(speech_file_path)
+
+ assert speech_file_path.exists()
+ assert speech_file_path.stat().st_size > 0
+
+ print(f"AWS Polly TTS audio saved to: {speech_file_path}")