mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-05 07:37:50 +00:00
Merge 20e6ea6f9f into 0657ed46cf
This commit is contained in:
commit
d03eaaa462
3 changed files with 90 additions and 0 deletions
|
|
@ -1557,6 +1557,12 @@ class CustomStreamWrapper:
|
|||
else: # openai / azure chat model
|
||||
if isinstance(chunk, ChatCompletionChunk):
|
||||
self._openai_handler_receives_chat_chunks = True
|
||||
if any(str(choice.finish_reason) == "abort" for choice in chunk.choices):
|
||||
raise litellm.InternalServerError(
|
||||
message="Upstream generation aborted (finish_reason=abort)",
|
||||
model=chunk.model,
|
||||
llm_provider=self.custom_llm_provider,
|
||||
)
|
||||
if self.custom_llm_provider in [
|
||||
LlmProviders.AZURE.value,
|
||||
LlmProviders.AZURE_AI.value,
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ else:
|
|||
|
||||
_NO_TOOLS_UPDATE: Final[Mapping[str, object]] = MappingProxyType({})
|
||||
_OBJECT_MAPPING: Final[TypeAdapter[Mapping[str, object]]] = TypeAdapter(Mapping[str, object])
|
||||
_CHOICE_MAPPINGS: Final = TypeAdapter(tuple[Mapping[str, object], ...])
|
||||
_DECODED_JSON: Final = TypeAdapter(object)
|
||||
|
||||
|
||||
|
|
@ -958,6 +959,11 @@ class OpenAIChatCompletionStreamingHandler(BaseModelResponseIterator):
|
|||
message=error_message,
|
||||
)
|
||||
choices = chunk.get("choices", [])
|
||||
if any(choice.get("finish_reason") == "abort" for choice in _CHOICE_MAPPINGS.validate_python(choices)):
|
||||
raise OpenAIError(
|
||||
status_code=500,
|
||||
message="Upstream generation aborted (finish_reason=abort)",
|
||||
)
|
||||
choices = self._map_reasoning_to_reasoning_content(choices)
|
||||
fingerprint: Final = _OBJECT_MAPPING.validate_python(chunk).get("system_fingerprint")
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
|||
|
||||
import httpx
|
||||
import pytest
|
||||
from openai import AsyncOpenAI, AsyncStream, OpenAI, Stream
|
||||
from openai.types.chat import ChatCompletionChunk
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
import litellm
|
||||
|
|
@ -28,6 +29,7 @@ from litellm.litellm_core_utils.streaming_handler import (
|
|||
_ProviderChunkParsed,
|
||||
)
|
||||
from litellm.llms.custom_httpx.http_handler import HTTPHandler
|
||||
from litellm.llms.openai.chat.gpt_transformation import OpenAIChatCompletionStreamingHandler
|
||||
from litellm.responses.mcp.chat_completions_handler import _add_mcp_metadata_to_response
|
||||
from litellm.types.utils import (
|
||||
CompletionTokensDetailsWrapper,
|
||||
|
|
@ -1367,6 +1369,82 @@ async def test_bedrock_validation_error_raises_directly(logging_obj: Logging):
|
|||
assert getattr(excinfo.value, "status_code", None) == 400
|
||||
|
||||
|
||||
@pytest.mark.parametrize("async_stream", [False, True])
|
||||
@pytest.mark.parametrize("transport", ["sdk", "http_handler"])
|
||||
@pytest.mark.parametrize("prefix", ["", "Partial answer"])
|
||||
@pytest.mark.parametrize("abort_index", [0, 1])
|
||||
@pytest.mark.parametrize("abort_content", ["", "unfinished"])
|
||||
async def test_aborted_stream_raises_instead_of_finishing_successfully(
|
||||
async_stream: bool, transport: str, prefix: str, abort_index: int, abort_content: str, logging_obj: Logging
|
||||
) -> None:
|
||||
initial: Final = (
|
||||
(({"index": 0, "delta": {"role": "assistant", "content": prefix}, "finish_reason": None},),) if prefix else ()
|
||||
)
|
||||
final: Final = tuple(
|
||||
{
|
||||
"index": index,
|
||||
"delta": {"content": abort_content},
|
||||
"finish_reason": "abort" if index == abort_index else "stop",
|
||||
}
|
||||
for index in range(abort_index + 1)
|
||||
)
|
||||
body: Final = (
|
||||
"".join(
|
||||
"data: "
|
||||
+ json.dumps(
|
||||
{
|
||||
"id": "chatcmpl-aborted",
|
||||
"object": "chat.completion.chunk",
|
||||
"created": 1,
|
||||
"model": "test-model",
|
||||
"choices": choices,
|
||||
}
|
||||
)
|
||||
+ "\n\n"
|
||||
for choices in (*initial, final)
|
||||
)
|
||||
+ "data: [DONE]\n\n"
|
||||
)
|
||||
raw_response: Final = httpx.Response(
|
||||
200,
|
||||
content=body,
|
||||
headers={"content-type": "text/event-stream"},
|
||||
request=httpx.Request("POST", "https://provider.example/v1/chat/completions"),
|
||||
)
|
||||
client: Final = AsyncOpenAI(api_key="unused") if async_stream else OpenAI(api_key="unused")
|
||||
stream: Final = (
|
||||
(
|
||||
AsyncStream(cast_to=ChatCompletionChunk, response=raw_response, client=client)
|
||||
if isinstance(client, AsyncOpenAI)
|
||||
else Stream(cast_to=ChatCompletionChunk, response=raw_response, client=client)
|
||||
)
|
||||
if transport == "sdk"
|
||||
else OpenAIChatCompletionStreamingHandler(
|
||||
streaming_response=raw_response.aiter_lines() if async_stream else raw_response.iter_lines(),
|
||||
sync_stream=not async_stream,
|
||||
)
|
||||
)
|
||||
response: Final = CustomStreamWrapper(
|
||||
completion_stream=stream,
|
||||
model="test-model",
|
||||
logging_obj=logging_obj,
|
||||
custom_llm_provider="openai" if transport == "sdk" else "hosted_vllm",
|
||||
)
|
||||
|
||||
try:
|
||||
with pytest.raises(MidStreamFallbackError, match="abort") as excinfo:
|
||||
_chunks: Final = [chunk async for chunk in response] if async_stream else list(response)
|
||||
|
||||
assert excinfo.value.is_pre_first_chunk is (not prefix)
|
||||
assert excinfo.value.generated_content == prefix
|
||||
assert getattr(excinfo.value.original_exception, "status_code", None) == 500
|
||||
finally:
|
||||
if isinstance(client, AsyncOpenAI):
|
||||
await client.close()
|
||||
else:
|
||||
client.close()
|
||||
|
||||
|
||||
def _hosted_vllm_stream_wrapper(logging_obj: Logging, error_payload: dict) -> CustomStreamWrapper:
|
||||
"""A CustomStreamWrapper over the real OpenAI-compatible line iterator,
|
||||
fed an HTTP 200 SSE body that carries an in-body error payload the way
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue