Describe the overall issue and situation
In the OpenAI provider's realtime (non-batch) inference path, a refusal or content-filtered completion is returned as a successful, empty extraction instead of raising.
langextract/providers/openai.py:252-254 (_process_single_prompt):
response = self._client.chat.completions.create(**api_params)
output_text = response.choices[0].message.content
return core_types.ScoredOutput(score=1.0, output=output_text)
message.content is None when the completion stops for content_filter, when a structured-output refusal is emitted (text lands in message.refusal), or when the message carries only tool calls. ScoredOutput.output is typed str | None, so None is accepted and returned with score=1.0. Downstream the resolver parses None into an empty/garbage result reported as success, and the refusal reason is silently discarded — no exception, no signal to the caller.
This is inconsistent with the batch path for the same provider, which already validates exactly these cases. langextract/providers/openai_batch.py:182-190:
if content is None:
refusal = message.get('refusal')
if refusal:
raise exceptions.InferenceRuntimeError(
f'OpenAI batch response refusal: {refusal}', ...)
raise exceptions.InferenceRuntimeError(
"OpenAI batch response body missing 'message.content'", ...)
The realtime path should apply the same guard.
Expected behavior
When choices[0].message.content is None (content filter, refusal, or tool-only message), the realtime path raises InferenceRuntimeError — surfacing the refusal reason when present — mirroring the batch path. An empty/missing completion should never be reported as a score=1.0 success.
Actual behavior
_process_single_prompt returns ScoredOutput(score=1.0, output=None). The caller receives a "successful" extraction with no content; the refusal reason (message.refusal) is lost.
Steps to reproduce the issue
- Configure
OpenAILanguageModel and issue a prompt that trips OpenAI's content filter, or set response_format and prompt something the model refuses.
- The API returns
choices[0].message.content == None with .refusal set (e.g. "I can't help with that.").
- Observe
_process_single_prompt returns ScoredOutput(score=1.0, output=None) instead of raising — the extraction is reported as a successful empty result.
Any additional content
- Suggested fix: after fetching the message, guard on empty
choices, and if message.content is None raise InferenceRuntimeError including getattr(message, 'refusal', None) when present — matching openai_batch.py:182-190.
- The Gemini realtime path has the same class of gap on a safety-blocked
response.text (version-dependent); worth folding the same guard in there too.
- Repro/verify without live API: mock
chat.completions.create to return a message with content is None and .refusal set, and assert _process_single_prompt raises InferenceRuntimeError (mirrors the existing batch refusal test).
- Environment: current
main (commit 0dff547).
Describe the overall issue and situation
In the OpenAI provider's realtime (non-batch) inference path, a refusal or content-filtered completion is returned as a successful, empty extraction instead of raising.
langextract/providers/openai.py:252-254(_process_single_prompt):message.contentisNonewhen the completion stops forcontent_filter, when a structured-output refusal is emitted (text lands inmessage.refusal), or when the message carries only tool calls.ScoredOutput.outputis typedstr | None, soNoneis accepted and returned withscore=1.0. Downstream the resolver parsesNoneinto an empty/garbage result reported as success, and the refusal reason is silently discarded — no exception, no signal to the caller.This is inconsistent with the batch path for the same provider, which already validates exactly these cases.
langextract/providers/openai_batch.py:182-190:The realtime path should apply the same guard.
Expected behavior
When
choices[0].message.content is None(content filter, refusal, or tool-only message), the realtime path raisesInferenceRuntimeError— surfacing the refusal reason when present — mirroring the batch path. An empty/missing completion should never be reported as ascore=1.0success.Actual behavior
_process_single_promptreturnsScoredOutput(score=1.0, output=None). The caller receives a "successful" extraction with no content; the refusal reason (message.refusal) is lost.Steps to reproduce the issue
OpenAILanguageModeland issue a prompt that trips OpenAI's content filter, or setresponse_formatand prompt something the model refuses.choices[0].message.content == Nonewith.refusalset (e.g."I can't help with that.")._process_single_promptreturnsScoredOutput(score=1.0, output=None)instead of raising — the extraction is reported as a successful empty result.Any additional content
choices, and ifmessage.content is NoneraiseInferenceRuntimeErrorincludinggetattr(message, 'refusal', None)when present — matchingopenai_batch.py:182-190.response.text(version-dependent); worth folding the same guard in there too.chat.completions.createto return a message withcontent is Noneand.refusalset, and assert_process_single_promptraisesInferenceRuntimeError(mirrors the existing batch refusal test).main(commit0dff547).