mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
177 lines
5.8 KiB
Python
177 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import AsyncIterable, Awaitable, Callable, Iterable
|
|
from dataclasses import dataclass
|
|
from typing import Final, Literal, TypeAlias
|
|
|
|
from .compare import assert_value_parity
|
|
from .models import (
|
|
SDKError,
|
|
SDKReport,
|
|
SDKStreamCompleted,
|
|
SDKStreamFailed,
|
|
SDKStreamReport,
|
|
sdk_chunk,
|
|
sdk_error_report,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class StreamCompleted:
|
|
kind: Literal["completed"] = "completed"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class StreamFailed:
|
|
phase: Literal["creation", "iteration"]
|
|
exception_type: type[BaseException]
|
|
error: SDKError
|
|
kind: Literal["failed"] = "failed"
|
|
|
|
|
|
StreamTerminal: TypeAlias = StreamCompleted | StreamFailed
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class StreamOutcome:
|
|
wrapper_type: type[object] | None
|
|
supports_sync_iteration: bool | None
|
|
supports_async_iteration: bool | None
|
|
chunks: tuple[object, ...]
|
|
chunk_types: tuple[type[object], ...]
|
|
terminal: StreamTerminal
|
|
|
|
|
|
ChunkNormalizer: TypeAlias = Callable[[object], object]
|
|
|
|
|
|
def drain_sync_stream(stream: Iterable[object]) -> None:
|
|
for _ in stream:
|
|
pass
|
|
|
|
|
|
async def drain_async_stream(stream: AsyncIterable[object]) -> None:
|
|
async for _ in stream:
|
|
pass
|
|
|
|
|
|
def capture_sync_stream(create: Callable[[], Iterable[object]]) -> SDKReport:
|
|
return _stream_report(consume_sync_stream(create))
|
|
|
|
|
|
async def capture_async_stream(create: Callable[[], Awaitable[AsyncIterable[object]]]) -> SDKReport:
|
|
return _stream_report(await consume_async_stream(create))
|
|
|
|
|
|
def _stream_report(outcome: StreamOutcome) -> SDKReport:
|
|
terminal: Final = outcome.terminal
|
|
if isinstance(terminal, StreamFailed) and terminal.phase == "creation":
|
|
return terminal.error
|
|
return SDKStreamReport(
|
|
chunks=tuple(sdk_chunk(chunk) for chunk in outcome.chunks),
|
|
terminal=SDKStreamFailed(error=terminal.error) if isinstance(terminal, StreamFailed) else SDKStreamCompleted(),
|
|
)
|
|
|
|
|
|
def _failed(phase: Literal["creation", "iteration"], error: Exception) -> StreamFailed:
|
|
return StreamFailed(
|
|
phase=phase,
|
|
exception_type=type(error),
|
|
error=sdk_error_report(error),
|
|
)
|
|
|
|
|
|
def consume_sync_stream(create: Callable[[], Iterable[object]]) -> StreamOutcome:
|
|
try:
|
|
stream: Final = create()
|
|
except Exception as error:
|
|
return StreamOutcome(
|
|
wrapper_type=None,
|
|
supports_sync_iteration=None,
|
|
supports_async_iteration=None,
|
|
chunks=(),
|
|
chunk_types=(),
|
|
terminal=_failed("creation", error),
|
|
)
|
|
|
|
chunks: list[object] = [] # mutable-ok: iterator consumption builds an ordered trace
|
|
try:
|
|
for chunk in stream:
|
|
chunks.append(chunk) # noqa: PERF402 # partial trace is required if iteration raises
|
|
except Exception as error:
|
|
recorded: Final = tuple(chunks)
|
|
return StreamOutcome(
|
|
wrapper_type=type(stream),
|
|
supports_sync_iteration=hasattr(stream, "__iter__"),
|
|
supports_async_iteration=hasattr(stream, "__aiter__"),
|
|
chunks=recorded,
|
|
chunk_types=tuple(type(chunk) for chunk in recorded),
|
|
terminal=_failed("iteration", error),
|
|
)
|
|
completed_chunks: Final = tuple(chunks)
|
|
return StreamOutcome(
|
|
wrapper_type=type(stream),
|
|
supports_sync_iteration=hasattr(stream, "__iter__"),
|
|
supports_async_iteration=hasattr(stream, "__aiter__"),
|
|
chunks=completed_chunks,
|
|
chunk_types=tuple(type(chunk) for chunk in completed_chunks),
|
|
terminal=StreamCompleted(),
|
|
)
|
|
|
|
|
|
async def consume_async_stream(create: Callable[[], Awaitable[AsyncIterable[object]]]) -> StreamOutcome:
|
|
try:
|
|
stream: Final = await create()
|
|
except Exception as error:
|
|
return StreamOutcome(
|
|
wrapper_type=None,
|
|
supports_sync_iteration=None,
|
|
supports_async_iteration=None,
|
|
chunks=(),
|
|
chunk_types=(),
|
|
terminal=_failed("creation", error),
|
|
)
|
|
|
|
chunks: list[object] = [] # mutable-ok: iterator consumption builds an ordered trace
|
|
try:
|
|
async for chunk in stream:
|
|
chunks.append(chunk)
|
|
except Exception as error:
|
|
recorded: Final = tuple(chunks)
|
|
return StreamOutcome(
|
|
wrapper_type=type(stream),
|
|
supports_sync_iteration=hasattr(stream, "__iter__"),
|
|
supports_async_iteration=hasattr(stream, "__aiter__"),
|
|
chunks=recorded,
|
|
chunk_types=tuple(type(chunk) for chunk in recorded),
|
|
terminal=_failed("iteration", error),
|
|
)
|
|
completed_chunks: Final = tuple(chunks)
|
|
return StreamOutcome(
|
|
wrapper_type=type(stream),
|
|
supports_sync_iteration=hasattr(stream, "__iter__"),
|
|
supports_async_iteration=hasattr(stream, "__aiter__"),
|
|
chunks=completed_chunks,
|
|
chunk_types=tuple(type(chunk) for chunk in completed_chunks),
|
|
terminal=StreamCompleted(),
|
|
)
|
|
|
|
|
|
def normalize_chunk(chunk: object) -> object:
|
|
return chunk
|
|
|
|
|
|
def assert_stream_parity(
|
|
baseline: StreamOutcome,
|
|
candidate: StreamOutcome,
|
|
*,
|
|
normalize: ChunkNormalizer = normalize_chunk,
|
|
) -> None:
|
|
assert baseline.wrapper_type is candidate.wrapper_type
|
|
assert baseline.supports_sync_iteration is candidate.supports_sync_iteration
|
|
assert baseline.supports_async_iteration is candidate.supports_async_iteration
|
|
assert baseline.chunk_types == candidate.chunk_types
|
|
assert len(baseline.chunks) == len(candidate.chunks)
|
|
for index, (baseline_chunk, candidate_chunk) in enumerate(zip(baseline.chunks, candidate.chunks, strict=True)):
|
|
assert_value_parity(normalize(baseline_chunk), normalize(candidate_chunk), path=f"$.chunks[{index}]")
|
|
assert baseline.terminal == candidate.terminal
|