mvanhorn/last30days-skill · error · ValueError

Expected JSON response, got empty text

Error message

Expected JSON response, got empty text

What it means

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.

Source

Thrown at skills/last30days/scripts/lib/providers.py:339

    - Any known pin (X_BACKEND_KNOWN) exclusively: returns pin if available, None otherwise
    - Unpinned: walks auto-chain (X_BACKEND_ORDER) only, never auto-selects opt-in backends
    """
    return env.get_x_source(config)


def _require_gemini_31(model: str, *, role: str) -> None:
    if model.startswith("gemini-3.1-"):
        return
    raise RuntimeError(
        f"{role} must use a Gemini 3.1 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 c7460f6114)

Solutions

  1. Retry the model call once - empty responses are frequently transient
  2. Inspect finishReason/safety metadata on the payload to distinguish blocks from truncation
  3. Raise or remove output-token caps that can zero out the reply
  4. Tighten the prompt to demand a JSON object as the entire response

Example fix

# before
data = extract_json(response_text)  # raises on ''

# after
for attempt in range(2):
    response_text = call_model(prompt)
    if response_text.strip():
        break
data = extract_json(response_text)
Defensive patterns

Strategy: retry

Validate before calling

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

if not has_content(model_reply):
    model_reply = call_model_again(prompt)

Type guard

def is_parseable_model_text(text: str | None) -> bool:
    return isinstance(text, str) and len(text.strip()) > 0

Try / catch

try:
    data = extract_json(reply)
except ValueError as e:
    if "empty text" in str(e):
        reply = call_model(prompt)  # one retry; empty replies are often transient
        data = extract_json(reply)
    raise

Prevention

When it happens

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

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

Related errors


AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15). Data as JSON: /api/errors/d0a0b5d216a381a9. Report an issue: GitHub.