Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/openai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
InvalidWebhookSignatureError,
ContentFilterFinishReasonError,
WebSocketConnectionClosedError,
IncompleteResponseError,
)
from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
from ._utils._logs import setup_logging as _setup_logging
Expand Down Expand Up @@ -71,6 +72,7 @@
"LengthFinishReasonError",
"ContentFilterFinishReasonError",
"InvalidWebhookSignatureError",
"IncompleteResponseError",
"Timeout",
"RequestOptions",
"Client",
Expand Down
24 changes: 24 additions & 0 deletions src/openai/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"SubjectTokenProviderError",
"WebSocketConnectionClosedError",
"WebSocketQueueFullError",
"IncompleteResponseError",
]


Expand Down Expand Up @@ -205,3 +206,26 @@ class WebSocketQueueFullError(OpenAIError):
"""Raised when the outgoing WebSocket message queue exceeds its byte-size limit."""

pass


class IncompleteResponseError(OpenAIError):
"""Raised when a streaming response ends with incomplete status.

This typically occurs when the response is truncated due to max_output_tokens
or content_filter restrictions.
"""

response_id: str
incomplete_details_reason: Optional[Literal["max_output_tokens", "content_filter"]]

def __init__(
self,
*,
response_id: str,
incomplete_details_reason: Optional[Literal["max_output_tokens", "content_filter"]],
) -> None:
reason_str = incomplete_details_reason or "unknown"
message = f"Response {response_id} is incomplete: {reason_str}"
super().__init__(message)
self.response_id = response_id
self.incomplete_details_reason = incomplete_details_reason
12 changes: 11 additions & 1 deletion src/openai/lib/streaming/responses/_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
ParsedResponseOutputMessage,
ParsedResponseFunctionToolCall,
)
from ...._exceptions import IncompleteResponseError


class ResponseStream(Generic[TextFormatT]):
Expand Down Expand Up @@ -276,6 +277,8 @@ def handle_event(self, event: RawResponseStreamEvent) -> List[ResponseStreamEven
content = output.content[event.content_index]
assert content.type == "output_text"

# Don't parse here - defer parsing until response.completed or response.incomplete
# is received, so we can properly handle incomplete responses
events.append(
build(
ResponseTextDoneEvent[TextFormatT],
Expand All @@ -286,7 +289,7 @@ def handle_event(self, event: RawResponseStreamEvent) -> List[ResponseStreamEven
logprobs=event.logprobs,
type="response.output_text.done",
text=event.text,
parsed=parse_text(event.text, text_format=self._text_format),
parsed=None, # type: ignore[arg-type]
)
)
elif event.type == "response.function_call_arguments.delta":
Expand Down Expand Up @@ -317,6 +320,13 @@ def handle_event(self, event: RawResponseStreamEvent) -> List[ResponseStreamEven
response=response,
)
)
elif event.type == "response.incomplete":
# Raise an error for incomplete responses instead of letting
# Pydantic JSON validation errors bubble up later
raise IncompleteResponseError(
response_id=event.response.id,
incomplete_details_reason=event.response.incomplete_details.reason if event.response.incomplete_details else None,
)
Comment on lines +323 to +329
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve response.incomplete events for non-parsed streams

Only text_format=PydanticModel needed special handling, but this change now raises IncompleteResponseError for every response.incomplete event, including plain client.responses.stream() usage with no parsing. That is a behavioral regression: callers that previously consumed partial output and handled response.incomplete as a normal terminal event now get an exception and lose the event path entirely. Please gate this exception to parse-mode streams (or still emit the event) so non-structured streaming remains backward-compatible.

Useful? React with 👍 / 👎.

else:
events.append(event)

Expand Down