解耦 Tiantoken 与 VectorEngine 的图片请求超时

- 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
This commit is contained in:
2026-09-22 14:08:09 +08:00
parent b0bb71a79e
commit c0d4e76a01
6 changed files with 39 additions and 9 deletions
+2
View File
@@ -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_* 只作用于 TiantokenVECTOR_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=
@@ -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」与代码事实不符。
@@ -238,7 +238,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 的 `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 图片 clientGPT Image 2.5 生成 / 编辑),后者只作用于 VectorEngine 图片 clientnanobanana 等),任一 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 自动验收。
+26 -6
View File
@@ -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<String>,
/// 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<String>,
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");
@@ -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,
+1 -1
View File
@@ -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(