通过 AI API 聚合平台调用 Nano Banana 2.1 实现图片生成、保存与编辑

简介: 本指南介绍如何调用 Vertex 提供的 Gemini 模型 `google/gemini-nano-banana-2.1` 进行图像生成与编辑,含完整 Python 示例、链式编辑、本地图片上传等功能。强调 API 鉴权、响应解析、token 计费及迁移注意事项,所有示例均为真实原生 Gemini 协议请求。

更新于 2026 年 10 月 13 日。 既是本指南的发布方,也是文中所述请求使用的 API 网关。本文由 AI 辅助撰写,根据真实 API 和工具结果进行核对,并在发布前经过独立 AI 审阅。下文讨论的六次图片请求,均是发出的真实原生 Gemini 协议请求;示例程序供你自行发起请求,并不表示测试过所有可能的路由或设置。

Nano Banana 2.1 在 上的模型 ID 是 google/gemini-nano-banana-2.1。本文用来比较的旧模型是 google/gemini-3.1-flash-image,通常称为 Nano Banana 2。迁移请求时,不要用旧 ID 替换新 ID。 公开模型页列出了 2.1 支持 Gemini 和 OpenAI 协议,并标明其提供方为 Vertex。本指南中实际运行的请求使用的是原生 Gemini generateContent 路由,而不是 OpenAI 兼容路由。

你将生成一张海报,把返回的图片数据写入磁盘,还可以把模型的完整响应传入后续两轮编辑。另有一个上传选项,展示如何提供已有的本地图片。若想专门了解编辑与版式决策,请参阅海报编辑指南。

发起第一个请求前的准备

你需要一个有权访问该模型的 API 密钥、足够的账户余额、Python 3 和 requests 包。安装依赖,并在 shell 中设置密钥: 在向 Nano Banana 2.1 发起首个图像生成请求之前,开发者需要完成端点配置与鉴权令牌的初始化,可参考 所提供的请求结构规范,该规范与 OpenRouter 的调用约定在参数字段命名上存在若干差异,需提前核对。

python -m pip install requests
export API_KEY="YOUR_OFOX_API_KEY"

不要把密钥放进源文件、截图或共享日志中。经验证的请求使用了以下 API 端点:

POST 
x-goog-api-key: YOUR_OFOX_API_KEY

下面的请求会同时要求文本和图片两种响应模态、正方形宽高比,以及 1K 图片尺寸。Google 图片生成文档介绍了 1K、2K 和 4K 选项;本文网关实际验证的只有 1K。不要把文档所述选项视为已经你所用的路由全部测试。512 不是该模型文档列出的尺寸选项。

完整的 Python 程序

将下面的代码保存为 nano21.py。默认运行会发起一次生成请求。添加 --chain 会先发起该请求,再发送测试中使用的两条原样编辑提示词。添加 --upload PATH 则会用指定的本地图片发起新请求,并附上修改标题的提示词;如果想让这条提示词有意义,请使用原始海报图片。这两个选项不能同时使用。

import argparse
import base64
import binascii
import json
import mimetypes
import os
from pathlib import Path
import sys

import requests

URL = (
 ""
 "google/gemini-nano-banana-2.1:generateContent"
)

PROMPTS = [
 "Create a square minimalist event poster on warm cream paper. Large exact headline: ORCHARD MARKET. Smaller exact line: SATURDAY 10 AM. Draw three red apples in a blue ceramic bowl below the text. No other text, no logos, no border.",
 "Change only the headline from ORCHARD MARKET to ORCHARD FAIR. Keep SATURDAY 10 AM exactly unchanged. Preserve the three red apples, blue bowl, cream background, composition and all other pixels as closely as possible.",
 "Now change only the bowl from blue to dark green. Preserve ORCHARD FAIR, SATURDAY 10 AM, the three red apples and the layout.",
]

EXTENSIONS = {
 "image/png": ".png",
 "image/jpeg": ".jpg",
 "image/webp": ".webp",
 }

