# Gemini / Nano Banana
> Gemini generateContent 原生图片接口,含同步与 SSE 图片读取、保存和中断处理。
Base URL:`https://platform.sgt.site/gemini`
本文中的接口路径均相对于此 Base URL;完整请求地址由二者拼接。
| 用途 | 方法 | 路径 |
| --- | --- | --- |
| 同步生成或编辑图片 | POST | `/v1beta/models/{model}:generateContent` |
| 流式生成或编辑图片 | POST | `/v1beta/models/{model}:streamGenerateContent?alt=sse` |
| 查询当前可用型号或一个型号详情 | GET | `/v1beta/models`、`/v1beta/models/{model}` |
| 计算输入 Token,不生成图片 | POST | `/v1beta/models/{model}:countTokens` |
使用平台虚拟密钥鉴权:`Authorization: Bearer YOUR_VIRTUAL_KEY` 或 `x-goog-api-key: YOUR_VIRTUAL_KEY`。不要填写 Google 上游账号密钥。本页参考图使用内联 `inlineData`;请求中的 Base64 不带 Data URI 前缀。
此厂商前缀只服务图片模型,文本模型返回 Gemini 格式的 HTTP 400;文本请使用[文本标准协议](llm-api.md)。
平台不提供 Google Files 上传与引用、Batch 或 Interactions:这些入口返回 HTTP 410,且不会访问厂商。参考图片放在内联 `inlineData` 中;只有 `generateContent`、`streamGenerateContent`、模型目录和 `countTokens` 可用。
## 路径和模型
| 已上架公开名称 | 对应的已有 Gemini 型号 |
| --- | --- |
| Nano Banana 2 | `gemini-3.1-flash-image` |
| Nano Banana Pro | `gemini-3-pro-image` |
| Nano Banana 2 Lite | `gemini-3.1-flash-lite-image` |
| Nano Banana | `gemini-2.5-flash-image` |
路径中的 `{model}` 使用本页原生目录原样提供的模型名称。目录返回 `Nano Banana 2 Lite` 时,路径写为 `Nano%20Banana%202%20Lite`;不能自行换成右列的内部能力模板名。已有 Gemini 名称只有在目录中独立可见时才能调用。公开名称用于授权、选渠道及价格,上游名称由所选渠道的模型映射决定。
四款 Nano Banana 也可使用[平台统一图片任务](media-images.md),通过 `references` 和 `wait` 提交并查询任务。本页原生入口仍直接返回 `candidates`,不能据此查询统一任务;Nano Banana / Gemini 非 Imagen 图片型号也不因此获得标准 OpenAI Images 入口。
## 参数
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `contents` | array | 必填,用户输入 |
| `contents[].role` | string | 常用 `user` |
| `contents[].parts[].text` | string | 文本提示词 |
| `contents[].parts[].inlineData` | object | 参考图片,含 `mimeType` 和不带 Data URI 前缀的 Base64 `data` |
| `generationConfig.responseModalities` | string[] | 图片示例使用 `["TEXT","IMAGE"]` |
| `generationConfig.imageConfig.aspectRatio` | string | 如 `1:1/4:3/16:9/9:16` |
| `generationConfig.imageConfig.imageSize` | string | Nano Banana 2 / Pro 及对应 Gemini 型号使用 `1K/2K/4K`;Nano Banana 2 Lite 仅 `1K`;普通 Nano Banana / 2.5 Flash Image 应省略此字段 |
`imageConfig` 作为配置对象传递,具体模型是否接受高级值以 Google 官方说明为准。提示词放在 `contents[].parts[].text`,参考图放在 `inlineData`,比例和分辨率放在 `generationConfig.imageConfig`;按上表的嵌套结构构造请求。
## 文生图
```bash
BASE_URL="https://platform.sgt.site/gemini"
curl -X POST "$BASE_URL/v1beta/models/gemini-3.1-flash-image:generateContent" \
-H 'Authorization: Bearer YOUR_VIRTUAL_KEY' \
-H 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"contents": [
{
"role": "user",
"parts": [
{"text": "生成一张极简风格的蓝色运动鞋产品图"}
]
}
],
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"],
"imageConfig": {
"aspectRatio": "1:1",
"imageSize": "1K"
}
}
}
JSON
```
参考图生成时,在同一个 `parts` 数组增加图片项:
```json
{
"inlineData": {
"mimeType": "image/png",
"data": "BASE64_IMAGE_DATA"
}
}
```
上面的 `BASE64_IMAGE_DATA` 为占位符,需替换为完整图片字节的 Base64;`mimeType` 必须与实际图片一致,例如 JPEG 使用 `image/jpeg`。多个参考图各占一个 `parts` 项,不把多张图的 Base64 拼成一个字段。
## 同步返回
```json
{
"candidates": [
{
"content": {
"role": "model",
"parts": [
{"text": "已生成图片"},
{
"inlineData": {
"mimeType": "image/png",
"data": "BASE64_IMAGE_DATA"
}
}
]
},
"finishReason": "STOP"
}
]
}
```
遍历所有 `parts`,提取带 `inlineData` 的图片,不要假设第一项一定是图片。HTTP 200 但没有图片时,应继续检查 `finishReason`、安全反馈或错误信息;该结果不等于生成成功。本接口直接返回生成内容,没有异步任务 ID 和任务查询。
## 流式图片:请求、事件与保存
下面是一笔新的生成请求,会产生生成费用。`-N` 关闭 curl 输出缓冲;`alt=sse` 请求 SSE。不需要上传文件接口,参考图仍放在 `inlineData` 中:
```bash
curl --fail-with-body -NsS \
"$GATEWAY_ORIGIN/gemini/v1beta/models/gemini-3.1-flash-image:streamGenerateContent?alt=sse" \
-H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
--data '{"contents":[{"role":"user","parts":[{"text":"生成一张蓝色陶瓷杯图片"}]}],"generationConfig":{"responseModalities":["TEXT","IMAGE"],"imageConfig":{"aspectRatio":"1:1","imageSize":"1K"}}}' \
-D gemini-headers.txt -o gemini.sse
```
先检查 curl 退出码和响应 HTTP 状态。`gemini-headers.txt` 可用于保留请求关联标识;不要把密钥放进日志。以下为协议返回格式片段,Base64 是占位,不是可解码图片:
```text
data: {"candidates":[{"index":0,"content":{"role":"model","parts":[{"text":"这是生成的图片:"}]}}]}
data: {"candidates":[{"index":0,"content":{"role":"model","parts":[{"inlineData":{"mimeType":"image/png","data":"BASE64_IMAGE_DATA"}}]}}]}
data: {"candidates":[{"index":0,"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":10,"candidatesTokenCount":100,"totalTokenCount":110}}
```
每个空行结束一个 SSE 事件,其 `data:` 是一个 `GenerateContentResponse` JSON。文本、图片和 `finishReason` 可以同处一个事件,也可以分开发送;没有固定的图片事件名。不使用其他协议的 `image_generation.completed`、`response.completed` 或 `[DONE]` 判断结束。
| 字段 | 类型与出现条件 | 读取方式 |
| --- | --- | --- |
| `candidates` | array,有候选内容或结束原因时 | 用量单独到达时可为空/缺失,继续读取 |
| `candidates[].index` | integer,候选编号,0 有效 | 同一候选按事件顺序累积;单候选未给编号时按 0 |
| `content.parts` | array,可选 | 遍历每项;文本与图片可混排,不能只看第一项 |
| `parts[].text` | string,可选 | 按候选顺序拼接文本增量 |
| `parts[].inlineData.mimeType` | string,有 inlineData 时必需 | 图片的 MIME,JPEG/PNG 分别保存;未知图片格式保留 `.bin` |
| `parts[].inlineData.data` | string,有 inlineData 时必需 | 完整 Blob 的 Base64,不含 Data URI 前缀,非空才解码 |
| `parts[].thought` | boolean,可选 | true 表示思考内容,不当作最终用户图片 |
| `finishReason` | string,可选枚举 | 缺失/空表示尚未结束;STOP 为正常结束。MAX_TOKENS、安全拦截或其他原因必须单独报告,不能当成功 |
| `usageMetadata` | object,可选;内部计数为 integer token | 未提供不等于 0,用量不是人民币账单 |
**拼接的是 HTTP 字节、SSE 数据行和同一候选的文本,不是任意图片的 Base64。** HTTP 网络分块可能切在 JSON/Base64 中间,先读到完整事件再解析;同一事件的多个 `data:` 行按 SSE 规则用换行连接。每个 `inlineData` Blob 独立解码并分别保存;不同图片或不同候选不能拼成一个文件,也不能把 Base64 字符串直接作为图片字节写盘。
把以下脚本保存为 `gemini_sse.py`,执行 `python gemini_sse.py` 读取上一步的 `gemini.sse`。它不调用生成接口,只有 Python 标准库依赖。未确认完整结束时图片保留为 `.part`;只有所有已见候选正常结束且至少有一张图片才改成正式文件名:
**完整脚本 `gemini_sse.py`**
```python
import base64
import hashlib
import json
from pathlib import Path
def sse_events(lines):
data = []
for raw in lines:
line = raw.rstrip("\r\n")
if not line:
if data:
yield json.loads("\n".join(data))
data = []
elif line.startswith("data:"):
value = line[5:]
data.append(value[1:] if value.startswith(" ") else value)
# 注释/心跳等非 data 字段不作为 JSON;不能用 TCP 分块当事件边界。
if data:
raise RuntimeError("SSE 末尾事件不完整;保留原响应,不自动重新生成")
def consume_sse(lines, folder="output"):
folder = Path(folder)
folder.mkdir(parents=True, exist_ok=True)
seen, finished, texts, images = set(), {}, {}, {}
for event in sse_events(lines):
if event.get("error") or (event.get("promptFeedback") or {}).get("blockReason"):
raise RuntimeError("流内错误或安全拦截;保留响应和请求标识核对")
for candidate in event.get("candidates") or []:
index = candidate.get("index", 0)
if not isinstance(index, int) or index < 0:
raise RuntimeError("无效候选编号")
seen.add(index)
for part in (candidate.get("content") or {}).get("parts") or []:
if part.get("thought"):
continue
if "text" in part:
texts.setdefault(index, []).append(part["text"])
blob = part.get("inlineData")
if blob is None:
continue
mime = blob.get("mimeType", "")
if not mime.startswith("image/") or not blob.get("data"):
raise RuntimeError("缺少图片 MIME 或 Base64 数据")
try:
data = base64.b64decode(blob["data"], validate=True)
except (ValueError, TypeError) as error:
raise RuntimeError("图片 Base64 不完整或非法") from error
if not data:
raise RuntimeError("图片字节为空")
identity = (index, hashlib.sha256(data).hexdigest())
if identity in images: # 相同候选的重复快照只保存一次
continue
extension = {"image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp"}.get(mime, ".bin")
target = folder / f"candidate-{index}-image-{len(images)}{extension}"
partial = Path(str(target) + ".part")
partial.write_bytes(data)
images[identity] = (partial, target)
if extension == ".bin":
print("图片格式未知,保留原始字节供识别:", partial)
if candidate.get("finishReason"):
finished[index] = candidate["finishReason"]
if not seen or any(finished.get(index) != "STOP" for index in seen) or not images:
raise RuntimeError(f"图片响应未完整成功,结束原因={finished};保留 .part,不自动重新生成")
for partial, target in images.values():
partial.replace(target)
for index, parts in texts.items():
(folder / f"candidate-{index}-text.txt").write_text("".join(parts), encoding="utf-8")
return [target for _, target in images.values()]
if __name__ == "__main__":
with open("gemini.sse", encoding="utf-8") as stream:
saved = consume_sse(stream)
print("图片已保存:", saved, ";客户端预览和账务需分别核对")
```
连接断开、JSON/Base64 不完整、没有 `finishReason`、非 STOP 或没有图片,都不能记为完整成功。`STOP` 也不证明账务已结算或客户端能显示图片。保留已收数据、响应头关联标识和错误供平台核对;这个同步流没有 `/media/v1/tasks` 任务 ID,不能查询统一任务或自动重新 POST 来“恢复”,避免重复生成和收费。
协议依据:[Google streamGenerateContent 与 GenerateContentResponse](https://ai.google.dev/api/generate-content#method:-models.streamgeneratecontent)、[图片生成](https://ai.google.dev/gemini-api/docs/image-generation)。
## 计算 Token
`countTokens` 只执行计数,不创建生成任务、不冻结生成资金。它仍要求当前密钥有目标型号权限;参考图片与生成请求一样使用内联 `inlineData`。
```bash
curl -X POST "https://platform.sgt.site/gemini/v1beta/models/gemini-3.1-flash-image:countTokens" \
-H 'x-goog-api-key: YOUR_VIRTUAL_KEY' \
-H 'Content-Type: application/json' \
--data '{"contents":[{"role":"user","parts":[{"text":"Draw a blue ceramic cup"}]}]}'
```
响应示例:`{"totalTokens":6}`。数值由当前输入和目标模型决定,不应按示例值估算图片费用。也可使用 Google 官方 `generateContentRequest` 形式传入完整计数请求,但其中的 `model` 必须与路径型号一致。
## 官方接口对照
### 搜索计量与型号限制
`gemini-3.1-flash-image`、`gemini-3-pro-image` 可使用 `tools: [{"googleSearch": {}}]`,是否实际上架仍以当前授权目录为准。`gemini-3.1-flash-lite-image` 和 `gemini-2.5-flash-image` 不支持搜索,传入 `googleSearch`、`google_search` 或对应 retrieval 工具时在 Provider 请求前返回 400;删除搜索工具或使用支持的型号。
Gemini 3 系列按查询数计量,2.5 及更早的支持搜索型号按 grounded prompt 计量,不把旧代单位套用于 Gemini 3 图片型号。非流式和流式使用相同规则:合并所有候选的 `groundingMetadata.webSearchQueries`、`imageSearchQueries` 及流式累计快照,忽略空白项,对唯一非空查询计数。账务保存计量模式、次数、规则版本、证据来源与请求冻结的有效单价;实际价格以平台当前价格方案为准,供应商免费额度不自动复制给每个调用方。
明确返回空查询数组可表示已知零次;未返回计量证据或流式未完整结束则保持账务待核对,保留预留,不默认计零次或一次。原生同步响应仍保留厂商格式,账务核对使用响应的关联 request_id,不新增平台任务查询或自动重新生成。
官方接口:`POST https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent`,平台接口 `POST /v1beta/models/{model}:generateContent`。Google Gen AI SDK 使用本页 Base URL,即 `base_url="https://platform.sgt.site/gemini"`、`api_version="v1beta"` 和平台虚拟密钥。`Authorization: Bearer`、`x-goog-api-key` 或 `?key=` 均可鉴权,优先使用请求头。当前可用图片模型以该前缀目录为准。