Skip to content

fix(openai): raise on empty chat completion instead of clean end_turn - #208

Open
gadievron wants to merge 1 commit into
masterfrom
fix/openai-chat-empty-content
Open

fix(openai): raise on empty chat completion instead of clean end_turn#208
gadievron wants to merge 1 commit into
masterfrom
fix/openai-chat-empty-content

Conversation

@gadievron

Copy link
Copy Markdown
Collaborator

Root cause

The OpenAI Chat Completions translation (_response_to_unified in utilities/llm/providers/openai.py) guarded an empty choices array but not an empty message. A choice whose message.content is None/empty with no tool_calls fell through to a clean end_turn carrying zero content blocks. For a security tool, an empty end_turn is read as a clean, passing verdict — so a blank completion became a silent false-negative.

This is the same silent-empty-completion shape #207 closed across the other paths, but the chat path never had the guard (it predates #207, which never touched openai.py). The three sibling paths already raise on empty content:

  • Responses path — openai.py _responses_to_unified "no usable content" guard
  • Anthropic adapter — R4-1 empty-completion guard
  • Gemini adapter — empty-candidates guard

Fix

Add the missing no-usable-content guard to the chat path, mirroring the siblings: after the content_filter refusal check (the more specific signal, raised first), if not content_blocks: raise LLMResponseError(...). A tool-use-only response stays valid because content_blocks is non-empty.

Reproduction

A ChatCompletion with one choice, message.content=None, tool_calls=None, finish_reason="stop".

Regression test

tests/test_llm_openai_adapter.py::test_empty_content_raises_llm_response_error — sibling of the existing test_empty_choices_raises_llm_response_error.

# RED (before fix):
FAILED tests/test_llm_openai_adapter.py::test_empty_content_raises_llm_response_error
# GREEN (after fix):
72 passed
# Full suite unchanged otherwise:
2413 passed, 149 skipped, 0 failed   (baseline 2412; +1 = this test)

Compatibility

No API/signature change. Behavior change is narrow: a chat response with neither text nor tool calls now raises LLMResponseError instead of returning an empty end_turn. Tool-use-only and normal text responses are unaffected; refusal/content_filter still raises first.

Surfaced by a post-merge collision review of #206/#207 (this gap is pre-existing, not introduced by either).

The Chat Completions path guarded an empty `choices` array but not an
empty message: a choice whose `message.content` is None/empty with no
`tool_calls` returned a clean `end_turn` carrying no content. For a
security tool an empty end_turn reads as a passing verdict, so a blank
completion became a silent false-negative.

Add the no-usable-content guard the three sibling paths already have --
the Responses path, the Anthropic adapter, and the Gemini adapter all
raise `LLMResponseError` on empty content. A tool-use-only response stays
valid (content_blocks is non-empty); refusal/content_filter still raises
first as the more specific signal.

Regression test: a choice with content=None and no tool_calls now raises
`LLMResponseError` (was a clean end_turn).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant