Nano Banana Pro RESOURCE_EXHAUSTED 429:排查与 Python、Node.js 恢复代码
Nano Banana Pro 的 429 不能一律靠重试解决:零额度、每日上限和计费阻塞应先停止,短时限流才按服务端最低等待时间重试。下面的 Python、Node.js 程序限制次数与请求等待总时长,验证并保存图片,续跑时跳过已确认完成的任务。
文章目录

Nano Banana Pro 生图返回 429 RESOURCE_EXHAUSTED,先看失败的是哪项限制,再决定重试。错误明确写出适用上限为零、每日额度耗尽或计费阻塞时,停下这批图片任务,修正条件或等待相应窗口;如果是短时限流或可重试的服务错误,再降低并发,尊重服务端等待时间,并给重试设置次数与总时间上限。
本文的 Python 和 Node.js 程序直接调用 Gemini Developer API 的 generateContent,以 gemini-3-pro-image 生成 1K 图片。它们会验证实际返回的图片、保存任务状态,并在再次运行时跳过已确认完成的图片。代码按当前文档编写并做了离线行为测试,没有调用真实生图 API,也没有验证实际账户的恢复率。文档和费用核对日期为 2026 年 10 月 5 日。
先确认调用路线,再读 429 的具体信息
记录请求域名、API 方法、版本、模型 ID、HTTP 状态和脱敏错误正文。不要只截取 RESOURCE_EXHAUSTED 一行:它能帮助识别失败,不能独自证明是哪种配额,也不能证明整个 Pro 服务都在故障。
| 实际调用路线 | 从哪里判断限制 | 本页代码是否直接适用 |
|---|---|---|
generativelanguage.googleapis.com 的 Gemini Developer API | 同项目、同模型的 AI Studio 有效限制和错误详情 | 适用下方 GenerateContent 示例 |
| Google Cloud / Vertex 路线 | 该路线的配额、按量容量或预留吞吐量规则 | 不直接适用,认证和端点需另行实现 |
| 第三方接口 | 提供方的模型映射、响应格式、限制和账单 | 不能只替换 URL 或 Key 就假定兼容 |
| Gemini 网页订阅、Code Assist 或 CLI | 对应产品自己的额度与错误 | 不能用本页 API 配额解释 |
Cloud 按量调用的 429 可能与分配容量不可用有关;预留吞吐量也有其容量和超额计费规则。因此,“429 一定是个人 RPM 用完”“付费即可永久消除 429”都不成立。Cloud 官方建议包括截断指数退避、平滑突发流量、逐步增加负载,以及在模型、地区和数据要求允许时考虑全局端点或适用容量方案。这些条件来自 Cloud 429 处理文档,并非 Developer API 的通用开关。
零额度、每日上限和短时限流分别怎么处理
Gemini Developer API 按项目限制,多个 Key 共享同项目配额。请求数、输入 token、图像数和适用支出窗口是不同维度:RPM 尚有空间,不代表其他限制也未用完。当前具体模型的有效值应在 AI Studio 速率限制核对,不把文本模型的固定表套到 Pro 图片模型。Google 速率限制说明
| 错误中可确认的条件 | 现在采取什么动作 | 什么时候继续 |
|---|---|---|
明确 limit: 0,或实际 QuotaFailure 提供 quotaValue: 0 | 停止自动重试,核对 Key 所属项目、目标模型资格和计费状态 | 缺失条件已修正,目标模型有可用额度后 |
quotaId、quotaMetric 或说明明确指向每日限制 | 暂停当天任务,不每隔几秒重新发 | 对 Developer API 的 RPD,按太平洋时间午夜重置;同时确认正的适用上限 |
| 正上限的每分钟请求、输入 token、图像或短时支出窗口耗尽 | 降并发、摊平突发,缩减不必要输入;有限等待 | 相应窗口释放且请求能在预算内完成时 |
| HTTP 402、明确月度支出上限或其他计费阻塞 | 停止,处理对应账务或支出控制 | 账户条件恢复后,由有权限的人决定验证 |
| 429 详情不完整,或 503 临时不可用 | 根因保留未知,先小规模有限重试 | 成功返图才算这次任务完成;持续失败则暂停排查 |
quotaValue 的定义是失败时执行的配额上限,不是已使用量,也不是剩余量。字段缺失时不能默认成零;RetryInfo 也不会为零资格创建额度。这些字段并非每次都返回,须结合当前响应与控制台核对。Google RPC 错误详情定义
截至核对日,gemini-3-pro-image 的 API 免费层输入与输出均标为 Not available。付费项目也仍有有效限制,消费端订阅不能证明 API 模型可用。Pro 图片模型定价与免费层说明
如果已经付费却始终报免费层零限制,继续看按实际 Key、项目和计费恢复 limit 0。它负责账户归属和状态不一致;本页下面解决的是已判定可以重试之后,如何把图片任务执行完整。HTTP 402 要处理适用账户的余额或计费状态;当前 Prepay 余额归零不会自动转回免费层。Google Prepay 说明

