invoke-ai/InvokeAI · error · ExternalProviderRequestError
Gemini response payload was not a JSON object
Error message
Gemini response payload was not a JSON object
What it means
ExternalProviderRequestError raised when Gemini responds 200 OK but response.json() yields something other than a JSON object (dict) — for example a JSON array, string, number, or null. The provider expects the standard GenerateContentResponse object shape and refuses to parse anything else. This almost always indicates the configured endpoint/proxy is not the real Gemini API or an intermediary mangled the response.
Source
Thrown at invokeai/app/services/external_generation/providers/gemini.py:126
params={"key": api_key},
json=payload,
timeout=120,
)
if not response.ok:
if response.status_code == 429:
retry_after = _parse_retry_after(response.headers.get("retry-after"))
raise ExternalProviderRateLimitError(
f"Gemini rate limit exceeded. {f'Retry after {retry_after:.0f}s.' if retry_after else 'Please try again later.'}",
retry_after=retry_after,
)
raise ExternalProviderRequestError(
f"Gemini request failed with status {response.status_code} for model '{model_id}': {response.text}"
)
data = response.json()
if not isinstance(data, dict):
raise ExternalProviderRequestError("Gemini response payload was not a JSON object")
images: list[ExternalGeneratedImage] = []
text_parts: list[str] = []
finish_messages: list[str] = []
candidates = data.get("candidates")
if not isinstance(candidates, list):
raise ExternalProviderRequestError("Gemini response payload missing candidates")
for candidate in candidates:
if not isinstance(candidate, dict):
continue
finish_message = candidate.get("finishMessage")
finish_reason = candidate.get("finishReason")
if isinstance(finish_message, str):
finish_messages.append(finish_message)
elif isinstance(finish_reason, str):
finish_messages.append(f"Finish reason: {finish_reason}")
for part in _iter_response_parts(candidate):
inline_data = part.get("inline_data") or part.get("inlineData")
if isinstance(inline_data, dict):View on GitHub (pinned to 0b6a024f2f)
Solutions
- Log/print the raw response body before parsing to see what was actually returned.
- Unset any custom external_gemini_base_url so the official https://generativelanguage.googleapis.com endpoint is used.
- If using a proxy, configure it to pass through Google's response unchanged rather than re-wrapping it.
- Fix test mocks to return a dict-shaped GenerateContentResponse.
Example fix
// before: assuming a proxy speaks Gemini's schema base_url = "https://my-proxy.internal/v1" // after: use the official endpoint (or verify the proxy passthrough) base_url = None # provider defaults to https://generativelanguage.googleapis.com/v1beta
Defensive patterns
Strategy: type-guard
Type guard
def is_gemini_response(data: object) -> bool:
return (
isinstance(data, dict)
and isinstance(data.get("candidates"), list)
and len(data["candidates"]) > 0
)
# use on the raw JSON before trusting the envelope:
# data = response.json(); if not is_gemini_response(data): fail fast Try / catch
try:
result = provider.generate(request)
except ExternalProviderRequestError as e:
if "not a JSON object" in str(e):
logger.error("Non-Gemini response shape; check external_gemini_base_url/proxy")
raise Prevention
- Avoid custom external_gemini_base_url values unless the proxy passes Google's schema through unchanged.
- In tests, make mocks return a realistic dict-shaped GenerateContentResponse.
- Log raw response bodies when a parse error occurs to spot proxies/interceptors quickly.
- Verify TLS-intercepting corporate proxies don't rewrite API responses.
When it happens
Trigger: data = response.json() returns a non-dict: a custom external_gemini_base_url (proxy/gateway) returning an error array or plain value with status 200; an auth-gateway returning JSON like {"list": ...} shapes; a misconfigured mock server in tests; a captive portal or JSON-API wrapper returning a top-level list.
Common situations: Pointing external_gemini_base_url at an OpenAI-compatible proxy that returns a different schema; corporate proxy intercepting responses; upgrading Gemini behind a translation layer that changed the envelope; unit-test fake returning the wrong top-level type.
Related errors
- Gemini response payload missing candidates
- Gemini returned fileUri instead of inline image data: {file_
- DashScope async response missing task_id: {data}
- Gemini API key is not configured
- Gemini rate limit exceeded. {f'Retry after {retry_after:.0f}
AI-assisted analysis of invoke-ai/InvokeAI@0b6a024f2f (2026-08-29).
Data as JSON: /api/errors/bca324d2bfb4f6f7.
Report an issue: GitHub.