From f5fea13dbc235438e8090cd9a142acce383ec107 Mon Sep 17 00:00:00 2001 From: Jonathan Hill Date: Mon, 18 May 2026 06:04:45 -0500 Subject: [PATCH] fix: guard against empty choices and None message in LLM responses (#924) The OpenAI-compatible API can return HTTP 200 with an empty `choices` list or with `choices[0].message = None` (e.g. content-filtered responses on Gemini, overwhelmed Ollama instances). Without a guard, both sites raise an unhandled IndexError or AttributeError. `_call_openai_compat` already documents this hazard ("Ollama can return HTTP 200 with empty/null content") and has `_response_is_hollow` logic downstream, but `_response_is_hollow` is unreachable when the choices list itself is empty. The new guard closes that gap. Co-authored-by: Claude Sonnet 4.6 --- graphify/llm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/graphify/llm.py b/graphify/llm.py index fb65ee11d..58786f681 100644 --- a/graphify/llm.py +++ b/graphify/llm.py @@ -345,6 +345,8 @@ def _call_openai_compat( keep_alive = os.environ.get("GRAPHIFY_OLLAMA_KEEP_ALIVE", "30m") kwargs["extra_body"] = {"options": {"num_ctx": num_ctx}, "keep_alive": keep_alive} resp = client.chat.completions.create(**kwargs) + if not resp.choices or resp.choices[0].message is None: + raise ValueError("LLM returned empty or filtered response") raw_content = resp.choices[0].message.content result = _parse_llm_json(raw_content or "{}") result["input_tokens"] = resp.usage.prompt_tokens if resp.usage else 0 @@ -1038,6 +1040,8 @@ def _call_llm(prompt: str, *, backend: str, max_tokens: int = 200) -> str: if "moonshot" in cfg["base_url"]: kwargs["extra_body"] = {"thinking": {"type": "disabled"}} resp = client.chat.completions.create(**kwargs) + if not resp.choices or resp.choices[0].message is None: + raise ValueError("LLM returned empty or filtered response") return resp.choices[0].message.content or ""