This commit is contained in:
Simon 2026-10-27 17:35:58 +00:00 • committed by GitHub
commit d03eaaa462
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 90 additions and 0 deletions

View file

@ -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,

View file

@ -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")

View file

@ -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