服务端要求等 8 秒,不能按本地 2 秒提前重试
读取 Retry-After 响应头和 google.rpc.RetryInfo。前者可以是整数秒,也可以是 HTTP 日期;后者的 retryDelay 表示至少等待的时长。如果两者都有,再与本地退避相比,取较大的等待值。Retry-After: 5、retryDelay: "8s" 和本地退避 2 秒同时出现时,应等至少 8 秒。HTTP Retry-After 定义、RetryInfo 定义
等待时间超过剩余预算,就把任务留到之后处理;不要为了“总时长不超限”把服务端最低等待截短。下一次请求的期限也必须取“单次上限”和“当前剩余预算”中的较小值,否则最后一次请求仍可能额外占用完整的单次时间。
下面自主选择每个任务最多 4 次请求,即首次加最多 3 次重试;每次请求最多等待 45 秒,一次运行中该任务的请求与退避预算为 120 秒,图片解码和本地文件操作可能增加少量时间。这三个数是示例策略,不是 Google 配额或恢复时间保证。每次退避加入抖动,避免多个任务同步重发。实际应用还应核对框架、HTTP 客户端和 SDK 的重试设置:外层 4 次与内层多次自动重试会让请求次数相乘。Google 重试指南

Python:生成、校验和保存每张图片
准备 Python 3.10 或以上和 Pillow;本文离线检查使用 Pillow 12.0.0。将下面代码保存为 recover.py。它使用标准库 HTTP 请求,不依赖 Gemini SDK;Pillow 用于实际解码图片,避免把只有 MIME 标签的任意字节保存成成功结果。
python3 -m pip install Pillow输入 jobs.json 是有稳定、唯一 ID 的任务数组。保留相同 ID 和相同提示词,才能识别已完成工作;更换提示词、模型或规格时使用新 ID,不复用旧结果。
[
{"id": "tea-001", "prompt": "生成一张白底产品照片:一杯透明玻璃杯装的热茶"},
{"id": "tea-002", "prompt": "生成一张白底产品照片:一杯陶瓷杯装的热茶"}
]import base64, hashlib, io, json, os, queue, random, re, sys, threading, time
from email.utils import parsedate_to_datetime
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError
from PIL import Image
MODEL = "gemini-3-pro-image"
URL = f"https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent"
def post(payload, key, timeout):
# urllib 无 SDK 重试;整个收响应过程有调用方等待期限。
box = queue.Queue()
def worker():
try:
request = Request(URL, data=json.dumps(payload).encode(), headers={
"x-goog-api-key": key, "Content-Type": "application/json"})
try:
response = urlopen(request, timeout=timeout)
except HTTPError as error:
response = error
with response:
result = (response.code, dict(response.headers), response.read())
box.put((True, result))
except Exception as error:
box.put((False, error))
threading.Thread(target=worker, daemon=True).start()
try:
ok, value = box.get(timeout=timeout)
except queue.Empty:
raise TimeoutError("请求结果未知") from None
if not ok:
raise value
return value
def decision(status, headers, body, wall=time.time):
details = body.get("error", {}).get("details", [])
minimum = 0.0
header = next((v for k, v in headers.items() if k.lower() == "retry-after"), "")
if re.fullmatch(r"\d+", header):
minimum = float(header)
elif header:
try:
minimum = max(0, parsedate_to_datetime(header).timestamp() - wall())
except (ValueError, TypeError, OverflowError):
pass
for detail in details:
if detail.get("@type", "").endswith("google.rpc.RetryInfo"):
delay = detail.get("retryDelay", "")
if re.fullmatch(r"\d+(?:\.\d+)?s", delay):
minimum = max(minimum, float(delay[:-1]))
if status == 429:
message = body.get("error", {}).get("message", "").lower()
if re.search(r"\blimit\s*:\s*0(?:\D|$)", message):
return "blocked", minimum
if "monthly" in message or "spending cap" in message:
return "blocked", minimum
for detail in details:
if not detail.get("@type", "").endswith("google.rpc.QuotaFailure"):
continue
for violation in detail.get("violations", []):
if "quotaValue" in violation and str(violation["quotaValue"]) == "0":
return "blocked", minimum
label = " ".join(str(violation.get(k, "")) for k in
("quotaMetric", "quotaId", "description")).lower()
if any(token in label for token in ("perday", "per_day", "daily")):
return "daily", minimum
return ("retry" if status in (429, 503) else "blocked"), minimum
def image_bytes(body):
for candidate in body.get("candidates", []):
for part in candidate.get("content", {}).get("parts", []):
if part.get("thought"):
continue
blob = part.get("inlineData", {})
if blob.get("mimeType") not in ("image/png", "image/jpeg", "image/webp"):
continue
raw = base64.b64decode(blob.get("data", ""), validate=True)
with Image.open(io.BytesIO(raw)) as image:
image.load()
actual = Image.MIME.get(image.format)
if actual != blob["mimeType"]:
raise ValueError("图片 MIME 与内容不符")
output = io.BytesIO()
image.save(output, format="PNG")
return output.getvalue()
raise ValueError("200 响应没有可保存的图片")
def atomic(path, raw):
temp = path.with_suffix(path.suffix + ".tmp")
temp.write_bytes(raw)
temp.replace(path)
def run(jobs, folder, key, send=post, clock=time.monotonic,
sleep=time.sleep, jitter=random.random, budget=120.0, wall=time.time):
folder = Path(folder)
folder.mkdir(parents=True, exist_ok=True)
ledger = folder / "state.json"
state = json.loads(ledger.read_text()) if ledger.exists() else {}
def save():
atomic(ledger, json.dumps(state, ensure_ascii=False, indent=2).encode())
ids = [job["id"] for job in jobs]
if len(ids) != len(set(ids)) or any(not re.fullmatch(r"[a-zA-Z0-9_-]+", x) for x in ids):
raise ValueError("job id 必须唯一,只用字母、数字、下划线或横线")
for job in jobs:
payload = {"contents": [{"parts": [{"text": job["prompt"]}]}],
"generationConfig": {"responseModalities": ["IMAGE"],
"imageConfig": {"aspectRatio": "1:1", "imageSize": "1K"}}}
fingerprint = hashlib.sha256(json.dumps([MODEL, payload], sort_keys=True).encode()).hexdigest()
record = state.setdefault(job["id"], {"fingerprint": fingerprint, "status": "pending"})
if record["fingerprint"] != fingerprint:
raise ValueError("同一 id 的请求已改变;请使用新 id")
output = folder / (job["id"] + ".png")
if record["status"] == "done":
if not output.exists() or hashlib.sha256(output.read_bytes()).hexdigest() != record["sha256"]:
record["status"] = "needs_check"
save()
continue
if record["status"] != "pending" or wall() < record.get("notBefore", 0):
continue
deadline = clock() + budget
for attempt in range(1, 5):
remaining = deadline - clock()
if remaining <= 0:
break
record["status"] = "unknown" # 在发送前持久化,崩溃后不盲目重发。
save()
try:
remaining = deadline - clock()
if remaining <= 0:
record["status"] = "pending"
save()
break
status, headers, raw = send(payload, key, min(45.0, remaining))
except Exception:
break # 超时/断连,后台可能仍完成,保留 unknown。
try:
body = json.loads(raw)
if not isinstance(body, dict):
raise ValueError("响应不是对象")
if status == 200:
image = image_bytes(body)
atomic(output, image)
record.update(status="done", sha256=hashlib.sha256(image).hexdigest(),
responseId=body.get("responseId"))
save()
break
action, minimum = decision(status, headers, body, wall=wall)
except Exception:
record["status"] = "needs_check"
save()
break
if action != "retry":
record.update(status=action, http=status)
save()
break
wait = max(minimum, min(2 ** attempt, 30) + jitter())
record.update(status="pending", http=status, notBefore=wall() + wait)
save()
if attempt == 4:
break
if wait >= deadline - clock():
break # 放回待处理,不能把服务端 minimum 截短。
sleep(wait)
return state
if __name__ == "__main__":
key = os.environ.get("GEMINI_API_KEY")
if not key:
raise SystemExit("请在运行环境配置 GEMINI_API_KEY")
jobs = json.loads(Path(sys.argv[1]).read_text())
result = run(jobs, sys.argv[2], key)
print(json.dumps({k: v["status"] for k, v in result.items()}, ensure_ascii=False))在实际运行环境配置 GEMINI_API_KEY 后,执行 python3 recover.py jobs.json output-python。此命令会请求收费的图片模型,运行前应确认项目资格和费用。密钥不要写进脚本或任务文件。程序将可解码的 PNG、JPEG 或 WebP 统一保存为 PNG,并在同一目录写 state.json;它保存的是状态、请求指纹、图片摘要和返回的 responseId,不会保存密钥。
Python 的请求线程以守护线程运行,调用方只等待本次剩余期限。期限过后底层网络操作可能仍在进行,程序不会自动再次发送这个任务;客户端超时不能取消已被服务器接收的生成。这是保留 unknown 的原因。
Node.js:用同一任务输入续跑
准备 Node.js 22 或以上,安装 sharp,把代码保存为 recover.mjs。本文离线检查使用 Node.js 24.11.0 和 sharp 0.35.4。fetch 的超时信号覆盖获取响应和读取响应体,sharp 实际解码后再写 PNG。
npm install sharpimport fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { pathToFileURL } from 'node:url';
import sharp from 'sharp';
const MODEL = 'gemini-3-pro-image';
const URL = `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent`;
const hash = value => crypto.createHash('sha256').update(value).digest('hex');
const atomic = (file, raw) => {
fs.writeFileSync(file + '.tmp', raw);
fs.renameSync(file + '.tmp', file);
};
export async function post(payload, key, timeout) {
const response = await fetch(URL, {
method: 'POST', headers: { 'Content-Type': 'application/json', 'x-goog-api-key': key },
body: JSON.stringify(payload), signal: AbortSignal.timeout(Math.max(1, Math.floor(timeout * 1000))),
redirect: 'error',
});
// timeout signal 同时覆盖读取响应体,不只覆盖响应头。
return [response.status, Object.fromEntries(response.headers), await response.text()];
}
export function decision(status, headers, body, wall = Date.now) {
const details = body.error?.details ?? [];
const header = Object.entries(headers).find(([k]) => k.toLowerCase() === 'retry-after')?.[1];
let minimum = /^\d+$/.test(header ?? '') ? Number(header) :
Math.max(0, (Date.parse(header ?? '') - wall()) / 1000);
if (!Number.isFinite(minimum)) minimum = 0;
for (const detail of details) {
if (detail['@type']?.endsWith('google.rpc.RetryInfo') && /^\d+(?:\.\d+)?s$/.test(detail.retryDelay ?? '')) {
minimum = Math.max(minimum, Number(detail.retryDelay.slice(0, -1)));
}
}
if (status === 429) {
const message = (body.error?.message ?? '').toLowerCase();
if (/\blimit\s*:\s*0(?:\D|$)/.test(message) || message.includes('monthly') || message.includes('spending cap')) {
return ['blocked', minimum];
}
for (const detail of details) {
if (!detail['@type']?.endsWith('google.rpc.QuotaFailure')) continue;
for (const violation of detail.violations ?? []) {
if (Object.hasOwn(violation, 'quotaValue') && String(violation.quotaValue) === '0') {
return ['blocked', minimum];
}
const label = ['quotaMetric', 'quotaId', 'description'].map(k => violation[k] ?? '').join(' ').toLowerCase();
if (['perday', 'per_day', 'daily'].some(token => label.includes(token))) return ['daily', minimum];
}
}
}
return [status === 429 || status === 503 ? 'retry' : 'blocked', minimum];
}
export async function imageBytes(body) {
for (const candidate of body.candidates ?? []) {
for (const part of candidate.content?.parts ?? []) {
if (part.thought) continue;
const blob = part.inlineData ?? {};
const formats = { 'image/png': 'png', 'image/jpeg': 'jpeg', 'image/webp': 'webp' };
if (!formats[blob.mimeType]) continue;
const encoded = blob.data ?? '';
if (!encoded || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
throw Error('图片 base64 无效');
}
const raw = Buffer.from(encoded, 'base64');
const metadata = await sharp(raw).metadata();
if (metadata.format !== formats[blob.mimeType]) throw Error('图片 MIME 与内容不符');
return sharp(raw).png().toBuffer(); // 实际解码,而不是只信 mimeType。
}
}
throw Error('200 响应没有可保存的图片');
}
export async function run(jobs, folder, key, options = {}) {
const { send = post, clock = () => performance.now() / 1000,
sleep = seconds => new Promise(resolve => setTimeout(resolve, seconds * 1000)),
jitter = Math.random, budget = 120, wall = () => Date.now() / 1000 } = options;
fs.mkdirSync(folder, { recursive: true });
const ledger = path.join(folder, 'state.json');
const state = fs.existsSync(ledger) ? JSON.parse(fs.readFileSync(ledger, 'utf8')) : {};
const save = () => atomic(ledger, JSON.stringify(state, null, 2));
const ids = jobs.map(job => job.id);
if (new Set(ids).size !== ids.length || ids.some(id => !/^[A-Za-z0-9_-]+$/.test(id))) {
throw Error('job id 必须唯一,只用字母、数字、下划线或横线');
}
for (const job of jobs) {
const payload = { contents: [{ parts: [{ text: job.prompt }] }], generationConfig: {
responseModalities: ['IMAGE'], imageConfig: { aspectRatio: '1:1', imageSize: '1K' },
} };
const fingerprint = hash(JSON.stringify([MODEL, payload]));
const record = state[job.id] ??= { fingerprint, status: 'pending' };
if (record.fingerprint !== fingerprint) throw Error('同一 id 的请求已改变;请使用新 id');
const output = path.join(folder, job.id + '.png');
if (record.status === 'done') {
if (!fs.existsSync(output) || hash(fs.readFileSync(output)) !== record.sha256) {
record.status = 'needs_check'; save();
}
continue;
}
if (record.status !== 'pending' || wall() < (record.notBefore ?? 0)) continue;
const deadline = clock() + budget;
for (let attempt = 1; attempt <= 4; attempt++) {
const remaining = deadline - clock();
if (remaining <= 0) break;
record.status = 'unknown'; save();
let status, headers, raw;
try {
const available = deadline - clock();
if (available <= 0) { record.status = 'pending'; save(); break; }
[status, headers, raw] = await send(payload, key, Math.min(45, available));
} catch { break; } // 结果未知,不自动再次发送。
let action, minimum;
try {
const body = JSON.parse(raw);
if (!body || typeof body !== 'object' || Array.isArray(body)) throw Error('响应不是对象');
if (status === 200) {
const image = await imageBytes(body);
atomic(output, image);
Object.assign(record, { status: 'done', sha256: hash(image), responseId: body.responseId });
save(); break;
}
[action, minimum] = decision(status, headers, body, () => wall() * 1000);
} catch { record.status = 'needs_check'; save(); break; }
if (action !== 'retry') {
Object.assign(record, { status: action, http: status }); save(); break;
}
const wait = Math.max(minimum, Math.min(2 ** attempt, 30) + jitter());
Object.assign(record, { status: 'pending', http: status, notBefore: wall() + wait }); save();
if (attempt === 4) break;
if (wait >= deadline - clock()) break;
await sleep(wait);
}
}
return state;
}
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
const key = process.env.GEMINI_API_KEY;
if (!key) throw Error('请在运行环境配置 GEMINI_API_KEY');
const jobs = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const result = await run(jobs, process.argv[3], key);
console.log(Object.fromEntries(Object.entries(result).map(([id, value]) => [id, value.status])));
}配置好实际运行环境的 GEMINI_API_KEY 后,执行 node recover.mjs jobs.json output-node。输入格式与 Python 示例相同;两个实现使用各自输出目录,不共享状态文件。代码使用 GenerateContent 的 generationConfig.imageConfig.imageSize、aspectRatio 和 candidates[].content.parts[].inlineData,这些字段可对照 GenerateContent 类型文档。请求使用 Pro 的 1K 规格,不能从通用类型列出 512 就推导 Pro 支持该规格。
当前图片生成指南也提供 Interactions 示例。它使用 input、response_format 等接口字段;SDK 的 output_image 便利属性也不是这里的 REST 读取路径。迁移时要按完整接口修改,不能把 Interactions 的参数和输出属性混入上面的 GenerateContent 程序。
哪些任务能续跑,哪些要先确认结果
再次执行相同命令,会读取既有 state.json。可重试拒绝会保存 notBefore(Unix 秒);尚未到这个时间,重启也会跳过任务,不用重启缩短服务端最低等待。请保持系统时钟准确。只让一个进程处理同一输出目录;这是单进程恢复示例,没有跨进程锁或分布式幂等保证。
| 状态 | 说明 | 再次运行如何处理 |
|---|---|---|
done | 收到、解码并保存了图片,图片摘要已写入状态 | 跳过;文件丢失或内容改变则转为 needs_check,不自动重生 |
pending | 收到了明确可重试的拒绝,但次数或等待预算用完 | 本次不再请求;问题缓解且状态中 notBefore 最早重试时间已到后重新运行,使用新的本次预算 |
daily | 返回详情明确指向每日上限 | 跳过;确认窗口重置且额度可用后,手动改为 pending |
blocked | 零上限、402、客户端错误或其他不在本例重试范围的状态 | 跳过;处理具体原因后再决定是否改为 pending |
unknown | 已准备发送,却没拿到可确认的完整结果,包括超时、断连或发送后的崩溃 | 跳过;先查看结果、日志或提供方记录,再决定是否重新提交 |
needs_check | 200 无图片、响应或图片无效、完成文件不完整 | 跳过;检查原始响应、内容阻断信息和本地结果 |
pending 表示本例收到明确的临时错误响应,并不保证该失败完全没有计费。unknown 更不能当作“没生成”:服务器可能已经完成,客户端却没收到。需要新请求时,应由应用负责人接受可能重复生成和费用的风险,保留旧记录后再作决定。响应 ID 可用于关联记录,不能凭它承诺 GenerateContent 支持幂等重放。
程序先保存发送标记,再调用 API,图片成功保存后才写 done。如果恰好在图片落盘与状态完成之间崩溃,重启会留下 unknown;人工确认文件后可以恢复完成状态,而不是整批重做。文件原子替换降低了部分写入风险,但没有实现跨图片与状态文件的事务,也没有保证断电时的持久性。
本文两份完整程序各通过 17 个离线用例:明确零上限与日限只请求一次;缺 quotaValue 不判零;正配额 429 和 503 可在模拟响应下继续;同时有 5 秒响应头和 8 秒 RetryInfo 时等待 8 秒;最低等待超过 120 秒时不提前请求,立即重启也会跳过尚未到最早重试时间的任务;5 秒预算中首次请求耗时 2 秒、退避 2 秒后,下一次期限降到 1 秒;持续 429 最多 4 次;402 停止;200 文本或无效图片不能算成功;超时续跑不重发;已完成图片续跑不重生;HTTP 日期可解析;完成文件改变后转人工检查。测试使用本地生成的 2×2 PNG、模拟时钟和注入响应,没有真实密钥、网络请求或付费生成。文件落盘和图片解码确实执行了,但这不证明实际模型能恢复或真实账户有额度。
持续失败时,先控制队列,再选择可用替代
让新任务进入队列,减少瞬时并发,按实际项目与模型配额出队;发生持续拒绝时暂停出队。多个进程共享一个项目时,要在共享层控制总请求,而不是每个客户端都自认为“每秒一张”就足够。单机计数器也不能证明提供方所有窗口的剩余额度。
如果业务允许改变模型,可以评估当前 gemini-3.1-flash-image 或 gemini-3.1-flash-lite-image;先确认团队已验证该路线的访问资格、提示词与图片效果,记录实际使用的模型,不把它当作 Pro 的无损替代。新模型同样可能受项目、账务或容量条件影响,切换不能保证立即成功。当前图片模型选择
不要继续使用旧文的 2.5 Flash Image → Imagen 自动回退链:Google 对 gemini-2.5-flash-image 公布的停止服务日期为 2026 年 10 月 2 日,而当前图片指南明确 Imagen 已不再通过 Gemini API 提供。这是文档中的服务计划和可用性说明,本文没有真实调用旧模型确认错误。2.5 Flash Image 服务计划、Imagen 可用性说明
比较替代成本时要对齐调用路线和规格。截至 2026 年 10 月 5 日,Developer API Standard 图片输出费用如下;它不含输入、文字或思考输出、适用搜索费用,也不等于可用成品的最终平均成本。
| 模型和输出规格 | 官方图片输出费 / 张 | 1,000 张对应输出费 |
|---|---|---|
| Pro Image,1K 或 2K | 0.134 美元 | 134 美元 |
| Pro Image,4K | 0.24 美元 | 240 美元 |
| 3.1 Flash Image,1K | 0.067 美元 | 67 美元 |
| 3.1 Flash Image,2K | 0.101 美元 | 101 美元 |
| 3.1 Flash Lite Image,1K | 0.0336 美元 | 33.60 美元 |
表中千张费用仅为单张输出费乘以 1,000,不代表购买该预算就能按时得到 1,000 张可用图片。价格依据分别为 Pro Image、Flash Image 和 Flash Lite Image。第三方路线需要另看费用、协议、数据处理与可用性条款,不能拿统一售价推导其无限额度或可靠性。
常见问题
已经付费,为什么 Nano Banana Pro 仍报 429?
付费不等于无限。核对失败进程实际 Key 的项目、同一图片模型的有效限制,以及错误指向的分钟、每日、图像或支出窗口;持续零上限则先处理归属和资格。Cloud 路线还可能有容量条件,不能把所有 429 归为 Developer API 的某个固定 RPM。Google 速率限制
报错让等 2 秒,等了还失败,要加到多久?
至少尊重响应给出的等待,后续可在总预算内增加退避并降低负载。等待只是允许再次尝试,不保证恢复。零额度和日限应先停止;最低等待超过剩余预算时,延后整个任务,不能提前请求。上方代码会在这种情况下保留 pending。
收到 HTTP 200,为什么没有保存图片?
200 只证明收到了成功 HTTP 响应。本例还要求存在非思考部分的 inlineData、支持的图片 MIME、有效 base64 和可解码图片。只返回文本或被阻断时,检查 promptFeedback、候选的 finishReason 与安全信息,不把该响应计为完成,也不无限重试同一提示词。GenerateContent 响应定义
重试用完后重启,能确保不会重复收费吗?
不能。程序会跳过有有效已存图片的 done,并保留超时或崩溃产生的 unknown,减少盲目重复生成;它无法证明服务器是否对未知请求完成了生成,也无法替提供方决定费用。只有确认任务结果后,才决定继续哪些工作。
错误变成 403,还要继续退避吗?
停止。403 需要按认证、权限、模型或失败动作核对,等待不能补齐授权。记录脱敏信息后,可按Gemini API 403 的失败位置排查继续;本例将这类响应记为 blocked。Google 客户端错误与重试说明
参考来源10
本文引用的外部页面,按正文出现顺序排列。最后更新于 2026年10月5日。
参考来源10
本文引用的外部页面,按正文出现顺序排列。最后更新于 2026年10月5日。
- 1.Cloud 429 处理文档docs.cloud.google.com/gemini-enterprise-agent-platform/models/deploy/error-code-429
- 2.AI Studio 速率限制aistudio.google.com/rate-limit
- 3.Google 速率限制说明ai.google.dev/gemini-api/docs/rate-limits
- 4.Google RPC 错误详情定义github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto
- 5.Pro 图片模型定价与免费层说明ai.google.dev/gemini-api/docs/pricing
- 6.Google Prepay 说明ai.google.dev/gemini-api/docs/billing
- 7.HTTP Retry-After 定义rfc-editor.org/rfc/rfc9110
- 8.Google 重试指南ai.google.dev/gemini-api/docs/troubleshooting
- 9.GenerateContent 类型文档ai.google.dev/api/generate-content
- 10.图片生成指南ai.google.dev/gemini-api/docs/image-generation





