From c0d4e76a01bf2707dad73de1f433e99a43df5ec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 22 Sep 2026 14:08:09 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E8=80=A6=20Tiantoken=20=E4=B8=8E=20Ve?= =?UTF-8?q?ctorEngine=20=E7=9A=84=E5=9B=BE=E7=89=87=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E8=B6=85=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppConfig 新增 tiantoken_image_request_timeout_ms(默认 1000000),env 只读 TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS - VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS 只写 vector_engine_image_request_timeout_ms,不再被 Tiantoken 变量覆盖 - Tiantoken 图片 client 与 OpenAiImageSettings.request_timeout_ms 改用新字段,两个 provider 各用自己的超时 - config 测试补 Tiantoken / VectorEngine 双向不覆盖断言,deploy env 示例补 VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000 - 同步开发运维文档与 decision-log --- deploy/env/api-server.env.example | 2 ++ .../shared-memory/decision-log.md | 8 +++++ ...发运维】本地开发验证与生产运维-2026-05-15.md | 2 +- server-rs/crates/api-server/src/config.rs | 32 +++++++++++++++---- .../api-server/src/openai_image_generation.rs | 2 +- server-rs/crates/api-server/src/state.rs | 2 +- 6 files changed, 39 insertions(+), 9 deletions(-) diff --git a/deploy/env/api-server.env.example b/deploy/env/api-server.env.example index c97ad913c..f67678de4 100644 --- a/deploy/env/api-server.env.example +++ b/deploy/env/api-server.env.example @@ -89,9 +89,11 @@ GENARRATIVE_LLM_ROUTER_API_KEY_ENCRYPTION_SECRET_FILE=/etc/genarrative/secrets/l GENARRATIVE_LLM_ROUTER_ADMIN_TOKEN_FILE=/etc/genarrative/secrets/llm-router-admin.token TIANTOKEN_BASE_URL=https://api.tiantoken.com TIANTOKEN_API_KEY= +# 两个图片 provider 的超时互相独立:TIANTOKEN_* 只作用于 Tiantoken,VECTOR_ENGINE_* 只作用于 VectorEngine。 TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS=1000000 VECTOR_ENGINE_BASE_URL=https://api.vectorengine.cn VECTOR_ENGINE_API_KEY= +VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000 VECTOR_ENGINE_AUDIO_REQUEST_TIMEOUT_MS=180000 ELEVENLABS_BASE_URL=https://api.elevenlabs.io ELEVENLABS_API_KEY= diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index cc16df83d..d0a851eef 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,13 @@ # 决策记录 +## 2026-09-22 图片 provider 单次 attempt 超时解耦 + +- 背景:`config.rs` 把 `TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS`(回退 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS`)统一写进 `vector_engine_image_request_timeout_ms`,`state.rs` 又用这一个字段同时构造 Tiantoken 与 VectorEngine 两个图片 client,`OpenAiImageSettings` 也取同一个值;结果是两个 provider 无法各自设置单次 attempt 超时,配置 Tiantoken 变量会顺带改写 VectorEngine 的 deadline(反之亦然)。 +- 决策:`AppConfig` 新增 `tiantoken_image_request_timeout_ms`(默认 `1000000`,env 只读 `TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS`);`VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 只写 `vector_engine_image_request_timeout_ms`(默认同样 `1000000`)。Tiantoken 图片 client 与 `OpenAiImageSettings.request_timeout_ms` 改用新字段,VectorEngine client 保持原字段,两边都不再从对方的环境变量取值。 +- 边界:单次 attempt 语义不变(仍受 worker 绝对 job deadline 约束),默认值不变,因此现网只配置 `TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS=1000000` 的行为与之前一致;`deploy/env/api-server.env.example` 同步补上 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS=1000000`,让两个 provider 的取值都显式可见。不修改 nanobanana / Suno 链路、扣费、审计或重试策略。 +- 影响范围:`server-rs/crates/api-server/src/{config.rs,state.rs,openai_image_generation.rs}`、`deploy/env/api-server.env.example`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 +- 验证:`cargo test -p api-server config::tests::from_env_reads_non_public_models_and_urls`(新增 Tiantoken / VectorEngine 双向不覆盖断言)、`cargo check -p api-server`。 + ## 2026-09-21 生成带参考图统一按编辑 concrete model 执行与计价 - 背景:GPT Image 2.5 迁移后,生成端在带参考图时会改走 `/v1/images/edits` 并提交 `gpt-image-2.5-sunburst-c`,但计价仍按 `gpt-image-2.5-flare-c` 生成档,dispatch 判断也分散在三个入口各自的 `if 带参考图` 表达式里;结果是「生成档扣费 + 编辑档执行/审计」分裂,admin 单独调价任一档位都会错价,ADR 里「普通生成即使因参考图使用 edits multipart,仍按生成 concrete model」与代码事实不符。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 1d62fffd9..27589193a 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -238,7 +238,7 @@ spacetime sql "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 && spacetime version use `,升级后重启 `npm run dev:spacetime` 再重试。当前 `scripts/dev.mjs` 会把 tool version 和 commit 一起写入 `dev-spacetime-tool-version`,启动新 standalone 与复用已有本地进程时都要求 `2.8.3 + 8e410d28...` 同时匹配;旧版本或旧单行版本记录会拒绝复用并要求重启。2.6.1 修复了 procedure context 中调用者 `Identity` / `ConnectionId` 始终为空的回归,依赖 `ctx.sender` 鉴权时必须同时确认宿主已升级。 -本地 `.env`、`.env.local` 或 `.env.secrets.local` 修改后必须重启 `api-server` 才会生效;若已经通过 `npm run dev` 启动完整联调,可在该终端输入 `rs api-server`。排查图片编辑器 Tiantoken 生成链路时,确认 `TIANTOKEN_BASE_URL`、`TIANTOKEN_API_KEY` 和 `TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS` 只在本地或服务器密钥文件中配置,不能写入 Git;同时确认 VectorEngine 的 `VECTOR_ENGINE_BASE_URL` / `VECTOR_ENGINE_API_KEY` 已配置,因为两个图片 client 都在 api-server 启动时构造,任一缺失都会阻止启动。VectorEngine 配置仍保留给 nanobanana 与 Suno 音乐任务。`TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS` 是单次 attempt 的配置上限,默认 `1000000`;配置加载层允许显式值低于该默认值,不再在读取环境变量时强制抬高。新生成任务使用业务模型 `gpt-image-2.5`,api-server 显式分派 `gpt-image-2.5-flare-c`(不带参考图的 generate)或 `gpt-image-2.5-sunburst-c`(edit 与带参考图的 generate,计价同步走编辑档);已持久化的 `gpt-image-2` 只在提交边界按兼容规则解析,不改写历史值,也不回退到旧 GPT Image 2 路由;已退役的 `gpt-image-2-c` 已从代码整体删除,传入即按不支持的值处理。图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image`,`api-server` 只做编辑器请求编排、OSS / asset 持久化、计费和失败审计落库。`platform-image` 会在 JSON 生成和 multipart 编辑请求发送前按同一 GPT-image family 规则归一显式像素尺寸;若请求发送失败,先按同一 `request_id` 查看 provider 日志与 `external_api_call_failure.metadata_json.errorSource`,当前 multipart `/v1/images/edits` 单独强制 HTTP/1.1。 +本地 `.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` 与 `VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS` 是两条互相独立、默认都为 `1000000` 的单次 attempt 上限:前者只作用于 Tiantoken 图片 client(GPT Image 2.5 生成 / 编辑),后者只作用于 VectorEngine 图片 client(nanobanana 等),任一 provider 的超时设置都不会改写另一个;配置加载层允许显式值低于默认值,不再在读取环境变量时强制抬高。新生成任务使用业务模型 `gpt-image-2.5`,api-server 显式分派 `gpt-image-2.5-flare-c`(不带参考图的 generate)或 `gpt-image-2.5-sunburst-c`(edit 与带参考图的 generate,计价同步走编辑档);已持久化的 `gpt-image-2` 只在提交边界按兼容规则解析,不改写历史值,也不回退到旧 GPT Image 2 路由;已退役的 `gpt-image-2-c` 已从代码整体删除,传入即按不支持的值处理。图片协议、URL / base64 响应解析、远端图片下载和 provider 侧结构化日志在 `server-rs/crates/platform-image`,`api-server` 只做编辑器请求编排、OSS / asset 持久化、计费和失败审计落库。`platform-image` 会在 JSON 生成和 multipart 编辑请求发送前按同一 GPT-image family 规则归一显式像素尺寸;若请求发送失败,先按同一 `request_id` 查看 provider 日志与 `external_api_call_failure.metadata_json.errorSource`,当前 multipart `/v1/images/edits` 单独强制 HTTP/1.1。 编辑器 ElevenLabs 音效生成只从服务端读取 `ELEVENLABS_BASE_URL`、`ELEVENLABS_API_KEY` 和 `ELEVENLABS_REQUEST_TIMEOUT_MS`,timeout 默认 `180000ms`;base URL 或 Key 缺失时失败关闭,不回退 Vidu。生产 API 与 external-generation worker 通过共享 API env 取得同一配置,模板见 `deploy/env/api-server.env.example`;Key 不得进入 Web/Vite 环境、命令参数、日志、fixture 或仓库。普通测试只使用 loopback mock,禁止把真实付费请求作为 T3 自动验收。 diff --git a/server-rs/crates/api-server/src/config.rs b/server-rs/crates/api-server/src/config.rs index 43c21ac01..90bfca4d2 100644 --- a/server-rs/crates/api-server/src/config.rs +++ b/server-rs/crates/api-server/src/config.rs @@ -18,6 +18,7 @@ const DEFAULT_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS: u64 = 600; const DEFAULT_EXTERNAL_GENERATION_WORKER_JOB_TIMEOUT_SECONDS: u64 = 900; const DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS: u64 = 1_800; pub(crate) const DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS: u64 = 1_000_000; +pub(crate) const DEFAULT_TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS: u64 = 1_000_000; pub(crate) const DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS: u64 = 180_000; const DEFAULT_EDITOR_BGFILTER_BASE_URL: &str = "http://58.87.105.82/bgfilter"; const DEFAULT_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS: u64 = 5_000; @@ -225,6 +226,12 @@ pub struct AppConfig { /// 启动时落到 `AppConfig` 字段,使 `AppState` 构造与测试构造复用同一条路径。 pub tiantoken_base_url: String, pub tiantoken_api_key: Option, + /// Tiantoken 图片链路(GPT Image 2.5 生成 / 编辑)的单次 attempt 超时。 + /// + /// 与 `vector_engine_image_request_timeout_ms` 相互独立,只由 + /// `TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS` 配置,避免一个 provider 的超时设置 + /// 顺带改写另一个 provider 的 deadline。 + pub tiantoken_image_request_timeout_ms: u64, pub elevenlabs_base_url: String, pub elevenlabs_api_key: Option, pub elevenlabs_request_timeout_ms: u64, @@ -541,6 +548,7 @@ impl Default for AppConfig { vector_engine_audio_request_timeout_ms: 180_000, tiantoken_base_url: String::new(), tiantoken_api_key: None, + tiantoken_image_request_timeout_ms: DEFAULT_TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS, elevenlabs_base_url: String::new(), elevenlabs_api_key: None, elevenlabs_request_timeout_ms: DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS, @@ -1338,18 +1346,25 @@ impl AppConfig { config.vector_engine_api_key = read_first_non_empty_env(&["VECTOR_ENGINE_API_KEY"]); + if let Some(vector_engine_image_request_timeout_ms) = + read_first_positive_u64_env(&["VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS"]) + { + // 单次 attempt 上限允许按环境收短;worker 调用还会受整次任务的绝对 deadline 约束。 + config.vector_engine_image_request_timeout_ms = vector_engine_image_request_timeout_ms; + } + // Tiantoken 只读取自己的环境变量,不从 VectorEngine 配置回退;启动时冻结进 AppConfig, // 供 AppState 构造编辑器 LLM / 图片 client 时复用同一条配置路径。 config.tiantoken_base_url = read_first_non_empty_env(&["TIANTOKEN_BASE_URL"]).unwrap_or_default(); config.tiantoken_api_key = read_first_non_empty_env(&["TIANTOKEN_API_KEY"]); - if let Some(tiantoken_image_request_timeout_ms) = read_first_positive_u64_env(&[ - "TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS", - "VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS", - ]) { - // 单次 attempt 上限允许按环境收短;worker 调用还会受整次任务的绝对 deadline 约束。 - config.vector_engine_image_request_timeout_ms = tiantoken_image_request_timeout_ms; + if let Some(tiantoken_image_request_timeout_ms) = + read_first_positive_u64_env(&["TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS"]) + { + // Tiantoken 图片链路独立计时:`TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS` 不再改写 + // VectorEngine 的图片超时,两个 provider 各用自己的配置与默认值。 + config.tiantoken_image_request_timeout_ms = tiantoken_image_request_timeout_ms; } if let Some(vector_engine_audio_request_timeout_ms) = @@ -2288,6 +2303,7 @@ mod tests { std::env::remove_var("VECTOR_ENGINE_BASE_URL"); std::env::remove_var("VECTOR_ENGINE_API_KEY"); std::env::remove_var("VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS"); + std::env::remove_var("TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS"); std::env::remove_var("HYPER3D_BASE_URL"); std::env::remove_var("DASHSCOPE_SCENE_IMAGE_MODEL"); std::env::remove_var("DASHSCOPE_REFERENCE_IMAGE_MODEL"); @@ -2306,6 +2322,7 @@ mod tests { std::env::set_var("VECTOR_ENGINE_BASE_URL", "https://vector.internal.example"); std::env::set_var("VECTOR_ENGINE_API_KEY", "vector-engine-key"); std::env::set_var("VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS", "210000"); + std::env::set_var("TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS", "180000"); std::env::set_var("HYPER3D_BASE_URL", "https://model.internal.example/api/v2"); std::env::set_var("DASHSCOPE_SCENE_IMAGE_MODEL", "scene-model"); std::env::set_var("DASHSCOPE_REFERENCE_IMAGE_MODEL", "reference-model"); @@ -2332,6 +2349,8 @@ mod tests { "https://vector.internal.example" ); assert_eq!(config.vector_engine_image_request_timeout_ms, 210_000); + // 两个 provider 的图片超时互不覆盖:Tiantoken 用 TIANTOKEN_*,VectorEngine 用 VECTOR_ENGINE_*。 + assert_eq!(config.tiantoken_image_request_timeout_ms, 180_000); assert_eq!( config.hyper3d_base_url, "https://model.internal.example/api/v2" @@ -2357,6 +2376,7 @@ mod tests { std::env::remove_var("VECTOR_ENGINE_BASE_URL"); std::env::remove_var("VECTOR_ENGINE_API_KEY"); std::env::remove_var("VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS"); + std::env::remove_var("TIANTOKEN_IMAGE_REQUEST_TIMEOUT_MS"); std::env::remove_var("HYPER3D_BASE_URL"); std::env::remove_var("DASHSCOPE_SCENE_IMAGE_MODEL"); std::env::remove_var("DASHSCOPE_REFERENCE_IMAGE_MODEL"); diff --git a/server-rs/crates/api-server/src/openai_image_generation.rs b/server-rs/crates/api-server/src/openai_image_generation.rs index f3aa2d797..41a5faa34 100644 --- a/server-rs/crates/api-server/src/openai_image_generation.rs +++ b/server-rs/crates/api-server/src/openai_image_generation.rs @@ -108,7 +108,7 @@ pub(crate) fn require_openai_image_settings( Ok(OpenAiImageSettings { base_url: base_url.to_string(), api_key: api_key.to_string(), - request_timeout_ms: state.config.vector_engine_image_request_timeout_ms.max(1), + request_timeout_ms: state.config.tiantoken_image_request_timeout_ms.max(1), request_deadline: None, external_api_audit_state: Some(state.clone()), external_api_audit_user_id: None, diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index 8496a5a94..ce50fd451 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -645,7 +645,7 @@ impl AppState { ImageProvider::Tiantoken, tiantoken_base_url.clone(), tiantoken_api_key.clone(), - config.vector_engine_image_request_timeout_ms, + config.tiantoken_image_request_timeout_ms, )?; let llm_client = build_llm_client(&config)?; let vector_engine_llm_client = build_vector_engine_llm_client(