litellm/tests/integration/_support/riva.py
quietjules ca24b396da
test(nvidia_riva): audit #41021 (#48801)
* test(nvidia_riva): audit #41021

Adds an owned Riva ASR gRPC server and provider integration cells for the riva_offline request-body ban, the offline and streaming paths, the per-deployment caller opt-in, and the 413 and 408 error mappings

* test(nvidia_riva): check word timing requests and audio samples in the Riva double

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-28 05:24:08 -07:00

143 lines
5.1 KiB
Python

from __future__ import annotations
import io
import threading
import wave
from array import array
from collections.abc import Iterable, Iterator
from concurrent import futures
from contextlib import contextmanager
from dataclasses import dataclass
from enum import Enum
from queue import SimpleQueue
from typing import Final
import grpc
from riva.client.proto import riva_asr_pb2, riva_asr_pb2_grpc
SAMPLE_RATE_HZ: Final = 16000
OFFLINE_ONLY_REFUSAL: Final = (
"Error: Unavailable model requested given these parameters: language_code=en-US; sample_rate=16000; type=online;"
)
_STALL_LIMIT_SECONDS: Final = 120
class Rpc(Enum):
RECOGNIZE = "Recognize"
STREAMING = "StreamingRecognize"
@dataclass(frozen=True, slots=True)
class Word:
word: str
start_ms: int
end_ms: int
@dataclass(frozen=True, slots=True)
class RivaCall:
rpc: Rpc
audio: bytes
cancelled: bool
@dataclass(frozen=True, slots=True)
class RivaServer:
address: str
calls: SimpleQueue[RivaCall]
def drain(self) -> tuple[RivaCall, ...]:
return tuple(self.calls.get_nowait() for _ in range(self.calls.qsize()))
def _clip_samples(seconds: float) -> array[int]:
return array("h", (((index * 37) % 2001 - 1000) * 16 for index in range(int(seconds * SAMPLE_RATE_HZ))))
def pcm_wav(seconds: float) -> bytes:
buffer: Final = io.BytesIO()
with wave.open(buffer, "wb") as writer:
writer.setnchannels(1)
writer.setsampwidth(2)
writer.setframerate(SAMPLE_RATE_HZ)
writer.writeframes(_clip_samples(seconds).tobytes())
return buffer.getvalue()
def pcm_bytes(seconds: float) -> int:
return int(seconds * SAMPLE_RATE_HZ) * 2
def carries_clip(audio: bytes, seconds: float) -> bool:
sent: Final = _clip_samples(seconds)
if len(audio) != len(sent) * 2:
return False
received: Final = array("h", audio)
return all(abs(left - right) <= 1 for left, right in zip(sent, received, strict=True))
class _Asr(riva_asr_pb2_grpc.RivaSpeechRecognitionServicer):
def __init__(self, words: tuple[Word, ...], offline_only: bool, stall: bool, calls: SimpleQueue[RivaCall]) -> None:
self._words: Final = words
self._offline_only: Final = offline_only
self._stall: Final = stall
self._calls: Final = calls
def _alternative(self, config: riva_asr_pb2.RecognitionConfig) -> riva_asr_pb2.SpeechRecognitionAlternative:
timed: Final = self._words if config.enable_word_time_offsets else ()
return riva_asr_pb2.SpeechRecognitionAlternative(
transcript=" ".join(word.word for word in self._words),
confidence=0.9,
words=[
riva_asr_pb2.WordInfo(word=word.word, start_time=word.start_ms, end_time=word.end_ms) for word in timed
],
)
def Recognize(
self, request: riva_asr_pb2.RecognizeRequest, context: grpc.ServicerContext
) -> riva_asr_pb2.RecognizeResponse:
if self._stall:
released: Final = threading.Event()
context.add_callback(released.set)
released.wait(timeout=_STALL_LIMIT_SECONDS)
self._calls.put(RivaCall(Rpc.RECOGNIZE, request.audio, cancelled=not context.is_active()))
return riva_asr_pb2.RecognizeResponse()
self._calls.put(RivaCall(Rpc.RECOGNIZE, request.audio, cancelled=False))
return riva_asr_pb2.RecognizeResponse(
results=[riva_asr_pb2.SpeechRecognitionResult(alternatives=[self._alternative(request.config)])]
)
def StreamingRecognize(
self, request_iterator: Iterable[riva_asr_pb2.StreamingRecognizeRequest], context: grpc.ServicerContext
) -> Iterator[riva_asr_pb2.StreamingRecognizeResponse]:
if self._offline_only:
self._calls.put(RivaCall(Rpc.STREAMING, b"", cancelled=False))
context.abort(grpc.StatusCode.INVALID_ARGUMENT, OFFLINE_ONLY_REFUSAL)
requests: Final = tuple(request_iterator)
config: Final = requests[0].streaming_config.config if requests else riva_asr_pb2.RecognitionConfig()
self._calls.put(
RivaCall(Rpc.STREAMING, b"".join(request.audio_content for request in requests), cancelled=False)
)
yield riva_asr_pb2.StreamingRecognizeResponse(
results=[riva_asr_pb2.StreamingRecognitionResult(alternatives=[self._alternative(config)], is_final=True)]
)
@contextmanager
def riva_server(
words: tuple[Word, ...],
*,
offline_only: bool = False,
stall: bool = False,
max_receive_bytes: int | None = None,
) -> Iterator[RivaServer]:
calls: Final[SimpleQueue[RivaCall]] = SimpleQueue()
options: Final = () if max_receive_bytes is None else (("grpc.max_receive_message_length", max_receive_bytes),)
server: Final = grpc.server(futures.ThreadPoolExecutor(max_workers=8), options=options)
riva_asr_pb2_grpc.add_RivaSpeechRecognitionServicer_to_server(_Asr(words, offline_only, stall, calls), server)
port: Final = server.add_insecure_port("127.0.0.1:0")
server.start()
try:
yield RivaServer(f"127.0.0.1:{port}", calls)
finally:
server.stop(grace=None).wait()