From 1a7f368324518838a58e22de2f0e71d0d3071b86 Mon Sep 17 00:00:00 2001 From: busya Date: Mon, 1 Jun 2026 10:17:41 +0300 Subject: [PATCH] fix(llm): handle null LLM content and skip retry on null/model errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Kilo gateway + Nvidia NeMo :free model returns content: null for image inputs, get_json_completion crashed with 'the JSON object must be str, bytes or bytearray, not NoneType' and retried 5 times (unnecessary). Fixes: - _should_retry no longer retries RuntimeError with 'null content' (retrying won't help — the model genuinely returned null) - get_json_completion now raises RuntimeError explicitly when content is None, with a clear message (instead of json.loads(None)) --- backend/src/plugins/llm_analysis/service.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/src/plugins/llm_analysis/service.py b/backend/src/plugins/llm_analysis/service.py index 70bebe54..8d5ec1fe 100644 --- a/backend/src/plugins/llm_analysis/service.py +++ b/backend/src/plugins/llm_analysis/service.py @@ -953,12 +953,19 @@ class LLMClient: # @POST Returns a parsed JSON dictionary. # @SIDE_EFFECT Calls external LLM API. def _should_retry(exception: Exception) -> bool: - """Custom retry predicate that excludes authentication errors.""" + """Custom retry predicate that excludes non-recoverable errors.""" # Don't retry on authentication errors if isinstance(exception, OpenAIAuthenticationError): return False - # Retry on rate limit errors and other exceptions - return isinstance(exception, (RateLimitError, Exception)) + # Don't retry on null content / model errors — retrying won't help + msg = str(exception).lower() + if "null content" in msg or "none" in msg: + return False + # Retry on rate limit errors + if isinstance(exception, RateLimitError): + return True + # For other exceptions, limit retries + return True @retry( stop=stop_after_attempt(5), @@ -1058,6 +1065,10 @@ class LLMClient: content = response.choices[0].message.content logger.debug(f"[get_json_completion] Raw content to parse: {content}") + # LLM returned null content — likely content filter or rate limit + if content is None: + raise RuntimeError("LLM returned null content (content filter or rate limit)") + try: return json.loads(content) except json.JSONDecodeError: