跳到正文

API 生图:GPT 和 Gemini ​

用普通 API Key 就能调用。先用同步请求跑通一张图,再按需要看参考图和异步任务。

每次生成都可能扣费

第一次只执行一种协议的示例,生成张数用 1。超时或连接中断时不要直接重发,先看结果未知与重复提交。

不想写代码?把 Key 交给 AI 配置,看 AI 生图;直接使用网页版,看 生图站使用教程。

1. 创建 Key 并选择分组 ​

  1. 打开 API 密钥页,点「添加令牌」。
  2. 选择包含目标生图模型的分组。调 Gemini 时,分组必须支持 Gemini 原生接口。
  3. 创建后立刻复制 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"
}
JSON

quality 和 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.json

Gemini 文生图的响应保存命令:

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 传线路支持的像素字符串,例如 1024x1024imageConfig.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/asyncAuthorization: Bearer,JSON 同同步文生图
GPT 参考图编辑POST /v1/images/edits/asyncAuthorization: Bearer,multipart 同同步编辑
Gemini 文生图或参考图编辑POST /v1beta/models/{model}:generateContent/asyncx-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"
}
JSON

Gemini 把前面同步请求的完整 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你要做什么
processingaccepted、upstream、materializing继续查询原任务,不要重新提交。
completedcompleted从 result 取图并立即保存。
failedfailed查看 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 是否真实。
401GPT 使用 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、请求时间和使用记录,到技术支持提交核对。不要为了测试扣费再生成一张。

Portdan API 接入、配置与排查指南。