diff --git a/.codex/skills/genarrative-external-editor-api/references/api-operations.md b/.codex/skills/genarrative-external-editor-api/references/api-operations.md index a744d8589..074bea5a0 100644 --- a/.codex/skills/genarrative-external-editor-api/references/api-operations.md +++ b/.codex/skills/genarrative-external-editor-api/references/api-operations.md @@ -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`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`. +- 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 `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`. diff --git a/.codex/skills/gpt-image-2-apimart/SKILL.md b/.codex/skills/gpt-image-2-apimart/SKILL.md index f350d98a5..38baf072c 100644 --- a/.codex/skills/gpt-image-2-apimart/SKILL.md +++ b/.codex/skills/gpt-image-2-apimart/SKILL.md @@ -1,11 +1,11 @@ --- name: gpt-image-2-apimart -description: Generate or inspect project image assets through this repository's VectorEngine gpt-image-2 workflow with gpt-image-2-c fallback. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical. +description: Generate or inspect project image assets through this repository's image workflow using the GPT Image 2.5 business model. Use when Codex needs to create puzzle template sample images, reproduce the server-rs image request body, dry-run image prompts, batch-generate local project thumbnails, or debug VECTOR_ENGINE_BASE_URL / VECTOR_ENGINE_API_KEY image-generation configuration without exposing secrets. The directory name is historical. --- -# gpt-image-2 VectorEngine +# GPT Image 2.5 project image workflow -Use this skill for project-local image asset generation that must match the repository's `server-rs` VectorEngine image path. Keep the product/price model identifier and primary provider request as `gpt-image-2`, then fall back once to `gpt-image-2-c` for eligible provider failures. The folder still contains `apimart` in its name for compatibility with existing local plugin references. +Use this skill for project-local image asset generation that must match the repository's image request contract. Use the business model identifier `gpt-image-2.5`; provider concrete model routing is owned by `server-rs`, and this client must not perform a cross-model fallback. The folder still contains `apimart` in its name for compatibility with existing local plugin references. ## Workflow @@ -40,7 +40,7 @@ Default body: ```json { - "model": "gpt-image-2", + "model": "gpt-image-2.5", "prompt": "", "n": 1, "size": "1024x1024" @@ -58,14 +58,14 @@ Content-Type: multipart/form-data Multipart fields: ```text -model=gpt-image-2 +model=gpt-image-2.5 prompt= n=1 size=1024x1024 image=@reference.png ``` -In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Both paths prefer `gpt-image-2`; on an eligible upstream/model failure they retry with `gpt-image-2-c`. Do not fall back for authentication, local validation, request-budget exhaustion, uncertain send/connection failure, content-safety rejection, or a generated image URL download failure. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part. +In this repository, calls with no reference images use `POST /v1/images/generations`; calls with any reference image use `POST /v1/images/edits` and pass references as one or more `image` form parts. Both paths send the business model `gpt-image-2.5`; provider routing and retry policy remain server-owned. Match3D container UI generation embeds `public/match3d-background-references/pot-fused-reference.png` into the edit request as an `image` part. Accept image output from `data[].url`, `data[].b64_json`, or direct nested `url` fields. VectorEngine image generation currently returns synchronously; do not poll APIMart task endpoints. diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs index 457374380..97bbae547 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-anthro-cat-illustrations.mjs @@ -9,8 +9,7 @@ const skillRoot = path.resolve(__dirname, '..'); const repoRoot = path.resolve(skillRoot, '..', '..', '..'); const defaultOutDir = path.join(repoRoot, 'public', 'anthro-cat-illustrations'); const defaultTimeoutMs = 1000000; -const preferredImageModel = 'gpt-image-2'; -const fallbackImageModel = 'gpt-image-2-c'; +const preferredImageModel = 'gpt-image-2.5'; const prompts = [ { @@ -256,41 +255,8 @@ async function fetchJson(url, options, timeoutMs) { } } -function shouldFallbackImageModel(error) { - const raw = - `${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase(); - if (error?.vectorEngineResponseParse) { - return !containsContentRejection(raw); - } - const status = Number(error?.vectorEngineStatus || 0); - if (status === 408 || status >= 500) { - return true; - } - if (status === 429) { - return !containsContentRejection(raw); - } - const mentionsImageModel = - raw.includes('model') || - raw.includes('模型') || - raw.includes(preferredImageModel) || - raw.includes(fallbackImageModel); - return ( - [400, 404, 422].includes(status) && - mentionsImageModel && - /(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test( - raw, - ) - ); -} - -function containsContentRejection(raw) { - return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test( - raw, - ); -} - async function requestImagePayload(env, entry) { - for (const model of [preferredImageModel, fallbackImageModel]) { + for (const model of [preferredImageModel]) { const requestBody = { model, prompt: buildPrompt(entry), @@ -322,12 +288,7 @@ async function requestImagePayload(env, entry) { error.vectorEngineBody = JSON.stringify(payload).slice(0, 600); throw error; } catch (error) { - if (model !== preferredImageModel || !shouldFallbackImageModel(error)) { - throw error; - } - console.warn( - `VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`, - ); + throw error; } } throw new Error(`VectorEngine returned no image for ${entry.id}`); @@ -408,7 +369,7 @@ if (dryRun) { requests: selectedPrompts.map((entry) => ({ id: entry.id, title: entry.title, - fallbackModel: fallbackImageModel, + fallbackModel: null, body: { model: preferredImageModel, prompt: buildPrompt(entry), diff --git a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs index bb1354921..aa39d5bb7 100644 --- a/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs +++ b/.codex/skills/gpt-image-2-apimart/scripts/generate-template-samples.mjs @@ -18,8 +18,7 @@ const defaultOutDir = path.join( 'puzzle-creation-templates', ); const defaultTimeoutMs = 1000000; -const preferredImageModel = 'gpt-image-2'; -const fallbackImageModel = 'gpt-image-2-c'; +const preferredImageModel = 'gpt-image-2.5'; const args = new Map(); for (let index = 2; index < process.argv.length; index += 1) { @@ -226,41 +225,8 @@ async function fetchJson(url, options, timeoutMs) { } } -function shouldFallbackImageModel(error) { - const raw = - `${error?.message || ''}\n${error?.vectorEngineBody || ''}`.toLowerCase(); - if (error?.vectorEngineResponseParse) { - return !containsContentRejection(raw); - } - const status = Number(error?.vectorEngineStatus || 0); - if (status === 408 || status >= 500) { - return true; - } - if (status === 429) { - return !containsContentRejection(raw); - } - const mentionsImageModel = - raw.includes('model') || - raw.includes('模型') || - raw.includes(preferredImageModel) || - raw.includes(fallbackImageModel); - return ( - [400, 404, 422].includes(status) && - mentionsImageModel && - /(not found|not supported|unsupported|unavailable|does not exist|invalid model|unknown model|不存在|不支持|不可用|未开通)/u.test( - raw, - ) - ); -} - -function containsContentRejection(raw) { - return /(invalid_prompt|safety|content[_ ]policy|moderation|prompt rejected|content rejected|prompt refusal|content refusal|rejected by safety|rejected by moderation|敏感|违规|安全策略|内容审核|提示词拒绝|内容拒绝)/u.test( - raw, - ); -} - async function requestImagePayload(env, template) { - for (const model of [preferredImageModel, fallbackImageModel]) { + for (const model of [preferredImageModel]) { const requestBody = { model, prompt: buildPrompt(template), @@ -294,12 +260,7 @@ async function requestImagePayload(env, template) { error.vectorEngineBody = JSON.stringify(payload).slice(0, 600); throw error; } catch (error) { - if (model !== preferredImageModel || !shouldFallbackImageModel(error)) { - throw error; - } - console.warn( - `VectorEngine ${preferredImageModel} failed, retrying with ${fallbackImageModel}: ${error.message}`, - ); + throw error; } } throw new Error(`VectorEngine returned no image for ${template.id}`); @@ -384,7 +345,7 @@ if (dryRun) { requests: selectedTemplates.map((template) => ({ id: template.id, title: template.title, - fallbackModel: fallbackImageModel, + fallbackModel: null, body: { model: preferredImageModel, prompt: buildPrompt(template), diff --git a/docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md b/docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md index e87f9338b..3b061f49e 100644 --- a/docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md +++ b/docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md @@ -8,6 +8,8 @@ 普通主站前端只接触业务模型和新生成展示名 `GPT Image 2.5`,admin Web/API 可以查看和编辑两个具体定价 key;普通生成即使因参考图使用 edits multipart,仍按生成 concrete model。旧 `gpt-image-2-c` 审计记录原样保留,新代码不再跨模型或跨 provider fallback。 +主站前端只把原本明确使用 GPT Image 2 的专用新任务改为业务模型值 `gpt-image-2.5`;普通图片、角色、场景、图标等原有 nanobanana 默认行为保持不变。画布参数或编辑布局读到 `gpt-image-2` / `gpt-image-2-c` 时,在使用端解析为 `gpt-image-2.5` 并触发参数迁移警告,原始资源、审计、metadata 和历史 fixture 不回写。AGC 现有资源生成界面与请求默认保持不变,不因本决策新增模型字段、选择器或尺寸行为。 + ## Consequences - 定价配置的活动 key 是两个具体 provider model;旧单 key 配置只允许受控 backfill,并留下兼容 TODO。 diff --git a/docs/openapi/genarrative-external-v1.openapi.json b/docs/openapi/genarrative-external-v1.openapi.json index a70761fa3..dc76e0815 100644 --- a/docs/openapi/genarrative-external-v1.openapi.json +++ b/docs/openapi/genarrative-external-v1.openapi.json @@ -3017,7 +3017,7 @@ }, "model": { "type": "string", - "description": "支持 gpt-image-2、gemini-3.1-flash-image-preview、nanobanana2、nano-banana。未传时沿用编辑器默认。" + "description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-banana;gpt-image-2 / gpt-image-2-c 仅作为历史值在新任务提交边界兼容解析。未传时沿用编辑器默认。" }, "aspectRatio": { "type": "string", @@ -3033,7 +3033,7 @@ "type": "array", "items": { "type": "string", - "description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS 再提交。禁止 Data URL / Blob URL。普通生成最多 5 张;kind=quick-edit 时 gpt-image-2 最多 5 张、nanobanana2 最多 9 张。超限返回 400,不会静默截断。" + "description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS 再提交。禁止 Data URL / Blob URL。普通生成最多 5 张;kind=quick-edit 时 GPT Image 2.5 最多 5 张、nanobanana2 最多 9 张。超限返回 400,不会静默截断。" }, "maxItems": 9 }, @@ -3170,7 +3170,7 @@ }, "model": { "type": "string", - "description": "支持 gpt-image-2、gemini-3.1-flash-image-preview、nanobanana2、nano-banana。" + "description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-banana;gpt-image-2 / gpt-image-2-c 仅作为历史值兼容解析。" }, "aspectRatio": { "type": "string", @@ -3184,7 +3184,7 @@ "type": "array", "items": { "type": "string", - "description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceReferenceId 对应的主来源原图占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。" + "description": "当前账号的 objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceReferenceId 对应的主来源原图占用 1 张 provider 容量,因此 GPT Image 2.5 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。" }, "maxItems": 8 }, @@ -3373,7 +3373,7 @@ "type": "array", "items": { "type": "string", - "description": "额外图标素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。referenceId 占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。" + "description": "额外图标素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。referenceId 占用 1 张 provider 容量,因此 GPT Image 2.5 最多再提交 4 张、nanobanana2 最多再提交 8 张;超限返回 400,不会静默截断。" }, "maxItems": 8 }, @@ -3489,13 +3489,13 @@ "model": { "type": "string", "default": "gemini-3.1-flash-image-preview", - "description": "支持 gpt-image-2、gemini-3.1-flash-image-preview、nanobanana2、nano-banana。未传时默认使用 nanobanana。" + "description": "支持 gpt-image-2.5、gemini-3.1-flash-image-preview、nanobanana2、nano-banana;gpt-image-2 / gpt-image-2-c 仅作为历史值兼容解析。未传时默认使用 nanobanana。" }, "referenceImageSrcs": { "type": "array", "items": { "type": "string", - "description": "额外 UI 素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceImageSrc 占用 1 张 provider 容量,因此 gpt-image-2 最多再提交 4 张、nanobanana2 最多再提交 5 张;超限返回 400,不会静默截断。" + "description": "额外 UI 素材参考图的稳定引用:objectKey、项目资源 ID 或素材 ID;本地临时图必须先上传 OSS。禁止 Data URL / Blob URL。sourceImageSrc 占用 1 张 provider 容量,因此 GPT Image 2.5 最多再提交 4 张、nanobanana2 最多再提交 5 张;超限返回 400,不会静默截断。" }, "maxItems": 5 }, diff --git a/docs/project-memory/plans/【实施计划】GPT Image 2.5 provider边界重构-2026-09-18.md b/docs/project-memory/plans/【实施计划】GPT Image 2.5 provider边界重构-2026-09-18.md index d342b69f5..a196d49f1 100644 --- a/docs/project-memory/plans/【实施计划】GPT Image 2.5 provider边界重构-2026-09-18.md +++ b/docs/project-memory/plans/【实施计划】GPT Image 2.5 provider边界重构-2026-09-18.md @@ -1,6 +1,6 @@ # GPT Image 2.5 provider 边界重构实施计划 -- Version: 2 +- Version: 3 - Status: active - Date: 2026-09-18 - Parent Milestone: `docs/project-memory/plans/【里程碑】GPT Image 2.5 provider边界重构-2026-09-18.md` @@ -14,6 +14,21 @@ 5. 迁移 api-server、Agent、raw edit、角色/图标/UI 入口和测试;核对 pricing/admin/public DTO 可见性。 6. 删除跨模型/跨 provider fallback 分支,保留同 concrete model retry。 +## 前端同步边界(已确认) + +1. 主站 `src/components/image-editor/` 中,凡是原本明确写死 `gpt-image-2` 的 GPT 专用新任务(快速编辑、UI 设计、宣发、规范及对应提交/锁定模型)统一改为业务模型值 `gpt-image-2.5`;普通图片、角色、场景、图标等原有 nanobanana 默认行为不变。 +2. `normalizeEditorImageModel` 在编辑面板、画布生成参数和历史布局恢复的使用端把 `gpt-image-2` 与 `gpt-image-2-c` 解析为 `gpt-image-2.5`。该兼容命中必须触发既有参数回退警告;未知模型仍按原有无效参数处理。原始资源、审计、metadata 和历史 fixture 不回写。 +3. 主站新请求的 `model` 字段只发送业务模型值或 nanobanana 业务值,不发送 `gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c` 等 concrete provider model。 +4. AGC(`apps/ai-game-creator-shell/src` 及其 Tauri 本地资源生成链路)本里程碑不增加模型字段、选择器或尺寸行为;继续使用现有请求默认值。AGC 代码只在确属历史兼容读取/测试证明的位置保留旧值,不借本次同步引入新功能。 +5. OpenAPI、External editor skill reference、现役脚本和当前技术方案中的“新任务使用 GPT Image 2 / 2-c fallback”改为 GPT Image 2.5 口径;静态已生成 manifest、历史审计和兼容 fixture 保持旧值并补充历史语义断言。 + +## 前端实现顺序 + +1. 先在 `ImageCanvasGenerationModel` 集中定义历史别名解析与迁移警告结果,保持 nanobanana 默认值、尺寸矩阵和可选模型顺序不变。 +2. 迁移 `ImageCanvasGenerationDialogModel`、`ImageCanvasGenerationSubmissionModel`、生成工作流、快速编辑弹窗、规范/宣发面板的 GPT 专用默认、锁定值和提交 payload。 +3. 为 `generationInputs` / 画布布局 / 资源历史恢复补齐旧值迁移警告测试;确认新请求字段为 `gpt-image-2.5`,而历史原文仍未被改写。 +4. 更新 OpenAPI 与现役工具/专题文档,区分业务模型、concrete provider model、历史值和产品展示名;不得把 AGC 默认行为改成前端显式模型选择。 + ## 验证命令 - `cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check` @@ -24,8 +39,12 @@ - `npm run check:doc-index` - `npm run check:encoding` - `git diff --check` +- 主站图片编辑定向测试:模型选项/展示名、GPT 专用提交值、历史 `gpt-image-2` 与 `gpt-image-2-c` 恢复及警告、nanobanana 默认回归。 +- AGC 定向测试:确认未新增 `model` IPC 入参、未改变原有尺寸/UI 行为,现有资源生成请求继续依赖服务端默认。 +- OpenAPI/External editor 契约测试:模型说明、参考图容量说明和新业务模型值一致,provider concrete model 不进入普通前端契约。 ## 风险与回滚 - 风险:启动阶段依赖变化、历史任务兼容解析遗漏、nanobanana 被误路由到 Tiantoken、provider key 泄露到公开 DTO。 -- 回滚:以 provider-neutral seam、启动配置、model route、调用方迁移四个局部提交边界回滚;不执行数据库历史迁移。 +- 风险:主站把 nanobanana 的默认路径误改成 GPT Image 2.5,或把历史兼容值静默吞掉导致用户无法识别参数迁移。 +- 回滚:以 provider-neutral seam、启动配置、model route、主站前端同步四个局部提交边界回滚;不执行数据库历史迁移,也不回滚历史资源值。