From 9b3cd480487854754e4a2a8ad0cce3197f2daeba Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Tue, 21 Apr 2026 05:59:07 -0500 Subject: [PATCH] Raise LLMOutputUnparseable on empty LLM responses instead of silently returning "" When Azure OpenAI or Anthropic returns null/empty content (content filter, truncation, transient failure), call_openai/call_anthropic now raise LLMOutputUnparseable instead of returning an empty string that silently flows through the pipeline and produces 422 "Could not generate any optimizations." All optimizer callers catch LLMOutputUnparseable to preserve cost tracking while returning None. --- django/aiservice/aiservice/llm.py | 26 ++++++++++++++++++- .../core/languages/java/optimizer.py | 5 +++- .../core/languages/java/optimizer_lp.py | 5 +++- .../core/languages/js_ts/optimizer.py | 5 +++- .../core/languages/js_ts/optimizer_lp.py | 5 +++- .../python/jit_rewrite/jit_rewrite.py | 5 +++- .../languages/python/optimizer/optimizer.py | 5 +++- .../optimizer/optimizer_line_profiler.py | 5 +++- 8 files changed, 53 insertions(+), 8 deletions(-) diff --git a/django/aiservice/aiservice/llm.py b/django/aiservice/aiservice/llm.py index e41831830..ceeb6ea71 100644 --- a/django/aiservice/aiservice/llm.py +++ b/django/aiservice/aiservice/llm.py @@ -198,6 +198,17 @@ class LLMClient: response = await self.anthropic_client.messages.create(**kwargs) # type: ignore[union-attr] content = "".join(block.text for block in response.content if hasattr(block, "text")) + if not content: + logger.warning( + "Anthropic returned empty content: model=%s, stop_reason=%s", + llm.name, + response.stop_reason, + ) + raise LLMOutputUnparseable( + f"Empty response from {llm.name} (stop_reason={response.stop_reason})", + cost=calculate_llm_cost(response, llm), + ) + return LLMResponse( content=content, usage=LLMUsage(input_tokens=response.usage.input_tokens, output_tokens=response.usage.output_tokens), @@ -216,8 +227,21 @@ class LLMClient: model=llm.name, messages=messages, max_tokens=max_tokens ) + content = response.choices[0].message.content if response.choices else None + if not content: + finish = response.choices[0].finish_reason if response.choices else "unknown" + logger.warning( + "OpenAI returned empty content: model=%s, finish_reason=%s", + llm.name, + finish, + ) + raise LLMOutputUnparseable( + f"Empty response from {llm.name} (finish_reason={finish})", + cost=calculate_llm_cost(response, llm), + ) + return LLMResponse( - content=response.choices[0].message.content or "", + content=content, usage=LLMUsage( input_tokens=response.usage.prompt_tokens if response.usage else 0, output_tokens=response.usage.completion_tokens if response.usage else 0, diff --git a/django/aiservice/core/languages/java/optimizer.py b/django/aiservice/core/languages/java/optimizer.py index 583703f38..24970e6c0 100644 --- a/django/aiservice/core/languages/java/optimizer.py +++ b/django/aiservice/core/languages/java/optimizer.py @@ -17,7 +17,7 @@ from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUs from aiservice.analytics.posthog import ph from aiservice.common_utils import validate_trace_id from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable -from aiservice.llm import llm_client +from aiservice.llm import LLMOutputUnparseable, llm_client from aiservice.llm_models import LLM, OPTIMIZE_MODEL from authapp.auth import AuthenticatedRequest from authapp.user import get_user_by_id @@ -119,6 +119,9 @@ async def optimize_java_code_single( python_version="N/A", # Not applicable for Java context=obs_context, ) + except LLMOutputUnparseable as e: + debug_log_sensitive_data(f"Empty LLM response for Java source:\n{source_code}") + return None, e.cost, optimize_model.name except Exception: debug_log_sensitive_data(f"Failed to generate code for Java source:\n{source_code}") return None, None, optimize_model.name diff --git a/django/aiservice/core/languages/java/optimizer_lp.py b/django/aiservice/core/languages/java/optimizer_lp.py index 0e37856b6..259eb15dc 100644 --- a/django/aiservice/core/languages/java/optimizer_lp.py +++ b/django/aiservice/core/languages/java/optimizer_lp.py @@ -17,7 +17,7 @@ from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUs from aiservice.analytics.posthog import ph from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable -from aiservice.llm import llm_client +from aiservice.llm import LLMOutputUnparseable, llm_client from aiservice.llm_models import OPTIMIZE_MODEL from aiservice.validators.java_validator import validate_java_syntax from core.languages.java.optimizer import is_multi_context_java @@ -166,6 +166,9 @@ Here is the code to optimize: python_version=f"Java {language_version}", context=obs_context, ) + except LLMOutputUnparseable as e: + debug_log_sensitive_data(f"Empty LLM response for source:\n{source_code}") + return None, e.cost, optimize_model.name except Exception: debug_log_sensitive_data(f"Failed to generate code for source:\n{source_code}") return None, None, optimize_model.name diff --git a/django/aiservice/core/languages/js_ts/optimizer.py b/django/aiservice/core/languages/js_ts/optimizer.py index d0e6bcee7..27edcdfe4 100644 --- a/django/aiservice/core/languages/js_ts/optimizer.py +++ b/django/aiservice/core/languages/js_ts/optimizer.py @@ -19,7 +19,7 @@ from aiservice.analytics.posthog import ph from aiservice.common.markdown_utils import split_markdown_code from aiservice.common_utils import validate_trace_id from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable -from aiservice.llm import llm_client +from aiservice.llm import LLMOutputUnparseable, llm_client from aiservice.llm_models import LLM, OPTIMIZE_MODEL from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax from authapp.auth import AuthenticatedRequest @@ -155,6 +155,9 @@ You MUST output the target file. You may also output helper files if you optimiz python_version=language_version, # Reusing python_version field for language version context=obs_context, ) + except LLMOutputUnparseable as e: + debug_log_sensitive_data(f"Empty LLM response for source:\n{source_code}") + return None, e.cost, optimize_model.name except Exception: debug_log_sensitive_data(f"Failed to generate code for source:\n{source_code}") return None, None, optimize_model.name diff --git a/django/aiservice/core/languages/js_ts/optimizer_lp.py b/django/aiservice/core/languages/js_ts/optimizer_lp.py index b4a73382e..2c6ca6a9b 100644 --- a/django/aiservice/core/languages/js_ts/optimizer_lp.py +++ b/django/aiservice/core/languages/js_ts/optimizer_lp.py @@ -18,7 +18,7 @@ from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUs from aiservice.analytics.posthog import ph from aiservice.common.markdown_utils import split_markdown_code from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable -from aiservice.llm import llm_client +from aiservice.llm import LLMOutputUnparseable, llm_client from aiservice.llm_models import OPTIMIZE_MODEL from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax from core.languages.js_ts.context_helpers import is_multi_context_js, is_multi_context_ts @@ -167,6 +167,9 @@ Here is the code to optimize: python_version=language_version, # Reusing python_version field for language version context=obs_context, ) + except LLMOutputUnparseable as e: + debug_log_sensitive_data(f"Empty LLM response for source:\n{source_code}") + return None, e.cost, optimize_model.name except Exception: debug_log_sensitive_data(f"Failed to generate code for source:\n{source_code}") return None, None, optimize_model.name diff --git a/django/aiservice/core/languages/python/jit_rewrite/jit_rewrite.py b/django/aiservice/core/languages/python/jit_rewrite/jit_rewrite.py index 163209785..0a7835bc6 100644 --- a/django/aiservice/core/languages/python/jit_rewrite/jit_rewrite.py +++ b/django/aiservice/core/languages/python/jit_rewrite/jit_rewrite.py @@ -16,7 +16,7 @@ from aiservice.analytics.posthog import ph from aiservice.background import fire_and_forget from aiservice.common_utils import parse_python_version, validate_trace_id from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable -from aiservice.llm import llm_client +from aiservice.llm import LLMOutputUnparseable, llm_client from aiservice.llm_models import LLM, OPTIMIZE_MODEL from authapp.auth import AuthenticatedRequest from authapp.user import get_user_by_id @@ -74,6 +74,9 @@ async def jit_rewrite_python_code_single( python_version=python_version_str, context=obs_context, ) + except LLMOutputUnparseable as e: + debug_log_sensitive_data(f"Empty LLM response for source:\n{ctx.source_code}") + return None, e.cost, jit_rewrite_model.name except Exception: debug_log_sensitive_data(f"Failed to generate code for source:\n{ctx.source_code}") return None, None, jit_rewrite_model.name diff --git a/django/aiservice/core/languages/python/optimizer/optimizer.py b/django/aiservice/core/languages/python/optimizer/optimizer.py index 70227feeb..ee2688dfa 100644 --- a/django/aiservice/core/languages/python/optimizer/optimizer.py +++ b/django/aiservice/core/languages/python/optimizer/optimizer.py @@ -15,7 +15,7 @@ from aiservice.analytics.posthog import ph from aiservice.background import fire_and_forget from aiservice.common_utils import parse_python_version, validate_trace_id from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable -from aiservice.llm import llm_client +from aiservice.llm import LLMOutputUnparseable, llm_client from aiservice.llm_models import LLM, OPTIMIZE_MODEL from authapp.user import get_user_by_id from core.languages.python.optimizer.context_utils.optimizer_context import BaseOptimizerContext @@ -82,6 +82,9 @@ async def generate_optimization_candidate( python_version=python_version_str, context=obs_context, ) + except LLMOutputUnparseable as e: + debug_log_sensitive_data(f"Empty LLM response for source:\n{ctx.source_code}") + return None, e.cost, optimize_model.name except Exception: debug_log_sensitive_data(f"Failed to generate code for source:\n{ctx.source_code}") return None, None, optimize_model.name diff --git a/django/aiservice/core/languages/python/optimizer/optimizer_line_profiler.py b/django/aiservice/core/languages/python/optimizer/optimizer_line_profiler.py index d0c86bd65..1d059c41f 100644 --- a/django/aiservice/core/languages/python/optimizer/optimizer_line_profiler.py +++ b/django/aiservice/core/languages/python/optimizer/optimizer_line_profiler.py @@ -13,7 +13,7 @@ from aiservice.background import fire_and_forget from aiservice.common.markdown_utils import split_markdown_code from aiservice.common_utils import parse_python_version, validate_trace_id from aiservice.env_specific import debug_log_sensitive_data, debug_log_sensitive_data_from_callable -from aiservice.llm import llm_client +from aiservice.llm import LLMOutputUnparseable, llm_client from aiservice.llm_models import OPTIMIZE_MODEL from aiservice.validators.javascript_validator import validate_javascript_syntax, validate_typescript_syntax from core.languages.java.optimizer_lp import optimize_java_code_line_profiler @@ -90,6 +90,9 @@ async def optimize_python_code_line_profiler_single( python_version=python_version_str, context=obs_context, ) + except LLMOutputUnparseable as e: + debug_log_sensitive_data(f"Empty LLM response for source:\n{ctx.source_code}") + return None, e.cost, optimize_model.name except Exception: debug_log_sensitive_data(f"Failed to generate code for source:\n{ctx.source_code}") return None, None, optimize_model.name