From d93319fe88f036ba598b78d66f995fa9abfc7372 Mon Sep 17 00:00:00 2001 From: Agent Board Date: Mon, 13 Jul 2026 09:37:03 +0800 Subject: [PATCH] fix: retire freefirst fast alias --- .../mnote-web/src/routes/ai_settings.rs | 57 +++++++++++++++---- scripts/task-ai-management-browser-smoke.js | 4 +- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/rust/crates/mnote-web/src/routes/ai_settings.rs b/rust/crates/mnote-web/src/routes/ai_settings.rs index ba3ee4e0..7584b274 100644 --- a/rust/crates/mnote-web/src/routes/ai_settings.rs +++ b/rust/crates/mnote-web/src/routes/ai_settings.rs @@ -326,8 +326,9 @@ impl EffectiveAiRuntimePolicy { requested_provider: Option<&str>, requested_model_id: Option<&str>, ) -> Result { - let default_ref = normalize_model_ref(None, &self.default_model) - .unwrap_or_else(|| normalize_model_ref(None, DEFAULT_PI_MODEL).expect("default model")); + let default_ref = canonical_ai_model_ref(None, &self.default_model).unwrap_or_else(|| { + canonical_ai_model_ref(None, DEFAULT_PI_MODEL).expect("default model") + }); let default_provider = default_ref .split_once('/') .map(|(provider, _)| provider.to_string()) @@ -340,7 +341,7 @@ impl EffectiveAiRuntimePolicy { .filter(|value| !value.is_empty()); let model_ref = if let Some(model_id) = requested_model_id { let normalized = - normalize_model_ref(requested_provider.or(Some(&default_provider)), model_id) + canonical_ai_model_ref(requested_provider.or(Some(&default_provider)), model_id) .ok_or_else(|| "请求模型为空".to_string())?; if let (Some(provider), Some((model_provider, _))) = (requested_provider, normalized.split_once('/')) @@ -358,7 +359,7 @@ impl EffectiveAiRuntimePolicy { let allowed = self .allowed_models .iter() - .filter_map(|value| normalize_model_ref(Some(&default_provider), value)) + .filter_map(|value| canonical_ai_model_ref(Some(&default_provider), value)) .any(|value| value == model_ref); if !allowed { return Err(format!("模型 {model_ref} 不在当前 AI 设置允许范围内")); @@ -1180,6 +1181,7 @@ pub async fn admin_put_user_settings( const DEFAULT_PI_MODEL: &str = "omniroute/gpt-5.4-mini"; const FREEFIRST_PI_MODEL: &str = "omniroute/freefirst"; +const RETIRED_FREEFIRST_FAST_MODEL: &str = "omniroute/freefirst-fast"; const LIGHTRAG_PROVIDER: &str = "lightrag"; const LIGHTRAG_PROVIDER_DESCRIPTION: &str = "MNote 知识库默认提供者(LightRAG)"; const SOURCE_OF_TRUTH: &str = "directory_grants"; @@ -1820,17 +1822,20 @@ fn merge_effective_user_policy(global_policy: &Value, user_policy: &Value) -> Va } fn effective_default_model(model_policy: &Value) -> String { - model_policy + let configured = model_policy .get("defaultModel") .or_else(|| model_policy.get("default_model")) .and_then(Value::as_str) .map(str::trim) .filter(|value| !value.is_empty()) .unwrap_or(DEFAULT_PI_MODEL) - .to_string() + .to_string(); + canonical_ai_model_ref(None, &configured).unwrap_or(configured) } fn effective_models(model_policy: &Value, default_model: &str) -> Vec { + let default_model = + canonical_ai_model_ref(None, default_model).unwrap_or_else(|| DEFAULT_PI_MODEL.to_string()); let mut model_ids = model_policy .get("allowedModels") .or_else(|| model_policy.get("allowed_models")) @@ -1838,14 +1843,18 @@ fn effective_models(model_policy: &Value, default_model: &str) -> Vec>(); - if !model_ids.iter().any(|value| value == default_model) { - model_ids.insert(0, default_model.to_string()); + if !model_ids.iter().any(|value| value == &default_model) { + model_ids.insert(0, default_model.clone()); } - model_ids + let mut deduped = Vec::with_capacity(model_ids.len()); + for model_id in model_ids { + if !deduped.iter().any(|value| value == &model_id) { + deduped.push(model_id); + } + } + deduped .into_iter() .map(|id| { let name = id.clone(); @@ -1880,6 +1889,14 @@ fn normalize_model_ref(default_provider: Option<&str>, value: &str) -> Option, value: &str) -> Option { + let normalized = normalize_model_ref(default_provider, value)?; + if normalized == RETIRED_FREEFIRST_FAST_MODEL { + return Some(FREEFIRST_PI_MODEL.to_string()); + } + Some(normalized) +} + fn pi_mcp_extension_bridge_enabled() -> bool { std::env::var("MNOTE_PAGE_AI_PI_MCP_EXTENSION") .ok() @@ -2902,6 +2919,22 @@ mod tests { assert!(models[0].is_default); } + #[test] + fn effective_models_canonicalize_retired_freefirst_fast_alias() { + let policy = json!({ + "defaultModel": "omniroute/freefirst-fast", + "allowedModels": ["omniroute/freefirst-fast", "omniroute/freefirst", "omniroute/gpt-5.4-mini"] + }); + assert_eq!(effective_default_model(&policy), "omniroute/freefirst"); + let models = effective_models(&policy, "omniroute/freefirst-fast"); + let ids = models + .iter() + .map(|model| model.id.as_str()) + .collect::>(); + assert_eq!(ids, vec!["omniroute/freefirst", "omniroute/gpt-5.4-mini"]); + assert!(models[0].is_default); + } + // ─── Admin body validation ──────────────────────────────────────────── #[test] diff --git a/scripts/task-ai-management-browser-smoke.js b/scripts/task-ai-management-browser-smoke.js index 579ebe54..617deca8 100644 --- a/scripts/task-ai-management-browser-smoke.js +++ b/scripts/task-ai-management-browser-smoke.js @@ -105,7 +105,7 @@ async function main() { await page.locator("#ai-admin-models.is-active").waitFor(); const modelPanel = page.locator("#ai-admin-models"); await modelPanel.locator('[data-field="allowedModels"]').fill( - "omniroute/freefirst, omniroute/freefirst-fast", + "omniroute/freefirst, omniroute/gpt-5.4-mini", ); await modelPanel.locator('[data-field="secretRef"]').fill("env://OMNIROUTE_API_KEY"); await modelPanel.getByRole("button", { name: "保存配置" }).click(); @@ -250,7 +250,7 @@ async function main() { }); assert.equal(effective.defaultModel, "omniroute/freefirst"); assert( - (effective.models || []).some((model) => model.id === "omniroute/freefirst-fast"), + (effective.models || []).some((model) => model.id === "omniroute/gpt-5.4-mini"), "effective models 应包含管理员允许的模型", ); assert(