def send(contents, api_key):
 body = {
 "contents": contents,
 "generationConfig": {
 "responseModalities": ["TEXT", "IMAGE"],
 "imageConfig": {
 "aspectRatio": "1:1",
 "imageSize": "1K",
 },
 },
 }
 try:
 # Deliberately one attempt: image requests are not automatically retried.
 response = requests.post(
 URL,
 headers={"x-goog-api-key": api_key},
 json=body,
 timeout=(15, 180), # connection timeout, then read timeout
 )
 except requests.RequestException as exc:
 raise RuntimeError(f"Request failed before a usable response: {exc}") from exc

 try:
 data = response.json
 except ValueError as exc:
 raise RuntimeError(
 f"HTTP {response.status_code}; non-JSON response: "
 f"{response.text[:1000]}"
 ) from exc

 if not response.ok:
 error = data.get("error", data) if isinstance(data, dict) else data
 raise RuntimeError(
 f"HTTP {response.status_code}; API error: "
 f"{json.dumps(error, ensure_ascii=False)[:2000]}"
 )
 if not isinstance(data, dict):
 raise RuntimeError("Successful HTTP response was not a JSON object.")
 return data

def save_response(data, output_dir, turn_number):
 candidates = data.get("candidates") or []
 if not candidates or not isinstance(candidates[0], dict):
 raise RuntimeError(f"No candidate returned: {json.dumps(data)[:2000]}")

 candidate = candidates[0]
 finish_reason = candidate.get("finishReason")
 if finish_reason not in (None, "STOP"):
 raise RuntimeError(
 f"Generation did not finish normally ({finish_reason}): "
 f"{json.dumps(candidate, ensure_ascii=False)[:2000]}"
 )

 # Keep this original object intact for the next turn. In particular, do not
 # rebuild its parts from printed text and saved image files.
 content = candidate.get("content")
 if not isinstance(content, dict):
 raise RuntimeError("First candidate has no content object.")

 output_dir.mkdir(parents=True, exist_ok=True)
 image_count = 0
 for part in content.get("parts", []):
 if "text" in part and part["text"]:
 print(f"text: {part['text']}")

 inline = part.get("inlineData")
 if not inline:
 continue
 mime = inline.get("mimeType", "").split(";", 1)[0].lower
 if not mime.startswith("image/"):
 continue
 encoded = inline.get("data")
 if not isinstance(encoded, str):
 raise RuntimeError("Image part has no base64 string.")
 try:
 image_bytes = base64.b64decode(encoded, validate=True)
 except (binascii.Error, ValueError) as exc:
 raise RuntimeError("Image part contains invalid base64.") from exc
 if not image_bytes:
 raise RuntimeError("Image part decoded to zero bytes.")

 image_count += 1
 extension = EXTENSIONS.get(mime, ".bin")
 path = output_dir / f"turn-{turn_number}-image-{image_count}{extension}"
 path.write_bytes(image_bytes)
 print(f"saved image: {path} ({mime})")

 if not image_count:
 raise RuntimeError(
 "Response contained no image inlineData; inspect the returned "
 "candidate and any text instead of treating text as an image."
 )
 if "usageMetadata" in data:
 print("usageMetadata:", json.dumps(data["usageMetadata"]))

 return content

def uploaded_image_part(path):
 mime, _ = mimetypes.guess_type(path.name)
 if mime not in EXTENSIONS:
 raise ValueError("Upload a .png, .jpg, .jpeg, or .webp image.")
 image_bytes = path.read_bytes
 if not image_bytes:
 raise ValueError("Upload file is empty.")
 return {
 "inlineData": {
 "mimeType": mime,
 "data": base64.b64encode(image_bytes).decode("ascii"),
 }
 }

def main:
 parser = argparse.ArgumentParser
 parser.add_argument("--chain", action="store_true")
 parser.add_argument("--upload", type=Path)
 parser.add_argument("--output-dir", type=Path, default=Path("outputs"))
 args = parser.parse_args
 if args.chain and args.upload:
 parser.error("--chain and --upload are separate workflows")

 api_key = os.environ.get("API_KEY")
 if not api_key:
 parser.error("Set API_KEY in the environment first")

 if args.upload:
 if not args.upload.is_file:
 parser.error(f"Image file does not exist: {args.upload}")
 # A new conversation with an uploaded image, not the earlier
 # generation/edit conversation.
 contents = [{
 "role": "user",
 "parts": [
 uploaded_image_part(args.upload),
 {"text": PROMPTS[1]},
 ],
 }]
 result = send(contents, api_key)
 save_response(result, args.output_dir, 1)
 return

 contents = []
 selected_prompts = PROMPTS if args.chain else PROMPTS[:1]
 for turn_number, prompt in enumerate(selected_prompts, start=1):
 contents.append({"role": "user", "parts": [{"text": prompt}]})
 result = send(contents, api_key)
 model_content = save_response(result, args.output_dir, turn_number)
 # Preserve the full returned content, including inline image data,
 # text, and any signatures on parts, for the next request.
 contents.append(model_content)

