mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-12 21:04:10 +00:00
6a9f542f81
* test: stabilize batch VCR coverage * test: replay bedrock batch s3 uploads * test: stop batch tests leaking live uploads * test: keep bedrock batch workflow off live s3 * test: mock bedrock batch workflow network * test: accept realtime guardrail refusal wording * test: update gemini thought signature model * test: quiet logging worker atexit flush * test: address Greptile review on batch VCR fixes Handle content= bodies in the bedrock batch post stub so payload extraction does not raise a TypeError when a request omits json and data. Restore litellm list state faithfully by preserving None instead of coercing it to an empty list, so callbacks that start as None are not turned into [] after a test. Set logging.raiseExceptions inside the try block in the atexit flush so the finally always restores the previous value. * test: scope atexit logging suppression to the drain loop Wrap only the queue drain loop in LoggingWorker._flush_on_exit with the logging.raiseExceptions toggle so the process-wide global is suppressed for the smallest possible window, keeping other threads' logging error reporting intact outside the loop. * test: cover atexit flush error-swallow branch in LoggingWorker The _flush_on_exit drain loop was wrapped in a try/finally to scope the logging.raiseExceptions toggle, which reindented the existing edge-case branches into the diff and dropped patch coverage below target. Add a regression test that enqueues a coroutine which raises during the atexit flush and asserts the failure is swallowed while later queued events are still drained, exercising the silent-failure path directly.
158 lines
3.9 KiB
Python
158 lines
3.9 KiB
Python
import asyncio
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(
|
|
0, os.path.abspath("../..")
|
|
) # Adds the parent directory to the system path
|
|
import litellm # noqa: E402,F401
|
|
|
|
from tests._vcr_conftest_common import ( # noqa: E402,F401
|
|
VerboseReporterState,
|
|
_pin_multipart_boundary,
|
|
apply_vcr_auto_marker_to_items,
|
|
emit_cassette_cache_session_banner,
|
|
emit_vcr_classification_summary,
|
|
emit_vcr_diagnostic_log,
|
|
install_live_call_probe,
|
|
record_vcr_outcome,
|
|
register_persister_if_enabled,
|
|
reset_vcr_diag_dir,
|
|
vcr_config_dict,
|
|
)
|
|
|
|
_verbose_state = VerboseReporterState()
|
|
|
|
_CALLBACK_ATTRS = (
|
|
"callbacks",
|
|
"success_callback",
|
|
"failure_callback",
|
|
"_async_success_callback",
|
|
"_async_failure_callback",
|
|
)
|
|
|
|
_SCALAR_ATTRS = (
|
|
"num_retries",
|
|
"set_verbose",
|
|
"cache",
|
|
"allowed_fails",
|
|
"disable_aiohttp_transport",
|
|
"force_ipv4",
|
|
"drop_params",
|
|
"modify_params",
|
|
"api_base",
|
|
"api_key",
|
|
"cohere_key",
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def vcr_config():
|
|
return vcr_config_dict()
|
|
|
|
|
|
def pytest_recording_configure(config, vcr):
|
|
register_persister_if_enabled(vcr)
|
|
|
|
|
|
@pytest.hookimpl(hookwrapper=True)
|
|
def pytest_runtest_makereport(item, call):
|
|
outcome = yield
|
|
rep = outcome.get_result()
|
|
setattr(item, f"rep_{rep.when}", rep)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _vcr_outcome_gate(request, vcr):
|
|
install_live_call_probe(request, vcr)
|
|
yield
|
|
record_vcr_outcome(request, vcr)
|
|
|
|
|
|
def pytest_configure(config):
|
|
_verbose_state.remember_pluginmanager(config)
|
|
reset_vcr_diag_dir()
|
|
|
|
|
|
def pytest_runtest_logreport(report):
|
|
_verbose_state.maybe_emit_verdict(report)
|
|
|
|
|
|
def pytest_terminal_summary(terminalreporter, exitstatus, config):
|
|
emit_cassette_cache_session_banner(terminalreporter)
|
|
emit_vcr_classification_summary(terminalreporter)
|
|
emit_vcr_diagnostic_log(terminalreporter)
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def event_loop():
|
|
try:
|
|
loop = asyncio.get_running_loop()
|
|
except RuntimeError:
|
|
loop = asyncio.new_event_loop()
|
|
yield loop
|
|
loop.close()
|
|
|
|
|
|
def _copy_litellm_state():
|
|
state = {}
|
|
for attr in _CALLBACK_ATTRS:
|
|
if hasattr(litellm, attr):
|
|
value = getattr(litellm, attr)
|
|
state[attr] = value.copy() if isinstance(value, list) else value
|
|
for attr in _SCALAR_ATTRS:
|
|
if hasattr(litellm, attr):
|
|
state[attr] = getattr(litellm, attr)
|
|
return state
|
|
|
|
|
|
def _restore_litellm_state(state) -> None:
|
|
for attr, value in state.items():
|
|
if hasattr(litellm, attr):
|
|
setattr(litellm, attr, value)
|
|
|
|
|
|
def _reset_litellm_callbacks() -> None:
|
|
for attr in _CALLBACK_ATTRS:
|
|
if hasattr(litellm, attr):
|
|
setattr(litellm, attr, [])
|
|
manager = getattr(litellm, "logging_callback_manager", None)
|
|
reset = getattr(manager, "_reset_all_callbacks", None)
|
|
if callable(reset):
|
|
reset()
|
|
|
|
|
|
def _clear_logging_queue(loop=None) -> None:
|
|
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
|
|
|
|
if loop is not None and not loop.is_closed() and not loop.is_running():
|
|
loop.run_until_complete(GLOBAL_LOGGING_WORKER.clear_queue())
|
|
return
|
|
asyncio.run(GLOBAL_LOGGING_WORKER.clear_queue())
|
|
|
|
|
|
@pytest.fixture(scope="function", autouse=True)
|
|
def setup_and_teardown(event_loop):
|
|
original_state = _copy_litellm_state()
|
|
_clear_logging_queue(event_loop)
|
|
_reset_litellm_callbacks()
|
|
asyncio.set_event_loop(event_loop)
|
|
|
|
yield
|
|
|
|
_clear_logging_queue(event_loop)
|
|
_reset_litellm_callbacks()
|
|
_restore_litellm_state(original_state)
|
|
|
|
pending = asyncio.all_tasks(event_loop)
|
|
for task in pending:
|
|
task.cancel()
|
|
if pending:
|
|
event_loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
|
|
|
|
|
def pytest_collection_modifyitems(config, items):
|
|
apply_vcr_auto_marker_to_items(items)
|