API 生图:GPT 和 Gemini
用普通 API Key 就能调用。先用同步请求跑通一张图,再按需要看参考图和异步任务。
每次生成都可能扣费
第一次只执行一种协议的示例,生成张数用 1。超时或连接中断时不要直接重发,先看结果未知与重复提交。
1. 创建 Key 并选择分组
- 打开 API 密钥页,点「添加令牌」。
- 选择包含目标生图模型的分组。调 Gemini 时,分组必须支持 Gemini 原生接口。
- 创建后立刻复制 Key。不要把它提交到仓库、发到群聊或放进截图。
在终端设置环境变量。把占位内容换成你自己的 Key:
bash
export PORTDAN_API_KEY="sk-your-api-key"本文命令在同一个终端里执行。Windows PowerShell 设置环境变量的写法不同,不要把这条 export 原样粘进去。
2. 查询当前 Key 的模型
模型名以当前 Key 的列表为准。文档里的占位名不代表你的账号有权限。
查询 GPT 模型
GET /v1/models
bash
curl "https://portdan.com/v1/models" \
-H "Authorization: Bearer $PORTDAN_API_KEY"从返回的 data[].id 复制生图模型名:
bash
export GPT_IMAGE_MODEL="粘贴 GPT 生图模型名"查询 Gemini 模型
GET /v1beta/models
bash
curl "https://portdan.com/v1beta/models" \
-H "x-goog-api-key: $PORTDAN_API_KEY"从 models[].name 复制支持图片输出的模型名。如果名称带 models/ 前缀,拼请求地址时要去掉这段前缀:
bash
export GEMINI_IMAGE_MODEL="粘贴 Gemini 生图模型名"只设置你要用的协议即可。创作台展示名可以由管理员修改,不一定等于 API 的 model 字段。同名模型换一条线路,能力也可能不同。
不要重复拼接地址
OpenAI SDK 的 Base URL 填 https://portdan.com/v1,不要再加 /v1。手写 GPT 请求时,在 Base URL 后拼 /images/generations 或 /images/edits。
手写 Gemini 请求使用 https://portdan.com/v1beta,再拼 /models/{模型名}:generateContent。不要把 /v1beta 再重复一次。
3. 第一次同步生图
下面两个示例任选其一。响应写入 JSON 文件,避免大段 Base64 刷满终端。
GPT 文生图
POST /v1/images/generations
bash
curl "https://portdan.com/v1/images/generations" \
-H "Authorization: Bearer $PORTDAN_API_KEY" \
-H "Content-Type: application/json" \
--output gpt-response.json \
--data-binary @- <<JSON
{
"model": "$GPT_IMAGE_MODEL",
"prompt": "画一个蓝色几何立方体,柔和棚拍光线,纯白背景",
"n": 1,
"quality": "medium",
"size": "1024x1024"
}
JSONquality 和 size 能否使用,要看所选线路。quality=medium 不是 2K,quality=high 也不是 4K。
Gemini 文生图
POST /v1beta/models/{model}:generateContent
bash
curl "https://portdan.com/v1beta/models/$GEMINI_IMAGE_MODEL:generateContent" \
-H "x-goog-api-key: $PORTDAN_API_KEY" \
-H "Content-Type: application/json" \
--output gemini-response.json \
--data-binary @- <<JSON
{
"contents": [
{
"role": "user",
"parts": [
{"text": "画一个蓝色几何立方体,柔和棚拍光线,纯白背景"}
]
}
],
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"],
"imageConfig": {
"aspectRatio": "3:4",
"imageSize": "1K"
}
}
}
JSON这里使用 Gemini 原生 imageSize 和 aspectRatio。不要添加 GPT 的 quality、size、background。
4. 保存返回的原图
GPT 可能返回 data[].b64_json,也可能返回 data[].url。Gemini 图片在 candidates[].content.parts 中。下面同一个脚本会处理这两种响应,并把图片保存到 generated-images/。
脚本只使用 Python 3 标准库。把它保存为 save_images.py:
python
import base64
import json
import mimetypes
import sys
import urllib.request
from pathlib import Path
source = Path(sys.argv[1])
payload = json.loads(source.read_text(encoding="utf-8"))
if isinstance(payload.get("result"), dict):
payload = payload["result"]
images = []
def download(url, mime=""):
with urllib.request.urlopen(url, timeout=60) as response:
return response.read(), mime or response.headers.get_content_type()
def suffix(data, mime):
mime = mime.lower().split(";", 1)[0]
by_mime = {
"image/jpeg": ".jpg",
"image/png": ".png",
"image/webp": ".webp",
"image/gif": ".gif",
}
if mime in by_mime:
return by_mime[mime]
if data.startswith(b"\x89PNG\r\n\x1a\n"):
return ".png"
if data.startswith(b"\xff\xd8\xff"):
return ".jpg"
if data.startswith(b"RIFF") and data[8:12] == b"WEBP":
return ".webp"
if mime.startswith("image/"):
return mimetypes.guess_extension(mime) or ".bin"
return ".bin"
if isinstance(payload.get("data"), list):
for item in payload["data"]:
if item.get("b64_json"):
images.append((base64.b64decode(item["b64_json"]), ""))
elif item.get("url"):
images.append(download(item["url"]))
else:
for candidate in payload.get("candidates", []):
for part in candidate.get("content", {}).get("parts", []):
if part.get("thought") is True:
continue
inline = part.get("inlineData") or part.get("inline_data") or {}
mime = inline.get("mimeType") or inline.get("mime_type") or ""
if inline.get("data"):
images.append((base64.b64decode(inline["data"]), mime))
file_data = part.get("fileData") or part.get("file_data") or {}
url = file_data.get("fileUri") or file_data.get("file_uri")
mime = file_data.get("mimeType") or file_data.get("mime_type") or ""
if url:
images.append(download(url, mime))
if not images:
raise SystemExit("响应中没有可保存的图片。请先检查响应,不要直接重复提交生成。")
output_dir = Path("generated-images")
output_dir.mkdir(parents=True, exist_ok=True)
for index, (data, mime) in enumerate(images, start=1):
target = output_dir / f"{source.stem}-{index}{suffix(data, mime)}"
target.write_bytes(data)
print(target)GPT 文生图的响应保存命令:
bash
python3 save_images.py gpt-response.jsonGemini 文生图的响应保存命令:
bash
python3 save_images.py gemini-response.json- 返回
b64_json时,脚本解码后直接写入原始字节,不重新编码图片。 - 返回
url时,脚本下载图片。请及时保存,图片链接可能有时效;不要把 API Key 转发给图片链接所在的站点。 - Gemini 会按返回的
mimeType选择扩展名,不会一律保存成 PNG;thought=true的部分也会跳过。
5. GPT 参考图编辑
POST /v1/images/edits
把单张参考图命名为 reference.png,放在当前目录。图片真实类型必须是 PNG,与示例中的 type=image/png 一致。
bash
curl "https://portdan.com/v1/images/edits" \
-H "Authorization: Bearer $PORTDAN_API_KEY" \
-F "model=$GPT_IMAGE_MODEL" \
-F "prompt=保留主体,把背景改成雨天的城市街道" \
-F "n=1" \
-F "quality=medium" \
-F "size=1024x1024" \
-F "[email protected];type=image/png" \
--output gpt-edit-response.json保存结果:
bash
python3 save_images.py gpt-edit-response.json单图使用 image。多图时,把上面的单图字段换成重复的 image[],每张图各写一次:
bash
-F "image[][email protected];type=image/png" \
-F "image[][email protected];type=image/png" \参考图至少要有一张。当前网关对每个 multipart 部分有 20 MiB 读取上限,因此单个文件必须小于 20 MiB;整个请求还有总大小限制。多图张数和图片格式也受所选线路限制,不要把这里的字段示例当成所有线路的统一张数承诺。
6. Gemini 参考图编辑
Gemini 图生图仍走 generateContent。先读取图片原始字节并编码为 Base64:
bash
IMAGE_BASE64="$(base64 < reference.png | tr -d '\r\n')"再把 inlineData 加到 contents[].parts。mimeType 必须填图片真实类型,data 里只放 Base64,不要放 data:image/png;base64, 前缀。
bash
curl "https://portdan.com/v1beta/models/$GEMINI_IMAGE_MODEL:generateContent" \
-H "x-goog-api-key: $PORTDAN_API_KEY" \
-H "Content-Type: application/json" \
--output gemini-edit-response.json \
--data-binary @- <<JSON
{
"contents": [
{
"role": "user",
"parts": [
{"text": "保留主体,把背景改成雨天的城市街道"},
{
"inlineData": {
"mimeType": "image/png",
"data": "$IMAGE_BASE64"
}
}
]
}
],
"generationConfig": {
"responseModalities": ["TEXT", "IMAGE"],
"imageConfig": {
"aspectRatio": "3:4",
"imageSize": "1K"
}
}
}
JSON保存结果:
bash
python3 save_images.py gemini-edit-response.json多张参考图就在同一个 parts 数组里继续添加 inlineData 对象,每张图使用自己的真实 MIME 类型和 Base64。
7. 尺寸、质量与透明背景
| 能力 | GPT 图片接口 | Gemini 原生接口 |
|---|---|---|
| 尺寸与比例 | size 传线路支持的像素字符串,例如 1024x1024 | imageConfig.imageSize 选原生档位,aspectRatio 选宽高比 |
| 出图质量 | quality 可传线路支持的 auto、low、medium、high | 不套用 GPT 的 quality |
| 透明背景 | 只有对应线路支持时才传 background=transparent | 不添加 GPT 的 background |
| 图片提取 | 读取 data[].b64_json 或 data[].url | 读取 candidates[].content.parts,跳过 thought=true |
出图质量和分辨率不是同一个参数。 请求成功、模型名称、图像 token 或返回像素,都不能证明内部推理强度;没有证据时只能写「未验证」。
Gemini 的档位和宽高比不是精确像素承诺。已有的一次 Gemini Flash 完整实测中,请求 1K、3:4,返回了 896×1200 的 JPEG,生成、保存、下载和扣费核对通过。这只是特定线路、参数和时间下的样本,不代表所有模型都返回这个尺寸或格式,也不代表所有组合都已实测。
透明背景同样不能一概而论。已有样本中,部分 GPT 线路返回了带真实 alpha 的图片;另一些线路接受了 transparent,原图仍不透明。被测的三个 Gemini 模型在那批样本中均返回不透明 JPEG。需要透明图时,必须下载原图并检查 alpha 通道,不能只看参数、PNG 后缀或画面里的棋盘格。
创作台展示不等于普通 Key 的接口规则
创作台的排序、展示名、管理员展示参考价和发布参数限制,不会自动变成普通 Key 的接口规则。直接调用时,以当前 Key 的分组权限、模型映射、余额、模型列表和实际计费规则为准。
8. 长时间生图:异步任务
下面的 /async 和 /images/tasks 是 Portdan/Sub2 网关扩展,不是 OpenAI 或 Google 官方原生端点。
异步有开放条件
站点必须开启异步图片任务并配置图片存储。未开启时会返回 404 async image tasks are not enabled,请先用同步接口或联系支持确认开放状态。
普通异步提交还要求当前分组允许生图,平台为 OpenAI、Grok 或 Gemini;当前实现不接受 composite 分组。同步能用不代表异步已经开放。异步不接受 stream=true,也不接受 streamGenerateContent/async。
提交任务
请求体与对应的同步接口相同,只是把地址换成异步路径:
| 协议 | 异步提交地址 | 鉴权与请求体 |
|---|---|---|
| GPT 文生图 | POST /v1/images/generations/async | Authorization: Bearer,JSON 同同步文生图 |
| GPT 参考图编辑 | POST /v1/images/edits/async | Authorization: Bearer,multipart 同同步编辑 |
| Gemini 文生图或参考图编辑 | POST /v1beta/models/{model}:generateContent/async | x-goog-api-key,原生 JSON 同同步请求 |
GPT 文生图的完整提交示例:
bash
curl "https://portdan.com/v1/images/generations/async" \
-H "Authorization: Bearer $PORTDAN_API_KEY" \
-H "Content-Type: application/json" \
--output async-submission.json \
--data-binary @- <<JSON
{
"model": "$GPT_IMAGE_MODEL",
"prompt": "画一座雨后的未来城市,霓虹灯倒映在街道上",
"n": 1,
"quality": "medium",
"size": "1024x1024"
}
JSONGemini 把前面同步请求的完整 URL 改为 https://portdan.com/v1beta/models/$GEMINI_IMAGE_MODEL:generateContent/async,请求体不变。GPT 编辑也把同步编辑地址末尾加上 /async,multipart 字段不变。
成功创建任务返回 HTTP 202,响应头带有 Location 和 Retry-After: 3。响应体示例:
json
{
"id": "imgtask_0123456789abcdef",
"task_id": "imgtask_0123456789abcdef",
"object": "image.generation.task",
"status": "processing",
"stage": "accepted",
"timings": {
"submitted_at": 1790000000123,
"accepted_at": 1790000000123
},
"created_at": 1790000000,
"expires_at": 1790086400,
"poll_url": "/v1/images/tasks/imgtask_0123456789abcdef"
}id 与 task_id 相同。created_at、expires_at 使用 Unix 秒;timings 里的时间点使用 Unix 毫秒。Gemini 提交返回的 poll_url 可能不带 /v1 前缀,按返回值查询即可。
普通公开任务不去重
每次重复 POST 都会创建新任务,可能再次生成和扣费。普通公开任务不支持用 Idempotency-Key 去重,也没有通过该键恢复丢失提交响应的公共接口。
每 3 秒查询一次原任务
GET /v1/images/tasks/{task_id}
从提交响应复制 task_id,不要再发一次生成:
bash
export IMAGE_TASK_ID="粘贴 task_id"
curl "https://portdan.com/v1/images/tasks/$IMAGE_TASK_ID" \
-H "Authorization: Bearer $PORTDAN_API_KEY" \
--output image-task.json查询必须使用提交时的同一把 Key。同一账号的另一把 Key 也不能读取。处理中每 3 秒重复一次 GET,直到 completed 或 failed。也可以按提交响应的 poll_url 查询。
status | 常见 stage | 你要做什么 |
|---|---|---|
processing | accepted、upstream、materializing | 继续查询原任务,不要重新提交。 |
completed | completed | 从 result 取图并立即保存。 |
failed | failed | 查看 error 和 http_status,先核对原任务与用量。 |
GET 返回 HTTP 200,只表示拿到了任务快照。 任务为 failed 时,GET 也可能是 200;http_status 才是后台执行或上游处理的状态。任务状态没有单独的 cancelled 或 expired。
取回并保存图片
result 保留对应协议的结构,不是跨协议的统一格式:
- GPT 通常从
result.data[].url取图;顶层也可能有便于读取的image_url。 - Gemini 从
result.candidates[].content.parts[].fileData.fileUri取图,并读取对应的mimeType。仍需跳过thought=true的部分。
前面的 save_images.py 也会读取任务响应里的 result,并支持 Gemini 的 fileData。任务完成后,对刚才保存的 image-task.json 执行:
bash
python3 save_images.py image-task.json普通公开任务没有 /content 或 /download 任务端点。图片在返回的存储 URL 上,下载时不要把 API Key 转发到该站点。
过期、失败与响应丢失
- 当前实现创建任务时,
expires_at是创建时间加 24 小时;进入completed或failed时,会改成终态时间加 24 小时。以响应里的实际值为准。 - 图片 URL 有自己的签名有效期,与任务
expires_at不是一回事。拿到图片后必须立即下载保存。 - 任务记录消失后,查询返回
404 IMAGE_TASK_NOT_FOUND。它也可能表示任务不存在或 Key 不对,没有单独的expired状态。 failed不代表没有生成或没有扣费。上游可能已经出图,后续下载或转存才失败;先查使用记录,不要自动重发。- 当前实现的普通任务执行上限为 30 分钟。超时通常表现为
failed、http_status=504和timeout_error,仍要先核对原任务与用量。 - 普通公开任务没有取消端点。客户端断连不会取消已经接受的任务。
- 如果 POST 的 202 响应完全丢失,又没有保存
task_id,普通公开接口没有幂等恢复入口。先查使用记录并联系支持核对,不要靠再次 POST 找回任务。
9. 常见错误、结果未知与重复提交
| 情况 | 怎么处理 |
|---|---|
400 | 检查协议字段、模型名和 JSON。GPT 编辑检查 multipart 字段;Gemini 检查 Base64 是否带错前缀、MIME 是否真实。 |
401 | GPT 使用 Authorization: Bearer;Gemini 使用 x-goog-api-key。检查 Key 是否完整、是否已删除。 |
403 | 检查 Key 分组、模型权限、生图权限、余额或订阅限制。 |
404 / model_not_found | 检查是否重复拼接 /v1 或 /v1beta,再从当前 Key 的模型列表复制名称。 |
413 | 减少参考图大小或张数,再检查整个请求体是否过大。结果未明确时不要直接重发。 |
429 | 检查额度、限流或并发限制;响应带 Retry-After 时按它等待。 |
502 / 503 | 不能据此认定模型不支持某个参数。保留报错,先核对原请求是否已有任务或用量,不要连续重发。 |
| 超时、连接中断、客户端退出 | 生成结果未知。异步任务先查原 task_id;同步请求先查使用记录,仍无法判断时联系支持核对。 |
| HTTP 成功但找不到图片 | 打开响应 JSON,检查文本、错误信息和实际字段。不要只因没有图片就立即再生成一次。 |
结果未知时,重新 POST 可能产生第二张图和第二笔扣费。 先查原任务与使用记录。需要技术支持时,带上请求时间、模型名、状态码、响应文件和 task_id;不要发送完整 API Key。
10. 查看实际用量与扣费
打开 使用记录,按请求时间、Key 和模型找到本次记录,核对实际用量与扣费。
响应里的 usage(如果返回)可以辅助核对,但最终以使用记录中的实际扣费为准。返回像素变小不代表一定少扣费,创作台的展示参考价也不等于普通 Key 的实际账单。
对账有疑问时,保留响应文件、task_id、请求时间和使用记录,到技术支持提交核对。不要为了测试扣费再生成一张。