From f685e5358e8c1e6500184daf97e214b62ac83ed4 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Thu, 7 May 2026 03:15:50 -0500 Subject: [PATCH] feat(models): surface DeepSeek alias retirement metadata Summary: - Add structured retirement metadata for deepseek-chat and deepseek-reasoner aliases. - Keep aliases valid while reporting V4 Flash capability metadata and replacement guidance. - Surface the warning in doctor human output and doctor JSON. Test plan: - cargo test -p deepseek-tui provider_capability_deepseek_chat_alias_has_v4_flash_caps_and_metadata --locked - cargo test -p deepseek-tui provider_capability_deepseek_reasoner_alias_has_v4_flash_caps_and_metadata --locked - cargo test -p deepseek-tui provider_capability_report_exposes_alias_deprecation_for_deepseek_chat --locked - cargo test -p deepseek-tui provider_capability_report_leaves_canonical_flash_alias_metadata_null --locked - cargo test -p deepseek-tui provider_capability_ --locked - cargo test -p deepseek-tui doctor_endpoint_tests --locked - cargo test -p deepseek-tui --locked - cargo clippy -p deepseek-tui --all-targets --all-features --locked -- -D warnings - cargo fmt --all -- --check - git diff --check origin/main...HEAD Closes #940 --- crates/tui/src/config.rs | 88 +++++++++++++++++++++++++++++++++++++++- crates/tui/src/main.rs | 43 ++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 28542788..512f08a1 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -160,6 +160,23 @@ pub struct ProviderCapability { pub cache_telemetry_supported: bool, /// Which request-payload dialect the provider uses. pub request_payload_mode: RequestPayloadMode, + /// Deprecation metadata for compatibility aliases that are still accepted. + #[serde(skip_serializing_if = "Option::is_none")] + pub alias_deprecation: Option, +} + +pub const DEEPSEEK_ALIAS_RETIREMENT_DATE: &str = "2026-07-24"; +pub const DEEPSEEK_ALIAS_RETIREMENT_UTC: &str = "2026-07-24T15:59:00Z"; +pub const DEEPSEEK_ALIAS_REPLACEMENT: &str = "deepseek-v4-flash"; + +/// Upstream retirement metadata for a model alias that remains compatible. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct ModelAliasDeprecation { + pub alias: String, + pub replacement: String, + pub retirement_date: String, + pub retirement_utc: String, + pub notice: String, } /// Which request-payload dialect the provider speaks. @@ -185,14 +202,21 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi thinking_supported: false, cache_telemetry_supported: false, request_payload_mode: RequestPayloadMode::ChatCompletions, + alias_deprecation: None, }; } let model_lower = resolved_model.to_ascii_lowercase(); + let alias_deprecation = if matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN) { + deepseek_alias_deprecation(&model_lower) + } else { + None + }; let is_v4_pro = model_lower.contains("v4-pro") || model_lower == "deepseek-v4pro"; let is_v4_flash = model_lower.contains("v4-flash") || model_lower == "deepseek-v4flash" - || model_lower == "deepseek-v4"; + || model_lower == "deepseek-v4" + || alias_deprecation.is_some(); // Context window: V4-class models get 1M, everything else falls through // to the model's own lookup or a default. @@ -232,6 +256,22 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi thinking_supported, cache_telemetry_supported, request_payload_mode, + alias_deprecation, + } +} + +fn deepseek_alias_deprecation(model_lower: &str) -> Option { + match model_lower { + "deepseek-chat" | "deepseek-reasoner" => Some(ModelAliasDeprecation { + alias: model_lower.to_string(), + replacement: DEEPSEEK_ALIAS_REPLACEMENT.to_string(), + retirement_date: DEEPSEEK_ALIAS_RETIREMENT_DATE.to_string(), + retirement_utc: DEEPSEEK_ALIAS_RETIREMENT_UTC.to_string(), + notice: format!( + "{model_lower} is a compatibility alias for {DEEPSEEK_ALIAS_REPLACEMENT} and is scheduled to retire on {DEEPSEEK_ALIAS_RETIREMENT_DATE}." + ), + }), + _ => None, } } @@ -4799,6 +4839,52 @@ model = "deepseek-v4-pro" assert!(cap.cache_telemetry_supported); } + #[test] + fn provider_capability_deepseek_chat_alias_has_v4_flash_caps_and_metadata() { + let cap = provider_capability(ApiProvider::Deepseek, "deepseek-chat"); + assert_eq!( + cap.context_window, + crate::models::DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS + ); + assert_eq!(cap.max_output, 384_000); + assert!(cap.thinking_supported); + assert!(cap.cache_telemetry_supported); + + let deprecation = cap + .alias_deprecation + .as_ref() + .expect("alias deprecation metadata"); + assert_eq!(deprecation.alias, "deepseek-chat"); + assert_eq!(deprecation.replacement, "deepseek-v4-flash"); + assert_eq!(deprecation.retirement_date, "2026-07-24"); + assert_eq!(deprecation.retirement_utc, "2026-07-24T15:59:00Z"); + } + + #[test] + fn provider_capability_deepseek_reasoner_alias_has_v4_flash_caps_and_metadata() { + let cap = provider_capability(ApiProvider::Deepseek, "deepseek-reasoner"); + assert_eq!( + cap.context_window, + crate::models::DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS + ); + assert_eq!(cap.max_output, 384_000); + assert!(cap.thinking_supported); + assert!(cap.cache_telemetry_supported); + + let deprecation = cap + .alias_deprecation + .as_ref() + .expect("alias deprecation metadata"); + assert_eq!(deprecation.alias, "deepseek-reasoner"); + assert_eq!(deprecation.replacement, "deepseek-v4-flash"); + } + + #[test] + fn provider_capability_deepseek_v4_flash_has_no_alias_deprecation() { + let cap = provider_capability(ApiProvider::Deepseek, "deepseek-v4-flash"); + assert!(cap.alias_deprecation.is_none()); + } + #[test] fn provider_capability_nvidia_nim_v4_pro_maps_correctly() { let cap = provider_capability(ApiProvider::NvidiaNim, DEFAULT_NVIDIA_NIM_MODEL); diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 10720dbb..c6e4eb26 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -1684,6 +1684,13 @@ async fn run_doctor(config: &Config, workspace: &Path, config_path_override: Opt println!(" · provider: {}", api_target.provider); println!(" · base_url: {}", api_target.base_url); println!(" · model: {}", api_target.model); + let capability = crate::config::provider_capability(config.api_provider(), &api_target.model); + if let Some(alias) = capability.alias_deprecation.as_ref() { + println!( + " ! model alias {} retires {}; switch to {}", + alias.alias, alias.retirement_date, alias.replacement + ); + } if has_api_key { print!(" {} Testing connection...", "·".dimmed()); use std::io::Write; @@ -2314,6 +2321,7 @@ fn provider_capability_report(config: &Config) -> serde_json::Value { "thinking_supported": cap.thinking_supported, "cache_telemetry_supported": cap.cache_telemetry_supported, "request_payload_mode": serde_json::to_value(cap.request_payload_mode).unwrap_or_default(), + "alias_deprecation": cap.alias_deprecation, }) } @@ -4290,6 +4298,41 @@ mod doctor_endpoint_tests { assert_eq!(target.model, crate::config::DEFAULT_TEXT_MODEL); } + #[test] + fn provider_capability_report_exposes_alias_deprecation_for_deepseek_chat() { + let config = Config { + default_text_model: Some("deepseek-chat".to_string()), + ..Default::default() + }; + + let report = provider_capability_report(&config); + + assert_eq!(report["resolved_model"], "deepseek-chat"); + assert_eq!(report["context_window"], 1_000_000); + assert_eq!(report["thinking_supported"], true); + assert_eq!( + report["alias_deprecation"]["replacement"], + "deepseek-v4-flash" + ); + assert_eq!( + report["alias_deprecation"]["retirement_utc"], + "2026-07-24T15:59:00Z" + ); + } + + #[test] + fn provider_capability_report_leaves_canonical_flash_alias_metadata_null() { + let config = Config { + default_text_model: Some("deepseek-v4-flash".to_string()), + ..Default::default() + }; + + let report = provider_capability_report(&config); + + assert_eq!(report["resolved_model"], "deepseek-v4-flash"); + assert!(report["alias_deprecation"].is_null()); + } + #[test] fn timeout_recovery_points_global_deepseek_users_to_cn_endpoint() { let config = Config::default();