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>
165 lines
4.7 KiB
Python
165 lines
4.7 KiB
Python
"""Client for the batches e2e suite: file upload/download and the batch
|
|
operations (create / retrieve / cancel / list) over the shared Gateway.
|
|
|
|
`create_batch` returns the raw HTTP outcome (StreamingResponse) so a 403 model
|
|
access denial and a provider-native batch body both surface; the test parses
|
|
BatchObject from the body. A `provider` arg routes a call to /{provider}/v1/...,
|
|
which the provider-fallback scenario needs (its ids are raw, not model-encoded).
|
|
The request/response models are co-located here because only this suite uses them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from e2e_gateway import Gateway, build_gateway
|
|
from e2e_http import (
|
|
FileUploadForm,
|
|
NoBody,
|
|
Result,
|
|
StreamingResponse,
|
|
UnknownApiError,
|
|
)
|
|
|
|
|
|
class FileObject(BaseModel):
|
|
id: str
|
|
object: str | None = None
|
|
purpose: str | None = None
|
|
bytes: int | None = None
|
|
status: str | None = None
|
|
created_at: int | None = None
|
|
|
|
|
|
class BatchObject(BaseModel):
|
|
id: str
|
|
object: str | None = None
|
|
status: str
|
|
endpoint: str | None = None
|
|
input_file_id: str | None = None
|
|
output_file_id: str | None = None
|
|
completion_window: str | None = None
|
|
created_at: int | None = None
|
|
model: str | None = None
|
|
|
|
|
|
class BatchList(BaseModel):
|
|
object: str | None = None
|
|
data: list[BatchObject] = []
|
|
|
|
|
|
class FileDeleteResponse(BaseModel):
|
|
id: str
|
|
object: str | None = None
|
|
deleted: bool
|
|
|
|
|
|
class BatchCreateBody(BaseModel):
|
|
input_file_id: str
|
|
endpoint: str = "/v1/chat/completions"
|
|
completion_window: str = "24h"
|
|
model: str | None = None
|
|
|
|
|
|
class ModelQuery(BaseModel):
|
|
model: str | None = None
|
|
|
|
|
|
def is_model_access_denied(resp: StreamingResponse) -> bool:
|
|
"""True if the proxy rejected the call because the key may not access the model."""
|
|
return resp.status_code == 403 and "key_model_access_denied" in resp.body
|
|
|
|
|
|
def is_result_access_denied[R: BaseModel](result: Result[R]) -> bool:
|
|
match result:
|
|
case UnknownApiError(status_code=403, body=body):
|
|
return "key_model_access_denied" in body
|
|
case _:
|
|
return False
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class BatchClient:
|
|
gateway: Gateway
|
|
|
|
def upload_file(
|
|
self,
|
|
*,
|
|
content: bytes,
|
|
form: FileUploadForm,
|
|
key: str,
|
|
model: str | None = None,
|
|
provider: str | None = None,
|
|
) -> Result[FileObject]:
|
|
return self.gateway.transport.upload(
|
|
_files_path(provider),
|
|
headers=self.gateway.transport.bearer(key),
|
|
form=form,
|
|
filename="batch_input.jsonl",
|
|
content=content,
|
|
params=ModelQuery(model=model),
|
|
response_type=FileObject,
|
|
)
|
|
|
|
def create_batch(
|
|
self, *, body: BatchCreateBody, key: str, provider: str | None = None
|
|
) -> StreamingResponse:
|
|
return self.gateway.transport.send(
|
|
_batches_path(provider),
|
|
headers=self.gateway.transport.bearer(key),
|
|
json=body,
|
|
)
|
|
|
|
def retrieve_batch(
|
|
self, batch_id: str, *, key: str, provider: str | None = None
|
|
) -> Result[BatchObject]:
|
|
return self.gateway.transport.get(
|
|
f"{_batches_path(provider)}/{batch_id}",
|
|
headers=self.gateway.transport.bearer(key),
|
|
params=NoBody(),
|
|
response_type=BatchObject,
|
|
)
|
|
|
|
def cancel_batch(
|
|
self, batch_id: str, *, key: str, provider: str | None = None
|
|
) -> Result[BatchObject]:
|
|
return self.gateway.transport.post(
|
|
f"{_batches_path(provider)}/{batch_id}/cancel",
|
|
headers=self.gateway.transport.bearer(key),
|
|
json=NoBody(),
|
|
response_type=BatchObject,
|
|
)
|
|
|
|
def list_batches(
|
|
self, *, key: str, provider: str | None = None
|
|
) -> Result[BatchList]:
|
|
return self.gateway.transport.get(
|
|
_batches_path(provider),
|
|
headers=self.gateway.transport.bearer(key),
|
|
params=NoBody(),
|
|
response_type=BatchList,
|
|
)
|
|
|
|
def delete_file(
|
|
self, file_id: str, *, key: str, provider: str | None = None
|
|
) -> Result[FileDeleteResponse]:
|
|
return self.gateway.transport.delete(
|
|
f"{_files_path(provider)}/{file_id}",
|
|
headers=self.gateway.transport.bearer(key),
|
|
json=NoBody(),
|
|
response_type=FileDeleteResponse,
|
|
)
|
|
|
|
|
|
def _files_path(provider: str | None) -> str:
|
|
return f"/{provider}/v1/files" if provider else "/v1/files"
|
|
|
|
|
|
def _batches_path(provider: str | None) -> str:
|
|
return f"/{provider}/v1/batches" if provider else "/v1/batches"
|
|
|
|
|
|
def build_client() -> BatchClient:
|
|
return BatchClient(gateway=build_gateway())
|