nexu-io/open-design · error · ValueError

Expected JSON response, got empty text

Error message

Expected JSON response, got empty text

What it means

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.

Source

Thrown at design-templates/last30days/scripts/lib/providers.py:359

    preferred = (config.get("LAST30DAYS_X_BACKEND") or "").lower()
    if preferred in {"xai", "bird"}:
        return preferred
    return env.get_x_source(config)


def _require_gemini_31_preview(model: str, *, role: str) -> None:
    if model.startswith("gemini-3.1-") and model.endswith("-preview"):
        return
    raise RuntimeError(
        f"{role} must use a Gemini 3.1 preview model. Got: {model}"
    )


def extract_json(text: str) -> dict[str, Any]:
    """Extract the first JSON object from a model response."""
    text = text.strip()
    if not text:
        raise ValueError("Expected JSON response, got empty text")
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        match = re.search(r"\{[\s\S]*\}", text)
        if not match:
            raise
        return json.loads(match.group(0))


def extract_gemini_text(payload: dict[str, Any]) -> str:
    for candidate in payload.get("candidates", []):
        content = candidate.get("content") or {}
        for part in content.get("parts", []):
            text = part.get("text")
            if text:
                return text
    if payload:
        print(f"[Providers] extract_gemini_text: no text in payload keys: {list(payload.keys())}", file=sys.stderr)

View on GitHub (pinned to 5be4028344)

Solutions

  1. Inspect the raw model payload (enable response logging) to confirm whether the body was actually empty.
  2. Retry the call; transient empty responses from safety filters or load balancers often succeed on retry.
  3. Tighten the prompt to avoid tripping safety filters, or request a non-empty structured schema explicitly.
  4. If using a streaming/relay layer, verify it forwards the full body to extract_json.

Example fix

# before
text = ""
extract_json(text)  # ValueError

# after
from lib import providers
if not text.strip():
    raise RuntimeError("provider returned empty body; retry or inspect filter")
providers.extract_json(text)
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

def has_json_body(text: str) -> bool:
    return bool(text and text.strip())

Try / catch

from lib.providers import extract_json

last_err = None
for attempt in range(3):
    text = client.complete(prompt)
    if text and text.strip():
        try:
            return extract_json(text)
        except (ValueError, json.JSONDecodeError) as e:
            last_err = e
    # empty or unparseable -> retry
if last_err:
    raise last_err
raise RuntimeError("provider returned empty body after retries")

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/3968da50337a781e. Report an issue: GitHub.