mvanhorn/last30days-skill · error · RuntimeError

Gemini HTTP {exc.code}: {detail}

Error message

Gemini HTTP {exc.code}: {detail}

What it means

call_gemini_judge() wraps urllib's HTTPError from urlopen(request, timeout=120) into a RuntimeError that embeds the HTTP status code and the raw error body read from the response stream. It fires only for non-2xx HTTP responses from the Gemini REST endpoint — anything the API rejects with 4xx/5xx.

Source

Thrown at skills/last30days/scripts/evaluate_search_quality.py:231


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")
        raise RuntimeError(f"Gemini HTTP {exc.code}: {detail}") from exc
    except URLError as exc:
        raise RuntimeError(f"Gemini request failed: {exc}") from exc
    return json.loads(extract_gemini_text(payload))


def build_judge_prompt(topic: str, query_type: str, items: list[dict[str, Any]]) -> str:
    item_lines = []
    for item in items:
        item_lines.append(
            "\n".join([
                f"- id: {item['key']}",
                f"  source: {item['source']}",
                f"  title: {item['text'][:220]}",
                f"  url: {item['url']}",
                f"  date: {item.get('date') or 'unknown'}",
            ])
        )
    return f"""

View on GitHub (pinned to c7460f6114)

Solutions

  1. Match on the embedded status: 400/403 means bad key or model — fix GEMINI_API_KEY/GOOGLE_GENAI_API_KEY or the --judge-model value; 404 means the model id in GEMINI_API_URL is wrong for your key.
  2. For 429: add exponential backoff retry around call_gemini_judge (honor retryDelay in the detail body).
  3. For 5xx: retry once or twice with the same backoff; the call is idempotent at temperature 0.
  4. Never log the URL — it contains the API key; log only exc.code and detail.

Example fix

# before
result = call_gemini_judge(api_key, model, prompt)

# after
for attempt in range(3):
    try:
        result = call_gemini_judge(api_key, model, prompt)
        break
    except RuntimeError as exc:
        if 'HTTP 429' not in str(exc) and 'HTTP 5' not in str(exc):
            raise
        if attempt == 2:
            raise
        time.sleep(2 ** attempt)
Defensive patterns

Strategy: retry

Try / catch

for attempt in range(3):
    try:
        return call_gemini_judge(api_key, model, prompt)
    except RuntimeError as exc:
        transient = 'HTTP 429' in str(exc) or 'HTTP 500' in str(exc) or 'HTTP 503' in str(exc)
        if not transient or attempt == 2:
            raise
        time.sleep(2 ** attempt)

Prevention

When it happens

Trigger: urlopen raising HTTPError: 400 (malformed JSON body or bad model name in GEMINI_API_URL.format), 401/403 (invalid or unmapped API key), 404 (unknown model id), 429 (rate limit), 5xx (server error). The api_key is interpolated directly into the URL, so a wrong key yields 400/403.

Common situations: GEMINI_API_KEY / GOOGLE_GENAI_API_KEY env var missing or set to a placeholder; using a deprecated model name; hitting free-tier RPM limits while scoring many topics; transient 500s from the generateContent endpoint.

Related errors


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