{"record":{"id":"3968da50337a781e","repo":"nexu-io/open-design","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":"design-templates/last30days/scripts/lib/providers.py","lineNumber":359,"sourceCode":"    preferred = (config.get(\"LAST30DAYS_X_BACKEND\") or \"\").lower()\n    if preferred in {\"xai\", \"bird\"}:\n        return preferred\n    return env.get_x_source(config)\n\n\ndef _require_gemini_31_preview(model: str, *, role: str) -> None:\n    if model.startswith(\"gemini-3.1-\") and model.endswith(\"-preview\"):\n        return\n    raise RuntimeError(\n        f\"{role} must use a Gemini 3.1 preview 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":341,"sourceCodeEnd":377,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/design-templates/last30days/scripts/lib/providers.py#L341-L377","documentation":"Raised by extract_json after stripping the model response text. An empty (or whitespace-only) response cannot even enter the json.loads fallback regex path, so it is rejected up front with ValueError. This is a model-output contract failure, not a config problem.","triggerScenarios":"A reasoning client (Gemini/OpenAI/xAI/OpenRouter) returns an empty string for a prompt that expected JSON. Causes include content filters blanking the response, token-budget exhaustion, model returning only whitespace, or a transport error returning an empty body.","commonSituations":"Safety filters triggered on the prompt and the API returned an empty completion. A streaming/middleware bug dropped the body. Rate-limited responses parsed as empty. Models configured with max_tokens=0 or broken tool-call wrappers.","solutions":["Inspect the raw model payload (enable response logging) to confirm whether the body was actually empty.","Retry the call; transient empty responses from safety filters or load balancers often succeed on retry.","Tighten the prompt to avoid tripping safety filters, or request a non-empty structured schema explicitly.","If using a streaming/relay layer, verify it forwards the full body to extract_json."],"exampleFix":"# before\ntext = \"\"\nextract_json(text)  # ValueError\n\n# after\nfrom lib import providers\nif not text.strip():\n    raise RuntimeError(\"provider returned empty body; retry or inspect filter\")\nproviders.extract_json(text)","handlingStrategy":"retry","validationCode":"null","typeGuard":"def has_json_body(text: str) -> bool:\n    return bool(text and text.strip())","tryCatchPattern":"from lib.providers import extract_json\n\nlast_err = None\nfor attempt in range(3):\n    text = client.complete(prompt)\n    if text and text.strip():\n        try:\n            return extract_json(text)\n        except (ValueError, json.JSONDecodeError) as e:\n            last_err = e\n    # empty or unparseable -> retry\nif last_err:\n    raise last_err\nraise RuntimeError(\"provider returned empty body after retries\")","preventionTips":["Log the raw provider response when extract_json fails so the empty-body cause is visible.","Retry empty responses once or twice; safety-filter blanks are often transient across prompt variations.","If empties persist, tighten the prompt to avoid tripping safety filters."],"tags":["parsing","json","llm-response","retry"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}