mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-12 03:48:56 +00:00
* perf(ci): distribute unit cases and reuse security sweep connections * revert(ci): reject first speed trial after new failures * perf(ci): avoid repeated test discovery and parsing * perf(ci): isolate parallel security workers * revert(ci): restore measured security execution isolation * perf(tests): reuse source scans and deduplicate cache cleanup * perf(ci): restore parallel security with safe log batching * test(ci): isolate fixture state and schema comparisons * test(ci): make schema and scheduler fixtures deterministic * test(ci): drain queued logging before fixture teardown * test(ci): preserve logging delivery without idle worker restarts * test(ci): keep guardrail sinks available during parallel bursts * test(ci): isolate full proxy lifecycle from gateway imports * test(ci): preserve first failure responses and sweep evidence * test(ci): restore documentation URL settings between cases * test(ci): isolate open MCP tools in private gateways * test(ci): group private MCP cases and handle exited workers * test(ci): retain proxy exits and MCP failure details * test(ci): repair merge interactions with main Key the schema shard's full clone on artifact-name, since main removed the unit-flag input. Give each owned Redis its own log file again. Hand the realtime transcription tests main added a logging object whose dispatch handlers are awaitable, so the per-test logging drain no longer waits on a MagicMock task forever * perf(ci): shard owned integration cases and close lifecycle leaks * fix(ci): qualify shard execution and harden interrupted cleanup * fix(ci): bound worker supervision and own fault traffic * perf(ci): reuse unparametrized fixtures across bounded case groups * fix(ci): preserve boot fault allowances and complete exit records * test(ci): tolerate startup retries and retain readiness details * perf(ci): balance integration shards by recorded duration * fix(ci): load migration database helpers only for workers
109 lines
4.4 KiB
Python
109 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import threading
|
|
from collections.abc import Iterator, Sequence
|
|
from typing import Final, TypedDict
|
|
|
|
import httpx
|
|
import pytest
|
|
from pydantic import TypeAdapter
|
|
from typing_extensions import ReadOnly
|
|
|
|
from litellm.integrations.generic_api.generic_api_callback import LOG_FORMAT_TYPES, GenericAPILogger
|
|
from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler
|
|
from tests.integration._support.wire import Reply as WireReply, Request as WireRequest, wire_server
|
|
|
|
|
|
class _QueuedLog(TypedDict):
|
|
id: ReadOnly[str]
|
|
|
|
|
|
_QUEUED_LOG: Final = TypeAdapter(_QueuedLog)
|
|
_LOG_BATCH: Final = TypeAdapter(tuple[_QueuedLog, ...])
|
|
|
|
|
|
def _batch_events(body: bytes, log_format: LOG_FORMAT_TYPES) -> tuple[_QueuedLog, ...]:
|
|
match log_format:
|
|
case "json_array":
|
|
return _LOG_BATCH.validate_json(body)
|
|
case "ndjson":
|
|
return tuple(_QUEUED_LOG.validate_json(line) for line in body.splitlines())
|
|
case "single":
|
|
return (_QUEUED_LOG.validate_json(body),)
|
|
|
|
|
|
def _received_log_ids(requests: Sequence[WireRequest], log_format: LOG_FORMAT_TYPES) -> Iterator[str]:
|
|
for request in requests:
|
|
for event in _batch_events(request.body, log_format):
|
|
yield event["id"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("log_format", ("json_array", "ndjson", "single"))
|
|
async def test_generic_api_keeps_failure_arriving_during_success_batch(log_format: LOG_FORMAT_TYPES) -> None:
|
|
loop: Final = asyncio.get_running_loop()
|
|
first_arrived: Final = asyncio.Event()
|
|
release: Final = threading.Event()
|
|
|
|
def respond(request: WireRequest) -> WireReply:
|
|
events: Final = _batch_events(request.body, log_format)
|
|
if any(event["id"] == "before" for event in events):
|
|
loop.call_soon_threadsafe(first_arrived.set)
|
|
assert release.wait(5), "The first HTTP batch was never released"
|
|
return WireReply()
|
|
|
|
with wire_server(respond) as wire:
|
|
handler: Final = AsyncHTTPHandler(transport=httpx.AsyncHTTPTransport(trust_env=False))
|
|
before_tasks: Final = frozenset(asyncio.all_tasks())
|
|
logger: Final = GenericAPILogger(
|
|
endpoint=wire.url,
|
|
async_httpx_client=handler,
|
|
log_format=log_format,
|
|
batch_size=1000,
|
|
flush_interval=3600,
|
|
)
|
|
owned_tasks: Final = frozenset(asyncio.all_tasks()) - before_tasks
|
|
try:
|
|
async with asyncio.TaskGroup() as flushes:
|
|
await logger.async_log_success_event({"standard_logging_object": {"id": "before"}}, None, None, None)
|
|
first_flush: Final = flushes.create_task(logger.flush_queue())
|
|
try:
|
|
await first_arrived.wait()
|
|
await logger.async_log_failure_event({"standard_logging_object": {"id": "during"}}, None, None, None)
|
|
finally:
|
|
release.set()
|
|
await first_flush
|
|
await logger.flush_queue()
|
|
assert tuple(_received_log_ids(wire.drain(), log_format)) == ("before", "during")
|
|
finally:
|
|
release.set()
|
|
for task in owned_tasks:
|
|
task.cancel()
|
|
await asyncio.gather(*owned_tasks, return_exceptions=True)
|
|
await handler.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("log_format", ("json_array", "ndjson", "single"))
|
|
async def test_generic_api_concurrent_flushes_deliver_each_event_once(log_format: LOG_FORMAT_TYPES) -> None:
|
|
with wire_server(lambda request: WireReply()) as wire:
|
|
handler: Final = AsyncHTTPHandler(transport=httpx.AsyncHTTPTransport(trust_env=False))
|
|
before_tasks: Final = frozenset(asyncio.all_tasks())
|
|
logger: Final = GenericAPILogger(
|
|
endpoint=wire.url,
|
|
async_httpx_client=handler,
|
|
log_format=log_format,
|
|
batch_size=1000,
|
|
flush_interval=3600,
|
|
)
|
|
owned_tasks: Final = frozenset(asyncio.all_tasks()) - before_tasks
|
|
try:
|
|
await logger.async_log_success_event({"standard_logging_object": {"id": "once"}}, None, None, None)
|
|
await asyncio.gather(logger.flush_queue(), logger.async_send_batch())
|
|
assert tuple(_received_log_ids(wire.drain(), log_format)) == ("once",)
|
|
finally:
|
|
for task in owned_tasks:
|
|
task.cancel()
|
|
await asyncio.gather(*owned_tasks, return_exceptions=True)
|
|
await handler.close()
|