Skip to content

test(e2e): batch/file API e2e suite - #31369

Open
mubashir1osmani wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_e2e_batches_perkeep
Open

test(e2e): batch/file API e2e suite#31369
mubashir1osmani wants to merge 1 commit into
litellm_internal_stagingfrom
litellm_e2e_batches_perkeep

Conversation

@mubashir1osmani

@mubashir1osmani mubashir1osmani commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Regression coverage for the managed-object poll OOM (#23472) and the batch-upload transform OOM (LIT-3382)

Linear ticket

LIT-3382

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Type

Test

Changes

A black-box e2e suite under tests/e2e/batches/ for the batches and files API. Nothing is imported from the litellm codebase; the tests drive the live proxy over HTTP and verify state through the generated prisma client, so they catch real regressions rather than re-asserting internal calls.

Coverage is the gemini managed-files upload, verified by reading the file back ACTIVE from the provider's own API; a managed-object poll-cap guard for the #23472 OOM that seeds more than one page of rows into real Postgres and confirms from the proxy logs that each poll cycle caps at exactly one page (drop the take and the newest rows show up, failing the assert); and a vertex streaming-upload memory guard for the LIT-3382 OOM, gated behind an env flag for a deployment with real memory headroom. The pure helpers have unit coverage, and the memory sampler now raises instead of passing vacuously when the cgroup read is unavailable (a -1 baseline would otherwise report zero growth and hide the regression).

Screenshots / Proof of Fix

gemini managed batch-file upload through the live proxy, then read back ACTIVE from the provider:

$ curl -s -X POST http://localhost:4000/v1/files -H "Authorization: Bearer sk-1234" \
    -F purpose=batch -F target_model_names=gemini-2.5-flash -F file=@batch.jsonl
{ "id": "bGl0ZWxsbV9wcm94eTphcHBsaWNhdGlvbi9qc29ubDt1bmlmaWVkX2lkL...",
  "status": "uploaded", "purpose": "batch", "object": "file" }

# decode the unified id -> the provider file URI, then read it back from Google:
$ curl -s "https://generativelanguage.googleapis.com/v1beta/files/<id>?key=$GEMINI_API_KEY"
{ "name": "files/...", "state": "ACTIVE", "sizeBytes": "354", "mimeType": "application/jsonl" }

@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a black-box e2e suite under tests/e2e/batches/ covering the batches/files API lifecycle, a perkeep OpenAI-files adapter for end-to-end storage tests, and regression guards for the batch-upload OOM (LIT-3382) and the unbounded managed-object poll (#23472).

  • tests/e2e/batches/: New test suite covering file create/retrieve/content/delete via perkeep, Gemini managed-file upload, the create_batch unsupported-provider error path, poll-cap regression (seeds Postgres rows directly and reads proxy logs), and a gated ~1 GB upload memory test; backed by batch_client.py, memory.py, and poll_cap.py helpers.
  • tests/e2e/perkeep_openai/: New Starlette adapter that translates OpenAI /v1/files into perkeep blob protocol via pk-put/pk-get; uploads stream to disk but the file_content download path buffers the full blob in memory.
  • tests/e2e/docker-compose.yml / gateway/litellm-config.yml: Adds perkeep and perkeep-openai services, perkeep-batch deployment, and shortens proxy_batch_polling_interval for observable poll cycles within test timeouts.

Confidence Score: 4/5

All changes are confined to tests/e2e/ and touch no production code; the proxy, SDK, and existing tests are unaffected.

The memory sampler in memory.py returns -1 when it cannot read the cgroup, causing growth_bytes to resolve to 0 and the ratio assertion to pass unconditionally. The test designed to catch the LIT-3382 OOM buffering regression would give a green result even on a fully broken proxy as long as the cgroup is unavailable. The rest of the suite is well-constructed.

tests/e2e/batches/memory.py and tests/e2e/batches/test_batch_upload_memory_e2e.py — the memory sampler needs an early failure when the cgroup read returns -1 so the OOM guard does not silently pass when the sampler is non-functional.

Important Files Changed

Filename Overview
tests/e2e/batches/memory.py DockerCgroupSampler silently passes the memory assertion when _read_bytes() consistently returns -1 (cgroup v2 unavailable or docker exec fails), masking the LIT-3382 regression.
tests/e2e/batches/test_batch_upload_memory_e2e.py When the DockerCgroupSampler fails to read cgroup data, growth_bytes=0 and the ratio assert vacuously passes — the OOM regression the test is meant to detect goes unnoticed.
tests/e2e/perkeep_openai/app.py Upload path correctly streams to disk, but file_content uses subprocess.run(capture_output=True) which buffers the entire file in memory — contradicts the module docstring's streaming claim.
tests/e2e/batches/batch_client.py Clean HTTP client wrapping the proxy's file/batch endpoints. parse_unified_file_id correctly decodes the proxy's base64 id and enforces the managed-id contract.
tests/e2e/batches/poll_cap.py ManagedObjectSeeder/ProxyLog helpers look correct; reset() intentionally clears all pollcap- rows for cross-run hygiene.
tests/e2e/batches/test_managed_poll_cap_e2e.py Black-box poll-cap regression test. Seeds rows directly in Postgres and observes proxy logs; uses pytest.skip when poll doesn't run in time rather than failing.
tests/e2e/batches/test_batch_lifecycle_e2e.py Full file lifecycle plus create_batch error-path pin. Clean and straightforward.
tests/e2e/batches/test_batch_large_upload_e2e.py ~1GB upload-to-perkeep test gated behind E2E_LARGE_BATCH_UPLOAD=1. Checks blob-store growth and memory ratio correctly when the sampler works.
tests/e2e/docker-compose.yml New docker-compose adding perkeep and perkeep-openai services. LITELLM_LICENSE sourced from gitignored .env, not hardcoded.

Reviews (1): Last reviewed commit: "test(e2e): comprehensive batch/file API ..." | Re-trigger Greptile

Comment on lines +66 to +86
def measure(self, during: Callable[[], T]) -> tuple[T, PeakMemory]:
baseline = self._read_bytes()
peak = baseline
stop = threading.Event()

def sample() -> None:
nonlocal peak
while not stop.is_set():
current = self._read_bytes()
if current > peak:
peak = current
time.sleep(self.interval_seconds)

sampler_thread = threading.Thread(target=sample)
sampler_thread.start()
try:
result = during()
finally:
stop.set()
sampler_thread.join()
return result, PeakMemory(baseline_bytes=baseline, peak_bytes=peak)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Sampler failure silently passes the OOM assertion

When _read_bytes() can't find the anon field (cgroup v2 unavailable, docker exec fails, or the kernel emits a different layout), it returns -1. Both baseline and peak are then -1, so growth_bytes = max(0, -1 - (-1)) = 0 and ratio = 0 / file_size = 0.0 < MAX_GROWTH_RATIO. The test passes as if the proxy used zero memory — the LIT-3382 buffering regression would go completely undetected.

A guard is needed at the top of measure() that raises RuntimeError when baseline < 0, ensuring a cgroup-unavailable environment fails loudly instead of vacuously passing.

Comment thread tests/e2e/perkeep_openai/app.py Outdated
Comment on lines +133 to +145
async def file_content(request: Request) -> Response:
file_id = request.path_params["file_id"]
if file_id not in _FILES:
return JSONResponse({"error": {"message": "file not found"}}, status_code=404)
got = subprocess.run(
["pk-get", "-contents", file_id], capture_output=True, timeout=7200
)
if got.returncode != 0:
return JSONResponse(
{"error": {"message": f"perkeep read failed: {got.stderr[:400]!r}"}},
status_code=502,
)
return Response(content=got.stdout, media_type="application/octet-stream")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Download path buffers entire file in memory, contradicting the module docstring

The module docstring states "a gigabyte upload can't OOM the adapter" because uploads stream to a spool file. However, file_content uses subprocess.run(capture_output=True), which collects all of pk-get's stdout into memory before sending it. A 1 GB file retrieved through this endpoint would allocate ~1 GB in the adapter process. The current test suite calls file_content only on tiny payloads, so it doesn't trigger in practice — but the comment creates a false expectation of safety for large files. Consider using subprocess.Popen with a streaming StreamingResponse, or at minimum update the docstring to note that downloads are not streaming.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Adds a black-box e2e suite under tests/e2e/batches/ for the batches and files
API. Nothing is imported from the litellm codebase; the tests drive the live
proxy over HTTP and verify state through the generated prisma client, so they
catch real regressions rather than re-asserting internal calls.

It covers the gemini managed-files upload, verified by reading the file back
ACTIVE from the provider; a managed-object poll-cap guard for the #23472 OOM
that seeds more than one page of rows into real Postgres and watches the poll
cycle in the proxy logs; and a vertex streaming-upload memory guard for the
LIT-3382 OOM (gated, for an environment with real memory headroom). The pure
helpers have unit coverage, and the memory sampler raises rather than passing
vacuously when the cgroup read is unavailable
@mubashir1osmani
mubashir1osmani force-pushed the litellm_e2e_batches_perkeep branch from ea17dd9 to ae5e7ab Compare June 26, 2026 00:03
@mubashir1osmani mubashir1osmani changed the title test(e2e): comprehensive batch/file API suite over a perkeep upstream test(e2e): batch/file API e2e suite Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant