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
17 changes: 17 additions & 0 deletions libs/openant-core/tests/test_llm_openai_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,23 @@ def test_empty_choices_raises_llm_response_error():
adapter.complete(model="gpt-4o", system=None, messages=_hi(), max_tokens=8)


def test_empty_content_raises_llm_response_error():
# A choice with neither text nor tool calls is an empty completion: it must
# surface via the taxonomy, not read as a clean end_turn. Parity with the
# Responses path's no-usable-content guard and the Anthropic/Gemini adapters
# -- for a security tool an empty end_turn would read as a clean pass.
empty = SimpleNamespace(
choices=[SimpleNamespace(
message=SimpleNamespace(content=None, tool_calls=None),
finish_reason="stop",
)],
usage=SimpleNamespace(prompt_tokens=1, completion_tokens=0),
)
adapter, _ = _stub(lambda **kw: empty)
with pytest.raises(LLMResponseError):
adapter.complete(model="gpt-4o", system=None, messages=_hi(), max_tokens=8)


# ---------------------------------------------------------------------------
# L3 — pricing table carries current models so they don't report $0
# ---------------------------------------------------------------------------
Expand Down
14 changes: 14 additions & 0 deletions libs/openant-core/utilities/llm/providers/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,20 @@ def _response_to_unified(response: Any) -> CompletionResult:
"or truncated by the moderation layer"
)

# An empty completion -- no text AND no tool calls (``message.content`` is
# None/empty with no ``tool_calls``) -- carries nothing the pipeline can act
# on. Surface it via the taxonomy instead of returning an empty end_turn
# (mirrors the Responses path's no-usable-content guard and the Anthropic/
# Gemini adapters); for a SECURITY tool an empty end_turn would read as a
# clean, passing result. A tool-use-only response is VALID and not caught
# here because ``content_blocks`` is non-empty. Refusal/content_filter is the
# more specific signal and already raised above.
if not content_blocks:
raise LLMResponseError(
"OpenAI returned an empty completion (no text or tool calls); the "
"request may have been filtered or the response was malformed"
)

if raw_finish not in _OPENAI_FINISH_REASONS:
should_warn = False
with _warned_finish_reasons_lock:
Expand Down
Loading