图像接口概览
土土金图像接口是一套 OpenAI 兼容的 Images API,模型 gpt-image-2,一个文生图端点、一个图生图端点。这一页讲清开工前必须知道的前提,并带你跑通第一张图。
这是什么
协议形态是 OpenAI 兼容的 Images API —— 你手上任何能调 OpenAI Images 的代码或 SDK,把 base URL 和 key 换掉基本就能直接跑。接口地址只有一个:https://api.tutujin.com/v1,模型名固定 gpt-image-2。
| 用途 | 端点 | 请求体 |
|---|---|---|
| 文生图(纯提示词出图) | /v1/images/generations |
application/json |
| 图生图(带参考图 / 改图) | /v1/images/edits |
multipart/form-data |
统一称呼是「土土金图像接口 / Tutujin Image API」。不要在代码、注释、日志、错误信息、commit message 里出现其它服务商的域名或品牌名,也不要把响应里返回的图片 URL 的域名写死进代码。
鉴权
每个请求带一个 header 就行,没有别的握手步骤:
Authorization: Bearer <TUTUJIN_API_KEY>Key 形如 sk-xxxxxxxx,找管理员要,文档里不会有。拿到之后放环境变量,不要硬编码进源码:
export const TUTUJIN = {
baseUrl: 'https://api.tutujin.com/v1',
apiKey: process.env.TUTUJIN_API_KEY, // 绝不硬编码,绝不进 git
model: 'gpt-image-2',
timeoutMs: 300_000, // 5 分钟。正常 50–110s,2K/4K 更久
};
Key 写进 .env,然后确认 .env 真的被 git 忽略了。跑一句 git check-ignore -v .env,有输出就说明已忽略;没输出就是没忽略,先补上再继续。Key 一旦跟着 commit 推上去,就得整体轮换,所有在用它的服务都要跟着改。
开工前必须知道的三件事
这三条不是细节,是决定你客户端整体怎么搭的前提。先看完再动手,能省掉后面大半天的排查。
一次请求 = 一张图,请求返回的时候图已经生成完毕了,直接从响应体里取。没有 webhook、没有轮询、没有任务 id、也没有"查询任务状态"这类端点。所以不要按异步任务队列那套去设计客户端 —— 接口里没有那些东西可给你查。
这是正常水平,不是出故障了。它对架构有几个直接影响:网关、反向代理、前端请求的超时都要放宽到分钟级;Serverless 函数的最长执行时长要够;不要把这个请求直接挂在用户点击后面同步等待,用户会以为页面死了。稳妥的做法是自己这一侧做成异步任务,把等待放到后台。
Node 内置的 fetch 不带任何默认超时。上游一旦卡住不返回,请求会永久挂在那儿:不报错、不返回、连接不释放,日志里干干净净什么都看不到,直到并发被占满、整个服务跟着一起僵。每一个 fetch 都必须传 signal: AbortSignal.timeout(...) —— 包括后面下载生成图那一次。
五分钟跑通第一张图
在写任何封装之前,先用一条命令确认「key 能用、网络能通、接口有反应」。这一步只花几分钟,能把"是我代码写错了"和"是链路不通"彻底分开。Node 20+ 直接整行复制:
TUTUJIN_API_KEY=你的key node -e "
fetch('https://api.tutujin.com/v1/images/generations', {
method:'POST',
headers:{Authorization:'Bearer '+process.env.TUTUJIN_API_KEY,'Content-Type':'application/json'},
body:JSON.stringify({model:'gpt-image-2',prompt:'a red apple',size:'1024x1024',quality:'high',response_format:'url'}),
signal:AbortSignal.timeout(300000)
}).then(async r=>console.log(r.status, (await r.text()).slice(0,500)))
"
它会安安静静地跑一到两分钟 —— 那不是卡住了,是在出图 —— 然后打印一个状态码和一段 JSON。
| 打印出来的东西 | 说明 |
|---|---|
200 + 一段带 data 的 JSON | 链路全通,可以开始写正式代码 |
401 / 403 | key 不对、没权限,或者环境变量没读到 |
402 | 余额不足,找管理员 |
命令直接抛 TimeoutError | 300 秒都没返回,见错误处理与避坑 |
一个完整的文生图最小示例
下面这段是可以直接放进项目的形状。它比"能跑"多做了三件事:设了超时、检查了状态码并把响应正文带出来、两种返回形式都兼容。这三样任缺一个,出问题时都会变得很难查。
const BASE = 'https://api.tutujin.com/v1';
const KEY = process.env.TUTUJIN_API_KEY;
export async function textToImage({ prompt, size = '1024x1024' }) {
const resp = await fetch(`${BASE}/images/generations`, {
method: 'POST',
headers: {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-image-2',
prompt, // 建议英文;中文也能理解,英文更稳
size, // 必须是 "宽x高" 像素串,而且永远要传
quality: 'high',
response_format: 'url', // 期望返回 URL,但也可能给 b64_json
}),
signal: AbortSignal.timeout(300_000), // ← 少了这行会永久挂住
});
// 非 2xx 一定要把响应正文带出来,否则后面无从判断是审核拦截还是参数错
if (!resp.ok) {
const text = await resp.text();
throw new Error(`图像接口返回 ${resp.status}: ${text.slice(0, 500)}`);
}
const json = await resp.json();
const d = json?.data?.[0];
if (!d) throw new Error('响应里没有 data');
// 两种返回形式都要无条件兼容,别赌只会返回其中一种
if (typeof d.b64_json === 'string' && d.b64_json) {
return Buffer.from(d.b64_json, 'base64');
}
if (typeof d.url === 'string' && /^https?:\/\//.test(d.url)) {
// 这个 URL 是临时的,拿到当下就下载转存
const r = await fetch(d.url, { signal: AbortSignal.timeout(60_000) });
if (!r.ok) throw new Error(`下载生成图失败: ${r.status}`);
return Buffer.from(await r.arrayBuffer());
}
throw new Error('data[0] 里既没有 url 也没有 b64_json');
}
两个细节值得单独说一句。第一,size 永远要传,而且只能是 "1536x864" 这样的像素串;传比例别名或者干脆省略,接口不会报错,只会悄悄给你一张不是你要的尺寸的图。第二,响应可能是 url,也可能是 b64_json,两种都得接住。
响应里那个 url 存活时间以小时计,域名也不固定。拿到的当下就下载转存到你自己的存储(对象存储或本地磁盘都行),不要存进数据库当长期地址,也不要直接把它发给终端用户 —— 今天入库、明天用户点开就是一张破图。上面的示例已经这么做了。
接下来看哪一页
这个接口最难缠的地方不是报错,是不报错:size 传了比例别名、多参考图只有第一张生效、n 参数根本不存在 —— 这几种情况都照常返回 200、照常计费,只是结果不对。花十分钟先看完错误处理与避坑,比事后对着一堆漂亮的 200 日志猜半天划算得多。