Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
27f1d33
Downgrade resource_class on over-provisioned CI jobs
yuneng-jiang Mar 15, 2026
eb7222c
[Infra] Reduce CI no_output_timeout and tighten Playwright timeouts
yuneng-jiang Mar 15, 2026
95b6821
Speed up ui_unit_tests CI job (~20min -> ~5-7min)
yuneng-jiang Mar 15, 2026
45ff66a
Add ui_unit_tests as requirement for publish_to_pypi
yuneng-jiang Mar 15, 2026
e38ee6a
Optimize ui_build job and gate ui_unit_tests behind it
yuneng-jiang Mar 15, 2026
6147682
Switch test_bad_database_url from Dockerfile.non_root to Dockerfile.d…
yuneng-jiang Mar 15, 2026
cc80771
Cache Next.js build cache for ui_build job
yuneng-jiang Mar 15, 2026
5091375
Consolidate duplicate Docker image builds to reuse pre-built image
yuneng-jiang Mar 15, 2026
f838bea
Optimize CI: parallelize router and guardrails test jobs, fix test is…
yuneng-jiang Mar 15, 2026
f73ff72
Fix router test isolation: update deprecated model, remove shared Red…
yuneng-jiang Mar 15, 2026
bcd887e
Fix test_async_fallbacks_streaming to use mock_response instead of re…
yuneng-jiang Mar 15, 2026
023654d
Fix flaky CI tests: mock timeout race, update deprecated model, fix c…
yuneng-jiang Mar 15, 2026
568726b
Fix test_aaarouter_dynamic_cooldown_message_retry_time isolation issue
yuneng-jiang Mar 15, 2026
968d7a3
Fix test isolation: save/restore pre_call_rules and post_call_rules
yuneng-jiang Mar 15, 2026
717d37c
Fix flaky CI: update deprecated model, filter leaked async task logs
yuneng-jiang Mar 15, 2026
ed1320e
Fix test_completion_sagemaker_messages_api retry flakiness
yuneng-jiang Mar 15, 2026
82d3b23
Update deprecated Together AI model in test_completion_together_ai_llama
yuneng-jiang Mar 15, 2026
2e27e32
Downsize remaining xlarge CI jobs to match actual workload
yuneng-jiang Mar 15, 2026
87e3e84
Set semgrep job to medium resource class explicitly
yuneng-jiang Mar 15, 2026
31e6393
Fix flaky proxy_e2e_azure_batches_tests: populate _hidden_params for …
yuneng-jiang Mar 15, 2026
67e905f
Fix flaky encrypted_content_affinity tests: clear HTTP client cache, …
yuneng-jiang Mar 15, 2026
ff869e9
Fix flaky caching tests: use mock_response, add parallelism, remove f…
yuneng-jiang Mar 15, 2026
a75964f
Fix batch retrieve double-encoding: resolve output_file_id via DB loo…
yuneng-jiang Mar 15, 2026
a66b635
Upsize ui_build and ui_unit_tests CI machines for faster feedback
yuneng-jiang Mar 15, 2026
1092c17
Fix flaky encrypted_content_affinity tests: mock at handler level
yuneng-jiang Mar 15, 2026
673f3d5
Increase file deletion retry budget to 50s for batch_processed race
yuneng-jiang Mar 15, 2026
65575f3
Fix pytest -n worker oversubscription to match available CPUs
yuneng-jiang Mar 15, 2026
4c4246a
Downgrade oversized resource classes to match actual workload
yuneng-jiang Mar 15, 2026
a81a196
Fix test isolation: clear litellm.callbacks and model_fallbacks betwe…
yuneng-jiang Mar 15, 2026
a95cae6
Speed up build_docker_database_image: drop Docker upgrade, use zstd
yuneng-jiang Mar 16, 2026
670f8a1
Fix flaky test_caching_with_ttl by using distinct mock responses
yuneng-jiang Mar 16, 2026
ec537dd
Add -n 2 parallelism to 8 medium jobs running serial tests
yuneng-jiang Mar 16, 2026
40edb16
Fix test isolation: run eager tiktoken tests in subprocesses
yuneng-jiang Mar 16, 2026
0b9a242
Fix flaky vertex pass-through spend test by polling instead of fixed …
yuneng-jiang Mar 16, 2026
beee329
Fix flaky test_gemini_image_generation_async by removing non-determin…
yuneng-jiang Mar 16, 2026
4030a8b
Fix flaky tests: anthropic error_msg state leak and vertex llama 404
yuneng-jiang Mar 16, 2026
3d45ba3
Fix flaky vertex_ai overhead test by mocking auth and HTTP calls
yuneng-jiang Mar 16, 2026
4fc0975
Fix flaky e2e batch test: set batch_processed=True on completion in r…
yuneng-jiang Mar 16, 2026
19e8a16
Optimize logging_testing CI: suppress DEBUG logs, fix xdist isolation
yuneng-jiang Mar 16, 2026
92ad90d
Fix logging_testing: expand save/restore to cover redaction and other…
yuneng-jiang Mar 16, 2026
f2edc52
Fix flaky batch tests: mock vertex auth and skip on DNS failure
yuneng-jiang Mar 16, 2026
13a4659
Fix logging_testing: clear _in_memory_loggers and add missing globals
yuneng-jiang Mar 16, 2026
27d0ffe
Fix flaky AWS secret manager tests by skipping on ThrottlingException
yuneng-jiang Mar 16, 2026
9b77524
Fix logging_testing: capture true defaults at conftest import time
yuneng-jiang Mar 16, 2026
cc027a2
Fix flaky test_langsmith_queue_logging: poll instead of fixed sleep
yuneng-jiang Mar 16, 2026
9d06f53
Fix flaky test_claude_agent_sdk_streaming: add retry and stronger prompt
yuneng-jiang Mar 16, 2026
09271a4
Mark test_redis_cache_completion_stream as flaky with retries
yuneng-jiang Mar 16, 2026
f3cc292
[Infra] Speed up CI: batch pip installs and fix pytest -n parallelism
yuneng-jiang Mar 16, 2026
59f0db0
[Fix] Remove anyio==4.2.0 pin from local_testing batched installs
yuneng-jiang Mar 16, 2026
c1efbd3
[Fix] Drop httpx and opentelemetry pins from local_testing batched in…
yuneng-jiang Mar 16, 2026
74e57bd
Optimize CI test jobs: increase xdist workers, drop coverage, add cac…
yuneng-jiang Mar 16, 2026
1a00dd4
Fix router test isolation for xdist and rebalance proxy unit tests
yuneng-jiang Mar 16, 2026
26207bb
[Infra] Speed up mapped test jobs: uv installs, site-packages caching…
yuneng-jiang Mar 16, 2026
379c395
[Fix] Use uv for requirements.txt only, pip for test deps with confli…
yuneng-jiang Mar 16, 2026
65b3335
[Infra] Use uv for requirements.txt installs across 22 CI jobs
yuneng-jiang Mar 16, 2026
9711e3a
Fix xdist test isolation: capture true defaults and poll instead of s…
yuneng-jiang Mar 16, 2026
f07301a
[Infra] Optimize CI: right-size resource classes, drop unused coverag…
yuneng-jiang Mar 16, 2026
ae1e827
[Infra] Optimize CI: add xdist to caching tests, drop Docker CLI inst…
yuneng-jiang Mar 16, 2026
b4f7d11
Revert "Fix xdist test isolation: capture true defaults and poll inst…
yuneng-jiang Mar 16, 2026
eba54ba
[Fix] Add aurelio_sdk --no-deps alongside semantic_router in guardrai…
yuneng-jiang Mar 16, 2026
f68a9be
[Infra] Optimize CI: migrate litellm_security_tests from machine to d…
yuneng-jiang Mar 16, 2026
96183e8
[Fix] Drop --no-deps from aurelio_sdk in guardrails and enterprise tests
yuneng-jiang Mar 16, 2026
5db6aef
[Fix] Restore xdist test isolation: capture true defaults and poll co…
yuneng-jiang Mar 16, 2026
acfaea9
[Fix] Reset api_base/api_key in xdist conftest to prevent cross-test …
yuneng-jiang Mar 16, 2026
f434cdb
[Fix] Remove flush_cache from llm_translation conftest to prevent con…
yuneng-jiang Mar 16, 2026
2372427
[Fix] Remove xdist from caching_unit_tests to fix GCS cache test fail…
yuneng-jiang Mar 16, 2026
ccfe4b5
[Fix] Restore unconditional importlib.reload for llm_translation conf…
yuneng-jiang Mar 16, 2026
9cec81a
[Fix] Revert proxy unit test groupings to prevent xdist state pollution
yuneng-jiang Mar 16, 2026
8f56ddb
Merge remote main into litellm_ci_optimize
yuneng-jiang Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,095 changes: 398 additions & 697 deletions .circleci/config.yml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ async def check_batch_cost(self):
# every subsequent poll cycle.
if self._has_batch_processed_column:
try:
# Include "complete"/"completed" batches: the retrieve_batch
# endpoint may transition a batch to "complete" before
# CheckBatchCost runs. The batch_processed=False filter
# already prevents reprocessing finished batches.
jobs = await self.prisma_client.db.litellm_managedobjecttable.find_many(
where={
"file_purpose": "batch",
Expand All @@ -140,8 +144,6 @@ async def check_batch_cost(self):
"failed",
"expired",
"cancelled",
"complete",
"completed",
"stale_expired",
]
},
Expand Down
10 changes: 7 additions & 3 deletions litellm/proxy/batches_endpoints/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
get_original_file_id,
prepare_data_with_credentials,
resolve_input_file_id_to_unified,
resolve_output_file_ids_to_unified,
update_batch_in_database,
)
from litellm.proxy.utils import handle_exception_on_proxy, is_known_model
Expand Down Expand Up @@ -405,9 +406,11 @@ async def retrieve_batch( # noqa: PLR0915
verbose_proxy_logger=verbose_proxy_logger,
)

