mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-18 04:44:37 +00:00
* test(integration): cover audio endpoint contracts Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): close audio SDK clients after each test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): avoid list mutation in audio tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): keep exact upstream call check in model resolution test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): cover audio translations, bracket spellings, streaming and scoped alias keys Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): pin audio metadata parse sites, exact cost and speech bug pins Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): drop inline comment on audio wire assertion Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): strengthen audio wire and authorization contracts Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: mateo <mateo@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
184 lines
7.5 KiB
Python
184 lines
7.5 KiB
Python
import json
|
|
from typing import Final
|
|
|
|
import httpx
|
|
import pytest
|
|
from integration._support.client import Gateway
|
|
from integration._support.wire import Reply, Request, wire_server
|
|
from openai import OpenAI
|
|
|
|
_SPEECH_BYTES: Final = bytes(range(256)) * 32
|
|
_SPEECH_REQUEST: Final = {
|
|
"model": "gpt-4o-mini-tts",
|
|
"input": "hello",
|
|
"voice": "alloy",
|
|
"response_format": "wav",
|
|
"speed": 1.25,
|
|
"instructions": "cheerful",
|
|
}
|
|
_FORMAT_MEDIA_TYPES: Final = {
|
|
"wav": "audio/wav",
|
|
"flac": "audio/flac",
|
|
"pcm": "audio/pcm",
|
|
"opus": "audio/opus",
|
|
}
|
|
|
|
|
|
def _sdk(gateway: Gateway, path: str, api_key: str) -> OpenAI:
|
|
return OpenAI(
|
|
base_url=str(gateway.client.base_url).rstrip("/") + path,
|
|
api_key=api_key,
|
|
max_retries=0,
|
|
http_client=httpx.Client(trust_env=False),
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("base_path", ("/v1", ""))
|
|
def test_audio_speech_forwards_the_full_json_body_and_streams_the_audio(gateway: Gateway, base_path: str) -> None:
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/v1/audio/speech"
|
|
assert request.headers["authorization"] == "Bearer synthetic-openai-key"
|
|
body: Final = json.loads(request.body)
|
|
assert body == _SPEECH_REQUEST
|
|
assert type(body["speed"]) is float
|
|
return Reply(body=_SPEECH_BYTES, content_type="audio/wav")
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(
|
|
model="openai/gpt-4o-mini-tts", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
|
|
)
|
|
key: Final = scenario.key(models=[model])
|
|
with _sdk(gateway, base_path, key) as sdk:
|
|
response: Final = sdk.audio.speech.with_raw_response.create(
|
|
model=model,
|
|
input="hello",
|
|
voice="alloy",
|
|
response_format="wav",
|
|
speed=1.25,
|
|
instructions="cheerful",
|
|
)
|
|
assert response.status_code == 200, response.read()
|
|
assert response.content == _SPEECH_BYTES
|
|
assert response.headers["content-type"] == "audio/wav"
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/audio/speech")]
|
|
|
|
|
|
def test_audio_speech_streams_chunked_audio_through_intact(gateway: Gateway) -> None:
|
|
audio: Final = bytes(index % 256 for index in range(64 * 1024))
|
|
chunks: Final = tuple(audio[start : start + 4096] for start in range(0, len(audio), 4096))
|
|
assert len(chunks) == 16
|
|
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/v1/audio/speech"
|
|
return Reply(chunks=chunks, content_type="audio/wav")
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(
|
|
model="openai/gpt-4o-mini-tts", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
|
|
)
|
|
with _sdk(gateway, "/v1", gateway.key) as sdk:
|
|
response: Final = sdk.audio.speech.with_raw_response.create(
|
|
model=model,
|
|
input="hello",
|
|
voice="alloy",
|
|
response_format="mp3",
|
|
)
|
|
assert response.status_code == 200, response.read()
|
|
assert response.content == audio
|
|
assert response.headers["content-type"] == "audio/wav"
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/audio/speech")]
|
|
|
|
|
|
@pytest.mark.parametrize("response_format,media_type", tuple(_FORMAT_MEDIA_TYPES.items()))
|
|
def test_audio_speech_derives_the_media_type_from_the_requested_format(
|
|
gateway: Gateway, response_format: str, media_type: str
|
|
) -> None:
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/v1/audio/speech"
|
|
return Reply(body=_SPEECH_BYTES, content_type="application/octet-stream")
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(
|
|
model="openai/gpt-4o-mini-tts", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
|
|
)
|
|
with _sdk(gateway, "/v1", gateway.key) as sdk:
|
|
response: Final = sdk.audio.speech.with_raw_response.create(
|
|
model=model,
|
|
input="hello",
|
|
voice="alloy",
|
|
response_format=response_format,
|
|
)
|
|
assert response.status_code == 200, response.read()
|
|
assert response.headers["content-type"] == media_type
|
|
assert response.content == _SPEECH_BYTES
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/audio/speech")]
|
|
|
|
|
|
def test_audio_speech_forwards_stream_format_sse(gateway: Gateway) -> None:
|
|
pytest.skip(
|
|
"BUG: speech stream_format='sse' is dropped from the upstream JSON body, which arrives as "
|
|
"{input, model, voice}, and the SSE reply is relabelled audio/mpeg"
|
|
)
|
|
sse_body: Final = (
|
|
b'data: {"type":"speech.audio.delta","audio":"aGVsbG8="}\n\ndata: {"type":"speech.audio.done"}\n\n'
|
|
)
|
|
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/v1/audio/speech"
|
|
assert json.loads(request.body) == {
|
|
"model": "gpt-4o-mini-tts",
|
|
"input": "hello",
|
|
"voice": "alloy",
|
|
"stream_format": "sse",
|
|
}
|
|
return Reply(chunks=(sse_body,), content_type="text/event-stream")
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(
|
|
model="openai/gpt-4o-mini-tts", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
|
|
)
|
|
with _sdk(gateway, "/v1", gateway.key) as sdk:
|
|
response: Final = sdk.audio.speech.with_raw_response.create(
|
|
model=model,
|
|
input="hello",
|
|
voice="alloy",
|
|
stream_format="sse",
|
|
)
|
|
assert response.status_code == 200, response.read()
|
|
assert response.headers["content-type"].startswith("text/event-stream"), response.read()
|
|
assert response.content == sse_body
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/audio/speech")]
|
|
|
|
|
|
def test_audio_speech_forwards_object_form_voice(gateway: Gateway) -> None:
|
|
pytest.skip(
|
|
"BUG: speech voice={'id': 'voice_123'} is rejected with 400 \"'voice' is required to be passed "
|
|
'as a string for OpenAI TTS" and never reaches the upstream'
|
|
)
|
|
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/v1/audio/speech"
|
|
assert json.loads(request.body) == {
|
|
"model": "gpt-4o-mini-tts",
|
|
"input": "hello",
|
|
"voice": {"id": "voice_123"},
|
|
}
|
|
return Reply(body=_SPEECH_BYTES, content_type="audio/mpeg")
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(
|
|
model="openai/gpt-4o-mini-tts", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
|
|
)
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/audio/speech",
|
|
{"model": model, "input": "hello", "voice": {"id": "voice_123"}},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.content == _SPEECH_BYTES
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/audio/speech")]
|