# 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=` 均可鉴权,优先使用请求头。当前可用图片模型以该前缀目录为准。