# If batch is in a terminal state, return immediately
# If batch is in a terminal state, return immediately.
# Include "complete" (DB-normalized form of "completed").
if response is not None and response.status in [
"completed",
"complete",
"failed",
"cancelled",
"expired",
Expand All @@ -417,10 +420,11 @@ async def retrieve_batch( # noqa: PLR0915
data=data, user_api_key_dict=user_api_key_dict, response=response
)

# async_post_call_success_hook replaces batch.id and output_file_id with unified IDs
# but not input_file_id. Resolve raw provider ID to unified ID.
# The DB may store raw provider file IDs (before hooks translate them).
# Resolve any raw input/output/error file IDs to unified IDs.
if unified_batch_id:
await resolve_input_file_id_to_unified(response, prisma_client)
await resolve_output_file_ids_to_unified(response, prisma_client)

asyncio.create_task(
proxy_logging_obj.update_request_status(
Expand Down
67 changes: 59 additions & 8 deletions litellm/proxy/openai_files_endpoints/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,28 @@ async def resolve_input_file_id_to_unified(response, prisma_client) -> None:
pass


async def resolve_output_file_ids_to_unified(response, prisma_client) -> None:
"""
If the batch response contains raw provider output_file_id or error_file_id
(not already unified IDs), look up the corresponding unified file IDs from
the managed file table and replace them in-place.
"""
if not prisma_client:
return
for attr in ("output_file_id", "error_file_id"):
raw_id = getattr(response, attr, None)
if not raw_id or _is_base64_encoded_unified_file_id(raw_id):
continue
try:
managed_file = await prisma_client.db.litellm_managedfiletable.find_first(
where={"flat_model_file_ids": {"has": raw_id}}
)
if managed_file:
setattr(response, attr, managed_file.unified_file_id)
except Exception:
pass


async def get_batch_from_database(
batch_id: str,
unified_batch_id: Union[str, Literal[False]],
Expand Down Expand Up @@ -809,14 +831,43 @@ async def update_batch_in_database(
# Normalize status for database storage
db_status = response.status if response.status != "completed" else "complete"

await prisma_client.db.litellm_managedobjecttable.update(
where={"unified_object_id": batch_id},
data={
"status": db_status,
"file_object": response.model_dump_json(),
"updated_at": litellm.utils.get_utc_datetime(),
},
)
update_data: dict = {
"status": db_status,
"file_object": response.model_dump_json(),
"updated_at": litellm.utils.get_utc_datetime(),
}

# When a batch reaches completion, also mark batch_processed=True.
# The cost callback is enqueued asynchronously during the
# aretrieve_batch call that detected completion (via the @client
# decorator). It is not awaited, so there is a theoretical window
# where the callback hasn't executed yet. In practice the callback
# completes reliably. Setting the flag here unblocks file deletion
# which queries batch_processed=False. CheckBatchCost acts as a
# safety net for the rare case where the callback fails.
if db_status == "complete":
update_data["batch_processed"] = True

try:
await prisma_client.db.litellm_managedobjecttable.update(
where={"unified_object_id": batch_id},
data=update_data,
)
except Exception as col_err:
# If the batch_processed column doesn't exist (old schema),
# retry without it so the status update still succeeds.
err_str = str(col_err).lower()
if "batch_processed" in err_str and update_data.get("batch_processed") is not None:
verbose_proxy_logger.warning(
f"batch_processed column not found, retrying update without it: {col_err}"
)
update_data.pop("batch_processed", None)
await prisma_client.db.litellm_managedobjecttable.update(
where={"unified_object_id": batch_id},
data=update_data,
)
else:
raise
Comment on lines +856 to +870

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.

CheckBatchCost safety-net comment is misleading

The inline comment says:

CheckBatchCost acts as a safety net for the rare case where the callback fails.

However, batch_processed=True is set here in update_batch_in_database before the async cost callback has a chance to run. Because CheckBatchCost now queries WHERE batch_processed = FALSE, any batch for which update_batch_in_database succeeds (i.e., the column exists and the write works) will have batch_processed=True and will not be picked up by CheckBatchCost, regardless of whether the cost callback later fails.

The true safety net only applies when update_batch_in_database itself fails to persist batch_processed (e.g., the column is absent and the fallback retry also errors). Consider updating the comment to accurately reflect this narrower guarantee, so future maintainers aren't misled into thinking cost-callback failures are automatically retried:

        # Setting batch_processed=True here unblocks file deletion (which
        # queries batch_processed=False).  NOTE: this flag is set eagerly,
        # so CheckBatchCost is only a safety net for the case where this
        # update itself fails (e.g. old schema without the column).  If the
        # async cost callback fails after this write succeeds, the cost will
        # NOT be re-calculated automatically.
        if db_status == "complete":
            update_data["batch_processed"] = True

except Exception as e:
verbose_proxy_logger.error(
f"Failed to update batch status in ManagedObjectTable: {e}"
Expand Down
54 changes: 38 additions & 16 deletions tests/batches_tests/test_openai_batches_and_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,26 @@
from litellm.integrations.custom_logger import CustomLogger
from litellm.types.utils import StandardLoggingPayload
import random
import socket
import httpx
from unittest.mock import patch, MagicMock


def _can_resolve_openai():
"""Check if api.openai.com is reachable (DNS resolves)."""
try:
socket.getaddrinfo("api.openai.com", 443, socket.AF_UNSPEC, socket.SOCK_STREAM)
return True
except socket.gaierror:
return False


skip_if_no_openai_network = pytest.mark.skipif(
not _can_resolve_openai(),
reason="Cannot resolve api.openai.com - skipping integration test due to DNS issues",
)


def load_vertex_ai_credentials():
# Define the path to the vertex_key.json file
print("loading vertex ai credentials")
Expand Down Expand Up @@ -78,6 +94,7 @@ def load_vertex_ai_credentials():

@pytest.mark.parametrize("provider", ["openai"]) # , "azure"
@pytest.mark.asyncio
@skip_if_no_openai_network
async def test_create_batch(provider):
"""
1. Create File for Batch completion
Expand Down Expand Up @@ -252,6 +269,7 @@ def cleanup_azure_ft_models():
@pytest.mark.parametrize("provider", ["openai"])
@pytest.mark.asyncio()
@pytest.mark.flaky(retries=3, delay=1)
@skip_if_no_openai_network
async def test_async_create_batch(provider):
"""
1. Create File for Batch completion
Expand Down Expand Up @@ -464,9 +482,24 @@ async def test_async_create_batch(provider):
@pytest.mark.asyncio
async def test_avertex_batch_prediction(monkeypatch):
monkeypatch.setenv("GCS_BUCKET_NAME", "litellm-local")
monkeypatch.setenv("VERTEXAI_PROJECT", "mock-project")
monkeypatch.setenv("VERTEXAI_LOCATION", "us-central1")

# Mock Google auth so the test doesn't need real credentials
mock_creds = MagicMock()
mock_creds.token = "mock-token"
mock_creds.valid = True
mock_creds.expiry = None
monkeypatch.setattr(
"google.auth.default",
lambda *args, **kwargs: (mock_creds, "mock-project"),
)

from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler

client = AsyncHTTPHandler()
# Configure mock response object
mock_response = MagicMock()
mock_response.raise_for_status.return_value = None

async def mock_side_effect(*args, **kwargs):
print("args", args, "kwargs", kwargs)
Expand All @@ -478,21 +511,10 @@ async def mock_side_effect(*args, **kwargs):
mock_response.status_code = 200
return mock_response

with patch.object(
client, "post", side_effect=mock_side_effect
) as mock_post, patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post"
with patch(
"litellm.llms.custom_httpx.http_handler.AsyncHTTPHandler.post",
side_effect=mock_side_effect,
) as mock_global_post:
# Configure mock responses
mock_response = MagicMock()
mock_response.raise_for_status.return_value = None

# Set up different responses for different API calls

mock_post.side_effect = mock_side_effect
mock_global_post.side_effect = mock_side_effect

# load_vertex_ai_credentials()
litellm.set_verbose = True
litellm._turn_on_debug()
file_name = "vertex_batch_completions.jsonl"
Expand All @@ -504,7 +526,6 @@ async def mock_side_effect(*args, **kwargs):
file=open(file_path, "rb"),
purpose="batch",
custom_llm_provider="vertex_ai",
client=client
)
print("Response from creating file=", file_obj)

Expand Down Expand Up @@ -623,6 +644,7 @@ async def test_vertex_async_create_batch_logs_error_body_on_http_error():


@pytest.mark.asyncio
@skip_if_no_openai_network
async def test_delete_batch_output_file():
"""
Test that deleting a batch output file works correctly.
Expand Down
106 changes: 69 additions & 37 deletions tests/guardrails_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
# conftest.py
#
# xdist-compatible test isolation for guardrails tests.
# Pattern matches tests/test_litellm/conftest.py:
# - Function-scoped fixture saves/restores litellm globals (no reload)
# - Module-scoped fixture reloads only in single-process mode

import importlib
import os
Expand All @@ -10,58 +15,85 @@
0, os.path.abspath("../..")
) # Adds the parent directory to the system path
import litellm
import asyncio

@pytest.fixture(scope="session")
def event_loop():
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
yield loop
loop.close()

@pytest.fixture(scope="function", autouse=True)
def setup_and_teardown():
def isolate_litellm_state():
"""
This fixture reloads litellm before every function. To speed up testing by removing callbacks being chained.
Per-function isolation fixture.

Saves and restores litellm callback/global state so tests don't leak
side effects. Works safely under pytest-xdist parallel execution.
"""
curr_dir = os.getcwd() # Get the current working directory
sys.path.insert(
0, os.path.abspath("../..")
) # Adds the project directory to the system path
# Save original callback state
original_state = {}
for attr in (
"callbacks",
"success_callback",
"failure_callback",
"_async_success_callback",
"_async_failure_callback",
):
if hasattr(litellm, attr):
val = getattr(litellm, attr)
original_state[attr] = val.copy() if val else []

# Save other globals that tests commonly mutate
for attr in ("set_verbose", "cache", "num_retries"):
if hasattr(litellm, attr):
original_state[attr] = getattr(litellm, attr)

# Flush cache before test
if hasattr(litellm, "in_memory_llm_clients_cache"):
litellm.in_memory_llm_clients_cache.flush_cache()

# Clear callbacks before test
for attr in (
"success_callback",
"failure_callback",
"_async_success_callback",
"_async_failure_callback",
):
if hasattr(litellm, attr):
setattr(litellm, attr, [])

import litellm
from litellm import Router
import asyncio
yield

from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
# flush all logs
asyncio.run(GLOBAL_LOGGING_WORKER.clear_queue())
# Restore all saved state
if hasattr(litellm, "in_memory_llm_clients_cache"):
litellm.in_memory_llm_clients_cache.flush_cache()

for attr, original_value in original_state.items():
if hasattr(litellm, attr):
setattr(litellm, attr, original_value)

importlib.reload(litellm)

try:
if hasattr(litellm, "proxy") and hasattr(litellm.proxy, "proxy_server"):
import litellm.proxy.proxy_server
@pytest.fixture(scope="module", autouse=True)
def setup_and_teardown():
"""
Module-scoped setup. Reloads litellm only in single-process mode
(skipped under xdist to avoid cross-worker interference).
"""
sys.path.insert(0, os.path.abspath("../.."))

importlib.reload(litellm.proxy.proxy_server)
except Exception as e:
print(f"Error reloading litellm.proxy.proxy_server: {e}")
import litellm

import asyncio
worker_id = os.environ.get("PYTEST_XDIST_WORKER", None)
if worker_id is None:
importlib.reload(litellm)

loop = asyncio.get_event_loop_policy().new_event_loop()
asyncio.set_event_loop(loop)
print(litellm)
# from litellm import Router, completion, aembedding, acompletion, embedding
yield
try:
if hasattr(litellm, "proxy") and hasattr(litellm.proxy, "proxy_server"):
import litellm.proxy.proxy_server

# Teardown code (executes after the yield point)
loop.close() # Close the loop created earlier
asyncio.set_event_loop(None) # Remove the reference to the loop
importlib.reload(litellm.proxy.proxy_server)
except Exception as e:
print(f"Error reloading litellm.proxy.proxy_server: {e}")

if hasattr(litellm, "in_memory_llm_clients_cache"):
litellm.in_memory_llm_clients_cache.flush_cache()

yield


def pytest_collection_modifyitems(config, items):
Expand Down
Loading
Loading