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
- 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
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
- 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
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
- {repo_dir.name} emitted the agent JSON profile; the evaluato
- [CompetitorsPlan] Invalid JSON: {exc}\n
- [CompetitorsPlan] Top-level must be a dict of {entity: {targ
- [Planner] Invalid --plan JSON: {exc}\n
- {label} {path} is not valid JSON: {exc}
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/d0a0b5d216a381a9.
Report an issue: GitHub.