What happens
About 0.5% of non-streaming POST https://generativelanguage.googleapis.com/v1/interactions calls to gemini-3.1-flash-lite return:
- HTTP 200 with
"status": "completed"and noerrorsfield; stepsholding a singlethoughtstep (signature only) and nomodel_outputstep.
The usage block still reports the text tokens of a full answer, output_tokens_by_modality: [{"modality": "text", "tokens": N}], and total_output_tokens is slightly higher (4–21 tokens more at thinking_level: minimal). So the answer seems to be generated and billed, but not returned.
The same request sent to v1/models/gemini-3.1-flash-lite:generateContent never came back without an answer in our runs. The same request to the Interactions API with gemini-3.5-flash-lite didn’t either.
Repro
repro.py (full source at the end) is Python standard library only: no SDK, plain HTTPS. It sends one text prompt plus one JPEG per call with thinking_level: minimal, max_output_tokens: 4096, a JSON response_format, store: false and image resolution: medium. It runs 16 calls in parallel and saves the raw body of every 200 reply that has no answer.
GEMINI_API_KEY=... python repro.py interactions short_prompt.txt images/ 1200 out/
GEMINI_API_KEY=... python repro.py generate_content short_prompt.txt images/ 1200 out_gc/
-
images/contains one public-domain photo: Label with care symbols.JPG (Wikimedia Commons, 960 px version), a sweater label reading “100% WOOL”. -
short_prompt.txt: Read the clothing label in this photo and extract the material composition. Return each section (for example shell or lining; null if the label has only one) with every material in English and its percentage.
Results (2026-10-30, 13:49–13:58 UTC, 1,200 calls per row)
Same photo and prompt in every row:
| Request | Model | HTTP 200 | 200 with no answer |
|---|---|---|---|
| v1 interactions | gemini-3.1-flash-lite | 1,200 | 12 (1.0%) |
v1 interactions, store: true |
gemini-3.1-flash-lite | 1,200 | 8 (0.67%) |
| v1 generateContent | gemini-3.1-flash-lite | 1,200 | 0 |
| v1 interactions | gemini-3.5-flash-lite | 1,200 | 0 |
| v1 generateContent | gemini-3.5-flash-lite | 1,200 | 0 |
This photo’s normal answer is {"s":[{"c":[{"m":"wool","p":100}],"n":null}]}, 56 output tokens; 1,156 of the 1,188 answered calls returned exactly that. All 12 empty replies report output_tokens_by_modality text = 56 and total_output_tokens 73–75.
It isn’t tied to this photo or prompt. With 300 other images and a longer prompt, Interactions on 3.1-flash-lite returned 0.25–0.58% empty replies per 1,200-call run, while generateContent and 3.5-flash-lite returned none.
Changing one setting at a time on 3.1-flash-lite did not make it go away (1,200 calls each):
| Setting changed | Inputs | Empty replies |
|---|---|---|
| none (baseline, same time as the three rows below) | 300 images, long prompt | 7 |
thinking_level: low |
300 images, long prompt | 8 |
no response_format |
300 images, long prompt | 2 |
| default image resolution | 300 images, long prompt | 5 |
store: true |
public photo, short prompt | 8 |
Empty reply (signature shortened; full body at the end)
{
"status": "completed",
"usage": {
"total_tokens": 656,
"total_input_tokens": 582,
"input_tokens_by_modality": [{"modality": "image", "tokens": 540}, {"modality": "text", "tokens": 42}],
"total_cached_tokens": 0,
"total_output_tokens": 74,
"output_tokens_by_modality": [{"modality": "text", "tokens": 56}],
"initial_input_tokens": 861
},
"created": "2026-10-30T13:54:25Z",
"updated": "2026-10-30T13:54:25Z",
"service_tier": "standard",
"steps": [{"signature": "EnMKcQFpFH0ThCZvZn66+pD0…", "type": "thought"}],
"object": "interaction",
"model": "gemini-3.1-flash-lite"
}
Normal reply, same photo and prompt (signature shortened; timestamps and service_tier left out; full body at the end)
{
"status": "completed",
"usage": {
"total_tokens": 638,
"total_input_tokens": 582,
"input_tokens_by_modality": [{"modality": "image", "tokens": 540}, {"modality": "text", "tokens": 42}],
"total_cached_tokens": 0,
"total_output_tokens": 56,
"total_tool_use_tokens": 0,
"total_thought_tokens": 0,
"initial_input_tokens": 861
},
"steps": [
{"signature": "EnMKcQFpFH0TcTP4ghlhvI86…", "type": "thought"},
{"type": "model_output", "content": [{"type": "text", "text": "{\n \"s\": [\n {\n \"c\": [\n {\n \"m\": \"wool\",\n \"p\": 100\n }\n ],\n \"n\": null\n }\n ]\n}"}]}
],
"object": "interaction",
"model": "gemini-3.1-flash-lite"
}
The usage blocks differ in shape. At thinking_level: minimal, all 49 empty replies have output_tokens_by_modality but no total_thought_tokens or total_tool_use_tokens; all 13,137 normal replies have the reverse. (With thinking_level: low, the 8 empty replies also include total_thought_tokens.)
Expected
Either a model_output step with the answer, or a status other than completed (or an error) so the client knows to retry. Output that isn’t delivered shouldn’t be billed.
Environment
Gemini Developer API with an API key, service_tier: standard, non-streaming calls, Python 3.9 urllib.
repro.py
"""Gemini API v1: count calls that come back without an answer.
Sends the same prompt + one image per call to gemini-3.1-flash-lite on the stable v1
endpoint, either through the Interactions API or generateContent, with the same
settings on both. Python standard library only.
GEMINI_API_KEY=... python repro.py interactions|generate_content <prompt.txt> <images_dir> <calls> <out_dir>
Writes every call to <out_dir>/calls.jsonl and every unanswered raw response to
<out_dir>/no_answer_N.json.
"""
import base64
import json
import os
import sys
import time
import urllib.error
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
MODEL = "gemini-3.1-flash-lite"
BASE = "https://generativelanguage.googleapis.com/v1"
SCHEMA = {
"type": "object",
"properties": {
"s": {
"type": "array",
"description": "Composition sections on the label. Usually 1; multi-layer items may have 2-3 (shell, lining, etc.)",
"items": {
"type": "object",
"properties": {
"n": {"type": ["string", "null"], "description": "Section name if specified, else null"},
"c": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"properties": {
"m": {"type": "string", "description": "Material name in English"},
"p": {"type": "integer", "minimum": 0, "maximum": 100, "description": "Percentage"},
},
"required": ["m", "p"],
},
},
},
"required": ["c"],
},
}
},
"required": ["s"],
}
api, prompt, images, calls, out = sys.argv[1], Path(sys.argv[2]).read_text(), sorted(Path(sys.argv[3]).glob("*.jpg")), int(sys.argv[4]), Path(sys.argv[5])
out.mkdir(parents=True, exist_ok=True)
def request(image):
data = base64.b64encode(image.read_bytes()).decode()
if api == "interactions":
return f"{BASE}/interactions", {
"model": MODEL,
"store": False,
"input": [{"type": "user_input", "content": [
{"type": "text", "text": prompt},
{"type": "image", "mime_type": "image/jpeg", "data": data, "resolution": "medium"},
]}],
"generation_config": {"thinking_level": "minimal", "max_output_tokens": 4096},
"response_format": {"type": "text", "mime_type": "application/json", "schema": SCHEMA},
}
return f"{BASE}/models/{MODEL}:generateContent", {
"contents": [{"role": "user", "parts": [{"text": prompt}, {"inline_data": {"mime_type": "image/jpeg", "data": data}}]}],
"generation_config": {
"thinking_config": {"thinking_level": "MINIMAL"},
"max_output_tokens": 4096,
"media_resolution": "MEDIA_RESOLUTION_MEDIUM",
"response_mime_type": "application/json",
"response_json_schema": SCHEMA,
},
}
def answer(body):
if api == "interactions":
steps = body.get("steps") or []
parts = [c for s in steps if s.get("type") == "model_output" for c in s.get("content") or []]
else:
parts = [p for c in body.get("candidates") or [] for p in (c.get("content") or {}).get("parts") or [] if not p.get("thought")]
return "".join(p.get("text") or "" for p in parts).strip()
def call(i):
image = images[i % len(images)]
url, payload = request(image)
req = urllib.request.Request(url, json.dumps(payload).encode(), {"Content-Type": "application/json", "x-goog-api-key": os.environ["GEMINI_API_KEY"]})
start = time.time()
try:
with urllib.request.urlopen(req, timeout=120) as response:
http, raw = response.status, response.read()
except urllib.error.HTTPError as error:
http, raw = error.code, error.read()
except (urllib.error.URLError, TimeoutError) as error:
return {"call": i, "image": image.name, "http": None, "seconds": round(time.time() - start, 2), "error": repr(error)}
body = json.loads(raw)
row = {"call": i, "image": image.name, "http": http, "seconds": round(time.time() - start, 2),
"answered": http == 200 and bool(answer(body)),
"status": body.get("status") or [c.get("finishReason") for c in body.get("candidates") or []],
"usage": body.get("usage") or body.get("usageMetadata"), "error": body.get("error")}
if http == 200 and not row["answered"]:
(out / f"no_answer_{i}.json").write_bytes(raw)
elif row["answered"] and not (out / "answered_example.json").exists():
(out / "answered_example.json").write_bytes(raw)
return row
with ThreadPoolExecutor(16) as pool, open(out / "calls.jsonl", "w") as log:
rows = []
for row in pool.map(call, range(calls)):
rows.append(row)
log.write(json.dumps(row) + "\n")
ok = [r for r in rows if r["http"] == 200]
print(json.dumps({"api": api, "calls": len(rows), "http_200": len(ok), "http_errors": len(rows) - len(ok),
"200_without_answer": sum(not r["answered"] for r in ok)}))
Full raw body: empty reply
{
"status": "completed",
"usage": {
"total_tokens": 656,
"total_input_tokens": 582,
"input_tokens_by_modality": [
{
"modality": "image",
"tokens": 540
},
{
"modality": "text",
"tokens": 42
}
],
"total_cached_tokens": 0,
"total_output_tokens": 74,
"output_tokens_by_modality": [
{
"modality": "text",
"tokens": 56
}
],
"initial_input_tokens": 861
},
"created": "2026-10-30T13:54:25Z",
"updated": "2026-10-30T13:54:25Z",
"service_tier": "standard",
"steps": [
{
"signature": "EnMKcQFpFH0ThCZvZn66+pD0I2OGpHCbKgPAvA6TAwzi2TPxjMfpnXR1SiMmU/U+8YAoVAgn2Vv36DXHLgQ5+grCm0D/eiyKKnYhmeAhznyX5aQIJG/L6q2P7p4HkqO1yS/0z3iKR1EiXSmtm1oAosMofRB5",
"type": "thought"
}
],
"object": "interaction",
"model": "gemini-3.1-flash-lite"
}
Full raw body: normal reply
{
"status": "completed",
"usage": {
"total_tokens": 638,
"total_input_tokens": 582,
"input_tokens_by_modality": [
{
"modality": "image",
"tokens": 540
},
{
"modality": "text",
"tokens": 42
}
],
"total_cached_tokens": 0,
"total_output_tokens": 56,
"total_tool_use_tokens": 0,
"total_thought_tokens": 0,
"initial_input_tokens": 861
},
"created": "2026-10-30T13:52:51Z",
"updated": "2026-10-30T13:52:51Z",
"service_tier": "standard",
"steps": [
{
"signature": "EnMKcQFpFH0TcTP4ghlhvI869d5iPZCf7u46jU0hsKnHl9Bx0/TqTWEbRa6/mEXGtioefVYHi61TDBhAp9QnkOejpnwsBiGufVPABDai0ihAI9pw2ggryDdEtxdWyhGVUEvVXGJJi4uIfU5kSPrVW7mIsPdw",
"type": "thought"
},
{
"content": [
{
"text": "{\n \"s\": [\n {\n \"c\": [\n {\n \"m\": \"wool\",\n \"p\": 100\n }\n ],\n \"n\": null\n }\n ]\n}",
"type": "text"
}
],
"type": "model_output"
}
],
"object": "interaction",
"model": "gemini-3.1-flash-lite"
}
