refactor(task-worker): call gpt-image-2 via the AI Gateway OpenAI route, not fal
**PR:**refactor(task-worker): call gpt-image-2 via the AI Gateway OpenAI route, not fal (#4314)
source_automation: “voyager-merged-pr” run_date: “2026-07-04” anchor_pr_number: 4314 pr_number: 4314 pr_title: “refactor(task-worker): call gpt-image-2 via the AI Gateway OpenAI route, not fal” pr_url: “https://github.com/adastralab-ai/voyager/pull/4314 ↗” author: “Horcrux / magicismight” merged_at: “2026-07-04T04:25:15Z” modules: [“backend/workers/task/src/vendors”,“backend/workers/task/src/executor/ops”,“backend/workers/task/scripts”,“backend/go/internal/image”] files_changed: 9 learning_tags: [“task-worker”,“image-generation”,“cloudflare-ai-gateway”,“vendor-boundary”,“safety-error”,“multipart-order”,“selftest”] business_line: null related_prs: [4156] line_stage: null open_questions: [] feishu_doc_url: “https://my.feishu.cn/docx/JIxsdLRNkoh0mwxMcwNcLU1Gnxf ↗”#
正文来源:飞书学习文档 ↗。以下为通过个人 Feishu API 获取并转换后的完整 Markdown 正文。
今日选择#
**PR:**refactor(task-worker): call gpt-image-2 via the AI Gateway OpenAI route, not fal (#4314)
**作者:**Horcrux / magicismight
**Merge 时间:**2026-07-04T04:25:15Z
链接:https://github.com/adastralab-ai/voyager/pull/4314 ↗
**模块:**backend/workers/task, backend/go/internal/image
**学习标签:**task-worker, image-generation, cloudflare-ai-gateway, vendor-boundary, safety-error, multipart-order, selftest
为什么值得学#
- 它把 gpt-image-2 从 fal 代理迁到 Cloudflare AI Gateway 的 provider-native OpenAI route,减少一个供应商中转,同时没有把 OpenAI key 下发给 worker,凭证边界更清楚。
- 它没有只做 endpoint 替换,而是把返回形态从 fal URL 改成 OpenAI inline base64,顺手删除结果 CDN 二次拉取,让 executor 的失败阶段更贴近真实责任边界。
- 它把内容安全拒绝映射到既有的 SafetyError / image_safety 语义,避免 moderation_blocked 被当成可重试 model_error。
- 它为 multipart edit 路径保留 referenceImages 顺序:并行 fetch 可以保留,但 append 顺序必须由输入数组决定,因为首图在 edit 端点里有主图语义。
关键代码#
Go 侧只传 provider-native model id#
case api.GptImage2:
return "gpt-image-2"plaintext**位置:**backend/go/internal/image/ops_generate.go:36-44。这里把旧的 openai/gpt-image-2 改成 gpt-image-2,说明 provider 路由选择被下沉到 worker vendor 层;Go 服务只负责公共模型枚举到 provider 模型名的契约,不再泄露 fal 的命名空间。
Executor 不再拉 fal CDN URL#
const result = await runStage("model_error", () =>
vendors.openai.generateGptImage({
model: payload.model,
prompt: payload.prompt,
imageUrls: payload.referenceImages.map((ref) => ref.url),
imageSize,
}),
);
return result.buffer;plaintext**位置:**backend/workers/task/src/executor/ops/generate.ts:77-85。旧路径是 vendors.fal.generateGptImage 返回 imageUrl,再用 fetchImageBytes 拉结果;新路径直接拿 buffer。这减少了一段外部依赖,也让 source_fetch_failed 不再混进模型结果下载失败。
OpenAI vendor 把网关认证、错误分类和返回解码集中起来#
headers: { "cf-aig-authorization": authHeader, ...init.headers },
...
if (isContentModerationError(body)) {
throw new SafetyError(`OpenAI gpt-image-2 refused: ${body}`);
}
...
return Buffer.from(b64, "base64");plaintext**位置:**backend/workers/task/src/vendors/openai.ts:63-83。这个 vendor 把 Cloudflare AI Gateway 的认证头、OpenAI error body 解析、base64 解码放在同一外部边界里,executor 只看到业务语义:成功是图片 bytes,安全拒绝是 SafetyError,其他失败是普通 Error。
Reference image 并行拉取,但按输入顺序 append#
const blobs = await Promise.all(options.imageUrls!.map(async (imageUrl) => {
const res = await fetch(imageUrl);
if (!res.ok) throw new Error(`reference image fetch failed: ${res.status}`);
return res.blob();
}));
blobs.forEach((blob, i) => {
form.append("image[]", blob, `reference-${i}.${ext}`);
});plaintext**位置:**backend/workers/task/src/vendors/openai.ts:101-113。这里的取舍是并发和确定性都要:fetch 可以并行,但 multipart 字段顺序必须回到原数组顺序,避免哪个 URL 先返回就变成主图。
Selftest 覆盖错误语义,而不是只测 happy path#
stubStatus(400, JSON.stringify({ error: { code: "moderation_blocked" } }));
check("openai vendor: moderation_blocked → SafetyError", (await capture()) instanceof SafetyError);
stubStatus(400, JSON.stringify({ error: { code: "invalid_value" } }));
check("openai vendor: non-moderation 400 → plain Error (retryable)", other instanceof Error && !(other instanceof SafetyError));plaintext**位置:**backend/workers/task/scripts/selftest.ts:488-513。测试没有 mock 掉内部 vendor 逻辑,只 mock 外部 fetch 边界,因此覆盖的是迁移最容易出错的错误分类契约。
和最近学习记录的关系#
和昨天记录的 PR #4124(sheet cell editor 对齐)没有直接模块关系。和 2026-07-02 记录的 PR #4156 有间接关系:#4156 学的是后台 worker 回调 API 的 workspace-scoped token,今天 #4314 学的是 task worker 调模型的出站供应商边界。两者都在收紧 worker runtime 的能力边界:一个是回调 API 的授权范围,一个是模型供应商凭证和错误语义的封装。
同日还有 #4316 移除 per-user image concurrency gate、#4307 改进 agent image placement,都是 image/agent/task 方向的主线演进;我选 #4314 是因为它的边界设计、错误语义和测试覆盖更集中。
我会怎么吸收#
- 供应商迁移不要只替换 SDK 调用;要同时审查凭证边界、返回形态、失败分类、重试语义和测试入口。
- 跨服务 payload 里尽量传稳定的领域模型标识,把 provider route / vendor namespace 关在 runtime adapter 里。
- 并发获取外部资源时,如果下游 API 对顺序有语义,必须显式恢复输入顺序再提交。
边界 / 风险#
未看到明显架构风险,但有两个边界值得盯:第一,live selftest 依赖 dev 凭证和真实网络,本次我没有运行,只阅读了 PR 的 test plan 和代码;第二,GitHub 上有一个同主题的 #4340,但它 merge 到 cy-gpt-image-ref-fetch-stage 分支,不是 origin/main,因此不计入今天主线候选。这提示 gpt-image reference fetch/selftest 方向仍在快速修补,后续如果相关修复进入 main,值得继续追。
候选说明#
按 Melbourne 2026-07-04 对应窗口(2026-07-03T14:00:00Z 到 2026-07-04T14:00:00Z)查到 10 个 merged PR,其中进入 origin/main 的高价值候选包括 #4314、#4316、#4307、#4334、#4304、#4211 等;#4340 被排除,因为 base 不是 main。昨天窗口查到 18 个 merged PR,但今天已有合适未讲过候选,所以没有回退选择昨天。去重方面,学习日志里已有 #4156 和 #4124,本次没有重复选择。