litellm/tests/integration/_support/wire.py
devin-ai-integration[bot] 56bafc29cf
feat(gemini): accept file content blocks with video_metadata on multimodal embeddings (#45305)
* feat(gemini): accept file content blocks with video_metadata on multimodal embeddings

* test(gemini): annotate the embedding mock helper and keep the type comment short

* fix(gemini-embeddings): keep file blocks through a partial embedding cache hit and validate video_metadata strictly

* test(caching): type the partial-hit cache test and pin its clock

* fix(gemini-embeddings): accept the Files API URI /v1/files returns as a file block's file_id

* fix(gemini-embeddings): 400 on bad input shapes, drop unknown block keys under drop_params, cache multi-block inputs

A bare object `input` answered 500 from both the caching handler and the
transformation; both now answer 400. An unresolved `files/` reference on
vertex_ai/ answered a ValueError 500; it now answers 400 naming the gemini/
provider. An empty `format` passed through to the provider; it now answers 400
naming file.format. Unknown block keys (`detail`, an unknown video_metadata
key, a top-level block key) are dropped under drop_params, global or
per-request, and still answer 400 without it. The embedding cache counted
file blocks against `max_messages`, so a request with 5 or more blocks was
never cached; file blocks no longer count.

* fix(caching): answer 400 for an object embedding input on the cache lookup

* test(gemini-embeddings): annotate the new embedding tests with return types and Final

* test(gemini-embeddings): annotate the transformation and files batch tests with return types and Final

* test(gemini-embeddings): audit file content blocks on the wire, in the cache, in batch uploads and under chaos

Integration cells for gemini/ and vertex_ai/ embedding file blocks with video_metadata: the batchEmbedContents and embedContent wire shapes, format overrides, nested and repeated inputs, every malformed block answering 400 before any provider call, drop_params at the request, deployment and YAML levels, the OpenAI SDK sync and async clients, Redis cache fills, hits, partial hits and the metadata in the key with zero-spend hit rows, chat, responses and messages controls, the vertex_ai/ batch JSONL upload, a mixed fast/slow/malformed/dropped burst and a worker kill. The wire peer now reads chunked request bodies, which the streamed GCS media upload sends

* test(integration): treat an upload the client abandons before its terminating chunk as a disconnect, never a stored request

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-08 13:20:03 -07:00

205 lines
7.1 KiB
Python

from __future__ import annotations
import ssl
import threading
import time
from collections.abc import Callable, Generator, Iterator, Mapping
from contextlib import contextmanager
from dataclasses import dataclass
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from queue import SimpleQueue
from types import MappingProxyType
from typing import BinaryIO, Final
@dataclass(frozen=True, slots=True)
class Request:
method: str
target: str
headers: Mapping[str, str]
body: bytes
class AbortedBody(Exception):
"""The client closed the connection before the body it announced was complete."""
def exactly(stream: BinaryIO, size: int) -> bytes:
data: Final = stream.read(size)
if len(data) < size:
raise AbortedBody
return data
def chunked_body(stream: BinaryIO) -> Iterator[bytes]:
while True:
size_line: Final = stream.readline()
if not size_line:
raise AbortedBody
size: Final = int(size_line.split(b";")[0].strip(), 16)
if size == 0:
while stream.readline().strip():
pass
return
yield exactly(stream, size)
stream.readline()
def read_body(headers: Mapping[str, str], stream: BinaryIO) -> bytes:
if headers.get("transfer-encoding", "").lower() == "chunked":
return b"".join(chunked_body(stream))
return exactly(stream, int(headers.get("content-length", "0")))
@dataclass(frozen=True, slots=True)
class Reply:
status: int = 200
body: bytes = b"{}"
content_type: str = "application/json"
chunks: tuple[bytes, ...] | None = None
abort_after: int | None = None
gate_after_first: threading.Event | None = None
pause_between_chunks: float = 0
headers: Mapping[str, str] = MappingProxyType({})
drop_connection: bool = False
@dataclass(frozen=True, slots=True)
class Wire:
url: str
received: SimpleQueue[Request]
disconnected: SimpleQueue[str]
connected: SimpleQueue[str]
def drain(self) -> tuple[Request, ...]:
return tuple(self.received.get_nowait() for _ in range(self.received.qsize()))
def connections(self) -> int:
return self.connected.qsize()
def _await_release(gate: threading.Event, closing: threading.Event) -> bool:
while not closing.is_set():
if gate.wait(timeout=0.05):
return True
return gate.is_set()
@contextmanager
def wire_server(
respond: Callable[[Request], Reply],
tls: ssl.SSLContext | None = None,
port: int = 0,
keep_alive: bool = False,
) -> Generator[Wire, None, None]:
"""Owned TCP peer; requests traverse the real HTTP client and serialization. With `keep_alive` the
peer honours HTTP/1.1 persistent connections so `connections()` counts the client's TCP sessions."""
received: Final[SimpleQueue[Request]] = SimpleQueue()
errors: Final[SimpleQueue[Exception]] = SimpleQueue()
disconnected: Final[SimpleQueue[str]] = SimpleQueue()
connected: Final[SimpleQueue[str]] = SimpleQueue()
closing: Final = threading.Event()
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
timeout = 5
def setup(self) -> None:
super().setup()
connected.put(f"{self.client_address[0]}:{self.client_address[1]}")
def respond(self) -> None:
headers: Final = {name.lower(): value for name, value in self.headers.items()}
try:
body: Final = read_body(headers, self.rfile)
except AbortedBody:
self.close_connection = True
disconnected.put(self.path)
return
request: Final = Request(self.command, self.path, headers, body)
received.put(request)
try:
reply = respond(request)
except Exception as error:
errors.put(error)
reply = Reply(status=500)
if reply.drop_connection:
self.close_connection = True
disconnected.put(request.target)
return
self.send_response(reply.status)
self.send_header("content-type", reply.content_type)
for name, value in reply.headers.items():
self.send_header(name, value)
if reply.chunks is None:
self.send_header("content-length", str(len(reply.body)))
else:
self.send_header("transfer-encoding", "chunked")
if not keep_alive:
self.send_header("connection", "close")
self.end_headers()
try:
if self.command == "HEAD":
self.wfile.flush()
elif reply.chunks is None:
self.wfile.write(reply.body)
else:
for index, chunk in enumerate(reply.chunks):
if reply.abort_after == index:
break
self.wfile.write(b"%x\r\n%s\r\n" % (len(chunk), chunk))
self.wfile.flush()
if (
index == 0
and reply.gate_after_first is not None
and not _await_release(reply.gate_after_first, closing)
):
break
if reply.pause_between_chunks and index + 1 < len(reply.chunks):
time.sleep(reply.pause_between_chunks)
else:
self.wfile.write(b"0\r\n\r\n")
self.wfile.flush()
except (BrokenPipeError, ConnectionResetError):
disconnected.put(request.target)
except Exception as error:
errors.put(error)
self.close_connection = not keep_alive
do_POST = respond
do_PUT = respond
do_GET = respond
do_DELETE = respond
do_PATCH = respond
do_HEAD = respond
def log_message(self, format: str, *args: object) -> None:
pass
class OwnedHTTPServer(ThreadingHTTPServer):
daemon_threads = False
request_queue_size = 128
def server_bind(self) -> None:
super().server_bind()
if tls is not None:
self.socket = tls.wrap_socket(self.socket, server_side=True)
with OwnedHTTPServer(("127.0.0.1", port), Handler) as server:
thread: Final = threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.05})
thread.start()
try:
yield Wire(
f"{'https' if tls is not None else 'http'}://127.0.0.1:{server.server_port}",
received,
disconnected,
connected,
)
finally:
closing.set()
server.shutdown()
thread.join(timeout=6)
assert not thread.is_alive(), "Owned HTTP server survived cleanup"
server.server_close()
failure: Final = None if errors.empty() else errors.get_nowait()
assert failure is None, f"Owned HTTP peer failed: {failure!r}"