Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1574f206a2 | |||
| 1367e9c04e | |||
| b294cbc3db | |||
| c34d24c2c8 | |||
| 0dbc34d13c |
+29
@@ -44,6 +44,35 @@ _Avoid_: 把同一资源的全局元数据和某一次摆放坐标混在同一
|
||||
由图片生成或图片修改流程产生的画布资源,必须记录来源资源、提示词、实际提示词、模型、provider、任务 ID 和生成时间;本期 `/editor` 的生成修改先允许 mock 生成资源,但仍按生成资源元数据形状保存。
|
||||
_Avoid_: 无来源的静态素材、只显示在 UI 但不落工程资源记录的生成结果
|
||||
|
||||
**图片模型历史值与使用端解析**:
|
||||
图片资源中已持久化的 `gpt-image-2` 是历史业务事实,读回时保持原值;新任务使用业务模型值 `gpt-image-2.5`。当用户基于历史资源再次发起生成或编辑任务时,服务端只在新任务的使用端把历史值解析为当前业务模型,不改写历史资源。provider route 属于服务端执行与审计边界,前端不接收、不持久化、不展示,也不据此分支。
|
||||
_Avoid_: 读取数据库时改写历史模型值、把 provider route 暴露为前端模型选项或公开 DTO
|
||||
|
||||
**图片 provider 显式路由**:
|
||||
api-server 在任务入口按业务语义显式选择具体 provider model name(生成或编辑),并把同一具体名传给图片平台适配器和后台定价解析;图片平台适配器不从参考图数量或前端字段猜测任务。具体 provider model name 只存在于服务端调用、定价配置和审计边界。
|
||||
后台管理 Web/API 是明确例外,可以查看和编辑两个具体定价 key;主站普通前端与公开定价 API 不接收这些 key。
|
||||
_Avoid_: 让图片适配器隐式猜路由、让主站前端携带 provider model name
|
||||
|
||||
**业务模型**:
|
||||
面向任务与产品契约的稳定模型值;当前 GPT 图片新任务的业务模型是 `gpt-image-2.5`。业务模型不等同于 provider 的具体计费/请求 model,也不暴露 provider 凭证或 endpoint。
|
||||
_Avoid_: 把 provider concrete model 当作前端业务选项、用业务模型值直接推断 provider 凭证
|
||||
|
||||
**具体模型**:
|
||||
服务端发送请求和定价使用的 concrete model name。GPT Image 2.5 生成与编辑分别是 `gpt-image-2.5-flare-c` 和 `gpt-image-2.5-sunburst-c`;nanobanana 仍使用 `gemini-3.1-flash-image-preview`。具体模型只在服务端执行、定价和审计边界出现。
|
||||
_Avoid_: 把具体模型写入普通前端 DTO、让未知字符串自动选择 provider
|
||||
|
||||
**provider client**:
|
||||
按具体模型选出的外部图片 provider 连接配置,包含 provider identity、base URL 和 API key;VectorEngine 与 Tiantoken client 共享图片协议执行器,不复制请求/响应业务逻辑。两套 required client 在 api-server 启动时构造。
|
||||
_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.5 新生成展示名**:
|
||||
`GPT Image 2.5` 是新生成任务的产品展示名;历史资源与既有编辑上下文不因新模型上线而改写展示语义。
|
||||
_Avoid_: 把新生成展示名扩散到历史记录、历史生成器或旧编辑上下文
|
||||
|
||||
**系列素材图集生成**:
|
||||
一组同类素材的统一批量生成方式,采用批量规划、sheet 生图、后端切图、透明化、OSS 持久化和局部重生成的通用流水线。
|
||||
_Avoid_: 为每个玩法单独发明素材流水线、把系列素材建模成任一玩法专属 DTO
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
- [画板音乐生成入口](./【编辑器】画板音乐生成入口设计-2026-06-18.md):BGM/SFX 共享视图、独立业务规则和当前发布门禁。
|
||||
- [画布 Agent 对话面板](./【编辑器】画布Agent对话面板-2026-07-03.md)
|
||||
- [画布 Agent 会话消息存 OSS](./adr/【ADR】画布Agent会话消息存OSS-2026-07-03.md)
|
||||
- [GPT Image 2.5 模型路由与历史值兼容](./adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md)
|
||||
- [编辑器模型定价配置](./【编辑器】模型定价配置管理方案-2026-06-22.md)
|
||||
|
||||
## 后端、运维与测试
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# GPT Image 2.5 模型路由与历史值兼容
|
||||
|
||||
状态:accepted
|
||||
|
||||
新任务使用业务模型值 `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 图片路由。
|
||||
|
||||
普通主站前端只接触业务模型和新生成展示名 `GPT Image 2.5`,admin Web/API 可以查看和编辑两个具体定价 key;普通生成即使因参考图使用 edits multipart,仍按生成 concrete model。旧 `gpt-image-2-c` 审计记录原样保留,新代码不再跨模型或跨 provider fallback。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 定价配置的活动 key 是两个具体 provider model;旧单 key 配置只允许受控 backfill,并留下兼容 TODO。
|
||||
- 新任务的同模型重试固定使用 api-server dispatch 的具体 model,不切换到另一个 model。
|
||||
- 两套 provider client 在 api-server 启动阶段同时构造;任一 required provider 配置缺失,启动失败而不是延迟到首次图片请求。
|
||||
- provider routing 只依据 concrete model 的白名单;provider client 不复制共享协议执行逻辑。
|
||||
- 公开资源、`generationInputs` 和普通前端契约不包含具体 provider key;admin 定价管理是明确例外。
|
||||
@@ -0,0 +1,31 @@
|
||||
# GPT Image 2.5 provider 边界重构实施计划
|
||||
|
||||
- Version: 2
|
||||
- Status: active
|
||||
- Date: 2026-09-18
|
||||
- Parent Milestone: `docs/project-memory/plans/【里程碑】GPT Image 2.5 provider边界重构-2026-09-18.md`
|
||||
|
||||
## 实施边界
|
||||
|
||||
1. 先抽象 provider-neutral settings/client 与共享图片执行器接口;保留一套 body、multipart、尺寸、retry、响应和 audit 逻辑。
|
||||
2. 在 api-server 配置/state 初始化阶段分别构造 VectorEngine 与 Tiantoken client;删除 Tiantoken 对 VectorEngine URL/key 的任何 fallback。
|
||||
3. 在 platform-image 建立 concrete model 白名单路由:GPT Image 2.5 → Tiantoken,nanobanana → VectorEngine;legacy/unknown 按合同处理。
|
||||
4. 重命名 provider-specific client/build/transport 符号,避免共享逻辑继续伪装成 `vector_engine_*`;仅保留确有 VectorEngine 语义的名称。
|
||||
5. 迁移 api-server、Agent、raw edit、角色/图标/UI 入口和测试;核对 pricing/admin/public DTO 可见性。
|
||||
6. 删除跨模型/跨 provider fallback 分支,保留同 concrete model retry。
|
||||
|
||||
## 验证命令
|
||||
|
||||
- `cargo fmt --all --manifest-path server-rs/Cargo.toml -- --check`
|
||||
- `cargo test -p platform-image`
|
||||
- `cargo test -p platform-editor-agent`
|
||||
- api-server 定向测试/`cargo check -p api-server`
|
||||
- `npm run typecheck`
|
||||
- `npm run check:doc-index`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
|
||||
## 风险与回滚
|
||||
|
||||
- 风险:启动阶段依赖变化、历史任务兼容解析遗漏、nanobanana 被误路由到 Tiantoken、provider key 泄露到公开 DTO。
|
||||
- 回滚:以 provider-neutral seam、启动配置、model route、调用方迁移四个局部提交边界回滚;不执行数据库历史迁移。
|
||||
@@ -0,0 +1,52 @@
|
||||
# GPT Image 2.5 provider 边界重构
|
||||
|
||||
- Version: 2
|
||||
- Status: active
|
||||
- Date: 2026-09-18
|
||||
- Parent Spec: `docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`
|
||||
|
||||
## 目标
|
||||
|
||||
在保持图片协议执行逻辑共享的前提下,建立明确的 provider client 边界:GPT Image 2.5 通过 Tiantoken,nanobanana 通过 VectorEngine;路由依据 concrete model 严格白名单决定;两个 client 在 api-server 启动阶段构造。
|
||||
|
||||
## 范围
|
||||
|
||||
- `platform-image`:provider-neutral 图片执行器、provider client 注入 seam、concrete model 路由和错误/审计 provider 标识。
|
||||
- `api-server`:启动时构造 VectorEngine/Tiantoken 两个 client,分别读取各自环境变量;任务提交边界的历史模型兼容解析。
|
||||
- 共享请求、multipart、尺寸、retry、响应和 audit 逻辑保持单一实现。
|
||||
- Agent 与其它 server-side 图片调用方迁移到业务模型/concrete model 合同。
|
||||
- 定价、公开 DTO、admin DTO 与 provider model 可见性保持既定 ADR 约束。
|
||||
|
||||
## 现役路由
|
||||
|
||||
| Concrete model | Provider client | 业务用途 |
|
||||
| --- | --- | --- |
|
||||
| `gpt-image-2.5-flare-c` | Tiantoken | Generate,包括带参考图的普通生成 |
|
||||
| `gpt-image-2.5-sunburst-c` | Tiantoken | Edit,包括快速编辑、原位修改、raw edit |
|
||||
| `gemini-3.1-flash-image-preview` | VectorEngine | nanobanana 生成/编辑能力 |
|
||||
|
||||
`gpt-image-2` 与 `gpt-image-2-c` 只保留为历史持久化/审计字符串;新任务不得 dispatch 到旧 GPT Image 2 路由。未知 model 直接拒绝。
|
||||
|
||||
## 必须成立的行为
|
||||
|
||||
1. api-server 启动时同时构造两个 required provider client;任一对应环境变量缺失,启动失败。
|
||||
2. Tiantoken 只读取 `TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY`;VectorEngine 只读取 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY`,互不回退。
|
||||
3. platform-image 根据 concrete model 选择已注入 client;共享执行器不复制 provider 协议逻辑。
|
||||
4. 同 concrete model 可以 retry,但永不跨 concrete model 或跨 provider fallback。
|
||||
5. 历史值读取不改写;新任务提交边界将旧值兼容为 GPT Image 2.5 业务任务。
|
||||
6. 普通前端不接收 concrete provider model;admin 定价界面可查看和编辑两个具体 pricing key。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不复制两套完整图片 client。
|
||||
- 不新增 GPT Image 2 现役 VectorEngine 路由。
|
||||
- 不修改历史数据库记录或旧审计字符串。
|
||||
- 不把 provider client 选择下沉给普通前端。
|
||||
|
||||
## 验收证据
|
||||
|
||||
- provider routing 单元测试覆盖 flare/sunburst/nanobanana/legacy/unknown。
|
||||
- 启动配置测试证明两套 client 独立读取环境变量,缺失任一配置即失败且无 VectorEngine/Tiantoken 回退。
|
||||
- 请求审计测试证明 provider 与 concrete model 正确记录。
|
||||
- platform-image 与 Agent 定向测试通过。
|
||||
- api-server 类型/编译检查、前端类型检查、编码/文档/diff 门禁通过。
|
||||
@@ -8840,3 +8840,20 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 边界:Deploy 阶段在远端 dev / release agent 执行,不受该上限约束。调整只动这两处:`systemctl set-property / revert jenkins.service`、`docker update --cpus=<n> gitea-runner` 加同步 compose(备份 `/opt/gitea-stack/compose.yml.bak-<时间戳>`)。
|
||||
- 验证:限速后 `Genarrative-Full-Build-And-Deploy` #289 / #290 SUCCESS;采样期 Jenkins 峰值 10.2~10.5 核、限流不足 2s(可忽略),runner 峰值 12.07 核且持续出现 throttling,整机回落到 2.6%~19.8%。
|
||||
- 关联文档:[开发运维](../../【开发运维】本地开发验证与生产运维-2026-05-15.md)。
|
||||
|
||||
## 2026-09-18 GPT Image 2.5 业务模型与具体 provider 定价路由
|
||||
|
||||
- **决策**:新任务使用业务模型值 `gpt-image-2.5`;api-server 按任务显式 dispatch 具体模型 `gpt-image-2.5-flare-c`(生成)或 `gpt-image-2.5-sunburst-c`(编辑),并把同一具体 key 交给 `platform-image` 与后台定价解析。普通生成即使因参考图使用 edits multipart,仍按生成 route;同模型重试不跨模型 fallback。
|
||||
- **历史兼容**:已持久化 `gpt-image-2` 读回原值不改写;基于旧资源发起新任务时,在提交边界解析为 `gpt-image-2.5`,新任务/新产物按新业务值和当前 task price 处理。旧 `gpt-image-2-c` 仅保留历史审计,不再作为 fallback 或业务模型。
|
||||
- **可见性**:普通主站前端和公开定价 API 不接收具体 provider key;新生成 UI label 为 `GPT Image 2.5`,历史资源/旧编辑上下文不扩散该 label。admin Web/API 是明确例外,可查看和编辑两个具体定价 key。旧单 key 定价配置允许受控 backfill,并加 compatibility TODO。
|
||||
- **关联 ADR**:[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)。
|
||||
|
||||
- **补充**:GPT Image 2.5 的两个具体模型通过显式 `TIANTOKEN_BASE_URL` 与独立 `TIANTOKEN_API_KEY` 发送;环境变量缺失时必须失败,禁止使用 VectorEngine 配置或 API key 回退。
|
||||
|
||||
## 2026-09-18 GPT Image 2.5 provider-neutral 执行器与双 client 启动边界
|
||||
|
||||
- **决策**:图片协议执行逻辑保持单一共享实现;只抽出 provider client 的 identity、base URL、API key 和 client 构造,通过依赖注入复用请求 body、multipart、尺寸、retry、响应和 audit。
|
||||
- **路由**:`gpt-image-2.5-flare-c` / `gpt-image-2.5-sunburst-c` 走 Tiantoken;`gemini-3.1-flash-image-preview`(nanobanana)走 VectorEngine;`gpt-image-2` / `gpt-image-2-c` 仅是历史字符串,新任务不再进入旧 GPT Image 2 路由;未知 model 拒绝。
|
||||
- **启动**:api-server 启动时同时构造 VectorEngine 与 Tiantoken 两个 required client;各自只读取自己的环境变量,任一配置缺失即启动失败,不延迟到首次请求。
|
||||
- **重试**:只在同一个 concrete model 内 retry,禁止跨 model、跨 provider fallback。
|
||||
- **关联文档**:[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)、[`docs/project-memory/plans/【里程碑】GPT Image 2.5 provider边界重构-2026-09-18.md`](../plans/【里程碑】GPT%20Image%202.5%20provider边界重构-2026-09-18.md)。
|
||||
|
||||
@@ -90,7 +90,7 @@ provider 响应只提取并透传 `data[].b64_json` 字符串,不在服务端
|
||||
## 代码拆分
|
||||
|
||||
- `server-rs/crates/api-server/src/raw_image.rs`:独立路由 handler、multipart 字段解析、请求/响应 DTO、PNG 输入校验、预检查和 raw billing 编排。
|
||||
- `server-rs/crates/platform-image/src/vector_engine/raw_edit.rs`:raw 编辑选项、严格尺寸校验、独立 provider 请求映射和 `b64_json` 响应透传;由 api-server 按现有图片 API 传统构造并传入共享的 VectorEngine `reqwest::Client`,不在每个 raw 调用内部重复构造 client。每次请求仍用 `effective_request_timeout_ms` 通过 request builder 设置剩余 deadline;不修改统一 transport builder 的连接池策略。
|
||||
- `server-rs/crates/platform-image/src/raw_image_edit/mod.rs`:raw 编辑选项、严格尺寸校验、独立 provider 请求映射和 `b64_json` 响应透传;由 api-server 按现有图片 API 传统构造并传入启动时构造的 Tiantoken `reqwest::Client`,不在每个 raw 调用内部重复构造 client。每次请求仍用 `effective_request_timeout_ms` 通过 request builder 设置剩余 deadline;不修改统一 transport builder 的连接池策略。
|
||||
|
||||
raw-edit 的图片输入使用独立的 `RawImageEditImage`(`bytes::Bytes`),由 reqwest `Part::stream(Body::from(Bytes))` 直接接管 multipart 请求体,避免整图和掩码在 `Part::bytes` 的 `Cow<[u8]>` 转换中再次复制。既有 `ReferenceImage`、`DownloadedImage` 及 curl/编辑器链路继续保持 `Vec<u8>` 契约,不因 raw-edit 引入全局字节类型迁移。
|
||||
|
||||
|
||||
@@ -168,7 +168,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、持久化和 VectorEngine 创建 / 编辑首选请求统一使用 `gpt-image-2`;只有符合回退条件时,provider 边界才切到兜底模型 `gpt-image-2-c`。URL / base64 图片解析、远端图片下载、请求超时 / 上游状态 / 响应解析 / 缺图 / 下载失败的结构化日志统一在 `server-rs/crates/platform-image/src/vector_engine/`;其中 `client.rs` 只保留 provider 调用编排,`transport.rs` 负责 HTTP client 与 reqwest 错误归一,`request.rs` 负责请求体和路径,`payload.rs` 负责响应 JSON 字段提取,`response.rs` 负责响应状态分流和图片结果归一。`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 读取。
|
||||
|
||||
@@ -226,7 +226,7 @@ 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 配置仅保留给 Suno 音乐任务。`TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS` 是单次 attempt 的配置上限,默认 `1000000`;配置加载层允许显式值低于该默认值,不再在读取环境变量时强制抬高。业务模型和 Tiantoken provider 首选请求都使用 `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-2 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-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。
|
||||
|
||||
编辑器 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 自动验收。
|
||||
|
||||
|
||||
@@ -15,6 +15,20 @@
|
||||
"2K": 5
|
||||
}
|
||||
},
|
||||
"gpt-image-2.5-flare-c": {
|
||||
"unit": "perGeneration",
|
||||
"prices": {
|
||||
"1K": 3,
|
||||
"2K": 5
|
||||
}
|
||||
},
|
||||
"gpt-image-2.5-sunburst-c": {
|
||||
"unit": "perGeneration",
|
||||
"prices": {
|
||||
"1K": 3,
|
||||
"2K": 5
|
||||
}
|
||||
},
|
||||
"seedance2.0-fast": {
|
||||
"unit": "perSecond",
|
||||
"prices": {
|
||||
|
||||
@@ -36,16 +36,16 @@ use crate::{
|
||||
},
|
||||
http_error::AppError,
|
||||
openai_image_generation::{
|
||||
DownloadedOpenAiImage, GPT_IMAGE_2_MODEL, OpenAiImageSettings,
|
||||
build_openai_image_http_client, create_openai_image_generation,
|
||||
require_openai_image_settings,
|
||||
DownloadedOpenAiImage, GPT_IMAGE_2_5_BUSINESS_NAME, GPT_IMAGE_2_5_GENERATION_MODEL,
|
||||
OpenAiImageSettings, build_openai_image_http_client,
|
||||
create_openai_image_generation_with_model, require_openai_image_settings,
|
||||
},
|
||||
platform_errors::map_oss_error,
|
||||
request_context::RequestContext,
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
const CHARACTER_VISUAL_MODEL: &str = GPT_IMAGE_2_MODEL;
|
||||
const CHARACTER_VISUAL_MODEL: &str = GPT_IMAGE_2_5_BUSINESS_NAME;
|
||||
const CHARACTER_VISUAL_ASSET_KIND: &str = "character_visual";
|
||||
const CHARACTER_VISUAL_ENTITY_KIND: &str = "character";
|
||||
const CHARACTER_VISUAL_SLOT: &str = "primary_visual";
|
||||
@@ -777,13 +777,13 @@ fn build_character_visual_job_payload(task: AiTaskSnapshot) -> CharacterAssetJob
|
||||
}
|
||||
|
||||
fn resolve_character_visual_model(value: &str) -> String {
|
||||
// 中文注释:旧前端和历史草稿可能仍传 wan2.7-image-pro;RPG 主图当前统一归一到 gpt-image-2。
|
||||
// 中文注释:旧前端和历史草稿可能仍传旧模型;只在新任务提交边界归一到当前业务模型。
|
||||
let trimmed = value.trim();
|
||||
if !trimmed.is_empty() && trimmed != CHARACTER_VISUAL_MODEL {
|
||||
tracing::warn!(
|
||||
requested_model = trimmed,
|
||||
effective_model = CHARACTER_VISUAL_MODEL,
|
||||
"角色主形象图片模型已归一到 gpt-image-2"
|
||||
"角色主形象图片模型已归一到当前业务模型"
|
||||
);
|
||||
}
|
||||
CHARACTER_VISUAL_MODEL.to_string()
|
||||
@@ -946,9 +946,10 @@ async fn create_character_visual_generation_once(
|
||||
candidate_count: u32,
|
||||
reference_images: &[String],
|
||||
) -> Result<GeneratedCharacterVisuals, AppError> {
|
||||
let generated = create_openai_image_generation(
|
||||
let generated = create_openai_image_generation_with_model(
|
||||
http_client,
|
||||
settings,
|
||||
GPT_IMAGE_2_5_GENERATION_MODEL,
|
||||
prompt,
|
||||
Some(build_character_visual_negative_prompt().as_str()),
|
||||
size,
|
||||
@@ -1921,12 +1922,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_character_visual_model_normalizes_to_gpt_image_2() {
|
||||
fn legacy_character_visual_model_normalizes_to_gpt_image_2_5() {
|
||||
assert_eq!(
|
||||
resolve_character_visual_model("wan2.7-image-pro"),
|
||||
"gpt-image-2"
|
||||
"gpt-image-2.5"
|
||||
);
|
||||
assert_eq!(resolve_character_visual_model(""), "gpt-image-2");
|
||||
assert_eq!(resolve_character_visual_model(""), "gpt-image-2.5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1428,18 +1428,15 @@ impl AppConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tiantoken 是图片、文本和旧版非 Suno 音频生成的新 provider。
|
||||
///
|
||||
/// 这里保留对 `AppConfig.vector_engine_*` 的回退,方便测试构造的旧配置继续工作;
|
||||
/// 生产环境一旦设置了新的 `TIANTOKEN_*` 变量,就不会再把非 Suno 请求发往 VectorEngine。
|
||||
/// Tiantoken 是 GPT Image 2.5 图片任务的独立 provider;凭证不得回退到 VectorEngine。
|
||||
pub(crate) fn tiantoken_base_url(config: &AppConfig) -> String {
|
||||
read_first_non_empty_env(&["TIANTOKEN_BASE_URL"])
|
||||
.unwrap_or_else(|| config.vector_engine_base_url.clone())
|
||||
let _ = config;
|
||||
read_first_non_empty_env(&["TIANTOKEN_BASE_URL"]).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn tiantoken_api_key(config: &AppConfig) -> Option<String> {
|
||||
let _ = config;
|
||||
read_first_non_empty_env(&["TIANTOKEN_API_KEY"])
|
||||
.or_else(|| config.vector_engine_api_key.clone())
|
||||
}
|
||||
|
||||
fn read_first_non_empty_env(keys: &[&str]) -> Option<String> {
|
||||
@@ -1864,14 +1861,8 @@ mod tests {
|
||||
std::env::remove_var("TIANTOKEN_BASE_URL");
|
||||
std::env::remove_var("TIANTOKEN_API_KEY");
|
||||
}
|
||||
assert_eq!(
|
||||
tiantoken_base_url(&config),
|
||||
"https://vector.example.invalid"
|
||||
);
|
||||
assert_eq!(
|
||||
tiantoken_api_key(&config).as_deref(),
|
||||
Some("legacy-vector-key")
|
||||
);
|
||||
assert_eq!(tiantoken_base_url(&config), "");
|
||||
assert_eq!(tiantoken_api_key(&config), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -17,6 +17,10 @@ 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";
|
||||
const EDITOR_IMAGE_MODEL_NANOBANANA2: &str = "gemini-3.1-flash-image-preview";
|
||||
const EDITOR_IMAGE_MODEL_NANOBANANA2_DISPLAY_ALIAS: &str = "nanobanana2";
|
||||
const EDITOR_IMAGE_MODEL_NANOBANANA_LEGACY_ALIAS: &str = "nano-banana";
|
||||
@@ -94,6 +98,25 @@ pub(crate) enum EditorGenerationPricingError {
|
||||
}
|
||||
|
||||
impl EditorGenerationPricingConfig {
|
||||
/// Public main-site projection: provider-specific GPT Image keys remain an
|
||||
/// admin/server concern and are represented by the business model name.
|
||||
pub(crate) fn public_projection(&self) -> Self {
|
||||
let mut models = self.models.clone();
|
||||
if let Some(generation) = models
|
||||
.get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION)
|
||||
.cloned()
|
||||
{
|
||||
models.insert(
|
||||
EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_BUSINESS.to_string(),
|
||||
generation,
|
||||
);
|
||||
}
|
||||
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);
|
||||
Self { models }
|
||||
}
|
||||
|
||||
pub(crate) fn image_model_mud_points(
|
||||
&self,
|
||||
model: Option<&str>,
|
||||
@@ -114,15 +137,35 @@ impl EditorGenerationPricingConfig {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn image_edit_model_mud_points(
|
||||
&self,
|
||||
model: Option<&str>,
|
||||
image_size: Option<&str>,
|
||||
) -> u32 {
|
||||
if let Some(price_mud_points) = current_external_generation_billing_price_mud_points() {
|
||||
return price_mud_points;
|
||||
}
|
||||
let normalized_model = normalize_editor_image_edit_model(model);
|
||||
let normalized_size =
|
||||
normalize_editor_generation_image_price_size(normalized_model, image_size);
|
||||
read_tier_price(
|
||||
&self.models,
|
||||
normalized_model,
|
||||
EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT,
|
||||
normalized_size,
|
||||
DEFAULT_IMAGE_PRICE_SIZE,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn spec_model_mud_points(&self, model: Option<&str>) -> u32 {
|
||||
if let Some(price_mud_points) = current_external_generation_billing_price_mud_points() {
|
||||
return price_mud_points;
|
||||
}
|
||||
let normalized_model = normalize_non_empty_model(model, EDITOR_IMAGE_MODEL_GPT_IMAGE_2);
|
||||
let normalized_model = normalize_editor_image_model(model);
|
||||
read_tier_price(
|
||||
&self.models,
|
||||
normalized_model,
|
||||
EDITOR_IMAGE_MODEL_GPT_IMAGE_2,
|
||||
EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION,
|
||||
SPEC_IMAGE_PRICE_SIZE,
|
||||
SPEC_IMAGE_PRICE_SIZE,
|
||||
)
|
||||
@@ -221,7 +264,13 @@ impl EditorGenerationPricingConfig {
|
||||
)?;
|
||||
validate_required_tier_prices(
|
||||
&self.models,
|
||||
EDITOR_IMAGE_MODEL_GPT_IMAGE_2,
|
||||
EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION,
|
||||
EditorGenerationPricingUnit::PerGeneration,
|
||||
REQUIRED_GPT_IMAGE_SIZES,
|
||||
)?;
|
||||
validate_required_tier_prices(
|
||||
&self.models,
|
||||
EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT,
|
||||
EditorGenerationPricingUnit::PerGeneration,
|
||||
REQUIRED_GPT_IMAGE_SIZES,
|
||||
)?;
|
||||
@@ -326,6 +375,7 @@ fn load_editor_generation_pricing_from_candidates(
|
||||
serde_json::from_str::<EditorGenerationPricingConfig>(override_json.as_str())
|
||||
.map_err(EditorGenerationPricingError::Json)?;
|
||||
backfill_legacy_sfx_pricing(&mut override_config, &config, source.as_ref())?;
|
||||
backfill_legacy_gpt_image_2_5_pricing(&mut override_config, &config, source.as_ref())?;
|
||||
override_config.validate().map_err(|error| match error {
|
||||
EditorGenerationPricingError::Invalid(message) => {
|
||||
EditorGenerationPricingError::Invalid(format!("{source}: {message}"))
|
||||
@@ -338,6 +388,43 @@ fn load_editor_generation_pricing_from_candidates(
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn backfill_legacy_gpt_image_2_5_pricing(
|
||||
config: &mut EditorGenerationPricingConfig,
|
||||
fallback: &EditorGenerationPricingConfig,
|
||||
source: &str,
|
||||
) -> Result<(), EditorGenerationPricingError> {
|
||||
if config
|
||||
.models
|
||||
.contains_key(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION)
|
||||
&& config
|
||||
.models
|
||||
.contains_key(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
// TODO: compatibility backfill for legacy single-key pricing; remove once
|
||||
// all persisted overrides contain the two explicit GPT Image 2.5 keys.
|
||||
let pricing = config
|
||||
.models
|
||||
.get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2)
|
||||
.or_else(|| fallback.models.get(EDITOR_IMAGE_MODEL_GPT_IMAGE_2))
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
EditorGenerationPricingError::Invalid(format!(
|
||||
"{source}: 受控默认配置缺少模型 {EDITOR_IMAGE_MODEL_GPT_IMAGE_2} 的兼容泥点配置"
|
||||
))
|
||||
})?;
|
||||
config
|
||||
.models
|
||||
.entry(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_GENERATION.to_string())
|
||||
.or_insert_with(|| pricing.clone());
|
||||
config
|
||||
.models
|
||||
.entry(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT.to_string())
|
||||
.or_insert(pricing);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn backfill_legacy_sfx_pricing(
|
||||
config: &mut EditorGenerationPricingConfig,
|
||||
fallback: &EditorGenerationPricingConfig,
|
||||
@@ -437,7 +524,13 @@ fn default_runtime_pricing() -> EditorGenerationPricingConfig {
|
||||
|
||||
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,
|
||||
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,
|
||||
Some(EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT) => EDITOR_IMAGE_MODEL_GPT_IMAGE_2_5_EDIT,
|
||||
Some(EDITOR_IMAGE_MODEL_NANOBANANA2)
|
||||
| Some(EDITOR_IMAGE_MODEL_NANOBANANA2_DISPLAY_ALIAS)
|
||||
| Some(EDITOR_IMAGE_MODEL_NANOBANANA_LEGACY_ALIAS) => EDITOR_IMAGE_MODEL_NANOBANANA2,
|
||||
@@ -445,6 +538,19 @@ fn normalize_editor_image_model(model: Option<&str>) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_editor_image_edit_model(model: Option<&str>) -> &'static str {
|
||||
match model.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
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,
|
||||
_ => normalize_editor_image_model(model),
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_editor_generation_image_price_size(
|
||||
model: &str,
|
||||
image_size: Option<&str>,
|
||||
|
||||
@@ -112,9 +112,10 @@ use crate::{
|
||||
},
|
||||
http_error::AppError,
|
||||
openai_image_generation::{
|
||||
DownloadedOpenAiImage, GPT_IMAGE_2_MODEL, OpenAiGeneratedImages, OpenAiImageSettings,
|
||||
OpenAiReferenceImage, build_openai_image_http_client,
|
||||
create_openai_image_edit_with_references_and_model,
|
||||
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,
|
||||
create_openai_image_generation_with_model, create_openai_nanobanana_generate_content,
|
||||
require_openai_image_settings,
|
||||
},
|
||||
@@ -374,6 +375,9 @@ pub(crate) struct EditorImagePromptBuildOutput {
|
||||
|
||||
pub(crate) struct EditorImageProviderRequest<'a> {
|
||||
pub(crate) model: &'a str,
|
||||
/// Concrete provider/pricing route selected by api-server. The business
|
||||
/// model remains `model`; this value must never be exposed to normal UI.
|
||||
pub(crate) provider_model: &'a str,
|
||||
pub(crate) prompt: &'a str,
|
||||
pub(crate) negative_prompt: Option<&'a str>,
|
||||
pub(crate) size: &'a str,
|
||||
@@ -392,7 +396,7 @@ pub(crate) async fn request_editor_generated_images(
|
||||
create_openai_nanobanana_generate_content(
|
||||
http_client,
|
||||
settings,
|
||||
request.model,
|
||||
request.provider_model,
|
||||
request.prompt,
|
||||
request.negative_prompt,
|
||||
request.aspect_ratio,
|
||||
@@ -405,7 +409,7 @@ pub(crate) async fn request_editor_generated_images(
|
||||
create_openai_image_generation_with_model(
|
||||
http_client,
|
||||
settings,
|
||||
request.model,
|
||||
request.provider_model,
|
||||
request.prompt,
|
||||
request.negative_prompt,
|
||||
request.size,
|
||||
@@ -418,7 +422,7 @@ pub(crate) async fn request_editor_generated_images(
|
||||
create_openai_image_edit_with_references_and_model(
|
||||
http_client,
|
||||
settings,
|
||||
request.model,
|
||||
request.provider_model,
|
||||
request.prompt,
|
||||
request.negative_prompt,
|
||||
request.size,
|
||||
@@ -1926,7 +1930,10 @@ pub async fn get_editor_generation_pricing(
|
||||
"message": error.to_string(),
|
||||
}))
|
||||
})?;
|
||||
Ok(json_success_body(Some(&request_context), pricing))
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
pricing.public_projection(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn list_editor_projects(
|
||||
@@ -2764,7 +2771,7 @@ pub(crate) async fn enqueue_editor_image_generation_for_owner(
|
||||
matches!(normalized_kind, Some("publication-material"));
|
||||
let generation_options = normalize_editor_generation_options(
|
||||
if is_ui_design_generation || is_publication_material_generation {
|
||||
Some(GPT_IMAGE_2_MODEL)
|
||||
Some(GPT_IMAGE_2_5_BUSINESS_NAME)
|
||||
} else {
|
||||
payload.model.as_deref()
|
||||
},
|
||||
@@ -2854,7 +2861,7 @@ pub(crate) async fn validate_editor_image_generation_parameters_for_owner(
|
||||
matches!(normalized_kind, Some("publication-material"));
|
||||
let generation_options = normalize_editor_generation_options(
|
||||
if is_ui_design_generation || is_publication_material_generation {
|
||||
Some(GPT_IMAGE_2_MODEL)
|
||||
Some(GPT_IMAGE_2_5_BUSINESS_NAME)
|
||||
} else {
|
||||
payload.model.as_deref()
|
||||
},
|
||||
@@ -2987,7 +2994,7 @@ where
|
||||
matches!(normalized_kind, Some("publication-material"));
|
||||
let generation_options = normalize_editor_generation_options(
|
||||
if is_ui_design_generation || is_publication_material_generation {
|
||||
Some(GPT_IMAGE_2_MODEL)
|
||||
Some(GPT_IMAGE_2_5_BUSINESS_NAME)
|
||||
} else {
|
||||
payload.model.as_deref()
|
||||
},
|
||||
@@ -3181,6 +3188,11 @@ where
|
||||
&settings,
|
||||
EditorImageProviderRequest {
|
||||
model: generation_options.model,
|
||||
provider_model: if generation_options.model == EDITOR_IMAGE_MODEL_NANOBANANA2 {
|
||||
generation_options.model
|
||||
} else {
|
||||
GPT_IMAGE_2_5_GENERATION_MODEL
|
||||
},
|
||||
prompt: submitted_prompt.as_str(),
|
||||
negative_prompt,
|
||||
size: provider_request_size.as_ref(),
|
||||
@@ -3832,7 +3844,7 @@ async fn resolve_editor_image_edit_price(
|
||||
"message": error.to_string(),
|
||||
}))
|
||||
})?
|
||||
.image_generation_mud_points(Some("quick-edit"), Some(model), Some(price_size));
|
||||
.image_edit_model_mud_points(Some(model), Some(price_size));
|
||||
Ok(expected_price_mud_points)
|
||||
}
|
||||
|
||||
@@ -3975,7 +3987,16 @@ pub(crate) fn normalize_editor_generation_options(
|
||||
image_size: Option<&str>,
|
||||
) -> EditorGenerationOptions {
|
||||
let normalized_model = match model.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
Some(GPT_IMAGE_2_MODEL) => GPT_IMAGE_2_MODEL,
|
||||
// TODO: compatibility for persisted/client legacy `gpt-image-2`; do not
|
||||
// rewrite the stored record, only use the current business value when a
|
||||
// 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",
|
||||
) => GPT_IMAGE_2_5_BUSINESS_NAME,
|
||||
Some(
|
||||
EDITOR_IMAGE_MODEL_NANOBANANA2
|
||||
| EDITOR_IMAGE_MODEL_NANOBANANA2_DISPLAY_ALIAS
|
||||
@@ -3983,7 +4004,7 @@ pub(crate) fn normalize_editor_generation_options(
|
||||
) => EDITOR_IMAGE_MODEL_NANOBANANA2,
|
||||
// 中文注释:未显式传模型的旧普通生成、快速编辑和生成规范继续走 gpt-image-2;
|
||||
// 角色 / 图标素材入口由前端显式传入 nanobanana2 默认值。
|
||||
None => GPT_IMAGE_2_MODEL,
|
||||
None => GPT_IMAGE_2_5_BUSINESS_NAME,
|
||||
_ => EDITOR_IMAGE_MODEL_NANOBANANA2,
|
||||
};
|
||||
let aspect_ratio = normalize_editor_generation_aspect_ratio(aspect_ratio);
|
||||
@@ -6048,7 +6069,7 @@ pub(crate) async fn edit_editor_image_for_owner_with_source_snapshot(
|
||||
create_openai_image_edit_with_references_and_model(
|
||||
&http_client,
|
||||
&settings,
|
||||
generation_options.model,
|
||||
GPT_IMAGE_2_5_EDIT_MODEL,
|
||||
prompt.as_str(),
|
||||
Some("文字、水印、边框、按钮、UI 控件、变形主体"),
|
||||
provider_size.as_str(),
|
||||
@@ -8493,7 +8514,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner(
|
||||
create_openai_image_edit_with_references_and_model(
|
||||
&http_client,
|
||||
&settings,
|
||||
generation_options.model,
|
||||
GPT_IMAGE_2_5_GENERATION_MODEL,
|
||||
prompt.as_str(),
|
||||
None,
|
||||
generation_options.provider_size.as_str(),
|
||||
@@ -17817,7 +17838,7 @@ mod tests {
|
||||
#[test]
|
||||
fn editor_generation_dimensions_follow_model_options() {
|
||||
let default_generation = normalize_editor_generation_options(None, Some("1:1"), Some("1K"));
|
||||
assert_eq!(default_generation.model, GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(default_generation.model, GPT_IMAGE_2_5_BUSINESS_NAME);
|
||||
assert_eq!(default_generation.size, "1024x1024");
|
||||
|
||||
let nanobanana = normalize_editor_generation_options(
|
||||
@@ -17847,7 +17868,7 @@ mod tests {
|
||||
assert_eq!(legacy_nanobanana_alias.aspect_ratio, "16:9");
|
||||
|
||||
let gpt = normalize_editor_generation_options(Some("gpt-image-2"), Some("2:3"), Some("1K"));
|
||||
assert_eq!(gpt.model, GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(gpt.model, GPT_IMAGE_2_5_BUSINESS_NAME);
|
||||
assert_eq!(gpt.size, "683x1024");
|
||||
assert_eq!(gpt.provider_size, "688x1024");
|
||||
assert_eq!(gpt.aspect_ratio, "2:3");
|
||||
@@ -17862,21 +17883,21 @@ mod tests {
|
||||
|
||||
let gpt_cover =
|
||||
normalize_editor_generation_options(Some("gpt-image-2"), Some("4:3"), Some("1K"));
|
||||
assert_eq!(gpt_cover.model, GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(gpt_cover.model, GPT_IMAGE_2_5_BUSINESS_NAME);
|
||||
assert_eq!(gpt_cover.size, "1024x768");
|
||||
assert_eq!(gpt_cover.provider_size, "1024x768");
|
||||
assert_eq!(gpt_cover.aspect_ratio, "4:3");
|
||||
|
||||
let gpt_landscape_2k =
|
||||
normalize_editor_generation_options(Some("gpt-image-2"), Some("16:9"), Some("2K"));
|
||||
assert_eq!(gpt_landscape_2k.model, GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(gpt_landscape_2k.model, GPT_IMAGE_2_5_BUSINESS_NAME);
|
||||
assert_eq!(gpt_landscape_2k.size, "2048x1152");
|
||||
assert_eq!(gpt_landscape_2k.aspect_ratio, "16:9");
|
||||
assert_eq!(gpt_landscape_2k.image_size, "2K");
|
||||
|
||||
let gpt_portrait_2k =
|
||||
normalize_editor_generation_options(Some("gpt-image-2"), Some("9:16"), Some("2K"));
|
||||
assert_eq!(gpt_portrait_2k.model, GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(gpt_portrait_2k.model, GPT_IMAGE_2_5_BUSINESS_NAME);
|
||||
assert_eq!(gpt_portrait_2k.size, "1152x2048");
|
||||
assert_eq!(gpt_portrait_2k.aspect_ratio, "9:16");
|
||||
assert_eq!(gpt_portrait_2k.image_size, "2K");
|
||||
@@ -19989,7 +20010,7 @@ mod tests {
|
||||
)
|
||||
.expect("UI extraction dimensions should pass");
|
||||
|
||||
assert_eq!(generation_options.model, GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(generation_options.model, GPT_IMAGE_2_5_BUSINESS_NAME);
|
||||
assert_eq!(generation_options.size, "2048x2048");
|
||||
assert_eq!(
|
||||
resolve_editor_ui_design_asset_extraction_price(
|
||||
|
||||
@@ -64,7 +64,7 @@ use crate::{
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
const ICON_SPEC_MODEL: &str = "gpt-image-2";
|
||||
const ICON_SPEC_MODEL: &str = "gpt-image-2.5";
|
||||
const ICON_SPEC_ASPECT_RATIO: &str = "16:9";
|
||||
const ICON_SPEC_IMAGE_SIZE: &str = "2K";
|
||||
const ICON_SPEC_SIZE: &str = "2048x1152";
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
use axum::http::StatusCode;
|
||||
use platform_image::{
|
||||
DownloadedImage, GeneratedImages, PlatformImageError, PlatformImageStatusHint, ReferenceImage,
|
||||
VECTOR_ENGINE_PROVIDER, VectorEngineImageSettings, build_vector_engine_image_http_client,
|
||||
create_vector_engine_image_edit, create_vector_engine_image_edit_with_references,
|
||||
create_vector_engine_image_edit_with_references_and_model,
|
||||
create_vector_engine_image_generation, create_vector_engine_image_generation_with_model,
|
||||
create_vector_engine_nanobanana_generate_content,
|
||||
DownloadedImage, GeneratedImages, ImageProvider, ImageProviderClient, ImageProviderSettings,
|
||||
NANOBANANA_2_MODEL, PlatformImageError, PlatformImageStatusHint, ReferenceImage,
|
||||
VECTOR_ENGINE_PROVIDER, build_image_http_client, 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,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use platform_image::{
|
||||
build_vector_engine_image_request_body, vector_engine_images_edit_url,
|
||||
vector_engine_images_generation_url,
|
||||
};
|
||||
use platform_image::{build_image_request_body, images_edit_url, images_generation_url};
|
||||
use serde_json::{Value, json};
|
||||
use std::time::Instant;
|
||||
use time::OffsetDateTime;
|
||||
@@ -27,9 +24,10 @@ use crate::{
|
||||
tracking::record_external_generation_run_after_success,
|
||||
};
|
||||
|
||||
pub(crate) use platform_image::GPT_IMAGE_2_MODEL;
|
||||
#[cfg(test)]
|
||||
use platform_image::VECTOR_ENGINE_GPT_IMAGE_2_MODEL;
|
||||
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_MODEL,
|
||||
};
|
||||
|
||||
pub(crate) type OpenAiGeneratedImages = GeneratedImages;
|
||||
pub(crate) type DownloadedOpenAiImage = DownloadedImage;
|
||||
@@ -45,6 +43,8 @@ pub(crate) struct OpenAiImageSettings {
|
||||
pub external_api_audit_user_id: Option<String>,
|
||||
pub external_api_audit_profile_id: Option<String>,
|
||||
pub external_api_audit_request_id: Option<String>,
|
||||
pub tiantoken_client: Option<ImageProviderClient>,
|
||||
pub vector_engine_client: Option<ImageProviderClient>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for OpenAiImageSettings {
|
||||
@@ -71,6 +71,11 @@ impl std::fmt::Debug for OpenAiImageSettings {
|
||||
"external_api_audit_request_id",
|
||||
&self.external_api_audit_request_id,
|
||||
)
|
||||
.field("tiantoken_client_enabled", &self.tiantoken_client.is_some())
|
||||
.field(
|
||||
"vector_engine_client_enabled",
|
||||
&self.vector_engine_client.is_some(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -110,18 +115,22 @@ pub(crate) fn require_openai_image_settings(
|
||||
external_api_audit_user_id: None,
|
||||
external_api_audit_profile_id: None,
|
||||
external_api_audit_request_id: None,
|
||||
tiantoken_client: Some(state.tiantoken_image_client().clone()),
|
||||
vector_engine_client: Some(state.vector_engine_image_client().clone()),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn build_openai_image_http_client(
|
||||
settings: &OpenAiImageSettings,
|
||||
) -> Result<reqwest::Client, AppError> {
|
||||
build_vector_engine_image_http_client(&settings.provider_settings())
|
||||
.map_err(map_platform_image_error)
|
||||
if let Some(client) = settings.tiantoken_client.as_ref() {
|
||||
return Ok(client.http_client().clone());
|
||||
}
|
||||
build_image_http_client(&settings.provider_settings()).map_err(map_platform_image_error)
|
||||
}
|
||||
|
||||
pub(crate) async fn create_openai_image_generation(
|
||||
http_client: &reqwest::Client,
|
||||
_http_client: &reqwest::Client,
|
||||
settings: &OpenAiImageSettings,
|
||||
prompt: &str,
|
||||
negative_prompt: Option<&str>,
|
||||
@@ -138,9 +147,11 @@ pub(crate) async fn create_openai_image_generation(
|
||||
"negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count),
|
||||
"referenceImageCount": reference_images.len(),
|
||||
});
|
||||
let result = create_vector_engine_image_generation(
|
||||
http_client,
|
||||
&settings.provider_settings(),
|
||||
let provider_client = settings.client_for_model(GPT_IMAGE_2_5_GENERATION_MODEL);
|
||||
let provider_settings = settings.provider_settings_for_model(GPT_IMAGE_2_5_GENERATION_MODEL);
|
||||
let result = create_image_generation(
|
||||
provider_client.http_client(),
|
||||
&provider_settings,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
size,
|
||||
@@ -162,7 +173,7 @@ pub(crate) async fn create_openai_image_generation(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn create_openai_image_generation_with_model(
|
||||
http_client: &reqwest::Client,
|
||||
_http_client: &reqwest::Client,
|
||||
settings: &OpenAiImageSettings,
|
||||
model: &str,
|
||||
prompt: &str,
|
||||
@@ -181,9 +192,11 @@ pub(crate) async fn create_openai_image_generation_with_model(
|
||||
"negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count),
|
||||
"referenceImageCount": reference_images.len(),
|
||||
});
|
||||
let result = create_vector_engine_image_generation_with_model(
|
||||
http_client,
|
||||
&settings.provider_settings(),
|
||||
let provider_client = settings.client_for_model(model);
|
||||
let provider_settings = settings.provider_settings_for_model(model);
|
||||
let result = create_image_generation_with_model(
|
||||
provider_client.http_client(),
|
||||
&provider_settings,
|
||||
model,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
@@ -206,7 +219,7 @@ pub(crate) async fn create_openai_image_generation_with_model(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn create_openai_nanobanana_generate_content(
|
||||
http_client: &reqwest::Client,
|
||||
_http_client: &reqwest::Client,
|
||||
settings: &OpenAiImageSettings,
|
||||
model: &str,
|
||||
prompt: &str,
|
||||
@@ -225,9 +238,11 @@ pub(crate) async fn create_openai_nanobanana_generate_content(
|
||||
"negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count),
|
||||
"referenceImageCount": reference_images.len(),
|
||||
});
|
||||
let result = create_vector_engine_nanobanana_generate_content(
|
||||
http_client,
|
||||
&settings.provider_settings(),
|
||||
let provider_client = settings.client_for_model(model);
|
||||
let provider_settings = settings.provider_settings_for_model(model);
|
||||
let result = create_nanobanana_generate_content(
|
||||
provider_client.http_client(),
|
||||
&provider_settings,
|
||||
model,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
@@ -249,7 +264,7 @@ pub(crate) async fn create_openai_nanobanana_generate_content(
|
||||
}
|
||||
|
||||
pub(crate) async fn create_openai_image_edit(
|
||||
http_client: &reqwest::Client,
|
||||
_http_client: &reqwest::Client,
|
||||
settings: &OpenAiImageSettings,
|
||||
prompt: &str,
|
||||
negative_prompt: Option<&str>,
|
||||
@@ -264,9 +279,11 @@ pub(crate) async fn create_openai_image_edit(
|
||||
"negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count),
|
||||
"referenceImageCount": 1,
|
||||
});
|
||||
let result = create_vector_engine_image_edit(
|
||||
http_client,
|
||||
&settings.provider_settings(),
|
||||
let provider_client = settings.client_for_model(GPT_IMAGE_2_5_EDIT_MODEL);
|
||||
let provider_settings = settings.provider_settings_for_model(GPT_IMAGE_2_5_EDIT_MODEL);
|
||||
let result = create_image_edit(
|
||||
provider_client.http_client(),
|
||||
&provider_settings,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
size,
|
||||
@@ -286,7 +303,7 @@ pub(crate) async fn create_openai_image_edit(
|
||||
}
|
||||
|
||||
pub(crate) async fn create_openai_image_edit_with_references(
|
||||
http_client: &reqwest::Client,
|
||||
_http_client: &reqwest::Client,
|
||||
settings: &OpenAiImageSettings,
|
||||
prompt: &str,
|
||||
negative_prompt: Option<&str>,
|
||||
@@ -303,9 +320,11 @@ pub(crate) async fn create_openai_image_edit_with_references(
|
||||
"negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count),
|
||||
"referenceImageCount": reference_images.len(),
|
||||
});
|
||||
let result = create_vector_engine_image_edit_with_references(
|
||||
http_client,
|
||||
&settings.provider_settings(),
|
||||
let provider_client = settings.client_for_model(GPT_IMAGE_2_5_EDIT_MODEL);
|
||||
let provider_settings = settings.provider_settings_for_model(GPT_IMAGE_2_5_EDIT_MODEL);
|
||||
let result = create_image_edit_with_references(
|
||||
provider_client.http_client(),
|
||||
&provider_settings,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
size,
|
||||
@@ -327,7 +346,7 @@ pub(crate) async fn create_openai_image_edit_with_references(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn create_openai_image_edit_with_references_and_model(
|
||||
http_client: &reqwest::Client,
|
||||
_http_client: &reqwest::Client,
|
||||
settings: &OpenAiImageSettings,
|
||||
model: &str,
|
||||
prompt: &str,
|
||||
@@ -345,9 +364,11 @@ pub(crate) async fn create_openai_image_edit_with_references_and_model(
|
||||
"negativePromptChars": negative_prompt.map(str::chars).map(Iterator::count),
|
||||
"referenceImageCount": reference_images.len(),
|
||||
});
|
||||
let result = create_vector_engine_image_edit_with_references_and_model(
|
||||
http_client,
|
||||
&settings.provider_settings(),
|
||||
let provider_client = settings.client_for_model(model);
|
||||
let provider_settings = settings.provider_settings_for_model(model);
|
||||
let result = create_image_edit_with_references_and_model(
|
||||
provider_client.http_client(),
|
||||
&provider_settings,
|
||||
model,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
@@ -376,7 +397,7 @@ pub(crate) fn build_openai_image_request_body(
|
||||
candidate_count: u32,
|
||||
reference_images: &[String],
|
||||
) -> Value {
|
||||
build_vector_engine_image_request_body(
|
||||
build_image_request_body(
|
||||
prompt,
|
||||
negative_prompt,
|
||||
size,
|
||||
@@ -386,6 +407,24 @@ pub(crate) fn build_openai_image_request_body(
|
||||
}
|
||||
|
||||
impl OpenAiImageSettings {
|
||||
fn client_for_model(&self, model: &str) -> &ImageProviderClient {
|
||||
if model == NANOBANANA_2_MODEL {
|
||||
self.vector_engine_client
|
||||
.as_ref()
|
||||
.expect("vector engine image client is initialized at startup")
|
||||
} else {
|
||||
self.tiantoken_client
|
||||
.as_ref()
|
||||
.expect("tiantoken image client is initialized at startup")
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_settings_for_model(&self, model: &str) -> ImageProviderSettings {
|
||||
let mut settings = self.client_for_model(model).settings().clone();
|
||||
settings.request_deadline = self.request_deadline;
|
||||
settings
|
||||
}
|
||||
|
||||
pub(crate) fn with_external_api_audit_actor(
|
||||
mut self,
|
||||
user_id: Option<String>,
|
||||
@@ -409,8 +448,9 @@ impl OpenAiImageSettings {
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn provider_settings(&self) -> VectorEngineImageSettings {
|
||||
VectorEngineImageSettings {
|
||||
pub(crate) fn provider_settings(&self) -> ImageProviderSettings {
|
||||
ImageProviderSettings {
|
||||
provider: ImageProvider::Tiantoken,
|
||||
base_url: self.base_url.clone(),
|
||||
api_key: self.api_key.clone(),
|
||||
request_timeout_ms: self.request_timeout_ms.max(1),
|
||||
@@ -578,13 +618,13 @@ pub(crate) fn map_platform_image_error(error: PlatformImageError) -> AppError {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn vector_engine_images_generation_url_for_test(settings: &OpenAiImageSettings) -> String {
|
||||
vector_engine_images_generation_url(&settings.provider_settings())
|
||||
fn images_generation_url_for_test(settings: &OpenAiImageSettings) -> String {
|
||||
images_generation_url(&settings.provider_settings())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn vector_engine_images_edit_url_for_test(settings: &OpenAiImageSettings) -> String {
|
||||
vector_engine_images_edit_url(&settings.provider_settings())
|
||||
fn images_edit_url_for_test(settings: &OpenAiImageSettings) -> String {
|
||||
images_edit_url(&settings.provider_settings())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -611,6 +651,8 @@ mod tests {
|
||||
external_api_audit_user_id: None,
|
||||
external_api_audit_profile_id: None,
|
||||
external_api_audit_request_id: None,
|
||||
tiantoken_client: None,
|
||||
vector_engine_client: None,
|
||||
}
|
||||
.with_external_api_audit_context(&request_context, None, None);
|
||||
|
||||
@@ -631,7 +673,7 @@ mod tests {
|
||||
&["data:image/png;base64,abcd".to_string()],
|
||||
);
|
||||
|
||||
assert_eq!(body["model"], GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(body["model"], GPT_IMAGE_2_5_GENERATION_MODEL);
|
||||
assert_eq!(body["size"], "1536x1024");
|
||||
assert_eq!(body["n"], 2);
|
||||
assert!(body.get("official_fallback").is_none());
|
||||
@@ -650,6 +692,8 @@ mod tests {
|
||||
external_api_audit_user_id: None,
|
||||
external_api_audit_profile_id: None,
|
||||
external_api_audit_request_id: None,
|
||||
tiantoken_client: None,
|
||||
vector_engine_client: None,
|
||||
};
|
||||
let v1_settings = OpenAiImageSettings {
|
||||
base_url: "https://vector.example/v1".to_string(),
|
||||
@@ -660,14 +704,16 @@ mod tests {
|
||||
external_api_audit_user_id: None,
|
||||
external_api_audit_profile_id: None,
|
||||
external_api_audit_request_id: None,
|
||||
tiantoken_client: None,
|
||||
vector_engine_client: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
vector_engine_images_generation_url_for_test(&root_settings),
|
||||
images_generation_url_for_test(&root_settings),
|
||||
"https://vector.example/v1/images/generations"
|
||||
);
|
||||
assert_eq!(
|
||||
vector_engine_images_generation_url_for_test(&v1_settings),
|
||||
images_generation_url_for_test(&v1_settings),
|
||||
"https://vector.example/v1/images/generations"
|
||||
);
|
||||
}
|
||||
@@ -683,6 +729,8 @@ mod tests {
|
||||
external_api_audit_user_id: None,
|
||||
external_api_audit_profile_id: None,
|
||||
external_api_audit_request_id: None,
|
||||
tiantoken_client: None,
|
||||
vector_engine_client: None,
|
||||
};
|
||||
let v1_settings = OpenAiImageSettings {
|
||||
base_url: "https://vector.example/v1".to_string(),
|
||||
@@ -693,14 +741,16 @@ mod tests {
|
||||
external_api_audit_user_id: None,
|
||||
external_api_audit_profile_id: None,
|
||||
external_api_audit_request_id: None,
|
||||
tiantoken_client: None,
|
||||
vector_engine_client: None,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
vector_engine_images_edit_url_for_test(&root_settings),
|
||||
images_edit_url_for_test(&root_settings),
|
||||
"https://vector.example/v1/images/edits"
|
||||
);
|
||||
assert_eq!(
|
||||
vector_engine_images_edit_url_for_test(&v1_settings),
|
||||
images_edit_url_for_test(&v1_settings),
|
||||
"https://vector.example/v1/images/edits"
|
||||
);
|
||||
}
|
||||
@@ -716,6 +766,8 @@ mod tests {
|
||||
external_api_audit_user_id: None,
|
||||
external_api_audit_profile_id: None,
|
||||
external_api_audit_request_id: None,
|
||||
tiantoken_client: None,
|
||||
vector_engine_client: None,
|
||||
};
|
||||
let http_client = reqwest::Client::new();
|
||||
|
||||
@@ -764,7 +816,7 @@ mod tests {
|
||||
latency_ms: Some(321),
|
||||
prompt_chars: Some(42),
|
||||
reference_image_count: Some(1),
|
||||
image_model: Some(VECTOR_ENGINE_GPT_IMAGE_2_MODEL),
|
||||
image_model: Some(GPT_IMAGE_2_MODEL),
|
||||
};
|
||||
let tracking = crate::external_api_audit::build_external_api_failure_tracking_draft(
|
||||
&build_external_api_failure_draft_from_platform_image_audit(&audit),
|
||||
@@ -782,10 +834,7 @@ mod tests {
|
||||
assert_eq!(tracking.metadata["retryable"], true);
|
||||
assert_eq!(tracking.metadata["promptChars"], 42);
|
||||
assert_eq!(tracking.metadata["referenceImageCount"], 1);
|
||||
assert_eq!(
|
||||
tracking.metadata["imageModel"],
|
||||
VECTOR_ENGINE_GPT_IMAGE_2_MODEL
|
||||
);
|
||||
assert_eq!(tracking.metadata["imageModel"], GPT_IMAGE_2_MODEL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ use axum::{
|
||||
use bytes::Bytes;
|
||||
use image::{ImageDecoder, ImageFormat, ImageReader};
|
||||
use platform_image::{
|
||||
GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, RAW_IMAGE_MAX_EDGE, RAW_IMAGE_MAX_PIXELS,
|
||||
RawImageEditImage, RawImageEditOptions, create_vector_engine_raw_image_edit,
|
||||
GPT_IMAGE_2_2K_LONG_EDGE_THRESHOLD, GPT_IMAGE_2_5_EDIT_MODEL, RAW_IMAGE_MAX_EDGE,
|
||||
RAW_IMAGE_MAX_PIXELS, RawImageEditImage, RawImageEditOptions, create_raw_image_edit,
|
||||
validate_raw_image_edit_dimensions,
|
||||
};
|
||||
use serde::Serialize;
|
||||
@@ -148,7 +148,7 @@ pub(crate) async fn edit_raw_image(
|
||||
});
|
||||
let started_at_micros = (OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000) as i64;
|
||||
let operation = async move {
|
||||
let generated = match create_vector_engine_raw_image_edit(
|
||||
let generated = match create_raw_image_edit(
|
||||
&http_client,
|
||||
&provider_settings,
|
||||
prepared.prompt.as_str(),
|
||||
@@ -572,7 +572,7 @@ async fn raw_image_edit_price(state: &AppState, width: u32, height: u32) -> Resu
|
||||
.editor_generation_pricing()
|
||||
.await
|
||||
.map(|pricing| {
|
||||
pricing.image_generation_mud_points(Some("quick-edit"), Some("gpt-image-2"), Some(tier))
|
||||
pricing.image_edit_model_mud_points(Some(GPT_IMAGE_2_5_EDIT_MODEL), Some(tier))
|
||||
})
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||||
|
||||
@@ -23,6 +23,9 @@ use platform_auth::{
|
||||
RefreshCookieConfig, RefreshCookieError, RefreshCookieSameSite, SmsAuthConfig, SmsAuthProvider,
|
||||
SmsAuthProviderKind, SmsProviderError, WechatProvider, sign_access_token, verify_access_token,
|
||||
};
|
||||
use platform_image::{
|
||||
ImageProvider, ImageProviderClient, ImageProviderSettings, build_image_provider_client,
|
||||
};
|
||||
use platform_llm::{LlmClient, LlmConfig, LlmError, LlmProvider, OpenAiChatTokenBudgetField};
|
||||
use platform_matting::{MattingClient, MattingConfig};
|
||||
use platform_oss::{OssClient, OssConfig, OssError};
|
||||
@@ -306,6 +309,8 @@ pub struct AppStateInner {
|
||||
/// 非 Suno 的文本、图片和旧版音频生成 provider 配置。
|
||||
tiantoken_base_url: String,
|
||||
tiantoken_api_key: Option<String>,
|
||||
vector_engine_image_client: ImageProviderClient,
|
||||
tiantoken_image_client: ImageProviderClient,
|
||||
matting_client: Option<MattingClient>,
|
||||
bgfilter_provider_http_client: reqwest::Client,
|
||||
bgfilter_worker_http_client: reqwest::Client,
|
||||
@@ -605,6 +610,18 @@ impl AppState {
|
||||
.map_err(|error| AppStateInitError::DependencyUnavailable(error.to_string()))?;
|
||||
let tiantoken_base_url = crate::config::tiantoken_base_url(&config);
|
||||
let tiantoken_api_key = crate::config::tiantoken_api_key(&config);
|
||||
let vector_engine_image_client = build_required_image_provider_client(
|
||||
ImageProvider::VectorEngine,
|
||||
config.vector_engine_base_url.clone(),
|
||||
config.vector_engine_api_key.clone(),
|
||||
config.vector_engine_image_request_timeout_ms,
|
||||
)?;
|
||||
let tiantoken_image_client = build_required_image_provider_client(
|
||||
ImageProvider::Tiantoken,
|
||||
tiantoken_base_url.clone(),
|
||||
tiantoken_api_key.clone(),
|
||||
config.vector_engine_image_request_timeout_ms,
|
||||
)?;
|
||||
let llm_client = build_llm_client(&config)?;
|
||||
let vector_engine_llm_client = build_vector_engine_llm_client(
|
||||
&config,
|
||||
@@ -688,6 +705,8 @@ impl AppState {
|
||||
vector_engine_llm_client,
|
||||
tiantoken_base_url,
|
||||
tiantoken_api_key,
|
||||
vector_engine_image_client,
|
||||
tiantoken_image_client,
|
||||
matting_client,
|
||||
bgfilter_provider_http_client,
|
||||
bgfilter_worker_http_client,
|
||||
@@ -1598,6 +1617,14 @@ impl AppState {
|
||||
self.tiantoken_api_key.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn vector_engine_image_client(&self) -> &ImageProviderClient {
|
||||
&self.vector_engine_image_client
|
||||
}
|
||||
|
||||
pub(crate) fn tiantoken_image_client(&self) -> &ImageProviderClient {
|
||||
&self.tiantoken_image_client
|
||||
}
|
||||
|
||||
pub fn matting_client(&self) -> Option<&MattingClient> {
|
||||
self.matting_client.as_ref()
|
||||
}
|
||||
@@ -2309,6 +2336,50 @@ impl AdminRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_required_image_provider_client(
|
||||
provider: ImageProvider,
|
||||
base_url: String,
|
||||
api_key: Option<String>,
|
||||
request_timeout_ms: u64,
|
||||
) -> Result<ImageProviderClient, AppStateInitError> {
|
||||
#[cfg(test)]
|
||||
let (base_url, api_key) = (
|
||||
if base_url.trim().is_empty() {
|
||||
"http://127.0.0.1".to_string()
|
||||
} else {
|
||||
base_url
|
||||
},
|
||||
api_key.or_else(|| Some("test-key".to_string())),
|
||||
);
|
||||
#[cfg(not(test))]
|
||||
let (base_url, api_key) = (base_url, api_key);
|
||||
let base_url = base_url.trim().trim_end_matches('/');
|
||||
if base_url.is_empty() {
|
||||
return Err(AppStateInitError::DependencyUnavailable(format!(
|
||||
"{} 图片 provider 缺少 BASE_URL 配置",
|
||||
provider.as_str()
|
||||
)));
|
||||
}
|
||||
let api_key = api_key
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
AppStateInitError::DependencyUnavailable(format!(
|
||||
"{} 图片 provider 缺少 API_KEY 配置",
|
||||
provider.as_str()
|
||||
))
|
||||
})?;
|
||||
build_image_provider_client(ImageProviderSettings {
|
||||
provider,
|
||||
base_url: base_url.to_string(),
|
||||
api_key: api_key.to_string(),
|
||||
request_timeout_ms: request_timeout_ms.max(1),
|
||||
request_deadline: None,
|
||||
})
|
||||
.map_err(|error| AppStateInitError::DependencyUnavailable(error.to_string()))
|
||||
}
|
||||
|
||||
fn build_oss_client(config: &AppConfig) -> Result<Option<OssClient>, AppStateInitError> {
|
||||
let oss_fields = [
|
||||
("ALIYUN_OSS_BUCKET", config.oss_bucket.as_deref()),
|
||||
|
||||
@@ -2,7 +2,7 @@ use crate::agent::asset::ImageId;
|
||||
use crate::agent::prompt::{PENDING_USER_CONFIRMATION_MESSAGE, edit_image_tool_description};
|
||||
use crate::agent::tools::context::EditorToolContext;
|
||||
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use platform_image::GPT_IMAGE_2_MODEL;
|
||||
use platform_image::GPT_IMAGE_2_5_BUSINESS_NAME;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use std::error::Error;
|
||||
@@ -33,7 +33,7 @@ impl Display for EditImageError {
|
||||
EditImageError::InvalidModel(model) => {
|
||||
write!(
|
||||
f,
|
||||
"{model} is not a valid model name, only {GPT_IMAGE_2_MODEL} is supported for now."
|
||||
"{model} is not a valid model name, only {GPT_IMAGE_2_5_BUSINESS_NAME} is supported for now."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ pub struct EditImageToolArgs {
|
||||
pub model: String,
|
||||
}
|
||||
fn default_model_name() -> String {
|
||||
GPT_IMAGE_2_MODEL.to_string()
|
||||
GPT_IMAGE_2_5_BUSINESS_NAME.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -90,9 +90,9 @@ impl Tool for EditImageTool {
|
||||
// TODO need to introduce size param, but that needs more metadata such as original image size, skip in this version
|
||||
"model": {
|
||||
"type": "string",
|
||||
"enum": [GPT_IMAGE_2_MODEL],
|
||||
"default": GPT_IMAGE_2_MODEL,
|
||||
"description": format!("图片编辑固定使用{GPT_IMAGE_2_MODEL}")
|
||||
"enum": [GPT_IMAGE_2_5_BUSINESS_NAME],
|
||||
"default": GPT_IMAGE_2_5_BUSINESS_NAME,
|
||||
"description": format!("图片编辑固定使用{GPT_IMAGE_2_5_BUSINESS_NAME}")
|
||||
}
|
||||
},
|
||||
"required": ["object_image_id", "prompt"],
|
||||
@@ -155,7 +155,7 @@ pub struct EditorImageEditResult {
|
||||
impl EditImageTool {
|
||||
/// Validate the semantic correctness of the arguments.
|
||||
pub fn validate_args(&self, args: &EditImageToolArgs) -> Option<EditImageError> {
|
||||
if args.model != GPT_IMAGE_2_MODEL {
|
||||
if args.model != GPT_IMAGE_2_5_BUSINESS_NAME {
|
||||
return Some(EditImageError::InvalidModel(args.model.clone()));
|
||||
}
|
||||
if args.prompt.trim().is_empty() {
|
||||
@@ -210,7 +210,7 @@ mod tests {
|
||||
"sourceType": "generated",
|
||||
"prompt": "修改图片",
|
||||
"actualPrompt": "修改后的图片",
|
||||
"model": "gpt-image-2",
|
||||
"model": "gpt-image-2.5",
|
||||
"taskId": "task-2",
|
||||
"resource": null,
|
||||
"asset": null,
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::agent::tools::image_generation_options::{
|
||||
validate_image_generation_options,
|
||||
};
|
||||
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL};
|
||||
use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, NANOBANANA_2_MODEL};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use std::error::Error;
|
||||
@@ -39,7 +39,7 @@ impl Display for GenerateIconSpritesheetError {
|
||||
match self {
|
||||
Self::InvalidModel(model) => write!(
|
||||
f,
|
||||
"{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_MODEL}"
|
||||
"{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_5_BUSINESS_NAME}"
|
||||
),
|
||||
Self::InvalidAspectRatio(aspect_ratio) => {
|
||||
write!(f, "invalid aspect ratio: {aspect_ratio}")
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::agent::tools::image_generation_options::{
|
||||
validate_image_generation_options,
|
||||
};
|
||||
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL};
|
||||
use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, NANOBANANA_2_MODEL};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use std::error::Error;
|
||||
@@ -33,11 +33,11 @@ impl Display for GenerateImageError {
|
||||
match self {
|
||||
Self::InvalidModel(model) => write!(
|
||||
f,
|
||||
"{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_MODEL}"
|
||||
"{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_5_BUSINESS_NAME}"
|
||||
),
|
||||
Self::UnsupportedUiDesignModel(model) => write!(
|
||||
f,
|
||||
"{model} is not supported for UI design generation; required model: {GPT_IMAGE_2_MODEL}"
|
||||
"{model} is not supported for UI design generation; required model: {GPT_IMAGE_2_5_BUSINESS_NAME}"
|
||||
),
|
||||
Self::InvalidAspectRatio(aspect_ratio) => {
|
||||
write!(f, "invalid aspect ratio: {aspect_ratio}")
|
||||
@@ -222,7 +222,7 @@ mod tests {
|
||||
"sourceType": "generated",
|
||||
"prompt": "生成一张图片",
|
||||
"actualPrompt": "生成一张清晰图片",
|
||||
"model": "gpt-image-2",
|
||||
"model": "gpt-image-2.5",
|
||||
"taskId": "task-1",
|
||||
"resource": null,
|
||||
"asset": null,
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::agent::tools::image_generation_options::{
|
||||
};
|
||||
use crate::framework::tool::ToolFailureKind;
|
||||
use crate::framework::tool::{Tool, ToolFailure};
|
||||
use platform_image::GPT_IMAGE_2_MODEL;
|
||||
use platform_image::GPT_IMAGE_2_5_BUSINESS_NAME;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -34,7 +34,7 @@ pub struct GenerateUiDesignToolArgs {
|
||||
}
|
||||
|
||||
fn default_ui_design_model() -> String {
|
||||
GPT_IMAGE_2_MODEL.to_string()
|
||||
GPT_IMAGE_2_5_BUSINESS_NAME.to_string()
|
||||
}
|
||||
|
||||
impl Tool for GenerateUiDesignTool {
|
||||
@@ -54,9 +54,9 @@ impl Tool for GenerateUiDesignTool {
|
||||
"prompt": { "type": "string", "description": "完整 UI 画面、信息层级、视觉风格和构图描述。" },
|
||||
"model": {
|
||||
"type": "string",
|
||||
"enum": [GPT_IMAGE_2_MODEL],
|
||||
"default": GPT_IMAGE_2_MODEL,
|
||||
"description": "UI 设计图固定使用 gpt-image-2。"
|
||||
"enum": [GPT_IMAGE_2_5_BUSINESS_NAME],
|
||||
"default": GPT_IMAGE_2_5_BUSINESS_NAME,
|
||||
"description": "UI 设计图固定使用 gpt-image-2.5。"
|
||||
},
|
||||
"reference_image_ids": { "type": "array", "items": { "type": "string" }, "description": "image_id(s) for desc UI 风格或布局" },
|
||||
"aspect_ratio": image_aspect_ratio_parameter_schema(),
|
||||
@@ -95,7 +95,7 @@ impl Tool for GenerateUiDesignTool {
|
||||
|
||||
impl GenerateUiDesignTool {
|
||||
pub fn validate_args(&self, args: &GenerateUiDesignToolArgs) -> Result<(), GenerateImageError> {
|
||||
if args.model != GPT_IMAGE_2_MODEL {
|
||||
if args.model != GPT_IMAGE_2_5_BUSINESS_NAME {
|
||||
return Err(GenerateImageError::UnsupportedUiDesignModel(
|
||||
args.model.clone(),
|
||||
));
|
||||
@@ -165,11 +165,11 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
parameters["properties"]["model"]["enum"],
|
||||
json!([GPT_IMAGE_2_MODEL])
|
||||
json!([GPT_IMAGE_2_5_BUSINESS_NAME])
|
||||
);
|
||||
assert_eq!(
|
||||
parameters["properties"]["model"]["default"],
|
||||
GPT_IMAGE_2_MODEL
|
||||
GPT_IMAGE_2_5_BUSINESS_NAME
|
||||
);
|
||||
assert_eq!(
|
||||
parameters["properties"]["image_size"]["enum"],
|
||||
@@ -184,7 +184,10 @@ mod tests {
|
||||
.as_array()
|
||||
.is_some_and(|required| required.contains(&json!("model")))
|
||||
);
|
||||
assert!(tool.validate_args(&args(GPT_IMAGE_2_MODEL)).is_ok());
|
||||
assert!(
|
||||
tool.validate_args(&args(GPT_IMAGE_2_5_BUSINESS_NAME))
|
||||
.is_ok()
|
||||
);
|
||||
assert!(matches!(
|
||||
tool.validate_args(&args(NANOBANANA_2_MODEL)),
|
||||
Err(GenerateImageError::UnsupportedUiDesignModel(model)) if model == NANOBANANA_2_MODEL
|
||||
@@ -198,7 +201,7 @@ mod tests {
|
||||
}))
|
||||
.expect("旧版 UI 设计参数应能反序列化");
|
||||
|
||||
assert_eq!(args.model, GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(args.model, GPT_IMAGE_2_5_BUSINESS_NAME);
|
||||
assert!(tool().validate_args(&args).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
+16
-12
@@ -1,4 +1,4 @@
|
||||
use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL};
|
||||
use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, NANOBANANA_2_MODEL};
|
||||
use serde_json::{Value, json};
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
@@ -21,7 +21,7 @@ impl Display for ImageGenerationOptionsError {
|
||||
match self {
|
||||
Self::InvalidModel(model) => write!(
|
||||
f,
|
||||
"{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_MODEL}"
|
||||
"{model} is not a valid image model; supported models: {NANOBANANA_2_MODEL}, {GPT_IMAGE_2_5_BUSINESS_NAME}"
|
||||
),
|
||||
Self::InvalidAspectRatio(aspect_ratio) => write!(
|
||||
f,
|
||||
@@ -75,9 +75,9 @@ pub fn validate_image_generation_options(
|
||||
pub fn image_model_parameter_schema() -> Value {
|
||||
json!({
|
||||
"type": "string",
|
||||
"enum": [NANOBANANA_2_MODEL, GPT_IMAGE_2_MODEL],
|
||||
"enum": [NANOBANANA_2_MODEL, GPT_IMAGE_2_5_BUSINESS_NAME],
|
||||
"default": NANOBANANA_2_MODEL,
|
||||
"description": "生图模型。默认 gemini-3.1-flash-image-preview(user may call it nanobanana2);也可选择 gpt-image-2。"
|
||||
"description": "生图模型。默认 gemini-3.1-flash-image-preview(user may call it nanobanana2);也可选择 gpt-image-2.5。"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ pub fn image_size_parameter_schema() -> Value {
|
||||
"type": "string",
|
||||
"enum": NANOBANANA_2_IMAGE_SIZES,
|
||||
"default": DEFAULT_IMAGE_SIZE,
|
||||
"description": "图片尺寸档位。nanobanana2 支持 0.5K、1K、2K;gpt-image-2 仅支持 1K、2K;默认 1K。"
|
||||
"description": "图片尺寸档位。nanobanana2 支持 0.5K、1K、2K;gpt-image-2.5 仅支持 1K、2K;默认 1K。"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,14 +104,14 @@ pub fn gpt_image_2_size_parameter_schema() -> Value {
|
||||
"type": "string",
|
||||
"enum": GPT_IMAGE_2_IMAGE_SIZES,
|
||||
"default": DEFAULT_IMAGE_SIZE,
|
||||
"description": "图片尺寸档位。gpt-image-2 仅支持 1K、2K;默认 1K。"
|
||||
"description": "图片尺寸档位。gpt-image-2.5 仅支持 1K、2K;默认 1K。"
|
||||
})
|
||||
}
|
||||
|
||||
pub fn image_model_size_constraint_schema() -> Value {
|
||||
json!({
|
||||
"if": {
|
||||
"properties": { "model": { "const": GPT_IMAGE_2_MODEL } },
|
||||
"properties": { "model": { "const": GPT_IMAGE_2_5_BUSINESS_NAME } },
|
||||
// model 省略时运行时默认 nanobanana2,仍允许 0.5K。
|
||||
"required": ["model"]
|
||||
},
|
||||
@@ -126,7 +126,7 @@ pub fn image_model_size_constraint_schema() -> Value {
|
||||
fn supported_image_sizes(model: &str) -> Option<&'static [&'static str]> {
|
||||
match model {
|
||||
NANOBANANA_2_MODEL => Some(NANOBANANA_2_IMAGE_SIZES),
|
||||
GPT_IMAGE_2_MODEL => Some(GPT_IMAGE_2_IMAGE_SIZES),
|
||||
GPT_IMAGE_2_5_BUSINESS_NAME => Some(GPT_IMAGE_2_IMAGE_SIZES),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -150,14 +150,18 @@ mod tests {
|
||||
}
|
||||
for image_size in GPT_IMAGE_2_IMAGE_SIZES {
|
||||
assert!(
|
||||
validate_image_generation_options(GPT_IMAGE_2_MODEL, aspect_ratio, image_size,)
|
||||
.is_ok()
|
||||
validate_image_generation_options(
|
||||
GPT_IMAGE_2_5_BUSINESS_NAME,
|
||||
aspect_ratio,
|
||||
image_size,
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(matches!(
|
||||
validate_image_generation_options(GPT_IMAGE_2_MODEL, "1:1", "0.5K"),
|
||||
validate_image_generation_options(GPT_IMAGE_2_5_BUSINESS_NAME, "1:1", "0.5K"),
|
||||
Err(ImageGenerationOptionsError::InvalidImageSize { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
@@ -192,7 +196,7 @@ mod tests {
|
||||
let model_size_constraint = image_model_size_constraint_schema();
|
||||
assert_eq!(
|
||||
model_size_constraint["if"]["properties"]["model"]["const"],
|
||||
GPT_IMAGE_2_MODEL
|
||||
GPT_IMAGE_2_5_BUSINESS_NAME
|
||||
);
|
||||
assert_eq!(model_size_constraint["if"]["required"], json!(["model"]));
|
||||
assert_eq!(
|
||||
|
||||
@@ -26,7 +26,7 @@ mod tests {
|
||||
use super::generate_video::{GenerateVideoTool, GenerateVideoToolArgs};
|
||||
use crate::framework::tool::Tool;
|
||||
use platform_audio::{ELEVENLABS_SOUND_EFFECT_MODEL, SUNO_DEFAULT_MODEL};
|
||||
use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL};
|
||||
use platform_image::{GPT_IMAGE_2_5_BUSINESS_NAME, NANOBANANA_2_MODEL};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
@@ -69,11 +69,11 @@ mod tests {
|
||||
assert_eq!(image.model, NANOBANANA_2_MODEL);
|
||||
assert_eq!(image.aspect_ratio, "1:1");
|
||||
assert_eq!(image.image_size, "1K");
|
||||
assert_eq!(edit.model, GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(edit.model, GPT_IMAGE_2_5_BUSINESS_NAME);
|
||||
assert_eq!(character.model, NANOBANANA_2_MODEL);
|
||||
assert_eq!(character.aspect_ratio, "1:1");
|
||||
assert_eq!(character.image_size, "1K");
|
||||
assert_eq!(ui_design.model, GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(ui_design.model, GPT_IMAGE_2_5_BUSINESS_NAME);
|
||||
assert_eq!(ui_design.aspect_ratio, "1:1");
|
||||
assert_eq!(ui_design.image_size, "1K");
|
||||
assert_eq!(icon.model, NANOBANANA_2_MODEL);
|
||||
@@ -133,14 +133,17 @@ mod tests {
|
||||
};
|
||||
|
||||
assert!(tool.validate_args(&args(NANOBANANA_2_MODEL)).is_ok());
|
||||
assert!(tool.validate_args(&args(GPT_IMAGE_2_MODEL)).is_ok());
|
||||
assert!(
|
||||
tool.validate_args(&args(GPT_IMAGE_2_5_BUSINESS_NAME))
|
||||
.is_ok()
|
||||
);
|
||||
assert!(matches!(
|
||||
tool.validate_args(&args("unknown-image-model")),
|
||||
Err(GenerateImageError::InvalidModel(_))
|
||||
));
|
||||
assert_eq!(
|
||||
tool.parameters()["properties"]["model"]["enum"],
|
||||
json!([NANOBANANA_2_MODEL, GPT_IMAGE_2_MODEL])
|
||||
json!([NANOBANANA_2_MODEL, GPT_IMAGE_2_5_BUSINESS_NAME])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,7 +169,7 @@ mod tests {
|
||||
));
|
||||
let ui_args = GenerateUiDesignToolArgs {
|
||||
prompt: "生成游戏主界面".to_string(),
|
||||
model: GPT_IMAGE_2_MODEL.to_string(),
|
||||
model: GPT_IMAGE_2_5_BUSINESS_NAME.to_string(),
|
||||
reference_image_ids: vec![missing_image.clone()],
|
||||
aspect_ratio: "16:9".to_string(),
|
||||
image_size: "1K".to_string(),
|
||||
@@ -208,7 +211,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
edit["properties"]["model"]["enum"],
|
||||
json!([GPT_IMAGE_2_MODEL])
|
||||
json!([GPT_IMAGE_2_5_BUSINESS_NAME])
|
||||
);
|
||||
assert_eq!(
|
||||
video["properties"]["duration_seconds"]["enum"],
|
||||
@@ -229,7 +232,7 @@ mod tests {
|
||||
for schema in [&image, &character, &icon] {
|
||||
assert_eq!(
|
||||
schema["allOf"][0]["if"]["properties"]["model"]["const"],
|
||||
json!(GPT_IMAGE_2_MODEL)
|
||||
json!(GPT_IMAGE_2_5_BUSINESS_NAME)
|
||||
);
|
||||
assert_eq!(
|
||||
schema["allOf"][0]["then"]["properties"]["image_size"]["enum"],
|
||||
|
||||
+7
-1
@@ -1,8 +1,14 @@
|
||||
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.
|
||||
pub const GPT_IMAGE_2_5_GENERATION_MODEL: &str = "gpt-image-2.5-flare-c";
|
||||
/// Provider/pricing key for explicit image-edit tasks.
|
||||
pub const GPT_IMAGE_2_5_EDIT_MODEL: &str = "gpt-image-2.5-sunburst-c";
|
||||
pub const NANOBANANA_2_MODEL: &str = "gemini-3.1-flash-image-preview";
|
||||
pub const VECTOR_ENGINE_GPT_IMAGE_2_MODEL: &str = GPT_IMAGE_2_MODEL;
|
||||
pub const VECTOR_ENGINE_PROVIDER: &str = "vector-engine";
|
||||
pub const TIANTOKEN_PROVIDER: &str = "tiantoken";
|
||||
pub const VECTOR_ENGINE_IMAGE_EDIT_MAX_REFERENCE_IMAGES: usize = 5;
|
||||
pub const VECTOR_ENGINE_NANOBANANA_MAX_REFERENCE_IMAGES: usize = 14;
|
||||
pub const GPT_IMAGE_2_MIN_PIXELS: u64 = 655_360;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user