mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
* 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>
147 lines
4.7 KiB
Python
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)
|