if __name__ == "__main__":
 try:
 main
 except (OSError, ValueError, RuntimeError) as exc:
 print(f"error: {exc}", file=sys.stderr)
 sys.exit(1)

运行 python nano21.py 发起一次调用。若要运行全部三轮,执行 python nano21.py --chain。若要在一个新对话中编辑磁盘上已有的海报,执行 python nano21.py --upload outputs/turn-1-image-1.png。这些是上文程序的 Python 调用示例,不是 专用 CLI 命令。

如果响应中包含图片,预期会看到类似 saved image: outputs/turn-1-image-1.png (image/png) 的一行,以及可能返回的文本和 usageMetadata。这行内容是输出格式示例,不是这段脚本另一次运行的记录。在经验证的海报请求中,响应状态为 HTTP 200,结束原因为 STOP,包含一张 1024 × 1024 的 PNG 图片,并报告 54 个输入 token、80 个文本输出 token、1,120 个图片输出 token 和 639 个思考 token。原始响应中没有可见的文本部分,尽管它报告了 80 个 TEXT 输出 token。请把这个数字视为返回的用量元数据,而不是可见文本。你实际收到的文本、元数据、耗时和图片都可能不同。

代码会保存第一个候选中的每个图片 inlineData 部分,不会假定第一部分就是图片。代码会根据响应中的 MIME 类型选择文件扩展名;如果图片 MIME 类型无法识别,就保存为 .bin,而不会错误地标成 PNG。代码会尽可能把 HTTP 错误解析为 JSON;如果缺少图片,则视为失败,而不是成功。程序不会自动重试:超时后盲目重复生成,可能会生成另一张图片并再次产生费用。如果请求失败,请检查其状态、错误 JSON 和账户状态,再决定是否重新提交。Google 故障排查指南有助于理解 Gemini 侧的错误类别,但 网关响应仍需根据其自身内容判断。

为什么编辑历史必须包含完整响应

保存的 PNG 是输出产物,不能替代对话历史。在 --chain 工作流中,每个后续请求都会包含先前的用户轮次、模型完整返回的 candidates[0].content,然后才是新的用户轮次。该内容可能包含文本、内嵌图片数据,以及各部分的思考签名。仅凭图片文件和可见文本重建它,可能会丢失后续有效跟进所需的数据。

这也是上传路径与连续编辑流程分开的原因。--upload 会将本地文件的 MIME 类型和 base64 字节作为 inlineData 部分,与指令一起发送。它不会把上传文件伪装成模型先前的一轮响应,也不会重建该响应的签名。其他产品保留相关任务,请参阅蒙版与产品保留工作流和使用一张还是两张场景参考图。

“只改……”是一种明确的编辑指令,不是像素锁定功能。在我们的三步测试中,2.1 和旧模型都在第一张海报上生成了要求的英文标题和时间;编辑时,两者都把标题改成了 ORCHARD FAIR,同时保留了 SATURDAY 10 AM、三个红苹果和一个蓝碗。第三轮中,两者都把碗改成了深绿色,并保留了要求的文字。两个模型的输出都没有显示可编辑图层。尽管提示词要求“无边框”,构图仍将海报放在带有外侧边距或阴影的纸张上。正式采用编辑结果前,请检查实际返回的图片,尤其是在精确文字、产品细节或版式很重要时。

每个模型都进行了一条三步编辑链测试,使用相同的提示词、1K 和 1:1 配置,且未设置 seed 或思考级别。六次请求均返回 HTTP 200 和 STOP,并生成 1024 × 1024 的 PNG。这不是重复可靠性测试,也不是总体质量排名;两个模型使用的默认设置也可能不同。下方图片是这些 API 请求的结果,并非 Playground 运行截图:

步骤 Nano Banana 2.1 较旧的 Nano Banana 2
海报
标题编辑
碗的编辑

