From a291a1ae9daeb216e650f8731df01bd746183c41 Mon Sep 17 00:00:00 2001 From: yoyoismee <14046637+yoyoismee@users.noreply.github.com> Date: Sat, 24 Oct 2026 19:02:40 +0700 Subject: [PATCH] feat(providers): add Paxa as an OpenAI compatible speech provider paxa/ now resolves to https://api.paxalabs.com/v1 with PAXA_API_KEY, and PAXA_API_BASE overrides the base. Requests go through the existing OpenAI speech path speech() also uses the key that get_llm_provider resolved for an OpenAI compatible provider, as completion and transcription already do --- README.md | 1 + litellm/constants.py | 1 + .../get_llm_provider_logic.py | 4 ++++ litellm/main.py | 1 + .../provider_create_fields.json | 18 ++++++++++++++++++ litellm/types/utils.py | 1 + provider_endpoints_support.json | 16 ++++++++++++++++ .../test_get_llm_provider_logic.py | 14 ++++++++++++++ tests/unit/test_main.py | 18 ++++++++++++++++++ 9 files changed, 74 insertions(+) diff --git a/README.md b/README.md index 4feb6e00ffa..c629e5ef2cf 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 c0fe0491775..7ae83a0f533 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1138,6 +1138,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 3130d49a5e2..7fff097328f 100644 --- a/litellm/litellm_core_utils/get_llm_provider_logic.py +++ b/litellm/litellm_core_utils/get_llm_provider_logic.py @@ -670,6 +670,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 327a7a1c4b5..e8421357eec 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -8680,6 +8680,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 4bec42a7868..caf7786f3e4 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -3027,6 +3027,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 5939a350970..57e7f053f5c 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4440,6 +4440,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 d65da3f41fd..bd9da827cb6 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -2095,6 +2095,22 @@ "a2a": 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 57e66805678..e8c54b28aae 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 @@ -74,6 +74,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"}