litellm/tests/integration/caching/test_embedding_file_block_cache.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

216 lines
9.8 KiB
Python

from __future__ import annotations
import asyncio
import json
import threading
import zlib
from collections.abc import Mapping
from hashlib import sha256
from typing import Final
import pytest
from openai import AsyncOpenAI, OpenAI
from openai.types import CreateEmbeddingResponse
from pydantic import JsonValue
from tests.integration._support.client import Gateway, Scenario, eventually, object_value, string_value
from tests.integration._support.database import read_rows
from tests.integration._support.wire import Reply, Request, Wire, wire_server
BACKEND: Final = "gemini-embedding-001"
TARGET: Final = f"/models/{BACKEND}:batchEmbedContents"
METADATA: Final[dict[str, JsonValue]] = {"fps": 1.0, "start_offset": "0s", "end_offset": "3s"}
WIRE_METADATA: Final[dict[str, JsonValue]] = {"fps": 1.0, "startOffset": "0s", "endOffset": "3s"}
TEXT_PART: Final[dict[str, JsonValue]] = {"text": "a red bus"}
SETTLE_SECONDS: Final = 15
SPEND_SQL: Final = (
'SELECT request_id, cache_hit, spend FROM "LiteLLM_SpendLogs" WHERE api_key = %s ORDER BY "startTime", request_id'
)
def clip(name: str) -> str:
return f"gs://scripted-bucket/cache/{name}.mp4"
def block(name: str, metadata: Mapping[str, JsonValue] | None = METADATA) -> dict[str, JsonValue]:
file: Final[dict[str, JsonValue]] = {"file_id": clip(name)}
return {"type": "file", "file": file if metadata is None else {**file, "video_metadata": dict(metadata)}}
def gcs_part(name: str, metadata: Mapping[str, JsonValue] | None = WIRE_METADATA) -> dict[str, JsonValue]:
part: Final[dict[str, JsonValue]] = {"file_data": {"mime_type": "video/mp4", "file_uri": clip(name)}}
return part if metadata is None else {**part, "video_metadata": dict(metadata)}
def list_value(value: JsonValue) -> list[JsonValue]:
assert isinstance(value, list), value
return value
def source_of(part: JsonValue) -> str:
item: Final = object_value(part)
if "text" in item:
return string_value(item["text"])
return string_value(object_value(item["file_data"])["file_uri"])
def vector(parts: list[JsonValue]) -> list[float]:
return [zlib.crc32("|".join(source_of(part) for part in parts).encode()) / 2**32, 0.5]
def request_parts(request: Request) -> list[list[JsonValue]]:
body: Final = object_value(json.loads(request.body))
return [list_value(object_value(object_value(item)["content"])["parts"]) for item in list_value(body["requests"])]
def embed_peer(request: Request) -> Reply:
assert request.method == "POST", request.method
assert request.target.split("?")[0] == TARGET, request.target
embeddings: Final = [{"values": vector(parts)} for parts in request_parts(request)]
return Reply(body=json.dumps({"embeddings": embeddings}).encode())
def wire_parts(wire: Wire) -> list[list[JsonValue]]:
received: Final = wire.drain()
assert len(received) == 1, [item.target for item in received]
return request_parts(received[0])
def embeddings_of(answer: Mapping[str, JsonValue]) -> list[JsonValue]:
data: Final = [object_value(item) for item in list_value(answer["data"])]
assert [item["index"] for item in data] == list(range(len(data))), answer
return [item["embedding"] for item in data]
def gemini_model(scenario: Scenario, url: str) -> str:
return scenario.model(model=f"gemini/{BACKEND}", api_key="scripted-gemini-key", api_base=url)
def embed_body(model: str, elements: JsonValue) -> dict[str, JsonValue]:
return {"model": model, "input": elements}
def spend_rows(key: str) -> list[dict[str, JsonValue]]:
return read_rows(SPEND_SQL, (sha256(key.encode()).hexdigest(),))
def served_from_cache(
gateway: Gateway, wire: Wire, body: Mapping[str, JsonValue], key: str
) -> dict[str, JsonValue] | None:
answer: Final = gateway.post("/v1/embeddings", body, key=key)
return None if wire.drain() else answer
def await_hit(gateway: Gateway, wire: Wire, body: Mapping[str, JsonValue], key: str) -> dict[str, JsonValue]:
served: Final = eventually(
lambda: served_from_cache(gateway, wire, body, key), lambda answer: answer is not None, SETTLE_SECONDS
)
assert served is not None
return served
def proxy_root(gateway: Gateway) -> str:
return str(gateway.client.base_url).rstrip("/")
def test_block_embedding_is_served_from_the_cache_with_a_zero_spend_row(gateway: Gateway) -> None:
with wire_server(embed_peer) as wire, gateway.scenario() as scenario:
model: Final = gemini_model(scenario, wire.url)
key: Final = scenario.key(models=[model])
body: Final = embed_body(model, [block("one")])
first: Final = gateway.request("POST", "/v1/embeddings", body, key=key)
assert first.status_code == 200, first.text
first_id: Final = first.headers["x-litellm-call-id"]
assert wire_parts(wire) == [[gcs_part("one")]]
served: Final = await_hit(gateway, wire, body, key)
assert embeddings_of(served) == embeddings_of(object_value(first.json())) == [vector([gcs_part("one")])]
rows: Final = eventually(
lambda: spend_rows(key), lambda found: any(row["cache_hit"] == "True" for row in found), seconds=70
)
by_id: Final = {string_value(row["request_id"]): row for row in rows}
assert by_id[first_id]["cache_hit"] != "True", rows
hits: Final = [row for row in rows if row["cache_hit"] == "True"]
assert hits and all(float(str(row["spend"])) == 0 for row in hits), rows
def test_cached_block_is_not_resent_when_a_new_text_joins_it(gateway: Gateway) -> None:
with wire_server(embed_peer) as wire, gateway.scenario() as scenario:
model: Final = gemini_model(scenario, wire.url)
key: Final = scenario.key(models=[model])
cached: Final = embed_body(model, [block("one")])
gateway.post("/v1/embeddings", cached, key=key)
assert wire_parts(wire) == [[gcs_part("one")]]
await_hit(gateway, wire, cached, key)
mixed: Final = gateway.post("/v1/embeddings", embed_body(model, [block("one"), "a red bus"]), key=key)
assert wire_parts(wire) == [[TEXT_PART]]
assert embeddings_of(mixed) == [vector([gcs_part("one")]), vector([TEXT_PART])]
@pytest.mark.parametrize("names", (("a", "b", "c", "d", "e"), ("same", "same")), ids=("five-distinct", "two-identical"))
def test_block_lists_are_cached_whole(gateway: Gateway, names: tuple[str, ...]) -> None:
with wire_server(embed_peer) as wire, gateway.scenario() as scenario:
model: Final = gemini_model(scenario, wire.url)
key: Final = scenario.key(models=[model])
body: Final = embed_body(model, [block(name) for name in names])
first: Final = gateway.post("/v1/embeddings", body, key=key)
assert wire_parts(wire) == [[gcs_part(name)] for name in names]
assert embeddings_of(first) == [vector([gcs_part(name)]) for name in names]
assert embeddings_of(await_hit(gateway, wire, body, key)) == embeddings_of(first)
def test_provider_failure_is_not_cached(gateway: Gateway) -> None:
healed: Final = threading.Event()
def respond(request: Request) -> Reply:
if healed.is_set():
return embed_peer(request)
return Reply(status=500, body=b'{"error": {"message": "scripted outage"}}')
with wire_server(respond) as wire, gateway.scenario() as scenario:
model: Final = gemini_model(scenario, wire.url)
key: Final = scenario.key(models=[model])
body: Final = embed_body(model, [block("flaky")])
failed: Final = gateway.request("POST", "/v1/embeddings", body, key=key)
assert failed.status_code >= 500, failed.text
assert wire.drain(), "the failing call never reached the provider"
healed.set()
recovered: Final = gateway.post("/v1/embeddings", body, key=key)
assert wire_parts(wire) == [[gcs_part("flaky")]]
assert embeddings_of(await_hit(gateway, wire, body, key)) == embeddings_of(recovered)
def test_video_metadata_is_part_of_the_cache_key(gateway: Gateway) -> None:
two_seconds: Final[dict[str, JsonValue]] = {**METADATA, "end_offset": "2s"}
with wire_server(embed_peer) as wire, gateway.scenario() as scenario:
model: Final = gemini_model(scenario, wire.url)
key: Final = scenario.key(models=[model])
settled: Final = embed_body(model, [block("keyed")])
gateway.post("/v1/embeddings", settled, key=key)
assert wire_parts(wire) == [[gcs_part("keyed")]]
await_hit(gateway, wire, settled, key)
variants: Final = (
(block("keyed", two_seconds), gcs_part("keyed", {**WIRE_METADATA, "endOffset": "2s"})),
(block("keyed", None), gcs_part("keyed", None)),
)
for variant, part in variants:
gateway.post("/v1/embeddings", embed_body(model, [variant]), key=key)
assert wire_parts(wire) == [[part]]
def test_openai_sdk_clients_fill_and_hit_the_cache(gateway: Gateway) -> None:
with wire_server(embed_peer) as wire, gateway.scenario() as scenario:
model: Final = gemini_model(scenario, wire.url)
key: Final = scenario.key(models=[model])
body: Final = embed_body(model, [block("sdk")])
with OpenAI(base_url=f"{proxy_root(gateway)}/v1", api_key=key, max_retries=0) as client:
filled: Final = client.post("/embeddings", body=body, cast_to=CreateEmbeddingResponse)
assert wire_parts(wire) == [[gcs_part("sdk")]]
await_hit(gateway, wire, body, key)
async def drive() -> CreateEmbeddingResponse:
async with AsyncOpenAI(base_url=f"{proxy_root(gateway)}/v1", api_key=key, max_retries=0) as client:
return await client.post("/embeddings", body=body, cast_to=CreateEmbeddingResponse)
served: Final = asyncio.run(drive())
assert served.data[0].embedding == filled.data[0].embedding == vector([gcs_part("sdk")])
assert wire.drain() == (), "the cached answer reached the provider again"