mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Merge c9d565b4dd into c80e6e2474
This commit is contained in:
commit
3b4b404a88
2 changed files with 173 additions and 0 deletions
|
|
@ -8721,6 +8721,50 @@ def speech(
|
|||
api_key=api_key,
|
||||
**kwargs,
|
||||
)
|
||||
elif custom_llm_provider == "openrouter":
|
||||
if voice is None or not (isinstance(voice, str)):
|
||||
raise litellm.BadRequestError(
|
||||
message="'voice' is required to be passed as a string for OpenRouter TTS",
|
||||
model=model,
|
||||
llm_provider=custom_llm_provider,
|
||||
)
|
||||
openrouter_api_base: Final = (
|
||||
api_base or litellm.api_base or get_secret_str("OPENROUTER_API_BASE") or "https://openrouter.ai/api/v1"
|
||||
)
|
||||
# set API KEY
|
||||
openrouter_api_key: Final = (
|
||||
api_key or litellm.api_key or litellm.openrouter_key or get_secret_str("OPENROUTER_API_KEY")
|
||||
)
|
||||
|
||||
OPENROUTER_DEFAULT_SITE_URL: Final = "https://litellm.ai"
|
||||
OPENROUTER_DEFAULT_APP_NAME: Final = "liteLLM"
|
||||
openrouter_site_url: Final = get_secret("OR_SITE_URL") or OPENROUTER_DEFAULT_SITE_URL
|
||||
openrouter_app_name: Final = get_secret("OR_APP_NAME") or OPENROUTER_DEFAULT_APP_NAME
|
||||
openrouter_headers: Final = {
|
||||
"HTTP-Referer": openrouter_site_url,
|
||||
"X-Title": openrouter_app_name,
|
||||
}
|
||||
if extra_headers:
|
||||
openrouter_headers.update(extra_headers)
|
||||
optional_params["extra_headers"] = openrouter_headers
|
||||
|
||||
openrouter_response: Final = openai_chat_completions.audio_speech(
|
||||
model=model,
|
||||
input=input,
|
||||
voice=voice,
|
||||
optional_params=optional_params,
|
||||
api_key=openrouter_api_key,
|
||||
api_base=openrouter_api_base,
|
||||
organization=organization,
|
||||
project=project,
|
||||
max_retries=max_retries,
|
||||
timeout=timeout,
|
||||
logging_obj=logging_obj,
|
||||
client=client,
|
||||
aspeech=aspeech,
|
||||
shared_session=shared_session,
|
||||
)
|
||||
return openrouter_response
|
||||
|
||||
if response is None:
|
||||
raise Exception(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
"""OpenRouter TTS is an OpenAI-compatible POST /api/v1/audio/speech passthrough."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import respx
|
||||
|
||||
import litellm
|
||||
|
||||
OPENROUTER_SPEECH_URL: Final = "https://openrouter.ai/api/v1/audio/speech"
|
||||
TTS_MODEL: Final = "openrouter/google/gemini-3.1-flash-tts-preview"
|
||||
UPSTREAM_MODEL: Final = "google/gemini-3.1-flash-tts-preview"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _flush_openai_client_cache():
|
||||
cache = getattr(litellm, "in_memory_llm_clients_cache", None)
|
||||
if cache is not None:
|
||||
cache.flush_cache()
|
||||
yield
|
||||
if cache is not None:
|
||||
cache.flush_cache()
|
||||
|
||||
|
||||
def test_speech_openrouter_posts_to_openrouter_speech_endpoint(
|
||||
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
||||
audio_bytes: Final = b"pcm-fake-audio"
|
||||
mock_route: Final = respx_mock.post(OPENROUTER_SPEECH_URL).mock(
|
||||
return_value=httpx.Response(200, content=audio_bytes, headers={"content-type": "audio/pcm"})
|
||||
)
|
||||
|
||||
response: Final = litellm.speech(
|
||||
model=TTS_MODEL,
|
||||
input="Hello world",
|
||||
voice="Kore",
|
||||
response_format="pcm",
|
||||
)
|
||||
|
||||
assert mock_route.called
|
||||
request: Final = mock_route.calls.last.request
|
||||
assert json.loads(request.content) == {
|
||||
"model": UPSTREAM_MODEL,
|
||||
"input": "Hello world",
|
||||
"voice": "Kore",
|
||||
"response_format": "pcm",
|
||||
}
|
||||
assert request.headers["authorization"] == "Bearer sk-or-test"
|
||||
assert request.headers["http-referer"] == "https://litellm.ai"
|
||||
assert request.headers["x-title"] == "liteLLM"
|
||||
assert response.content == audio_bytes
|
||||
|
||||
|
||||
def test_speech_openrouter_uses_openrouter_api_base_env(respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
||||
monkeypatch.setenv("OPENROUTER_API_BASE", "https://openrouter.env.example/api/v1")
|
||||
monkeypatch.setattr(litellm, "api_base", None)
|
||||
audio_bytes: Final = b"env-base-audio"
|
||||
env_route: Final = respx_mock.post("https://openrouter.env.example/api/v1/audio/speech").mock(
|
||||
return_value=httpx.Response(200, content=audio_bytes)
|
||||
)
|
||||
|
||||
response: Final = litellm.speech(
|
||||
model=TTS_MODEL,
|
||||
input="Hello world",
|
||||
voice="Kore",
|
||||
)
|
||||
|
||||
assert env_route.called
|
||||
assert response.content == audio_bytes
|
||||
|
||||
|
||||
def test_speech_openrouter_uses_chat_ranking_header_env(respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
||||
monkeypatch.setenv("OR_SITE_URL", "https://example.com")
|
||||
monkeypatch.setenv("OR_APP_NAME", "MyApp")
|
||||
mock_route: Final = respx_mock.post(OPENROUTER_SPEECH_URL).mock(return_value=httpx.Response(200, content=b"hdr"))
|
||||
|
||||
litellm.speech(model=TTS_MODEL, input="Hello world", voice="Kore")
|
||||
|
||||
request: Final = mock_route.calls.last.request
|
||||
assert request.headers["http-referer"] == "https://example.com"
|
||||
assert request.headers["x-title"] == "MyApp"
|
||||
|
||||
|
||||
def test_speech_openrouter_uses_configured_api_base(respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
||||
audio_bytes: Final = b"gateway-audio"
|
||||
gateway_route: Final = respx_mock.post("https://openrouter.gateway.internal/api/v1/audio/speech").mock(
|
||||
return_value=httpx.Response(200, content=audio_bytes)
|
||||
)
|
||||
|
||||
response: Final = litellm.speech(
|
||||
model=TTS_MODEL,
|
||||
input="Hello world",
|
||||
voice="Kore",
|
||||
api_base="https://openrouter.gateway.internal/api/v1",
|
||||
)
|
||||
|
||||
assert gateway_route.called
|
||||
assert response.content == audio_bytes
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aspeech_openrouter_posts_to_openrouter_speech_endpoint(
|
||||
respx_mock: respx.MockRouter, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "sk-or-test")
|
||||
audio_bytes: Final = b"async-pcm-audio"
|
||||
mock_route: Final = respx_mock.post(OPENROUTER_SPEECH_URL).mock(
|
||||
return_value=httpx.Response(200, content=audio_bytes)
|
||||
)
|
||||
|
||||
response: Final = await litellm.aspeech(
|
||||
model=TTS_MODEL,
|
||||
input="Hello world",
|
||||
voice="Kore",
|
||||
response_format="pcm",
|
||||
)
|
||||
|
||||
assert mock_route.called
|
||||
assert json.loads(mock_route.calls.last.request.content)["model"] == UPSTREAM_MODEL
|
||||
assert response.content == audio_bytes
|
||||
Loading…
Add table
Reference in a new issue