feat(providers): add Paxa as an OpenAI compatible speech provider

paxa/<model> 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
This commit is contained in:
yoyoismee 2026-10-24 19:02:40 +07:00
parent e04e89c701
commit a291a1ae9d
No known key found for this signature in database
9 changed files with 74 additions and 0 deletions

View file

@ -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) | ✅ | ✅ | ✅ | | | | | | | |

View file

@ -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))

View file

@ -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")

View file

@ -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")

View file

@ -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",

View file

@ -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"

View file

@ -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",

View file

@ -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")

View file

@ -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"}