gemini-3.1-flash-image 返回 400,通常不是图片损坏,而是请求结构没通过校验。优先检查 5 项:MIME 与文件签名是否一致、Base64 前缀是否剥离、宽高比参数是否重复、图片数组有没有多套一层、Base64 膨胀后的请求体是否过大。
10 月 29 号我排查一个图片编辑接口,浏览器预览完全正常,传到 API 却一直报 400。折腾到最后才发现,前端声明的是 image/png,实际文件头却是 JPEG。挺烦人的。
按 2026 年 10 月 31 日实时模型目录核对,聚合接口中的完整 model ID 是 google/gemini-3.1-flash-image。如果走 Gemini 原生端点,还是要以 models.list() 对当前 Key 返回的名称为准,别把 GitHub 仓库名直接复制过去。
为什么图片能打开,API 还是拒绝?
浏览器和图片查看器很宽容。扩展名写成 .png,文件实际是 JPEG,它们照样可能根据文件头识别并显示。
API 参数校验没这么好说话:
- 你声明
image/png,二进制却以 JPEG 魔数开头; - Base64 里混入
data:image/png;base64,; - SDK 要
contents,你却贴了兼容接口的messages; - 图片数组本应是一维,实际变成了二维;
imageConfig里给了宽高比,请求外层又传了一份。
典型错误响应长这样。素材没有提供该模型的原始日志,下面只能标记为 待验证,发布时应替换成自己的完整响应:
text
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"error":{"code":400,"message":"Request contains an invalid argument.","status":"INVALID_ARGUMENT"}}
Python SDK 可能包装成:
text
google.genai.errors.ClientError: 400 INVALID_ARGUMENT.
{'error': {'code': 400, 'message': 'Request contains an invalid argument.', 'status': 'INVALID_ARGUMENT'}}
400 指向参数校验。它不等同于额度耗尽,也不能直接归因于服务故障。
先按这个顺序排查
| 当前现象 | 先查什么 | 判断方式 |
|---|---|---|
| 纯文本也报 400 | 模型 ID、端点、Key 可见范围 | 执行 models.list() |
| 加图片后才报错 | MIME、Base64、数组层级 | 打印文件头和结构摘要 |
| 加宽高比后报错 | 配置字段冲突 | 删除全部图片配置再逐项恢复 |
| 小图成功、大图失败 | 请求体尺寸 | 计算 Base64 膨胀体积 |
| 换 SDK 后出现 | API 方言混用 | 对照实际端点文档 |
别一上来同时改十个字段。那不是排错,是抽卡。
方案一:确认模型真的可见
GitHub 搜索快照里有一个名称包含 gemini-3.1-flash-image-preview-official-api-ja 的仓库,快照显示 84 Stars。但仓库名不是 API model ID,星数也会变化,这两项都标记为 待验证。
直接列出当前凭据可见的模型更靠谱:
python
import os
from google import genai
c = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
for m in c.models.list(): print(m.name)
如果列表里没有目标模型,就先停。继续调整 MIME、Base64 没有意义。
我会在本地主动抛一个更明确的诊断错误:
text
RuntimeError: Requested model 'gemini-3.1-flash-image' is not present in models.list() for the current API key/project.
这条是本地诊断信息,不是服务端原始返回。SDK 版本也要记下来。当前素材没有可复现的版本号,别凭感觉写"最新版"。
bash
pip show google-genai
方案二:专查图片请求结构
1. MIME 类型必须匹配真实文件
不要相信扩展名,也别只信前端传来的 file.type。JPEG 常见文件头是 FF D8 FF,PNG 是 89 50 4E 47。
python
head = image_bytes[:4]
mime = "image/png" if head == b"\x89PNG" else None
if head[:3] == b"\xff\xd8\xff": mime = "image/jpeg"
if not mime: raise ValueError(f"Unsupported image signature: {head.hex()}")
这里检查的是二进制签名与声明值是否一致。只是把 image/jpeg 改成 image/png,不会把 JPEG 转成 PNG。
2. Base64 前缀必须剥离
浏览器经常生成 Data URL:
text
data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...
多数 API 图片字段只接收逗号后面的纯 Base64。可以在请求前统一处理:
python
def strip_data_url(value):
return value.split(",", 1)[1] if value.startswith("data:") else value
还要防止二次编码。如果原始字符串已经是 Base64,又对字符串本身编码一次,服务端解出来的只会是另一段文本。说实话,这个坑看着低级,排起来却能耗半天。
3. 宽高比参数只能留一个来源
别猜字段名。先删掉所有图片配置,只保留 prompt 和一张输入图;请求成功后,再恢复宽高比。
python
config = {}
# 请求成功后,再按当前端点文档加入一个宽高比字段
常见冲突有两类:外层尺寸和 imageConfig 同时设置,或者宽高与宽高比一起指定。还有一种更隐蔽,直接把另一套 API 方言的字段原样塞了进来。
我也不确定每个代理端点会不会自动归一化冲突字段。目前没找到比"全部删掉,再逐项恢复"更稳的办法。
4. 图片数组不要多套一层
很多 400 都是动态拼装 JSON 时搞出来的。单图本来是一组内容部件,经过 append([image_part]) 后就成了二维数组。
python
bad = [prompt_part, [image_part]]
good = [prompt_part, image_part]
assert all(not isinstance(x, list) for x in good)
日志里别打印完整 Base64。记录数组深度、MIME、原始字节数和 Base64 长度就够了,否则一张图就能把日志平台刷屏。
5. 计算 Base64 膨胀后的请求体
Base64 长度可以近似按下面计算:
text
base64_bytes = 4 × ceil(raw_bytes ÷ 3)
request_bytes ≈ JSON固定字段 + base64_bytes
例如原图是 2.8 MiB,仅 Base64 部分大约就会变成 3.73 MiB,还没算 JSON 字段、转义和多张图片。一个月的日志量也会跟着膨胀,属实有点离谱。
当前资料没有提供该模型的请求体上限,不能随便编一个"最大多少 MB"。工程上应该记录实际请求字节数,并把 HTTP timeout 明确设为例如 30000 ms。这个值只是客户端超时配置,不是模型延迟数据。
方案三:确认端点与请求方言没有混用
Gemini 原生 API、Vertex AI、OpenAI 风格兼容端点和聚合端点,字段结构并不相同。contents、messages、generationConfig、response_modalities 不能随手混搭。
如果需要切换聚合端点,可以同时核对 OpenRouter 与 ofox.io 的模型目录和协议说明;ofox.io 支持 Gemini 原生协议,并能按 Model、User 或 API Key 查看请求成功失败、平均延迟和 Token 消耗。这里的关键不是换地址"碰运气",而是确认端点要求的模型 ID 与请求结构。
按下面的顺序逐步加回功能:
- 发送最小纯文本请求;
- 开启图片输出;
- 加入一张经过校验的输入图;
- 恢复一个图片配置字段;
- 再增加第二张图片。
每一步只改一个变量。踩了一天坑后,我现在宁愿多发 4 次测试请求,也不愿盯着一坨 200 行 JSON 猜。
请求前校验清单
发请求前,我会强制跑下面 10 项:
-
models.list()中能看到目标模型; - 模型 ID 与当前端点一致;
- SDK 版本已记录;
- 图片二进制不是空文件;
- MIME 与文件头一致;
- Base64 不包含 Data URL 前缀;
- Base64 能在本地成功解码;
- 图片内容数组没有意外嵌套;
- 宽高比、宽高和图片配置没有重复来源;
- 已计算编码后的完整请求体尺寸。
再额外记录 request ID 和字段违规信息。服务端只返回一句 Request contains an invalid argument 时,这些上下文比反复重试有用得多。
常见问题 FAQ
Q:gemini-3.1-flash-image 返回 400 是额度不足吗?
通常不是。400 INVALID_ARGUMENT 表示请求没有通过参数校验。额度或频率限制要结合实际状态码与响应体判断,别看到请求失败就立刻充值或无限重试。
Q:图片浏览器能打开,为什么 API 说参数无效?
浏览器可能根据文件头自动识别格式,API 则会核对声明的 MIME、Base64 内容和 JSON 结构。最常见的情况是扩展名、file.type 与真实二进制格式不一致。
Q:Base64 图片前面的 data:image/png;base64, 要删除吗?
如果接口字段要求纯 Base64,就必须删除。不要只删固定的 PNG 前缀,应该按第一个逗号切分,因为输入也可能是 JPEG。
Q:模型名称应该填 gemini-3.1-flash-image 还是完整 ID?
按 2026 年 10 月 31 日模型目录,聚合接口完整 ID 为 google/gemini-3.1-flash-image。原生端点以当前 Key 的 models.list() 返回结果为准,不能拿仓库名或产品展示名代替。
Q:请求体多大才会报 400?
现有素材没有该模型可信的上限数据。先用 4 × ceil(raw_bytes ÷ 3) 计算 Base64 大小,再加上 JSON 开销。然后从单张小图开始递增,记录首次失败时的实际请求字节数。
我的最终处理方式
我现在固定按"模型可见性 → 最小请求 → 文件签名 → Base64 → 数组层级 → 图片配置 → 请求体尺寸"这条链路排查。遇到 400 先不重试,结构错了,重试 100 次还是错。
图片能打开,只能证明查看器愿意容忍它,不能证明 MIME、Base64 和请求 JSON 都合法。gemini-3.1-flash-image 图片请求里的大多数 INVALID_ARGUMENT,问题基本都藏在这里。