Bug: Gemini batch path returns empty success on a safety-blocked or refused response
Describe the overall issue and situation
In the Gemini batch inference path, a batch item that was safety-blocked or refused is silently reported as a successful empty extraction instead of surfacing the block/refusal reason.
langextract/providers/gemini_batch.py (_parse_batch_line):
resp = obj.get("response", {})
text = _extract_text(resp) or ""_extract_text returns None when it can't find resp.text / candidates[0].content.parts[0].text — which includes both "the field genuinely doesn't exist" and "this candidate was blocked/refused with no text content." The or "" collapses both cases into an empty string, indistinguishable from a model that actually returned empty text. There's no check of obj["response"]'s own block/finish-reason fields before falling back.
This is the same class of bug as the realtime paths: #491 (OpenAI, fixed in #496) and #508 (Gemini realtime, PR #507 open) — both raise InferenceRuntimeError with a diagnostic instead of silently returning empty output on a safety-blocked/refused response. Neither #496 nor #507 touched gemini_batch.py, so the batch path still has the gap.
Expected behavior
When a batch item's response has no extractable text because it was blocked or refused (as opposed to a response that's simply malformed/missing), _parse_batch_line should surface that as an error for the item rather than silently writing "" to outputs[idx].
Actual behavior
_parse_batch_line writes "" to outputs[idx] for a blocked/refused item, indistinguishable from a genuinely empty extraction. The block/refusal reason (if present in the response's own feedback/finish-reason fields) is discarded.
Steps to reproduce the issue
- Submit a batch job containing a prompt that trips Gemini's safety filters, or otherwise produces a candidate with no text content.
- In the resulting JSONL output, that line's
responsehas no extractabletext(mirrors the realtime path'sresponse.text is Nonecase, confirmed againstgoogle-genai's response shape in #508). - Observe
_parse_batch_linesetsoutputs[idx] = ""instead of raising or otherwise flagging the item.
Any additional content
- Verified without live API: current
main(langextract/providers/gemini_batch.py:622) still hastext = _extract_text(resp) or ""with no check of the response's own block/finish-reason fields first. - Fix should follow the same shape as #507's realtime-path fix: inspect the response for a block/finish reason before falling back to empty, and raise
InferenceRuntimeErrorwith the available diagnostic when one is found — while leaving a response that's genuinely just empty (no diagnostic available) ascfg.ignore_item_errors-gated behavior, matching how_parse_batch_linealready treats the siblingobj["error"]field.
Source: google/langextract