可下载的对比资料包包含相关文件。如果你正在迁移现有集成,2.1 与 2 的对比会将观察到的行为与更广泛的迁移决策分开讨论。

公开模型页与经过身份验证的 API 请求

公开模型页截图用于说明界面可用性和请求准备选项;它并不能证明 Playground 曾生成图片。 公开模型页面所展示的能力描述仅供参考,实际经过身份验证的 API 请求需携带有效的 Bearer Token, 与 OpenRouter 均采用此标准鉴权机制,但两者在速率限制策略与响应头字段的具体实现上有所不同。

模型页提供了一个内嵌 Playground,包含提示词输入框、附件功能和后续编辑入口。使用该 Playground 需要登录。界面中的宽高比说明也提示,请求的比例尽力满足;因此,在 UI 中选择某个比例,不代表每次结果都有保证。本指南不声称测试过经过身份验证的 Agent 界面,也不声称 Agent 一定会选择 2.1。如果需要可复现的集成,请使用准确的模型 ID,并像上文所示检查 API 响应。

迁移时需要核对的事项、限制与费用

将 ID 从 google/gemini-3.1-flash-image 改为 google/gemini-nano-banana-2.1,不只是拼写上的变化。请核对响应解析器、图片尺寸选择、输出 token 限制,以及对文本或思考输出的预期。在实测海报请求中,2.1 报告了文本输出 token 和思考 token,而旧模型的海报响应没有报告这些类别。某个类别未被报告,不代表其用量为零;提示词相同,也不能保证用量或默认设置相同。请保留完整响应并记录返回的用量,不要沿用旧模型的计费统计假设。 将现有图像生成工作流迁移至 Nano Banana 2.1 时,需逐项核对输出分辨率上限、并发请求数限制以及按图像计费的单价结构, 的计费文档对上述参数均有明确说明,建议与 OpenRouter 的对应条款对照审阅。

当前公开目录快照列出的 2.1 费率为:每百万输入 token 1.50 美元、每百万文本输出 token 7.50 美元、每百万图片输出 token 30 美元;目录还单独列出了按请求计费的网络搜索费率。这些是目录中的观察值,不是经过验证的账单,也不是每张生成图片的固定价格。Google 的定价页面也列出 2.1 生成 1K 图片输出需 1,120 个图片 token,但 Google 发布的材料对 4K 图片 token 数量存在差异;本文没有网关运行 4K 请求。在估算批量成本或假设图片 token 费率较低就意味着完整请求更便宜之前,请先阅读定价分析。

Google 的模型文档介绍了多张图片参考等能力,图片生成指南则涵盖图片配置。这些官方介绍的能力,并不意味着每种设置、参考图数量或分辨率都已经这条 路由进行过测试。尤其不要想当然地认为 seed、temperature 或 topP 参数适用于这个由 Vertex 提供支持的模型,然后把未经验证的调参方法加入请求。

最后,生成失败和使用权是两个不同的问题。遇到空图片响应或请求错误时,请根据实际返回的 JSON 和状态进行诊断;未返回图片错误指南对此有更详细的说明。用于发布或商业用途时,请确保你有权使用所提供的图片,并遵守适用的 Gemini 条款。SynthID 相关信息并不构成可见水印、所有权、独占权、不侵权或机密数据方面的保证。


参考来源

