Skip to content

Commit c523fef

Browse files
gadievrongadievronclaude
authored
fix(openai): raise on empty chat completion instead of clean end_turn (#208)
Co-authored-by: gadievron <gadi@unpromptedcon.org> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 59983d6 commit c523fef

2 files changed

Lines changed: 31 additions & 0 deletions

File tree

libs/openant-core/tests/test_llm_openai_adapter.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,23 @@ def test_empty_choices_raises_llm_response_error():
172172
adapter.complete(model="gpt-4o", system=None, messages=_hi(), max_tokens=8)
173173

174174

175+
def test_empty_content_raises_llm_response_error():
176+
# A choice with neither text nor tool calls is an empty completion: it must
177+
# surface via the taxonomy, not read as a clean end_turn. Parity with the
178+
# Responses path's no-usable-content guard and the Anthropic/Gemini adapters
179+
# -- for a security tool an empty end_turn would read as a clean pass.
180+
empty = SimpleNamespace(
181+
choices=[SimpleNamespace(
182+
message=SimpleNamespace(content=None, tool_calls=None),
183+
finish_reason="stop",
184+
)],
185+
usage=SimpleNamespace(prompt_tokens=1, completion_tokens=0),
186+
)
187+
adapter, _ = _stub(lambda **kw: empty)
188+
with pytest.raises(LLMResponseError):
189+
adapter.complete(model="gpt-4o", system=None, messages=_hi(), max_tokens=8)
190+
191+
175192
# ---------------------------------------------------------------------------
176193
# L3 — pricing table carries current models so they don't report $0
177194
# ---------------------------------------------------------------------------

libs/openant-core/utilities/llm/providers/openai.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,20 @@ def _response_to_unified(response: Any) -> CompletionResult:
797797
"or truncated by the moderation layer"
798798
)
799799

800+
# An empty completion -- no text AND no tool calls (``message.content`` is
801+
# None/empty with no ``tool_calls``) -- carries nothing the pipeline can act
802+
# on. Surface it via the taxonomy instead of returning an empty end_turn
803+
# (mirrors the Responses path's no-usable-content guard and the Anthropic/
804+
# Gemini adapters); for a SECURITY tool an empty end_turn would read as a
805+
# clean, passing result. A tool-use-only response is VALID and not caught
806+
# here because ``content_blocks`` is non-empty. Refusal/content_filter is the
807+
# more specific signal and already raised above.
808+
if not content_blocks:
809+
raise LLMResponseError(
810+
"OpenAI returned an empty completion (no text or tool calls); the "
811+
"request may have been filtered or the response was malformed"
812+
)
813+
800814
if raw_finish not in _OPENAI_FINISH_REASONS:
801815
should_warn = False
802816
with _warned_finish_reasons_lock:

0 commit comments

Comments
 (0)