litellm/tests/e2e/batches/capabilities.py
Sameer Kankute a16d9c6f9e
test(e2e): add live batches suite across providers and routing scenarios (#30958)
* tests: add e2e tests for spend, budgets and llms

* style: make chained comparison of status_code clearer

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* remove e2e_tests folder

* test: add spend tracking tests

* fix: p0 issues, added types and shared functions for each test suite

* style: carry clearer status_code comparison into renamed e2e dir

* refactor: migrate to gateway client

* fix: add new tests, split gateway

* test(e2e): add live batches suite across providers and routing scenarios

* test(batches): cover real cost tracking on completed batch retrieve

* test(e2e): assert managed vs raw file and batch id shapes per routing scenario

* test(e2e): assert full response shape of each batches and files endpoint

* test(e2e): only accept transitional statuses for a freshly created batch

* test(prompt-factory): make test_convert_url deterministic with a data URL

picsum.photos is down (HTTP 522), so test_convert_url failed on every
run. Swap the live external image for an inline data: URL and assert the
round-trip through convert_url_to_base64 genuinely.

A data URL is already inline base64 image data, so convert_url_to_base64
now short-circuits it instead of attempting an impossible HTTP fetch;
add a regression for that branch in the mapped image_handling test

* fix: pass through async image data urls

* fix(image-handling): short-circuit data URLs in async path too

Bugbot flagged that convert_url_to_base64 returns data: base64 URLs
unchanged but async_convert_url_to_base64 still tried to fetch them,
so async OCR flows (Bedrock, Azure) would reject inline images the sync
path accepts. Add the same guard to the async function and a regression
test that asserts the async path returns the data URL without touching
the HTTP client

* Fix: openai batches lifecycle

* Fix: add e2e azure openai tests

* Fix e2e for vertex ai

* Add all models for testing

* test(managed-files): assert idempotent upsert in store_unified_file_id

store_unified_file_id switched from create to upsert to avoid
UniqueViolationError when re-storing the same unified_file_id (e.g.
batch output files stored before metadata is available). Update the
unit test to assert the upsert call and its create payload instead of
the removed create call.

* test(batches): reconcile vertex_ai native batch-id comment with fallback guard

* fix(test-config): keep rust-ocr models in model_list by moving files_settings after it

* fix(test-config): move batch models after OCR block to keep merge with internal_staging clean

* fix(batches): use '24hrs' completion window and allow managed-files listing with provider filter

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* style: ruff format transformation.py and endpoints.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(e2e/batches): set Azure raw_model to gpt-4.1-mini-batch to match deployed model

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(vertex-ai/batches): correct completion_window to 24h per Literal type definition

* test(vertex-ai/batches): align completion_window assertion to 24h

* fix: update managed file metadata on upsert

---------

Co-authored-by: mubashir1osmani <mubashir.osmani777@gmail.com>
Co-authored-by: Mateo Wang <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 08:05:23 -07:00

147 lines
4.7 KiB
Python

"""The declarative provider x routing-scenario matrix the lifecycle test runs.
One Capability per supported (provider, scenario) pair, so the parametrized test
has no dead/skipped cells. `provider` is litellm's custom_llm_provider, used to
route provider-fallback calls to /{provider}/v1/... and to assert the raw batch id
shape (the only scenario whose id is not re-encoded by the proxy). Operations that
a provider does not support (Bedrock: no cancel, no list) are gated per row.
"""
from __future__ import annotations
import base64
from dataclasses import dataclass
from typing import Literal
Scenario = Literal["encoded", "unified", "model_param", "provider_fallback"]
IdShape = Literal["managed", "model_encoded", "raw"]
SCENARIOS: tuple[Scenario, ...] = (
"encoded",
"unified",
"model_param",
"provider_fallback",
)
@dataclass(frozen=True, slots=True)
class Provider:
name: str
model: str
raw_model: str
can_cancel: bool
can_list: bool
@dataclass(frozen=True, slots=True)
class Capability:
provider: str
model: str
raw_model: str
scenario: Scenario
can_cancel: bool
can_list: bool
@property
def id(self) -> str:
return f"{self.provider}-{self.scenario}"
@property
def jsonl_model(self) -> str:
"""Model name embedded in the uploaded JSONL ``body.model``.
Only the unified upload path rewrites JSONL on upload
(``target_model_names`` → ``llm_router.acreate_file`` →
``replace_model_in_jsonl``), so that scenario can use the LiteLLM alias
and rely on the proxy to swap it to the deployment model. Every other
scenario uploads raw JSONL with no rewrite, so the provider's real
deployment name is required or create fails upstream validation."""
return self.model if self.scenario == "unified" else self.raw_model
PROVIDERS: tuple[Provider, ...] = (
Provider("openai", "openai-batch", "gpt-4o-mini", can_cancel=True, can_list=True),
Provider("azure", "azure-batch", "gpt-4.1-mini-batch", can_cancel=True, can_list=True),
Provider(
"vertex_ai", "vertex-batch", "gemini-2.5-flash", can_cancel=True, can_list=True
),
# Provider(
# "bedrock",
# "bedrock-batch",
# "us.anthropic.claude-haiku-4-5-20251001-v1:0",
# can_cancel=False,
# can_list=False,
# ),
)
CAPABILITIES: tuple[Capability, ...] = tuple(
Capability(p.name, p.model, p.raw_model, scenario, p.can_cancel, p.can_list)
for p in PROVIDERS
for scenario in SCENARIOS
)
def raw_id_matches_provider(provider: str, batch_id: str) -> bool:
"""The provider-fallback path returns the provider's native batch id (unencoded),
so its shape discriminates which provider actually handled the batch."""
if provider in ("openai", "azure"):
return batch_id.startswith("batch")
if provider == "vertex_ai":
# Vertex returns the batch prediction job id, which depending on the
# routing path arrives either as the full resource name
# (projects/.../batchPredictionJobs/<id>) or as just the trailing
# numeric id, so accept either form.
return (
batch_id.startswith("projects/")
or "batchPredictionJobs" in batch_id
or batch_id.isdigit()
)
if provider == "bedrock":
return batch_id.startswith("arn:aws")
return True
FILE_ID_SHAPE: dict[Scenario, IdShape] = {
"encoded": "model_encoded",
"unified": "managed",
"model_param": "raw",
"provider_fallback": "raw",
}
BATCH_ID_SHAPE: dict[Scenario, IdShape] = {
"encoded": "model_encoded",
"unified": "managed",
"model_param": "model_encoded",
"provider_fallback": "raw",
}
def _b64_decode(value: str) -> str:
padded = value + "=" * (-len(value) % 4)
try:
return base64.urlsafe_b64decode(padded).decode()
except Exception:
return ""
def is_managed_id(id_str: str) -> bool:
"""A litellm managed unified file/batch id base64-decodes to a litellm_proxy marker."""
return _b64_decode(id_str).startswith("litellm_proxy")
def is_model_encoded_id(id_str: str) -> bool:
"""A model-encoded id keeps the provider prefix and base64-encodes litellm:<id>;model,<m>."""
for prefix in ("file-", "batch_"):
if id_str.startswith(prefix):
decoded = _b64_decode(id_str[len(prefix) :])
return decoded.startswith("litellm:") and ";model," in decoded
return False
def matches_id_shape(shape: IdShape, id_str: str) -> bool:
if shape == "managed":
return is_managed_id(id_str)
if shape == "model_encoded":
return is_model_encoded_id(id_str)
return not is_managed_id(id_str) and not is_model_encoded_id(id_str)