整体删除已退役的 gpt-image-2-c
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m59s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m47s
Project CI / Backend tests (pull_request) Successful in 5m4s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m30s
Project CI / Native shell tests (pull_request) Successful in 6m36s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m48s
Project CI / Frontend tests (pull_request) Failing after 3m14s
Project CI / Repository checks (pull_request) Successful in 2m46s
Project CI / AI game creator shell web tests (pull_request) Failing after 2m24s

- 删除 platform-image 的 GPT_IMAGE_2_C_MODEL 常量、re-export、尺寸语义与审计标签
- 移除 api-server 三处任务边界对 gpt-image-2-c 的兼容解析分支
- 移除主站前端历史模型常量与恢复分支,并同步其测试
- 同步 External v1 OpenAPI 描述、ADR、CONTEXT、pitfalls 与后端/运维文档
- 数据库既有 gpt-image-2-c 审计字符串保持原样,不回写不改写
This commit is contained in:
2026-09-21 20:31:16 +08:00
parent fc53f362e4
commit f3c362b1ec
20 changed files with 51 additions and 102 deletions
@@ -102,7 +102,7 @@ Use OpenAPI as the final authority; these common values are a routing aid:
- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it.
- External v1 currently has no structured game-scene generation operation. Do not send `kind: "scene"` or `assetKind: "scene"` through generic image generation; the server rejects both before queueing.
- Image `model`: `gpt-image-2.5`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`. Persisted `gpt-image-2` / `gpt-image-2-c` are legacy values resolved only when submitting a new task.
- Image `model`: `gpt-image-2.5`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`. Persisted `gpt-image-2` is a legacy value resolved only when submitting a new task; the retired `gpt-image-2-c` is no longer accepted and is handled as an unsupported value.
- Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`.
- Image `imageSize`: `0.5K`, `1K`, `2K`.
- Video `model`: `seedance2.0`, `seedance2.0-fast`, `kling3.0`, `kling3.0-omni`, `veo3.1`, `veo3.1-fast`.
+2 -2
View File
@@ -66,8 +66,8 @@ _Avoid_: 把具体模型写入普通前端 DTO、让未知字符串自动选择
_Avoid_: 在首次请求时才创建 client、在 provider client 中复制尺寸/重试/审计逻辑、跨 provider credential fallback
**历史模型值**:
已持久化的 `gpt-image-2``gpt-image-2-c` 字符串,只作为历史事实原样读取和审计;基于历史资源提交新任务时,在使用端解析为当前 GPT Image 2.5 业务任务,不回写历史记录,也不把旧值作为现役 provider route。
_Avoid_: 数据库批量改写历史值、把历史值重新路由到 VectorEngine、把兼容解析扩散到普通前端
已持久化的 `gpt-image-2` 字符串,只作为历史事实原样读取和审计;基于历史资源提交新任务时,在使用端解析为当前 GPT Image 2.5 业务任务,不回写历史记录,也不把旧值作为现役 provider route。已退役的 `gpt-image-2-c` 已从代码整体删除,只有数据库里的历史审计字符串原样保留,任何入口传入该值都按不支持的值处理。
_Avoid_: 数据库批量改写历史值、把历史值重新路由到 VectorEngine、把兼容解析扩散到普通前端、为已删除的 `gpt-image-2-c` 重新加回常量或解析分支
**GPT Image 2.5 新生成展示名**:
`GPT Image 2.5` 是新生成任务的产品展示名;历史资源与既有编辑上下文不因新模型上线而改写展示语义。
@@ -4,13 +4,15 @@
新任务使用业务模型值 `gpt-image-2.5`api-server 按任务显式选择具体 model:生成使用 `gpt-image-2.5-flare-c`,编辑使用 `gpt-image-2.5-sunburst-c`。这两个 GPT Image 2.5 model 必须通过启动时构造的 Tiantoken client 发送;Tiantoken client 只读取显式配置的 `TIANTOKEN_BASE_URL`(部署值由环境设置为 `https://api.tiantoken.com`)和独立 `TIANTOKEN_API_KEY`,缺失即阻止 api-server 启动,不得回退到 VectorEngine 或其 API key。
图片协议执行逻辑保持 provider-neutral:请求 body、multipart、尺寸约束、重试、响应解码和审计由共享 image executor 承担;VectorEngine 与 Tiantoken client 只提供相同协议所需的 base URL、API key 和 provider identity。platform-image 根据 concrete model 做严格白名单路由:`gpt-image-2.5-flare-c``gpt-image-2.5-sunburst-c` 走 Tiantoken`gemini-3.1-flash-image-preview`nanobanana)走 VectorEngine;未知 model 直接拒绝。已持久化的 `gpt-image-2` / `gpt-image-2-c` 只在新任务提交边界按兼容规则解析为当前 GPT Image 2.5 任务,不改写历史资源,也不进入旧 VectorEngine 图片路由。
图片协议执行逻辑保持 provider-neutral:请求 body、multipart、尺寸约束、重试、响应解码和审计由共享 image executor 承担;VectorEngine 与 Tiantoken client 只提供相同协议所需的 base URL、API key 和 provider identity。platform-image 根据 concrete model 做严格白名单路由:`gpt-image-2.5-flare-c``gpt-image-2.5-sunburst-c` 走 Tiantoken`gemini-3.1-flash-image-preview`nanobanana)走 VectorEngine;未知 model 直接拒绝。已持久化的 `gpt-image-2` 只在新任务提交边界按兼容规则解析为当前 GPT Image 2.5 任务,不改写历史资源,也不进入旧 VectorEngine 图片路由。
provider 白名单同时拒绝两类非 provider model:业务模型名 `gpt-image-2.5` 必须先由 api-server 在任务边界解析成 `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c``gpt-image-2-c` 自兜底移除后只是历史 fallback / 审计字符串,永不作为 provider 或业务 model 传入,命中即拒绝。`gpt-image-2` 继续作为历史可读值接受。历史值仍只出现在审计与提交边界兼容层,不进入 provider 路由。
provider 白名单同时拒绝非 provider model:业务模型名 `gpt-image-2.5` 必须先由 api-server 在任务边界解析成 `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c``gpt-image-2` 继续作为历史可读值接受。两者都不进入 provider 路由。
普通主站前端只接触业务模型和新生成展示名 `GPT Image 2.5`admin Web/API 可以查看和编辑两个具体定价 key;普通生成即使因参考图使用 edits multipart,仍按生成 concrete model。旧 `gpt-image-2-c` 审计记录原样保留,新代码不再跨模型或跨 provider fallback
`gpt-image-2-c` 已整体删除:它自兜底移除后只剩历史审计字符串,从未进入业务模型、持久化模型字段或前端契约,因此不再保留任何常量、解析分支、尺寸语义或 provider 路由。数据库里既有的审计字符串保持原样,不参与新任务
主站前端只把原本明确使用 GPT Image 2 的专用新任务改为业务模型值 `gpt-image-2.5`;普通图片、角色、场景、图标等原有 nanobanana 默认行为保持不变。画布参数或编辑布局读到 `gpt-image-2` / `gpt-image-2-c` 时,在使用端解析为 `gpt-image-2.5` 并触发参数迁移警告,原始资源、审计、metadata 和历史 fixture 不回写。AGC 现有资源生成界面与请求默认保持不变,不因本决策新增模型字段、选择器或尺寸行为
普通主站前端只接触业务模型和新生成展示名 `GPT Image 2.5`admin Web/API 可以查看和编辑两个具体定价 key;普通生成即使因参考图使用 edits multipart,仍按生成 concrete model。新代码不再跨模型或跨 provider fallback,已退役的 `gpt-image-2-c` 审计记录原样保留在数据库,代码不再引用该值
主站前端只把原本明确使用 GPT Image 2 的专用新任务改为业务模型值 `gpt-image-2.5`;普通图片、角色、场景、图标等原有 nanobanana 默认行为保持不变。画布参数或编辑布局读到 `gpt-image-2` 时,在使用端解析为 `gpt-image-2.5` 并触发参数迁移警告,原始资源、审计、metadata 和历史 fixture 不回写。AGC 现有资源生成界面与请求默认保持不变,不因本决策新增模型字段、选择器或尺寸行为。
## Consequences
@@ -18,5 +20,5 @@ provider 白名单同时拒绝两类非 provider model:业务模型名 `gpt-im
- 新任务的同模型重试固定使用 api-server dispatch 的具体 model,不切换到另一个 model。
- 两套 provider client 在 api-server 启动阶段同时构造;任一 required provider 配置缺失,启动失败而不是延迟到首次图片请求。
- 两套 provider client 的缺失配置报错点名对应环境变量(`VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY``TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY`),构造成功后用启动日志记录已启用的 provider。
- provider routing 只依据 concrete model 的白名单,业务名与已退役的 `gpt-image-2-c` 一律拒绝;provider client 不复制共享协议执行逻辑。
- provider routing 只依据 concrete model 的白名单,业务名与已退役的 `gpt-image-2-c` 一律拒绝`gpt-image-2-c` 已从代码中整体删除,传入即按未知 model 处理)provider client 不复制共享协议执行逻辑。
- 公开资源、`generationInputs` 和普通前端契约不包含具体 provider key;admin 定价管理是明确例外。
@@ -3017,7 +3017,7 @@
},
"model": {
"type": "string",
"description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-bananagpt-image-2 / gpt-image-2-c 仅作为历史值在新任务提交边界兼容解析。未传时沿用编辑器默认。"
"description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-bananagpt-image-2 仅作为历史值在新任务提交边界兼容解析;已退役的 gpt-image-2-c 不再接受,命中按不支持的值处理。未传时沿用编辑器默认。"
},
"aspectRatio": {
"type": "string",
@@ -3170,7 +3170,7 @@
},
"model": {
"type": "string",
"description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-bananagpt-image-2 / gpt-image-2-c 仅作为历史值兼容解析。"
"description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-bananagpt-image-2 仅作为历史值兼容解析;已退役的 gpt-image-2-c 不再接受,命中按不支持的值处理。"
},
"aspectRatio": {
"type": "string",
@@ -3488,7 +3488,7 @@
"model": {
"type": "string",
"default": "gemini-3.1-flash-image-preview",
"description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-bananagpt-image-2 / gpt-image-2-c 仅作为历史值兼容解析。未传时默认使用 nanobanana。"
"description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-bananagpt-image-2 仅作为历史值兼容解析;已退役的 gpt-image-2-c 不再接受,命中按不支持的值处理。未传时默认使用 nanobanana。"
},
"referenceImageSrcs": {
"type": "array",
@@ -1,11 +1,11 @@
# 决策记录
## 2026-09-21 图片 provider 路由白名单拒绝业务名与已退役的 gpt-image-2-c
## 2026-09-21 图片 provider 白名单收紧,并整体删除 gpt-image-2-c
- 背景:GPT Image 2.5 迁移后,`resolve_image_provider` 仍把业务模型名 `gpt-image-2.5`历史 `gpt-image-2-c` 一起判为 Tiantoken,等于把「非 provider model」留在 provider 边界白名单里。`gpt-image-2-c` 自 2026-07-21 起只是首选模型失败时的兜底 provider model兜底移除后仅剩历史审计字符串api-server 的图片请求一律传具体 `provider_model``gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c` / nanobanana
- 决策:`resolve_image_provider` 白名单只接受现役具体 provider model业务名 `gpt-image-2.5` `gpt-image-2-c` 命中即拒绝(`不支持的图片模型`)。业务名仍由 api-server 在任务边界解析成具体 key`gpt-image-2-c` 作为历史 fallback / 审计字符串存在,永不参与路由;`gpt-image-2` 继续作为历史可读值接受
- 影响范围:`server-rs/crates/platform-image/src/image_provider/protocol/request.rs` 与其集成测试。提交边界(画布参数、Agent 工具参数、External v1 请求 DTO、前端历史布局恢复)对 `gpt-image-2` / `gpt-image-2-c` 的兼容解析保持不变
- 验证:`cargo test -p platform-image` 22 项通过,新增 `resolve_image_provider_rejects_business_model_and_accepts_concrete_models``cargo test -p api-server` 1115 项通过。
- 背景:GPT Image 2.5 迁移后,`resolve_image_provider` 仍把业务模型名 `gpt-image-2.5``gpt-image-2-c` 一起判为 Tiantoken,等于把「非 provider model」留在 provider 边界白名单里。`gpt-image-2-c` 自 2026-07-21 起只是首选模型失败时的兜底 provider model兜底移除后仅剩历史审计字符串),从未进入业务模型、持久化 `model` 字段或前端契约——api-server 持久化的模型一律取自 `generation_options.model`(业务值),前端在 2.5 迁移前只写 `gpt-image-2`
- 决策:`resolve_image_provider` 白名单只接受现役具体 provider model业务名 `gpt-image-2.5` 命中即拒绝,必须先在任务边界解析成具体 key`gpt-image-2` 继续作为历史可读值接受。`gpt-image-2-c` 不再只做「拒绝」,而是整体删除:常量、re-export、`is_gpt_image_2_family_model` 尺寸语义、`auditable_image_model` 审计标签、api-server 的三处提交边界解析、前端常量与历史恢复分支全部移除;数据库既有审计字符串原样保留,代码不再引用该值
- 影响范围:`platform-image`constants / lib / image_provider mod / protocol request / runtime executor / 集成测试)、`api-server``openai_image_generation``editor_project``editor_generation_config``editor_agent/tool`)、主站前端 `ImageCanvasGenerationModel` 与其测试、External v1 OpenAPI 描述、ADR 与本文档。前端与 External v1 传入 `gpt-image-2-c` 现在按「不支持的值」处理(走既有未知值分支),不再解析为 GPT Image 2.5
- 验证:`cargo test -p platform-image` 全绿(含新增 `resolve_image_provider_rejects_business_model_and_accepts_concrete_models``cargo test -p api-server` 1115 项`npx vitest run src/components/image-editor/ImageCanvasGenerationModel.test.ts` 34 项通过;`check:encoding``check:doc-index` 通过。
## 2026-09-21 图片 provider 启动期缺配置的报错点名环境变量
@@ -2503,7 +2503,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 现象:配置了 `APIMART_BASE_URL` / `APIMART_API_KEY` 后,RPG、拼图或方洞的 GPT-image-2 生图仍返回缺配置,或请求体里还出现 `official_fallback` / `image_urls`
- 原因:2026-05-21 后 GPT-image-2 图片生成按 VectorEngine 创建/编辑接口分流;2026-07-05 后创意 Agent 文本链路也改为 VectorEngine Chat Completions `gpt-5.4-mini`,APIMart 不再作为当前创意 Agent 来源。
- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai``VECTOR_ENGINE_API_KEY``VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认无参考图路径为 `/v1/images/generations`、有参考图路径为 `/v1/images/edits`业务 / 计费与 provider 首发模型`gpt-image-2`,仅在符合条件的 provider 失败后切到兜底模型 `gpt-image-2-c`
- 处理:为图片生成配置 `VECTOR_ENGINE_BASE_URL=https://api.vectorengine.ai``VECTOR_ENGINE_API_KEY``VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`;排查请求体时确认无参考图路径为 `/v1/images/generations`、有参考图路径为 `/v1/images/edits`新任务业务模型为 `gpt-image-2.5`provider 具体模型为 `gpt-image-2.5-flare-c`(生成)/ `gpt-image-2.5-sunburst-c`(编辑),只在同一具体模型内重试。历史兜底模型 `gpt-image-2-c` 已从代码整体删除,不再存在任何 fallback 路径,请求体里出现该值即视为异常
- 验证:运行 `cargo test -p api-server openai_image --manifest-path server-rs/Cargo.toml` 和相关玩法图片生成测试;真实联调只在本地私密环境放置 VectorEngine key。
- 关联:`docs/technical/VECTOR_ENGINE_GPT_IMAGE_2_GENERATION_2026-05-09.md``server-rs/crates/api-server/src/openai_image_generation.rs`
@@ -184,7 +184,7 @@ npm run check:server-rs-ddd
2. Adapter 输入应显式包含 provider、prompt、reference images、OSS prefix/path/file name、asset kind、entity kind/id、slot、owner/profile/source job、metadata 和可选透明背景后处理。
3. Adapter 输出应保留 legacy public path、object key、asset object id、MIME、extension、task id 和实际 prompt。
4. Adapter 不负责扣费、退款或钱包读取;计费仍由调用方显式包裹。
5. 图片 provider 协议不再放在玩法模块里实现。产品、计费、DTO、持久化和 GPT Image 2.5 创建 / 编辑请求统一使用业务模型 `gpt-image-2.5`api-server 在提交边界分派 `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c`;已持久化的 `gpt-image-2` / `gpt-image-2-c` 只按兼容规则读取,不改写历史值。URL / base64 图片解析、远端图片下载、请求超时 / 上游状态 / 响应解析 / 缺图 / 下载失败的结构化日志统一在 `server-rs/crates/platform-image/src/image_provider/`;其中 `runtime/executor.rs` 负责共享 provider-neutral 执行编排,`transport/` 负责 HTTP client 与 curl 传输,`protocol/` 负责请求体、路径和响应 JSON 字段。raw image edit 的 multipart 与严格尺寸校验位于独立的 `server-rs/crates/platform-image/src/raw_image_edit/``api-server` 只负责配置校验、玩法 prompt 编排、OSS / asset object / binding 持久化、计费和外部 API 失败审计落库。
5. 图片 provider 协议不再放在玩法模块里实现。产品、计费、DTO、持久化和 GPT Image 2.5 创建 / 编辑请求统一使用业务模型 `gpt-image-2.5`api-server 在提交边界分派 `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c`;已持久化的 `gpt-image-2` 只按兼容规则读取,不改写历史值;已退役的 `gpt-image-2-c` 已从代码整体删除,传入即按不支持的值处理。URL / base64 图片解析、远端图片下载、请求超时 / 上游状态 / 响应解析 / 缺图 / 下载失败的结构化日志统一在 `server-rs/crates/platform-image/src/image_provider/`;其中 `runtime/executor.rs` 负责共享 provider-neutral 执行编排,`transport/` 负责 HTTP client 与 curl 传输,`protocol/` 负责请求体、路径和响应 JSON 字段。raw image edit 的 multipart 与严格尺寸校验位于独立的 `server-rs/crates/platform-image/src/raw_image_edit/``api-server` 只负责配置校验、玩法 prompt 编排、OSS / asset object / binding 持久化、计费和外部 API 失败审计落库。
6. OSS 平台适配日志统一在 `server-rs/crates/platform-oss` 输出,覆盖 `sign_post_object``sign_get_object_url``head_object``put_object`。日志字段固定使用 `provider``operation``bucket``endpoint``object_key` / `key_prefix``access``content_type``content_length``status``status_class``error_kind``elapsed_ms`,只记录对象定位和排障信息;不得输出 AccessKey、policy、signature、Authorization header 或完整 signed URL。generated 私有对象上传时必须由 OSS 对象头承载浏览器 / CDN 缓存策略,默认写入 `Cache-Control: public, max-age=31536000, immutable`,不得改成 api-server 本地磁盘静态资源兜底。
7. Puzzle、Match3D、音频、GLB、视频等复杂媒体可以复用 OSS + asset object + binding 的底层持久化能力,但玩法专属处理规则留在各自编排层,不塞进公共接口。
8. 拼图入口页与结果页新增关卡的本地参考图不走浏览器直传 OSS,前端读取为 Data URL 后随创作 action 提交,并在读取前限制 6MB、显示“图片≤6MB”。`api-server` 必须对 Data URL 实际字节数再次校验;历史图片才提交 `referenceImageAssetObjectId(s)`,后端校验 `asset_object` 的 bucket、kind、图片 MIME、大小和 owner 后签发只读 URL 给 VectorEngine 读取。
@@ -318,7 +318,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
错误边界固定如下:`StreamUnavailable` 只表示流式响应已给出 `tool_use` / `tool_calls` 完成原因但没有聚合出任何工具 slot,供调用方回退非流式,它不承担截断语义;`EmptyResponse` 表示最终文本和工具调用都为空,纯工具响应合法;`Deserialize` 覆盖 JSON / SSE / UTF-8 解析失败、缺少 `choices[0]`、流式工具身份缺失、流式工具槽位身份冲突、流式参数不完整,以及上述工具流未收尾截断。Anthropic 仍不支持 `web_search`、图片内容和纯 system 消息,必须至少有一条非 system 文本消息。
- 图片生成:VectorEngine 图片 provider 归属 `platform-image`,密钥只在后端环境变量中;逻辑 SKU provider 首选模型固定为 `gpt-image-2`,只在明确模型不可用、408 / 非拒绝类 429 / 5xx、响应解析失败或非拒绝类缺图时切换兜底模型 `gpt-image-2-c`。401 / 403、普通参数或安全拒绝、本地配置 / 参考图错误、无法确认上游是否已受理的发送错误、request budget 耗尽和生成成功后的图片下载失败不得切模型。一次业务请求总发送上限仍为 5 次;切换兜底模型会消耗后续 attempt,不允许两个模型各重试 5 次。`api-server` 内的 `openai_image_generation.rs` 只是兼容调用面和外部失败审计桥接,不再承载 provider 协议实现。实际外部生成运行记录统一落 `tracking_event``event_key = external_generation_run`metadata 记录开始 / 结束时间、耗时、状态、成功标记、失败原因、provider task id、结果摘要和 recovered failure 数量;首选模型失败但兜底模型成功时,首选失败仍落 `external_api_call_failure`。DashScope 只按仍在使用的历史能力单独处理,不作为 GPT-image-2 兜底。VectorEngine `/v1/images/generations``/v1/images/edits` 上游 POST 使用 `libcurl` 发送;`reqwest` 只保留给参考图 URL 下载和响应中图片 URL 下载。`/v1/images/edits` 的 multipart 参考图必须作为 libcurl 文件上传 part 发送,字段名为 `image`,实现上使用 `Form::buffer(file_name, bytes)` 并设置 `Content-Type`;不能只用 `contents(...).filename(...)`,否则上游会把请求转码为缺少图片并返回 `image is required``request_send` 阶段的 curl timeout / connect error 按可重试传输错误处理,最多尝试 5 次,并使用指数退避加短抖动;排障时优先看 `attempt``max_attempts``retry_delay_ms``fallback_from_model``fallback_to_model``reference_image_bytes_total``request_params`,不要把 `SendRequest` 当成上游业务错误。
- 图片生成:VectorEngine 图片 provider 归属 `platform-image`,密钥只在后端环境变量中;逻辑 SKU 固定为业务模型 `gpt-image-2.5`provider 具体模型固定为 `gpt-image-2.5-flare-c`(生成)/ `gpt-image-2.5-sunburst-c`(编辑);历史兜底模型 `gpt-image-2-c` 已从代码整体删除,只在同一具体模型内重试,不做跨模型 fallback。401 / 403、普通参数或安全拒绝、本地配置 / 参考图错误、无法确认上游是否已受理的发送错误、request budget 耗尽和生成成功后的图片下载失败不得切模型。一次业务请求总发送上限仍为 5 次;切换兜底模型会消耗后续 attempt,不允许两个模型各重试 5 次。`api-server` 内的 `openai_image_generation.rs` 只是兼容调用面和外部失败审计桥接,不再承载 provider 协议实现。实际外部生成运行记录统一落 `tracking_event``event_key = external_generation_run`metadata 记录开始 / 结束时间、耗时、状态、成功标记、失败原因、provider task id、结果摘要和 recovered failure 数量;失败 attempt 仍落 `external_api_call_failure`,后续 attempt 成功时在运行摘要里计入 recovered failure。DashScope 只按仍在使用的历史能力单独处理,不作为 GPT-image-2 兜底。VectorEngine `/v1/images/generations``/v1/images/edits` 上游 POST 使用 `libcurl` 发送;`reqwest` 只保留给参考图 URL 下载和响应中图片 URL 下载。`/v1/images/edits` 的 multipart 参考图必须作为 libcurl 文件上传 part 发送,字段名为 `image`,实现上使用 `Form::buffer(file_name, bytes)` 并设置 `Content-Type`;不能只用 `contents(...).filename(...)`,否则上游会把请求转码为缺少图片并返回 `image is required``request_send` 阶段的 curl timeout / connect error 按可重试传输错误处理,最多尝试 5 次,并使用指数退避加短抖动;排障时优先看 `attempt``max_attempts``retry_delay_ms``reference_image_bytes_total``request_params`,不要把 `SendRequest` 当成上游业务错误。
- 抠图输入以私有 OSS 作为内存生命周期边界:生成原图和角色动作抽取帧上传时消费图片字节所有权,上传完成后不保留原图缓冲;手动去背景直接解析并校验已有 OSS object key,不下载原图。BgFilter 必须为 object key 签发 600 秒 GET URL 并通过 multipart `image_url` 提交,不用 `file` 重传;flat 链路进入阿里云 fallback 时由 `platform-matting` URL 接口单独下载并上传 `AuthorizeFileUpload` 临时对象,在推理前释放下载缓冲,继续 fallback 到本地键色时再单独下载一次原图,本地产出后释放本次原图下载缓冲。签名 URL 不得写入日志、审计或持久化。
- 角色动作抠图输入像素边界:仅图片画布角色动作链路在 FFmpeg 抽帧后、源帧上传 OSS 前,把帧解码为 RGB8,并按最终 `frameWidth × frameHeight` 的 contain 比例使用 `Triangle` 只缩放到内容尺寸;该阶段不得创建最终目标尺寸画布、不得引入 Alpha 通道,也不得插入任何 padding。BgFilter、阿里云通用抠图和本地键色降级共享这个无补边源帧 object key。抠图返回后才统一转为 RGBA8,按相同比例居中放入最终目标尺寸画布,并用 `RGBA(0,0,0,0)` 补齐透明 padding。以 `560×752 → 323×480` 为例,抠图输入固定为无 Alpha、无补边的 `323×434 RGB8 PNG`,最终输出为上下各 `23px` 透明补边的 `323×480 RGBA8 PNG`。旧 `/api/assets/character-animation/*` 动作发布链路继续保留原有帧 finalizer,不适用该输入规则。抽帧解码后若携带 Alpha 通道,必须先把像素按白底合成为不透明再转 RGB8,禁止直接丢弃 Alpha——全透明像素下未定义的 RGB 值会以杂色进入抠图输入,重新引入杂色边缘;共享 FFmpeg 抽帧命令保持不固定 `-pix_fmt`,白底合成只属于该链路的 BgFilter 输入准备阶段。
- 阿里云通用抠图的非上海地域输入不得使用 `viapiutils/GetOssStsToken`、固定 `viapi-customer-temp` 或 OSS V1 PUT。`platform-matting` 必须按官方新版 SDK Advance 协议调用 `AuthorizeFileUpload`,使用动态返回的单对象 Policy 执行 multipart POST,再把临时上海 OSS URL 交给 `SegmentCommonImage`;输入归一化、结果下载与原尺寸 Alpha 回贴继续留在同一适配器内。该协议仍上传图片字节,不等同于阿里云服务端直接抓取任意公网 URL,也不改变上层 BgFilter → 阿里云 → 本地降级顺序。
@@ -238,13 +238,13 @@ spacetime sql <database> "SELECT * FROM runtime_setting LIMIT 1" --server http:/
本地 `spacetime` CLI / standalone 版本必须和 `server-rs/Cargo.toml` 里锁定的 `spacetimedb` 版本一致;当前统一版本为 `2.8.3`CLI / standalone commit 固定核对为 `8e410d2842147bd8e5a32a9589cc00c19f7478e2`。若版本或 commit 错配,procedure 返回值可能在宿主侧触发 `Failed to BSATN deserialize procedure return value`api-server 最终表现为现役 settings、editor project 或 profile procedure 超时。排障时先运行 `spacetime --version`,再对照 `server-rs/Cargo.toml``spacetimedb = "..."`;其它版本可执行 `spacetime version install <version> && spacetime version use <version>`,升级后重启 `npm run dev:spacetime` 再重试。当前 `scripts/dev.mjs` 会把 tool version 和 commit 一起写入 `dev-spacetime-tool-version`,启动新 standalone 与复用已有本地进程时都要求 `2.8.3 + 8e410d28...` 同时匹配;旧版本或旧单行版本记录会拒绝复用并要求重启。2.6.1 修复了 procedure context 中调用者 `Identity` / `ConnectionId` 始终为空的回归,依赖 `ctx.sender` 鉴权时必须同时确认宿主已升级。
本地 `.env``.env.local``.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查图片编辑器 Tiantoken 生成链路时,确认 `TIANTOKEN_BASE_URL``TIANTOKEN_API_KEY``TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git;同时确认 VectorEngine 的 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 已配置,因为两个图片 client 都在 api-server 启动时构造,任一缺失都会阻止启动。VectorEngine 配置仍保留给 nanobanana 与 Suno 音乐任务。`TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS` 是单次 attempt 的配置上限,默认 `1000000`;配置加载层允许显式值低于该默认值,不再在读取环境变量时强制抬高。新生成任务使用业务模型 `gpt-image-2.5`api-server 显式分派 `gpt-image-2.5-flare-c`generate)或 `gpt-image-2.5-sunburst-c`edit);已持久化的 `gpt-image-2` / `gpt-image-2-c` 只在提交边界按兼容规则解析,不改写历史值,也不回退到旧 GPT Image 2 路由。图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image``api-server` 只做编辑器请求编排、OSS / asset 持久化、计费和失败审计落库。`platform-image` 会在 JSON 生成和 multipart 编辑请求发送前按同一 GPT-image family 规则归一显式像素尺寸;若请求发送失败,先按同一 `request_id` 查看 provider 日志与 `external_api_call_failure.metadata_json.errorSource`,当前 multipart `/v1/images/edits` 单独强制 HTTP/1.1。
本地 `.env``.env.local``.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查图片编辑器 Tiantoken 生成链路时,确认 `TIANTOKEN_BASE_URL``TIANTOKEN_API_KEY``TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git;同时确认 VectorEngine 的 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 已配置,因为两个图片 client 都在 api-server 启动时构造,任一缺失都会阻止启动。VectorEngine 配置仍保留给 nanobanana 与 Suno 音乐任务。`TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS` 是单次 attempt 的配置上限,默认 `1000000`;配置加载层允许显式值低于该默认值,不再在读取环境变量时强制抬高。新生成任务使用业务模型 `gpt-image-2.5`api-server 显式分派 `gpt-image-2.5-flare-c`generate)或 `gpt-image-2.5-sunburst-c`edit);已持久化的 `gpt-image-2` 只在提交边界按兼容规则解析,不改写历史值,也不回退到旧 GPT Image 2 路由;已退役的 `gpt-image-2-c` 已从代码整体删除,传入即按不支持的值处理。图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image``api-server` 只做编辑器请求编排、OSS / asset 持久化、计费和失败审计落库。`platform-image` 会在 JSON 生成和 multipart 编辑请求发送前按同一 GPT-image family 规则归一显式像素尺寸;若请求发送失败,先按同一 `request_id` 查看 provider 日志与 `external_api_call_failure.metadata_json.errorSource`,当前 multipart `/v1/images/edits` 单独强制 HTTP/1.1。
编辑器 ElevenLabs 音效生成只从服务端读取 `ELEVENLABS_BASE_URL``ELEVENLABS_API_KEY``ELEVENLABS_REQUEST_TIMEOUT_MS`timeout 默认 `180000ms`base URL 或 Key 缺失时失败关闭,不回退 Vidu。生产 API 与 external-generation worker 通过共享 API env 取得同一配置,模板见 `deploy/env/api-server.env.example`Key 不得进入 Web/Vite 环境、命令参数、日志、fixture 或仓库。普通测试只使用 loopback mock,禁止把真实付费请求作为 T3 自动验收。
SFX V2 发布必须使用维护窗:先关闭 SFX 入队,再对显式目标执行只读 `spacetime sql <database> --server <server-url> --format json "SELECT job_id, status, request_payload_json FROM external_generation_job WHERE job_kind = 'editor_sound_effect_generation' AND (status = 'pending' OR status = 'running')"`;结果非零时保持旧 Worker drain,不得删除任务或让新 Worker 解析旧 Vidu payload。禁止依赖默认 server,禁止使用 `--root-dir`。清零后先部署共享 env 已对齐的 api-server / external-generation worker,检查 `/healthz` 和 Worker 启动,再部署 Web 并小流量开放 SFX。灰度对账 job 完成数、退款数、ElevenLabs POST 数、完成资源数和孤儿资源;翻译失败仍调用 provider、单 job provider POST 大于一次、成功退款或失败未退款均应立即停止放量。回滚先停止入队并收口 V2 pending / running job,不自动切回 Vidu,不执行 SpacetimeDB schema 或数据回滚。完整清单见 `docs/【实施记录】SFX生成优化V2.0T6测试与发布门禁-2026-08-07.md`
VectorEngine 图片生成 / 编辑在 `request_send` 阶段出现 `timeout``connect`、libcurl 35 SSL connect reset、libcurl 56 receive error / `unexpected eof while reading`、recv failure 等临时传输错误,或在 `upstream_status` 阶段收到 408 / 429 / 5xx(例如 Nginx HTML `502 Bad Gateway`)时,`platform-image` 会在一次业务请求总上限 5 次内处理;multipart 图片编辑每次重试都会重新构造 form,避免复用已消费的 body。首个 provider attempt 使用 `gpt-image-2`;明确模型不可用、408 / 非拒绝类 429 / 5xx、响应解析失败或非拒绝类缺图时,下一 attempt 直接切兜底模型 `gpt-image-2-c`,之后只在剩余次数内重试兜底模型。发送 / 连接错误无法确认上游是否已受理,只重试同一首选模型,不切模型;认证、普通参数、安全拒绝、图片下载和 budget 错误同样不切。worker 从 job 开始的同一时钟起点计算绝对 deadline,常规保留最后 `60` 秒给审计、OSS 和终态写回;job 预算小于 `120` 秒时保留一半。VectorEngine 单次 attempt timeout 取配置值和剩余 provider 预算的较小值;退避或模型切换后已没有下一次 attempt 的预算时立即停止。该 deadline 覆盖参考图、provider 请求 / 响应和响应图片下载的整次 provider future,但只在 worker 进程内通过 `RequestContext` 传递;普通 HTTP / `inline` 没有该 deadline,继续保持原有 timeout 和重试行为。日志中 `VectorEngine 首选图片模型失败,切换兼容模型` 会携带 `fallback_from_model` / `fallback_to_model`;即使回退成功,首选模型错误仍写入 `external_api_call_failure`,成功运行摘要的 `recoveredFailureCount` 同时递增。排查生产失败时应同时统计 fallback / retry 日志和最终 audit,避免把一次用户请求内的多次发送误判成多个用户请求。这项收口不修改 lease 续租 / fencing、迟到写回仲裁、attempt 耗尽与原子退款语义。
VectorEngine 图片生成 / 编辑在 `request_send` 阶段出现 `timeout``connect`、libcurl 35 SSL connect reset、libcurl 56 receive error / `unexpected eof while reading`、recv failure 等临时传输错误,或在 `upstream_status` 阶段收到 408 / 429 / 5xx(例如 Nginx HTML `502 Bad Gateway`)时,`platform-image` 会在一次业务请求总上限 5 次内处理;multipart 图片编辑每次重试都会重新构造 form,避免复用已消费的 body。每次 provider attempt 都按任务确定的具体模型发送(`gpt-image-2.5-flare-c` 生成 / `gpt-image-2.5-sunburst-c` 编辑 / `gemini-3.1-flash-image-preview` nanobanana;明确模型不可用、408 / 非拒绝类 429 / 5xx、响应解析失败或非拒绝类缺图时只在同一具体模型内重试,不切模型,历史兜底模型 `gpt-image-2-c` 已从代码整体删除。发送 / 连接错误无法确认上游是否已受理,同样只重试同一模型;认证、普通参数、安全拒绝、图片下载和 budget 错误不重试。worker 从 job 开始的同一时钟起点计算绝对 deadline,常规保留最后 `60` 秒给审计、OSS 和终态写回;job 预算小于 `120` 秒时保留一半。VectorEngine 单次 attempt timeout 取配置值和剩余 provider 预算的较小值;退避后已没有下一次 attempt 的预算时立即停止。该 deadline 覆盖参考图、provider 请求 / 响应和响应图片下载的整次 provider future,但只在 worker 进程内通过 `RequestContext` 传递;普通 HTTP / `inline` 没有该 deadline,继续保持原有 timeout 和重试行为。重试过程中的失败 attempt 仍写入 `external_api_call_failure`;即使后续 attempt 成功,成功运行摘要的 `recoveredFailureCount` 同时递增。排查生产失败时应同时统计 retry 日志和最终 audit,避免把一次用户请求内的多次发送误判成多个用户请求。这项收口不修改 lease 续租 / fencing、迟到写回仲裁、attempt 耗尽与原子退款语义。
图片编辑器生成属于持久队列长任务:提交接口返回 job 后,前端通过 `/api/runtime/external-generation/jobs/{jobId}` 与编辑器项目资源状态收敛。生产排查小程序或 WebView `Failed to fetch` 时,若 Nginx access log 为 `499``upstream_status=-`,先按提交请求的 `request_id`、job id、worker 日志和 `external_api_call_failure` 对齐真实任务,不把客户端断开直接判定为 provider 失败。
@@ -29,7 +29,7 @@ use platform_editor_agent::agent::tools::generate_video::{
};
use platform_editor_agent::framework::error::PromptError;
use platform_editor_agent::framework::tool::{Tool, ToolDyn, null_tool_args_as_missing};
use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL};
use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_MODEL};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
@@ -225,7 +225,7 @@ fn parse_internal<T: DeserializeOwned>(
})
}
/// 把持久化的历史 GPT Image 2 模型别名(`gpt-image-2` / `gpt-image-2-c`)改写为当前
/// 把持久化的历史 GPT Image 2 模型别名(`gpt-image-2`)改写为当前
/// 业务模型 `gpt-image-2.5`。
///
/// 平台层图片工具只接受 `gemini-3.1-flash-image-preview` 与 `gpt-image-2.5`,但历史会话
@@ -235,7 +235,7 @@ fn normalize_legacy_image_model_args(value: &Value) -> Cow<'_, Value> {
let Some(model) = value.get("model").and_then(Value::as_str) else {
return Cow::Borrowed(value);
};
if !matches!(model.trim(), GPT_IMAGE_2_MODEL | GPT_IMAGE_2_C_MODEL) {
if model.trim() != GPT_IMAGE_2_MODEL {
return Cow::Borrowed(value);
}
let mut normalized = value.clone();
@@ -1562,7 +1562,7 @@ mod tests {
let normalized = image
.validate_args(&json!({
"prompt": "生成图片",
"model": GPT_IMAGE_2_C_MODEL,
"model": GPT_IMAGE_2_MODEL,
"image_size": "2K"
}))
.expect("legacy generation alias should validate");
@@ -17,7 +17,6 @@ pub(crate) const EDITOR_GENERATION_PRICING_DEFAULT_JSON: &str =
include_str!("../config/editor-generation-pricing.default.json");
const EDITOR_IMAGE_MODEL_GPT_IMAGE_2: &str = "gpt-image-2";
const EDITOR_IMAGE_MODEL_GPT_IMAGE_2_C: &str = "gpt-image-2-c";
pub(crate) const EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_BUSINESS: &str = "gpt-image-2.5";
pub(crate) const EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION: &str = "gpt-image-2.5-flare-c";
pub(crate) const EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT: &str = "gpt-image-2.5-sunburst-c";
@@ -114,7 +113,6 @@ impl EditorGenerationPricingConfig {
models.remove(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION);
models.remove(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT);
models.remove(EDITOR_IMAGE_MODEL_GPT_IMAGE_2);
models.remove(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_C);
Self { models }
}
@@ -537,7 +535,6 @@ fn normalize_editor_image_model(model: Option<&str>) -> &'static str {
match model.map(str::trim).filter(|value| !value.is_empty()) {
Some(
EDITOR_IMAGE_MODEL_GPT_IMAGE_2
| EDITOR_IMAGE_MODEL_GPT_IMAGE_2_C
| EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_BUSINESS
| EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION,
) => EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION,
@@ -554,7 +551,6 @@ fn normalize_editor_image_edit_model(model: Option<&str>) -> &'static str {
Some(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT) => EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT,
Some(
EDITOR_IMAGE_MODEL_GPT_IMAGE_2
| EDITOR_IMAGE_MODEL_GPT_IMAGE_2_C
| EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_BUSINESS
| EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION,
) => EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT,
@@ -114,9 +114,9 @@ use crate::{
http_error::AppError,
openai_image_generation::{
DownloadedOpenAiImage, GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL,
GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL,
OpenAiGeneratedImages, OpenAiImageSettings, OpenAiReferenceImage,
build_openai_image_http_client, create_openai_image_edit_with_references_and_model,
GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_MODEL, OpenAiGeneratedImages,
OpenAiImageSettings, OpenAiReferenceImage, build_openai_image_http_client,
create_openai_image_edit_with_references_and_model,
create_openai_image_generation_with_model, create_openai_nanobanana_generate_content,
require_openai_image_settings,
},
@@ -4097,7 +4097,6 @@ pub(crate) fn normalize_editor_generation_options(
// new task is submitted.
Some(
GPT_IMAGE_2_MODEL
| GPT_IMAGE_2_C_MODEL
| GPT_IMAGE_2_5_BUSINESS_NAME
| "gpt-image-2.5-flare-c"
| "gpt-image-2.5-sunburst-c",
@@ -26,7 +26,7 @@ use crate::{
pub(crate) use platform_image::{
GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL,
GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL,
GPT_IMAGE_2_MODEL,
};
pub(crate) type OpenAiGeneratedImages = GeneratedImages;
@@ -1,5 +1,4 @@
pub const GPT_IMAGE_2_MODEL: &str = "gpt-image-2";
pub const GPT_IMAGE_2_C_MODEL: &str = "gpt-image-2-c";
/// Current business model exposed to callers for new image tasks.
pub const GPT_IMAGE_2_5_BUSINESS_NAME: &str = "gpt-image-2.5";
/// Provider/pricing key for new generation tasks.
@@ -13,7 +13,7 @@ pub(crate) use transport::curl as curl_transport;
pub use constants::TIANTOKEN_PROVIDER;
pub use constants::{
GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL,
GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL, VECTOR_ENGINE_PROVIDER,
GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL, VECTOR_ENGINE_PROVIDER,
};
pub use protocol::{
build_image_request_body, build_image_request_body_with_model,
@@ -3,8 +3,8 @@ use serde_json::{Map, Value, json};
use crate::image_provider::{
constants::{
GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL,
GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_DIMENSION_ALIGNMENT, GPT_IMAGE_2_MAX_EDGE,
GPT_IMAGE_2_MAX_PIXELS, GPT_IMAGE_2_MIN_PIXELS, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL,
GPT_IMAGE_2_DIMENSION_ALIGNMENT, GPT_IMAGE_2_MAX_EDGE, GPT_IMAGE_2_MAX_PIXELS,
GPT_IMAGE_2_MIN_PIXELS, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL,
},
types::{ImageProvider, ImageProviderSettings, ReferenceImage},
};
@@ -105,9 +105,8 @@ pub fn normalize_image_model(model: &str) -> &str {
/// 中文注释:白名单是 provider 边界,不放业务名和已退役模型——
/// `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c` 走 Tiantoken
/// nanobanana 走 VectorEngine。业务模型名 `gpt-image-2.5`
/// (`GPT_IMAGE_2_5_BUSINESS_NAME`) 必须在任务边界解析成具体 key;`gpt-image-2-c`
/// 只是历史 fallback / 审计字符串,永远不会作为 provider 或业务 model 传入,
/// 命中即拒绝。`gpt-image-2` 仍按历史可读值接受。
/// (`GPT_IMAGE_2_5_BUSINESS_NAME`) 必须在任务边界解析成具体 key;已删除的
/// `gpt-image-2-c` 命中即拒绝。`gpt-image-2` 仍按历史可读值接受。
pub fn resolve_image_provider(model: &str) -> Result<ImageProvider, &'static str> {
match normalize_image_model(model) {
GPT_IMAGE_2_MODEL | GPT_IMAGE_2_5_GENERATION_MODEL | GPT_IMAGE_2_5_EDIT_MODEL => {
@@ -122,7 +121,6 @@ pub(crate) fn is_gpt_image_2_family_model(model: &str) -> bool {
matches!(
normalize_image_model(model),
GPT_IMAGE_2_MODEL
| GPT_IMAGE_2_C_MODEL
| GPT_IMAGE_2_5_BUSINESS_NAME
| GPT_IMAGE_2_5_GENERATION_MODEL
| GPT_IMAGE_2_5_EDIT_MODEL
@@ -11,8 +11,7 @@ use crate::image_provider::{
},
constants::{
GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL,
GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL,
VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES,
GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL, VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES,
VECTOR_ENGINE_NANOBANANA_MAX_REFERENCE_IMAGES,
},
curl_transport::{
@@ -735,7 +734,6 @@ fn finish_image_model_fallback_error(
fn auditable_image_model(model: &str) -> Option<&'static str> {
match model {
GPT_IMAGE_2_C_MODEL => Some(GPT_IMAGE_2_C_MODEL),
GPT_IMAGE_2_MODEL => Some(GPT_IMAGE_2_MODEL),
GPT_IMAGE_2_5_BUSINESS_NAME => Some(GPT_IMAGE_2_5_BUSINESS_NAME),
GPT_IMAGE_2_5_GENERATION_MODEL => Some(GPT_IMAGE_2_5_GENERATION_MODEL),
+5 -6
View File
@@ -6,12 +6,11 @@ pub mod raw_image_edit;
pub use image_provider::{
DownloadedImage, GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL,
GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, GeneratedImages,
ImageProvider, ImageProviderClient, ImageProviderSettings, NANOBANANA_2_MODEL,
PlatformImageError, PlatformImageFailureAudit, PlatformImageStatusHint, ReferenceImage,
TIANTOKEN_PROVIDER, VECTOR_ENGINE_PROVIDER, build_image_http_client,
build_image_provider_client, build_image_request_body,
build_nanobanana_generate_content_request_body, create_image_edit,
GPT_IMAGE_2_5_GENERATION_MODEL, GPT_IMAGE_2_MODEL, GeneratedImages, ImageProvider,
ImageProviderClient, ImageProviderSettings, NANOBANANA_2_MODEL, PlatformImageError,
PlatformImageFailureAudit, PlatformImageStatusHint, ReferenceImage, TIANTOKEN_PROVIDER,
VECTOR_ENGINE_PROVIDER, build_image_http_client, build_image_provider_client,
build_image_request_body, build_nanobanana_generate_content_request_body, create_image_edit,
create_image_edit_with_references, create_image_edit_with_references_and_model,
create_image_generation, create_image_generation_with_model,
create_nanobanana_generate_content, download_remote_image, images_edit_url,
@@ -1,11 +1,11 @@
use platform_image::image_provider::{
GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_EDIT_MODEL, GPT_IMAGE_2_5_GENERATION_MODEL,
GPT_IMAGE_2_C_MODEL, GPT_IMAGE_2_MODEL, ImageProvider, ImageProviderSettings,
PlatformImageError, ReferenceImage, VECTOR_ENGINE_PROVIDER, build_image_http_client,
build_image_request_body, build_image_request_body_with_model,
build_nanobanana_generate_content_request_body, create_image_edit, create_image_generation,
create_nanobanana_generate_content, images_edit_url, images_generation_url,
nanobanana_generate_content_url, resolve_image_provider,
GPT_IMAGE_2_MODEL, ImageProvider, ImageProviderSettings, PlatformImageError, ReferenceImage,
VECTOR_ENGINE_PROVIDER, build_image_http_client, build_image_request_body,
build_image_request_body_with_model, build_nanobanana_generate_content_request_body,
create_image_edit, create_image_generation, create_nanobanana_generate_content,
images_edit_url, images_generation_url, nanobanana_generate_content_url,
resolve_image_provider,
};
use std::{
sync::{
@@ -33,7 +33,6 @@ fn image_provider_module_exposes_provider_protocol_helpers() {
let body = build_image_request_body("雾海神殿", Some("文字,水印"), "16:9", 9, &[]);
assert_eq!(GPT_IMAGE_2_MODEL, "gpt-image-2");
assert_eq!(GPT_IMAGE_2_C_MODEL, "gpt-image-2-c");
assert_eq!(VECTOR_ENGINE_PROVIDER, "vector-engine");
assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL);
assert_eq!(body["size"], "1536x1024");
@@ -114,17 +113,7 @@ fn image_provider_only_enforces_the_gpt_image_2_pixel_budget_for_that_model() {
1,
&[],
);
let fallback_gpt_body = build_image_request_body_with_model(
GPT_IMAGE_2_C_MODEL,
"小尺寸图",
None,
"640x640",
1,
&[],
);
assert_eq!(gpt_body["size"], "816x816");
assert_eq!(fallback_gpt_body["size"], "816x816");
assert_eq!(nanobanana_body["size"], "640x640");
assert_eq!(oversized_gpt_body["size"], "2880x2880");
}
@@ -232,8 +221,8 @@ fn nanobanana_generate_content_url_blank_model_falls_back_to_nanobanana_model()
fn resolve_image_provider_rejects_business_model_and_accepts_concrete_models() {
// 中文注释:业务模型名不是 provider model,必须在 api-server 任务边界先解析成具体 model。
assert!(resolve_image_provider(GPT_IMAGE_2_5_BUSINESS_NAME).is_err());
// 中文注释:`gpt-image-2-c` 只是历史 fallback / 审计字符串,永不作为 provider model
assert!(resolve_image_provider(GPT_IMAGE_2_C_MODEL).is_err());
// 中文注释:已删除的 gpt-image-2-c 不再被任何层接受
assert!(resolve_image_provider("gpt-image-2-c").is_err());
assert_eq!(
resolve_image_provider(GPT_IMAGE_2_5_GENERATION_MODEL),
@@ -49,7 +49,6 @@ import {
getGenerationFrameLabel,
IMAGE_MODEL_GPT_IMAGE_2,
IMAGE_MODEL_GPT_IMAGE_2_5,
IMAGE_MODEL_GPT_IMAGE_2_C,
IMAGE_MODEL_NANOBANANA2,
isNormalizedCanvasGenerationInputs,
isQuickEditSupportedLayer,
@@ -726,32 +725,6 @@ describe('ImageCanvasGenerationModel', () => {
).toBe(IMAGE_MODEL_NANOBANANA2);
});
it('restores the concrete legacy GPT image value to GPT Image 2.5 with a warning', () => {
const decoded = decodeCanvasGenerationInputs({
version: 2,
action: 'image.generate',
fields: [
{ id: 'prompt', title: '生成提示词', value: '旧模型素材' },
{ id: 'model', title: '模型', value: IMAGE_MODEL_GPT_IMAGE_2_C },
{ id: 'aspectRatio', title: '比例', value: '1:1' },
{ id: 'imageSize', title: '尺寸', value: '1K' },
],
references: [],
});
expect(decoded).toMatchObject({
ok: true,
inputs: {
fields: expect.arrayContaining([
{ id: 'model', title: '模型', value: IMAGE_MODEL_GPT_IMAGE_2_5 },
]),
},
warnings: expect.arrayContaining([
expect.objectContaining({ fieldIds: ['model'] }),
]),
});
});
it('falls invalid image and workflow parameters back within V2', () => {
const image = decodeCanvasGenerationInputs({
version: 2,
@@ -44,7 +44,6 @@ import {
// 中文注释:与 api-server/src/editor_generation_config.rs 保持同名模型定价语义,前端仅作为展示兜底。
export const IMAGE_MODEL_GPT_IMAGE_2 = 'gpt-image-2';
export const IMAGE_MODEL_GPT_IMAGE_2_C = 'gpt-image-2-c';
export const IMAGE_MODEL_GPT_IMAGE_2_5 = 'gpt-image-2.5';
export const IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview';
export const DEFAULT_IMAGE_MODEL = IMAGE_MODEL_NANOBANANA2;
@@ -54,10 +53,7 @@ const IMAGE_MODEL_NANOBANANA_ALIASES = new Set([
'nano-banana',
]);
// 历史 GPT Image 2 别名统一映射到当前业务模型,判定与归一复用同一集合。
const LEGACY_EDITOR_IMAGE_MODELS = new Set([
IMAGE_MODEL_GPT_IMAGE_2,
IMAGE_MODEL_GPT_IMAGE_2_C,
]);
const LEGACY_EDITOR_IMAGE_MODELS = new Set([IMAGE_MODEL_GPT_IMAGE_2]);
export const SPEC_GENERATION_MODEL = IMAGE_MODEL_GPT_IMAGE_2_5;
export const SPEC_GENERATION_ASPECT_RATIO = '16:9';
export const SPEC_GENERATION_IMAGE_SIZE = '2K';