相关文章
|
1月前
|
缓存 编解码 测试技术
GPT Image 2.5 图片生成费用详解:Flare 与 Sunburst 两种模式定价解析
OpenAI发布GPT-Image 2.5双模型:Flare(重速度)与Sunburst(重精度),均按token计费——文字输入$5M、图片输入$8M、输出$30M/百万token,缓存另计。单价统一,但实际单图成本因用量而异,官方提供输出token估算器。
|
1月前
|
缓存 安全 前端开发
Qwen3.8-Max-0902 更新解读:价格不变但每次请求额外消耗 38 个输入 token
Qwen3.8-Max-0902版发布:编程能力跃升,CodeArena前端榜夺冠(1691分),8项编码测试全面超越前版;价格不变(输入$2/百万token、输出$6),但单次请求固定多耗38 token;100万上下文,支持图文输入与深度推理。
|
1月前
|
缓存 API 网络安全
Claude Fable 5.1 API 接入指南:模型标识、五级定价、effort 参数与缓存 TTL 详解
Claude Fable 5.1(2026年9月发布)核心变化:上下文1M token、输出128K、Adaptive thinking常驻开启(按$50/M token计费);effort调高增token用量而非单价;缓存读仅$0.25/M,复用百次成本降为Fable 5的1/3;TTL选型影响显著——5分钟档续期依赖活跃调用,空闲超时将倍增写入费。迁移仅需替换模型ID。
|
20天前
|
编解码 API 开发工具
GPT Image 2.5 API 实战教程:Python 调用实现图片生成与编辑
OpenAI Images API接入GPT-Image-2.5需选模型:Flare(快,适合草稿/批量)、Sunburst(精,适合精细编辑/交付)。调用`generate`或`edit`,结果从`b64_json`解码保存。支持PNG/WebP透明输出,尺寸、画质可调,需注意权限与计费细节。
|
1月前
|
Web App开发 缓存 数据安全/隐私保护
ChatGPT DeviceCheck 验证报错 403/500/503 的有效解决方案
DeviceCheck 报错是设备认证失败(非账号问题),发生在密码校验前,仅影响原生App。根因包括系统缓存异常、未完成更新、越狱/root、Play Integrity失效或macOS证书链中断。网页版完全不受影响。
|
3月前
|
JSON Shell API
Claude Code 转向 Codex 实战指南:12 项关键配置与 1 个踩坑记录
从 Claude Code 迁移到 Codex,基本上就是一场重命名加重排格式的活儿,而 Codex 如今还自带一条单命令导入器,帮你完成其中大部分工作。真正的麻烦藏在它遗留下来的东西里,而其中一项根本就不是配置文件。
|
1月前
|
运维 安全 API
OpenAI Agents API 正式发布:Codex 底层能力开放后,开发者的角色如何转变
OpenAI于2026年9月10日推出Agents API公测版,开放Codex托管运行底座,提供会话管理、自动上下文压缩、多智能体编排及可替换沙箱等能力。开发者专注业务逻辑,平台接管模型调用、工具编排与状态维护,按token与容器时长计费,无平台附加费。
|
4月前
|
缓存 Shell API
Codex 周限额提前耗尽怎么办:5 种替代方案及按量计费 API 接入配置指南
2026 年 5 月 17 日,一位 Plus 用户眼看着 Codex 周计数器一天从剩余 96% 掉到 0%——而承认这起事件的 OpenAI 工程师,仍然没法保证计数器在下一个周窗口前会重置。如果你的周限额每个月清零两次,2026 年正确的做法不是反复刷 /status,而是把 Codex CLI 接到按量计费的端点上,让钱包封顶替代日历封顶。
|
4月前
|
Shell iOS开发 MacOS
npm全局安装后提示codex command not found的7种解决方法(2026)
2026 年呼声最高的 Claude Code 功能,悄悄在 v2.1.169 上线了,多数人还不知道它切目录时能把提示缓存保住,不是清掉重来。 如果你以前每次想去 git worktree 或者隔壁仓库工作,都得退出 Claude Code 重启——这套仪式从 2026-06-08 起可以扔掉了。新出的 /cd 命令让会话中途切到任何目录,前缀缓存还能活着。对 Claude Opus 4.8(输入 5 美元/M token、缓存读 0.5 美元/M token)来说,这意味着原本要重发的那一堆上下文打了一折。 但”保住缓存”这句话带 3 个星号。这篇把可验证的机制、操作步骤、3 个会让缓
|
3月前
|
人工智能 缓存 IDE
Gemini CLI 免费配额缩减后的 6 种有效应对方案(2026)
2026 年 6 月 18 日,Gemini CLI 停止为免费的『用 Google 账号登录』通道处理请求,AI Pro 和 Ultra 登录也一并停掉。这个二进制没死:它仍是 Apache-2.0,仍在维护(v0.49.0),配上 API key 照样能跑。如果你正盯着『This client is no longer supported for Gemini Code Assist for individuals』发愁,那是认证问题,不是装坏了。修法就是别再用 Google 账号登录,改用一个开通了计费的 API key。2026 年 9 月还有第二堵墙等着你,届时不受限的 Standa

热门文章

最新文章