diff --git a/README.md b/README.md index 5dfa8d01fc4..325680363d8 100644 --- a/README.md +++ b/README.md @@ -398,6 +398,7 @@ Set `LITELLM_PROXY_API_BASE` and `LITELLM_PROXY_API_KEY` and every model call th | [OpenAI-like (`openai_like`)](https://docs.litellm.ai/docs/providers/openai_compatible) | | | | ✅ | | | | | | | | [OpenRouter (`openrouter`)](https://docs.litellm.ai/docs/providers/openrouter) | ✅ | ✅ | ✅ | | | | | | | | | [OVHCloud AI Endpoints (`ovhcloud`)](https://docs.litellm.ai/docs/providers/ovhcloud) | ✅ | ✅ | ✅ | | | | | | | | +| [Paxa (`paxa`)](https://docs.litellm.ai/docs/providers/paxa) | | | | | | | ✅ | | | | | [Perplexity AI (`perplexity`)](https://docs.litellm.ai/docs/providers/perplexity) | ✅ | ✅ | ✅ | | | | | | | | | [Petals (`petals`)](https://docs.litellm.ai/docs/providers/petals) | ✅ | ✅ | ✅ | | | | | | | | | [Pinstripes (`pinstripes`)](https://docs.litellm.ai/docs/providers/pinstripes) | ✅ | ✅ | ✅ | | | | | | | | diff --git a/litellm/constants.py b/litellm/constants.py index 8cc51d0ab5e..357144dfac9 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1142,6 +1142,7 @@ openai_compatible_providers: Final[list[str]] = [ "prism", "sail", "reka", + "paxa", ] OPENAI_AUDIO_TRANSCRIPTION_PROVIDERS: Final = frozenset({"openai"} | frozenset(openai_compatible_providers)) diff --git a/litellm/litellm_core_utils/get_llm_provider_logic.py b/litellm/litellm_core_utils/get_llm_provider_logic.py index 19c1d5c414c..7f0957157db 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -678,6 +678,10 @@ def _get_openai_compatible_provider_info( elif custom_llm_provider == "soniox": api_base = api_base or get_secret_str("SONIOX_API_BASE") or "https://api.soniox.com" dynamic_api_key = api_key or get_secret_str("SONIOX_API_KEY") + elif custom_llm_provider == "paxa": + default_paxa_base: Final = get_secret_str("PAXA_API_BASE") or "https://api.paxalabs.com/v1" + api_base = api_base or default_paxa_base # rebind-ok: mirrors sibling provider branches + dynamic_api_key = api_key or get_secret_str("PAXA_API_KEY") # rebind-ok: mirrors sibling provider branches elif custom_llm_provider == "cerebras": api_base = api_base or get_secret("CEREBRAS_API_BASE") or "https://api.cerebras.ai/v1" dynamic_api_key = api_key or get_secret_str("CEREBRAS_API_KEY") diff --git a/litellm/main.py b/litellm/main.py index 9f412e1f24b..7a36d90c9d0 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -8720,6 +8720,7 @@ def speech( # set API KEY api_key = ( api_key + or dynamic_api_key or litellm.api_key # for deepinfra/perplexity/anyscale we check in get_llm_provider and pass in the api key from there or litellm.openai_key or get_secret("OPENAI_API_KEY") diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index d96f2976508..2ffd7841b70 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -3073,6 +3073,24 @@ ], "default_model_placeholder": "gpt-3.5-turbo" }, + { + "provider": "Paxa", + "provider_display_name": "Paxa", + "litellm_provider": "paxa", + "credential_fields": [ + { + "key": "api_key", + "label": "API Key", + "placeholder": null, + "tooltip": null, + "required": true, + "field_type": "password", + "options": null, + "default_value": null + } + ], + "default_model_placeholder": "paxa/paxa-tts-flash-v2" + }, { "provider": "Perplexity", "provider_display_name": "Perplexity", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 2869f9b2577..3ae86f0ad4a 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4475,6 +4475,7 @@ class LlmProviders(str, Enum): NVIDIA_NIM = "nvidia_nim" NVIDIA_RIVA = "nvidia_riva" SONIOX = "soniox" + PAXA = "paxa" CEREBRAS = "cerebras" NADIR = "nadir" AI21_CHAT = "ai21_chat" diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 9ba6ed342ea..8406a4ff7d5 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -3085,6 +3085,22 @@ "generateContent": false } }, + "paxa": { + "display_name": "Paxa (`paxa`)", + "url": "https://docs.litellm.ai/docs/providers/paxa", + "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 + } + }, "perplexity": { "display_name": "Perplexity AI (`perplexity`)", "url": "https://docs.litellm.ai/docs/providers/perplexity", diff --git a/tests/unit/litellm_core_utils/test_get_llm_provider_logic.py b/tests/unit/litellm_core_utils/test_get_llm_provider_logic.py index e9e3f7b8174..c66585d2771 100644 --- a/tests/unit/litellm_core_utils/test_get_llm_provider_logic.py +++ b/tests/unit/litellm_core_utils/test_get_llm_provider_logic.py @@ -79,6 +79,20 @@ def test_get_llm_provider_leaves_fal_ai_api_base_unset_for_global_fallback() -> assert explicit == "http://edge.local/fal" +def test_get_llm_provider_reads_the_paxa_key_and_base_from_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("PAXA_API_KEY", "fake-paxa-key") + monkeypatch.delenv("PAXA_API_BASE", raising=False) + assert get_llm_provider(model="paxa/paxa-tts-flash-v2") == ( + "paxa-tts-flash-v2", + "paxa", + "fake-paxa-key", + "https://api.paxalabs.com/v1", + ) + + monkeypatch.setenv("PAXA_API_BASE", "http://paxa.local/v1") + assert get_llm_provider(model="paxa/paxa-tts-flash-v2")[3] == "http://paxa.local/v1" + + def test_image_generation_fal_ai_egresses_to_global_api_base(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(litellm, "api_base", "http://gateway.local/fal") monkeypatch.setenv("FAL_AI_API_KEY", "test") diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 70b268bf2a2..7ee16db9c83 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -5223,6 +5223,24 @@ def test_groq_speech_honors_base_url_alias(respx_mock: respx.MockRouter): assert response.content == b"mp3-bytes" +def test_paxa_speech_uses_the_paxa_key_from_the_environment( + respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("PAXA_API_KEY", "fake-paxa-key") + monkeypatch.delenv("PAXA_API_BASE", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + route: Final = respx_mock.post("https://api.paxalabs.com/v1/audio/speech").mock( + return_value=httpx.Response(200, content=b"mp3-bytes") + ) + + response: Final = litellm.speech(model="paxa/paxa-tts-flash-v2", input="hello", voice="foithong") + + request: Final = route.calls.last.request + assert request.headers["authorization"] == "Bearer fake-paxa-key" + assert json.loads(request.content) == {"model": "paxa-tts-flash-v2", "input": "hello", "voice": "foithong"} + assert response.content == b"mp3-bytes" + + FORWARDED_CLIENT_HEADERS: Final = {"x-forwarded-for": "10.0.0.1", "x-amzn-trace-id": "Root=1-lit7694"}