{"record":{"id":"bc944e1cc946cbe6","repo":"srbhr/Resume-Matcher","slug":"empty-response-from-llm","errorCode":null,"errorMessage":"Empty response from LLM","messagePattern":"Empty response from LLM","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"apps/backend/app/llm.py","lineNumber":1263,"sourceCode":"                attempt > 0\n                and config.provider == \"azure_foundry\"\n                and reasoning_effort in (\"low\", \"medium\", \"high\")\n            ):\n                reasoning_effort = \"minimal\"\n            if reasoning_effort:\n                kwargs[\"reasoning_effort\"] = reasoning_effort\n\n            # JSON-012: Fallback to prompt-only JSON mode after JSON-mode failure.\n            # LiteLLM registry may report support for models that the upstream\n            # aggregator (OpenRouter) cannot actually serve with response_format.\n            if use_json_mode and not json_mode_failed:\n                kwargs[\"response_format\"] = {\"type\": \"json_object\"}\n\n            response = await router.acompletion(**kwargs)\n            content = _extract_choice_text(response.choices[0])\n\n            if not content:\n                raise ValueError(\"Empty response from LLM\")\n\n            logging.debug(\n                f\"LLM response (attempt {attempt + 1}): {content[:300]}\")\n\n            # Extract and parse JSON\n            json_str = _extract_json(content)\n            result = json.loads(json_str)\n\n            # LLM-001: Check if parsed result appears truncated\n            if isinstance(result, dict) and _appears_truncated(result, schema_type):\n                if attempt < retries:\n                    logging.warning(\n                        \"Parsed JSON appears truncated (attempt %d/%d), retrying\",\n                        attempt + 1,\n                        retries + 1,\n                    )\n                    if schema_type == \"resume\":\n                        hint = (","sourceCodeStart":1245,"sourceCodeEnd":1281,"githubUrl":"https://github.com/srbhr/Resume-Matcher/blob/116f9cc3b00e1ac91734a6c2679bf41ea64a0edc/apps/backend/app/llm.py#L1245-L1281","documentation":"complete_json extracts the first choice's text via _extract_choice_text and raises this ValueError when the content is empty/whitespace — i.e. the provider returned a successful response with no usable text (finish_reason may be length, content filter, or an empty message).","triggerScenarios":"router.acompletion succeeds but choices[0] has empty text — e.g. max_tokens too low so all budget went to thinking tokens, content filter stripped output, or a reasoning model emitted only hidden reasoning.","commonSituations":"max_tokens set below the model's thinking overhead on reasoning models (deepseek-r1/qwq); provider content-filter; transient provider bug returning an empty message.","solutions":["Increase max_tokens — reasoning models consume budget with thinking before any visible output","Switch to a non-reasoning model or one whose thinking tags are stripped into content","complete_json already retries with backoff — check logs for repeated empty responses and the finish_reason","Verify the provider isn't applying content filters to your prompt"],"exampleFix":"// before\nkwargs[\"max_tokens\"] = 256;\n// after\nkwargs[\"max_tokens\"] = 4096; // leave headroom for reasoning tokens","handlingStrategy":"retry","validationCode":"def response_has_text(resp) -> bool:\n    try:\n        return bool(resp[\"choices\"][0][\"message\"][\"content\"].strip())\n    except (KeyError, IndexError, AttributeError, TypeError):\n        return False","typeGuard":"function hasChoiceText(resp: unknown): resp is { choices: { message: { content: string } }[] } {\n  const r = resp as any;\n  return Array.isArray(r?.choices) && r.choices.length > 0 &&\n    typeof r.choices[0]?.message?.content === 'string' &&\n    r.choices[0].message.content.trim().length > 0;\n}","tryCatchPattern":"try:\n    data = await complete_json(prompt)\nexcept ValueError as e:\n    if e.message.includes('Empty response from LLM'):\n        data = await complete_json(prompt, { maxTokens: 8192 })  // retry with more headroom\n    else:\n        throw e","preventionTips":["Set generous max_tokens, especially for reasoning models whose thinking consumes the budget","Check finish_reason (length/content_filter) in raw responses","Prefer non-reasoning models for structured JSON output","Rely on complete_json's built-in retry/backoff rather than calling once"],"tags":["llm","empty-response","backend","retry"],"backgroundTag":"empty-llm-response","analyzedSha":"116f9cc3b00e1ac91734a6c2679bf41ea64a0edc","analyzedAt":"2026-08-28T22:51:40.999Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}