Skip to content

Commit fb29cb3

Browse files
committed
fix: handle whitespace-only content in structured output parsing
This commit fixes an issue where the API client would crash with JSON parsing errors when a model returns whitespace-only content (spaces, newlines, etc.) during structured output parsing. Changes: - Add a check in _parse_content to detect and gracefully handle empty or whitespace-only content before attempting JSON parsing - Improve streaming parser to skip JSON parsing for whitespace-only content - Update maybe_parse_content to catch and log parsing errors instead of letting them propagate - Add similar checks for tool argument parsing This fixes cases where users were getting "EOF while parsing a value" errors when using client.beta.chat.completions.parse with models that occasionally return only whitespace instead of structured JSON. With this change, parsing whitespace-only content now returns None for the parsed field instead of raising an exception, with an appropriate warning logged.
1 parent 17d7867 commit fb29cb3

File tree

2 files changed

+28
-9
lines changed

2 files changed

+28
-9
lines changed

src/openai/lib/_parsing/_completions.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,13 @@ def maybe_parse_content(
158158
message: ChatCompletionMessage | ParsedChatCompletionMessage[object],
159159
) -> ResponseFormatT | None:
160160
if has_rich_response_format(response_format) and message.content and not message.refusal:
161-
return _parse_content(response_format, message.content)
161+
try:
162+
return _parse_content(response_format, message.content)
163+
except ValueError as e:
164+
# if parsing fails due to whitespace content, log a warning and return None
165+
import logging
166+
logging.warning(f"Failed to parse content: {e}")
167+
return None
162168

163169
return None
164170

@@ -217,6 +223,13 @@ def is_parseable_tool(input_tool: ChatCompletionToolParam) -> bool:
217223

218224

219225
def _parse_content(response_format: type[ResponseFormatT], content: str) -> ResponseFormatT:
226+
# checking here if the content is empty or contains only whitespace
227+
if not content or content.isspace():
228+
raise ValueError(
229+
f"Cannot parse empty or whitespace-only content as {response_format.__name__}. "
230+
"The model returned content with no valid JSON."
231+
)
232+
220233
if is_basemodel_type(response_format):
221234
return cast(ResponseFormatT, model_parse_json(response_format, content))
222235

src/openai/lib/streaming/chat/_completions.py

+14-8
Original file line numberDiff line numberDiff line change
@@ -435,10 +435,13 @@ def _accumulate_chunk(self, chunk: ChatCompletionChunk) -> ParsedChatCompletionS
435435
and not choice_snapshot.message.refusal
436436
and is_given(self._rich_response_format)
437437
):
438-
choice_snapshot.message.parsed = from_json(
439-
bytes(choice_snapshot.message.content, "utf-8"),
440-
partial_mode=True,
441-
)
438+
# skipping parsing if content is just whitespace
439+
content = choice_snapshot.message.content
440+
if content.strip():
441+
choice_snapshot.message.parsed = from_json(
442+
bytes(content, "utf-8"),
443+
partial_mode=True,
444+
)
442445

443446
for tool_call_chunk in choice.delta.tool_calls or []:
444447
tool_call_snapshot = (choice_snapshot.message.tool_calls or [])[tool_call_chunk.index]
@@ -453,10 +456,13 @@ def _accumulate_chunk(self, chunk: ChatCompletionChunk) -> ParsedChatCompletionS
453456
and input_tool.get("function", {}).get("strict")
454457
and tool_call_snapshot.function.arguments
455458
):
456-
tool_call_snapshot.function.parsed_arguments = from_json(
457-
bytes(tool_call_snapshot.function.arguments, "utf-8"),
458-
partial_mode=True,
459-
)
459+
arguments = tool_call_snapshot.function.arguments
460+
# skipping parsing if arguments is just whitespace
461+
if arguments.strip():
462+
tool_call_snapshot.function.parsed_arguments = from_json(
463+
bytes(arguments, "utf-8"),
464+
partial_mode=True,
465+
)
460466
elif TYPE_CHECKING: # type: ignore[unreachable]
461467
assert_never(tool_call_snapshot)
462468

0 commit comments

Comments
 (0)