{"record":{"id":"d0a0b5d216a381a9","repo":"mvanhorn/last30days-skill","slug":"expected-json-response-got-empty-text","errorCode":null,"errorMessage":"Expected JSON response, got empty text","messagePattern":"Expected JSON response, got empty text","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/last30days/scripts/lib/providers.py","lineNumber":339,"sourceCode":"    - Any known pin (X_BACKEND_KNOWN) exclusively: returns pin if available, None otherwise\n    - Unpinned: walks auto-chain (X_BACKEND_ORDER) only, never auto-selects opt-in backends\n    \"\"\"\n    return env.get_x_source(config)\n\n\ndef _require_gemini_31(model: str, *, role: str) -> None:\n    if model.startswith(\"gemini-3.1-\"):\n        return\n    raise RuntimeError(\n        f\"{role} must use a Gemini 3.1 model. Got: {model}\"\n    )\n\n\ndef extract_json(text: str) -> dict[str, Any]:\n    \"\"\"Extract the first JSON object from a model response.\"\"\"\n    text = text.strip()\n    if not text:\n        raise ValueError(\"Expected JSON response, got empty text\")\n    try:\n        return json.loads(text)\n    except json.JSONDecodeError:\n        match = re.search(r\"\\{[\\s\\S]*\\}\", text)\n        if not match:\n            raise\n        return json.loads(match.group(0))\n\n\ndef extract_gemini_text(payload: dict[str, Any]) -> str:\n    for candidate in payload.get(\"candidates\", []):\n        content = candidate.get(\"content\") or {}\n        for part in content.get(\"parts\", []):\n            text = part.get(\"text\")\n            if text:\n                return text\n    if payload:\n        print(f\"[Providers] extract_gemini_text: no text in payload keys: {list(payload.keys())}\", file=sys.stderr)","sourceCodeStart":321,"sourceCodeEnd":357,"githubUrl":"https://github.com/mvanhorn/last30days-skill/blob/c7460f6114449ddfe6ea3fc2f23c3d910c0e740c/skills/last30days/scripts/lib/providers.py#L321-L357","documentation":"ValueError from extract_json when a reasoning-model response strips to an empty string - there is no JSON (or fenced JSON) to extract. It is the earliest failure of the response-parsing chain, raised before json.loads is even attempted.","triggerScenarios":"extract_json called on a model reply that is empty or whitespace-only: safety-blocked responses with no parts, finishReason MAX_TOKENS cutting output at zero tokens, network layers returning empty bodies, or candidates with no content part.","commonSituations":"Gemini/OpenAI safety filters blocking the prompt; token budgets set so low the model emits nothing; prompt templates asking for JSON but the model returns only whitespace; API outages returning 200 with empty payloads.","solutions":["Retry the model call once - empty responses are frequently transient","Inspect finishReason/safety metadata on the payload to distinguish blocks from truncation","Raise or remove output-token caps that can zero out the reply","Tighten the prompt to demand a JSON object as the entire response"],"exampleFix":"# before\ndata = extract_json(response_text)  # raises on ''\n\n# after\nfor attempt in range(2):\n    response_text = call_model(prompt)\n    if response_text.strip():\n        break\ndata = extract_json(response_text)","handlingStrategy":"retry","validationCode":"def has_content(text: str) -> bool:\n    return bool(text and text.strip())\n\nif not has_content(model_reply):\n    model_reply = call_model_again(prompt)","typeGuard":"def is_parseable_model_text(text: str | None) -> bool:\n    return isinstance(text, str) and len(text.strip()) > 0","tryCatchPattern":"try:\n    data = extract_json(reply)\nexcept ValueError as e:\n    if \"empty text\" in str(e):\n        reply = call_model(prompt)  # one retry; empty replies are often transient\n        data = extract_json(reply)\n    raise","preventionTips":["Check reply.strip() before parsing instead of catching after","Inspect finishReason/safety block metadata to distinguish blocked from truncated replies","Keep output-token budgets above the minimum needed for a JSON object"],"tags":["providers","llm-parsing","json","valueerror"],"backgroundTag":null,"analyzedSha":"c7460f6114449ddfe6ea3fc2f23c3d910c0e740c","analyzedAt":"2026-08-15T03:34:49.540Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}