修复编辑器 LLM mock 用例无法构造 Tiantoken client 的问题
- AppConfig 新增 tiantoken_base_url / tiantoken_api_key 字段,from_env 只读 TIANTOKEN_* 环境变量,不回退 VectorEngine 凭据 - AppState 改用配置字段构造编辑器 LLM 与 Tiantoken 图片 client,删除现读环境变量的 config::tiantoken_* 访问器 - editor_background_music_prompt_assist / editor_sound_effect_prompt_assist / sound_effect_translation 三处 mock LLM 夹具改用 tiantoken_* 字段,恢复 28 条失败用例 - config 用例改为断言 from_env 的 Tiantoken 凭据来源,state 用例改名为从 Tiantoken 设置构造编辑器 LLM client - 同步更新后端架构文档与 decision-log 记录
This commit is contained in:
@@ -8857,3 +8857,11 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- **启动**: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)。
|
||||
|
||||
## 2026-09-19 Tiantoken 凭据落到 AppConfig 字段并修复编辑器 LLM 测试夹具
|
||||
|
||||
- **决策**:Tiantoken 凭据仍然只从 `TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY` 读取,不回退 VectorEngine;读取点从 `AppState::new` 前移到 `AppConfig::from_env()`,落到 `AppConfig.tiantoken_base_url` / `AppConfig.tiantoken_api_key` 两个字段,与其它 provider 的配置形态一致,`AppState` 构造与测试构造复用同一条路径;删除 `config::tiantoken_base_url()` / `config::tiantoken_api_key()` 两个现读环境变量的访问器。
|
||||
- **原因**:2026-09-18 的「双 provider 启动边界」改成现读环境变量后,`editor_background_music_prompt_assist`、`editor_sound_effect_prompt_assist`、`vector_engine_audio_generation/sound_effect_translation` 三处 mock LLM 夹具仍用 `AppConfig.vector_engine_*` 构造状态,28 条用例拿到 503 `editor_llm_unavailable`。改用进程环境变量做夹具会让并行用例互相踩 `TIANTOKEN_*`,因此把凭据落到 config 字段而不是在测试里 set_var。
|
||||
- **边界**:ADR 的「不得回退 VectorEngine 或其 API key」仍然成立,`from_env` 只读 `TIANTOKEN_*`,并有用例固定该行为。
|
||||
- **验证**:`cargo test -p api-server` 1060 passed / 0 failed / 6 ignored;`cargo fmt --all --check`、`npm run check:encoding`、`git diff --check` 通过。
|
||||
- **关联文档**:[`docs/adr/【ADR】GPT Image 2.5模型路由与历史值兼容-2026-09-18.md`](../../adr/【ADR】GPT%20Image%202.5模型路由与历史值兼容-2026-09-18.md)、[`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`](../../【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md)。
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -204,6 +204,12 @@ pub struct AppConfig {
|
||||
pub vector_engine_api_key: Option<String>,
|
||||
pub vector_engine_image_request_timeout_ms: u64,
|
||||
pub vector_engine_audio_request_timeout_ms: u64,
|
||||
/// Tiantoken 是 GPT Image 2.5 图片链路和创意 Agent 文本链路的 provider。
|
||||
///
|
||||
/// 只从 `TIANTOKEN_BASE_URL` / `TIANTOKEN_API_KEY` 读取,刻意不回退 VectorEngine 凭据;
|
||||
/// 启动时落到 `AppConfig` 字段,使 `AppState` 构造与测试构造复用同一条路径。
|
||||
pub tiantoken_base_url: String,
|
||||
pub tiantoken_api_key: Option<String>,
|
||||
pub elevenlabs_base_url: String,
|
||||
pub elevenlabs_api_key: Option<String>,
|
||||
pub elevenlabs_request_timeout_ms: u64,
|
||||
@@ -510,6 +516,8 @@ impl Default for AppConfig {
|
||||
vector_engine_api_key: None,
|
||||
vector_engine_image_request_timeout_ms: DEFAULT_VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS,
|
||||
vector_engine_audio_request_timeout_ms: 180_000,
|
||||
tiantoken_base_url: String::new(),
|
||||
tiantoken_api_key: None,
|
||||
elevenlabs_base_url: String::new(),
|
||||
elevenlabs_api_key: None,
|
||||
elevenlabs_request_timeout_ms: DEFAULT_ELEVENLABS_REQUEST_TIMEOUT_MS,
|
||||
@@ -1277,6 +1285,12 @@ impl AppConfig {
|
||||
|
||||
config.vector_engine_api_key = read_first_non_empty_env(&["VECTOR_ENGINE_API_KEY"]);
|
||||
|
||||
// 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",
|
||||
@@ -1428,17 +1442,6 @@ impl AppConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tiantoken 是 GPT Image 2.5 图片任务的独立 provider;凭证不得回退到 VectorEngine。
|
||||
pub(crate) fn tiantoken_base_url(config: &AppConfig) -> String {
|
||||
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"])
|
||||
}
|
||||
|
||||
fn read_first_non_empty_env(keys: &[&str]) -> Option<String> {
|
||||
keys.iter().find_map(|key| {
|
||||
env::var(key).ok().and_then(|value| {
|
||||
@@ -1744,7 +1747,6 @@ mod tests {
|
||||
DEFAULT_EXTERNAL_GENERATION_WORKER_LEASE_SECONDS,
|
||||
DEFAULT_EXTERNAL_GENERATION_WORKER_LONG_JOB_TIMEOUT_SECONDS, ExternalGenerationMode,
|
||||
LlmProvider, ProcessRole, parse_bool, parse_external_generation_mode, parse_process_role,
|
||||
tiantoken_api_key, tiantoken_base_url,
|
||||
};
|
||||
use std::{
|
||||
fs,
|
||||
@@ -1838,31 +1840,52 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiantoken_provider_prefers_new_env_names_over_legacy_vector_engine_config() {
|
||||
fn tiantoken_credentials_never_fall_back_to_vector_engine_env() {
|
||||
let _guard = ENV_LOCK
|
||||
.get_or_init(|| Mutex::new(()))
|
||||
.lock()
|
||||
.expect("env lock should not poison");
|
||||
let mut config = AppConfig::default();
|
||||
config.vector_engine_base_url = "https://vector.example.invalid".to_string();
|
||||
config.vector_engine_api_key = Some("legacy-vector-key".to_string());
|
||||
unsafe {
|
||||
std::env::set_var("TIANTOKEN_BASE_URL", " https://api.tiantoken.example/ ");
|
||||
std::env::set_var("TIANTOKEN_API_KEY", " tiantoken-key ");
|
||||
std::env::set_var("VECTOR_ENGINE_BASE_URL", "https://vector.example.invalid");
|
||||
std::env::set_var("VECTOR_ENGINE_API_KEY", "legacy-vector-key");
|
||||
}
|
||||
|
||||
let config = AppConfig::from_env();
|
||||
|
||||
assert_eq!(config.tiantoken_base_url, "https://api.tiantoken.example/");
|
||||
assert_eq!(config.tiantoken_api_key.as_deref(), Some("tiantoken-key"));
|
||||
assert_eq!(
|
||||
tiantoken_base_url(&config),
|
||||
"https://api.tiantoken.example/"
|
||||
config.vector_engine_base_url,
|
||||
"https://vector.example.invalid"
|
||||
);
|
||||
assert_eq!(
|
||||
config.vector_engine_api_key.as_deref(),
|
||||
Some("legacy-vector-key")
|
||||
);
|
||||
assert_eq!(tiantoken_api_key(&config).as_deref(), Some("tiantoken-key"));
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("TIANTOKEN_BASE_URL");
|
||||
std::env::remove_var("TIANTOKEN_API_KEY");
|
||||
}
|
||||
assert_eq!(tiantoken_base_url(&config), "");
|
||||
assert_eq!(tiantoken_api_key(&config), None);
|
||||
let config = AppConfig::from_env();
|
||||
|
||||
assert_eq!(config.tiantoken_base_url, "");
|
||||
assert_eq!(config.tiantoken_api_key, None);
|
||||
assert_eq!(
|
||||
config.vector_engine_base_url,
|
||||
"https://vector.example.invalid"
|
||||
);
|
||||
assert_eq!(
|
||||
config.vector_engine_api_key.as_deref(),
|
||||
Some("legacy-vector-key")
|
||||
);
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("VECTOR_ENGINE_BASE_URL");
|
||||
std::env::remove_var("VECTOR_ENGINE_API_KEY");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1480,8 +1480,8 @@ mod tests {
|
||||
|
||||
fn editor_llm_test_config(base_url: String) -> AppConfig {
|
||||
AppConfig {
|
||||
vector_engine_base_url: base_url,
|
||||
vector_engine_api_key: Some("test-vector-engine-key".to_string()),
|
||||
tiantoken_base_url: base_url,
|
||||
tiantoken_api_key: Some("test-tiantoken-key".to_string()),
|
||||
llm_max_retries: 0,
|
||||
..AppConfig::default()
|
||||
}
|
||||
|
||||
@@ -574,8 +574,8 @@ mod tests {
|
||||
|
||||
fn editor_llm_test_config(base_url: String, max_retries: u32) -> AppConfig {
|
||||
AppConfig {
|
||||
vector_engine_base_url: base_url,
|
||||
vector_engine_api_key: Some("test-vector-engine-key".to_string()),
|
||||
tiantoken_base_url: base_url,
|
||||
tiantoken_api_key: Some("test-tiantoken-key".to_string()),
|
||||
llm_max_retries: max_retries,
|
||||
..AppConfig::default()
|
||||
}
|
||||
|
||||
@@ -615,8 +615,8 @@ impl AppState {
|
||||
config.editor_generation_pricing_override_path.clone(),
|
||||
)
|
||||
.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 tiantoken_base_url = config.tiantoken_base_url.clone();
|
||||
let tiantoken_api_key = config.tiantoken_api_key.clone();
|
||||
let vector_engine_image_client = build_required_image_provider_client(
|
||||
ImageProvider::VectorEngine,
|
||||
config.vector_engine_base_url.clone(),
|
||||
@@ -3176,13 +3176,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_state_builds_editor_agent_llm_client_from_vector_engine_settings() {
|
||||
fn app_state_builds_editor_agent_llm_client_from_tiantoken_settings() {
|
||||
let mut config = AppConfig::default();
|
||||
config.llm_api_key = None;
|
||||
config.llm_max_retries = 2;
|
||||
config.llm_retry_backoff_ms = 120_000;
|
||||
config.vector_engine_base_url = "https://api.vectorengine.test".to_string();
|
||||
config.vector_engine_api_key = Some("ve-key".to_string());
|
||||
config.tiantoken_base_url = "https://api.tiantoken.test".to_string();
|
||||
config.tiantoken_api_key = Some("tiantoken-key".to_string());
|
||||
|
||||
let state = AppState::new(config).expect("state should build");
|
||||
let client = state
|
||||
@@ -3195,7 +3195,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
client.config().chat_completions_url(),
|
||||
"https://api.vectorengine.test/v1/chat/completions"
|
||||
"https://api.tiantoken.test/v1/chat/completions"
|
||||
);
|
||||
assert!(!client.config().official_fallback());
|
||||
assert_eq!(
|
||||
|
||||
+2
-2
@@ -578,8 +578,8 @@ mod tests {
|
||||
|
||||
fn editor_llm_test_state(base_url: String, max_retries: u32) -> AppState {
|
||||
AppState::new(AppConfig {
|
||||
vector_engine_base_url: base_url,
|
||||
vector_engine_api_key: Some("test-vector-engine-key".to_string()),
|
||||
tiantoken_base_url: base_url,
|
||||
tiantoken_api_key: Some("test-tiantoken-key".to_string()),
|
||||
llm_max_retries: max_retries,
|
||||
..AppConfig::default()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user