mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-16 04:29:31 +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>
104 lines
4.5 KiB
Python
104 lines
4.5 KiB
Python
import json
|
|
import mimetypes
|
|
from email.parser import BytesParser
|
|
from email.policy import HTTP
|
|
from typing import Final
|
|
|
|
import pytest
|
|
|
|
from integration._support.client import Gateway
|
|
from integration._support.wire import Reply, Request, wire_server
|
|
|
|
_WAV_BYTES: Final = (
|
|
b"RIFF\x30\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00"
|
|
b"\x40\x1f\x00\x00\x40\x1f\x00\x00\x01\x00\x08\x00data\x08\x00\x00\x00"
|
|
b"\x00\x01\x02\x03\x04\x05\x06\x07"
|
|
)
|
|
_WAV_CONTENT_TYPE: Final = mimetypes.guess_type("a.wav")[0] or "application/octet-stream"
|
|
_SPEECH_BYTES: Final = bytes(range(256)) * 32
|
|
|
|
|
|
def _transcription_model(request: Request) -> str:
|
|
envelope: Final = f"content-type: {request.headers['content-type']}\r\n\r\n".encode() + request.body
|
|
parsed: Final = BytesParser(policy=HTTP).parsebytes(envelope)
|
|
assert parsed.is_multipart(), request.headers["content-type"]
|
|
fields: Final = {
|
|
part.get_param("name", header="content-disposition"): part.get_payload(decode=True).decode()
|
|
for part in parsed.iter_parts()
|
|
if part.get_filename() is None
|
|
}
|
|
return fields["model"]
|
|
|
|
|
|
@pytest.mark.parametrize("path", ("/v1/audio/transcriptions", "/audio/transcriptions"))
|
|
def test_audio_transcription_enforces_key_model_access(gateway: Gateway, path: str) -> None:
|
|
def respond(request: Request) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/v1/audio/transcriptions"
|
|
assert _transcription_model(request) == "whisper-1"
|
|
return Reply(body=json.dumps({"text": "hello world"}).encode())
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
allowed: Final = scenario.model(
|
|
model="openai/whisper-1", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
|
|
)
|
|
other: Final = scenario.model(
|
|
model="openai/whisper-1", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
|
|
)
|
|
key: Final = scenario.key(models=[allowed])
|
|
denied: Final = gateway.request_multipart(
|
|
path, {"model": other}, {"file": ("a.wav", _WAV_BYTES, _WAV_CONTENT_TYPE)}, key=key
|
|
)
|
|
assert denied.status_code == 403, denied.text
|
|
error: Final = denied.json()["error"]
|
|
assert error["type"] == "key_model_access_denied" and error["param"] == "model", denied.text
|
|
assert wire.drain() == ()
|
|
granted: Final = gateway.request_multipart(
|
|
path, {"model": allowed}, {"file": ("a.wav", _WAV_BYTES, _WAV_CONTENT_TYPE)}, key=key
|
|
)
|
|
assert granted.status_code == 200, granted.text
|
|
assert [(request.method, request.target) for request in wire.drain()] == [
|
|
("POST", "/v1/audio/transcriptions")
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("path", ("/v1/audio/speech", "/audio/speech"))
|
|
def test_audio_speech_enforces_key_model_access(gateway: Gateway, 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"
|
|
assert json.loads(request.body) == {
|
|
"model": "gpt-4o-mini-tts",
|
|
"input": "hello",
|
|
"voice": "alloy",
|
|
}
|
|
return Reply(body=_SPEECH_BYTES, content_type="audio/wav")
|
|
|
|
with wire_server(respond) as wire, gateway.scenario() as scenario:
|
|
allowed: Final = scenario.model(
|
|
model="openai/gpt-4o-mini-tts", api_base=f"{wire.url}/v1", api_key="synthetic-openai-key"
|
|
)
|
|
other: 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=[allowed])
|
|
denied: Final = gateway.request(
|
|
"POST",
|
|
path,
|
|
{"model": other, "input": "hello", "voice": "alloy"},
|
|
key=key,
|
|
)
|
|
assert denied.status_code == 403, denied.text
|
|
error: Final = denied.json()["error"]
|
|
assert error["type"] == "key_model_access_denied" and error["param"] == "model", denied.text
|
|
assert wire.drain() == ()
|
|
granted: Final = gateway.request(
|
|
"POST",
|
|
path,
|
|
{"model": allowed, "input": "hello", "voice": "alloy"},
|
|
key=key,
|
|
)
|
|
assert granted.status_code == 200, granted.text
|
|
assert granted.content == _SPEECH_BYTES
|
|
assert [(request.method, request.target) for request in wire.drain()] == [("POST", "/v1/audio/speech")]
|