litellm/tests/integration/authorization/test_audio_model_access.py
devin-ai-integration[bot] 8a2f706ab7
test(integration): cover audio endpoint contracts (#44699)
* 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>
2026-10-14 03:41:20 +00:00

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