mvanhorn/last30days-skill · error · ValueError
Gemini response did not contain text.
Error message
Gemini response did not contain text.
What it means
Raised by extract_gemini_text() in the search-quality evaluator after a Gemini generateContent call succeeded at the HTTP level but no candidate contained a non-empty text part. This happens when the model returns only empty parts, a finishReason like SAFETY/MAX_TOKENS with no content, or an empty candidates array. The evaluator requires JSON text output (responseMimeType=application/json), so a contentless response cannot be scored.
Source
Thrown at skills/last30days/scripts/evaluate_search_quality.py:212
def resolve_google_judge_api_key(config: dict[str, Any]) -> str | None:
return (
os.environ.get("GOOGLE_API_KEY")
or config.get("GOOGLE_API_KEY")
or os.environ.get("GEMINI_API_KEY")
or config.get("GEMINI_API_KEY")
or os.environ.get("GOOGLE_GENAI_API_KEY")
or config.get("GOOGLE_GENAI_API_KEY")
)
def extract_gemini_text(payload: dict[str, Any]) -> str:
for candidate in payload.get("candidates") or []:
content = candidate.get("content") or {}
for part in content.get("parts") or []:
if part.get("text"):
return part["text"]
raise ValueError("Gemini response did not contain text.")
def call_gemini_judge(api_key: str, model: str, prompt: str) -> dict[str, Any]:
body = {
"contents": [{"parts": [{"text": prompt}]}],
"generationConfig": {"temperature": 0, "responseMimeType": "application/json"},
}
request = Request(
GEMINI_API_URL.format(model=model, api_key=api_key),
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urlopen(request, timeout=120) as response:
payload = json.loads(response.read().decode("utf-8"))
except HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")View on GitHub (pinned to c7460f6114)
Solutions
- Inspect payload['candidates'][0].get('finishReason') and promptFeedback.blockReason before treating the response as scorable; log the full payload once.
- If blockReason/SAFETY: shorten or sanitize the judge prompt items (titles/urls at line 235-240) and retry.
- If empty candidates persists, retry with a different model or lower maxOutputTokens pressure; add a retry loop around call_gemini_judge.
- Catch ValueError at the call site in evaluate_search_quality.py and surface 'judge returned no text (finishReason=X)' instead of a bare ValueError.
Example fix
// before
return json.loads(extract_gemini_text(payload))
// after
finish = (payload.get('candidates') or [{}])[0].get('finishReason')
block = (payload.get('promptFeedback') or {}).get('blockReason')
if block or not finish or finish not in ('STOP', 'MAX_TOKENS'):
raise RuntimeError(f'Gemini judge produced no text (finishReason={finish}, blockReason={block})')
return json.loads(extract_gemini_text(payload)) Defensive patterns
Strategy: try-catch
Validate before calling
def gemini_payload_has_text(payload: dict) -> bool:
return any(
part.get('text')
for cand in payload.get('candidates') or []
for part in (cand.get('content') or {}).get('parts') or []
) Type guard
def is_text_gemini_payload(payload: Any) -> TypeGuard[dict]:
return (
isinstance(payload, dict)
and isinstance(payload.get('candidates'), list)
and gemini_payload_has_text(payload)
) Try / catch
try:
verdict = call_gemini_judge(api_key, model, prompt)
except ValueError as exc:
finish = (payload.get('candidates') or [{}])[0].get('finishReason', 'UNKNOWN')
raise RuntimeError(f'judge produced no text (finishReason={finish}); retry with sanitized prompt') from exc Prevention
- Check promptFeedback.blockReason and candidates[0].finishReason before extracting text.
- Keep judge prompts free of raw scraped URLs/titles that trip safety filters.
- Cap items per judge prompt so MAX_TOKENS cannot consume the entire response.
When it happens
Trigger: POST to GEMINI_API_URL generateContent with generationConfig {temperature: 0, responseMimeType: 'application/json'}; response payload where candidates is [], or every candidate's content.parts contains no part with a truthy 'text' field (e.g. finishReason=SAFETY, RECITATION, or empty completion).
Common situations: Safety filters blocking the prompt (judge prompt includes scraped titles/urls that trip filters); API key quota/billing issues that return a 200 with empty candidates; model returning thoughts only; prompt too long causing MAX_TOKENS with no text.
Related errors
- Gemini HTTP {exc.code}: {detail}
- Gemini request failed: {exc}
- {repo_dir.name} failed for '{topic}' with exit {result.retur
- {repo_dir.name} emitted the agent JSON profile; the evaluato
- Gemini selected but no Google API key is configured.
AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15).
Data as JSON: /api/errors/55806171c2e004f1.
Report an issue: GitHub.