Skip to content

Bug: OpenAI realtime path returns empty success (score=1.0, output=None) on a refusal / content-filtered response #491

Description

@thejesh23

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

  1. Configure OpenAILanguageModel and issue a prompt that trips OpenAI's content filter, or set response_format and prompt something the model refuses.
  2. The API returns choices[0].message.content == None with .refusal set (e.g. "I can't help with that.").
  3. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions