diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index eba6a459..5aed06f2 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -2172,12 +2172,13 @@ mod tests { #[test] fn sanitize_thinking_mode_returns_none_for_non_thinking_model() { let mut body = json!({ - "model": "deepseek-chat", + "model": "deepseek-v4-flash", "messages": [ { "role": "user", "content": "hi" } ] }); - let result = sanitize_thinking_mode_messages(&mut body, "deepseek-chat", None); + let result = sanitize_thinking_mode_messages(&mut body, "deepseek-v4-flash", None); + // reasoning_effort is None → no thinking injection, result is None assert!(result.is_none()); } diff --git a/crates/tui/src/client/chat.rs b/crates/tui/src/client/chat.rs index 97a35345..f72ab448 100644 --- a/crates/tui/src/client/chat.rs +++ b/crates/tui/src/client/chat.rs @@ -825,8 +825,7 @@ fn log_thinking_mode_violations(body: &Value) { fn requires_reasoning_content(model: &str) -> bool { let lower = model.to_lowercase(); - lower.contains("deepseek-v3.2") - || lower.contains("deepseek-v4") + lower.contains("deepseek-v4") || lower.contains("reasoner") || lower.contains("-reasoning") || lower.contains("-thinking") diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 7f4b0532..4dd7a202 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -137,8 +137,6 @@ pub struct ProviderCapability { pub cache_telemetry_supported: bool, /// Which request-payload dialect the provider uses. pub request_payload_mode: RequestPayloadMode, - /// Deprecation notice for legacy model aliases (empty when not deprecated). - pub deprecation: Option, } /// Which request-payload dialect the provider speaks. @@ -150,61 +148,7 @@ pub enum RequestPayloadMode { ResponsesApi, } -/// Deprecation metadata for a legacy model alias. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] -pub struct ModelDeprecation { - /// Legacy alias that is deprecated (e.g. "deepseek-chat"). - pub alias: String, - /// Canonical replacement model. - pub replacement: String, - /// Human-readable deprecation date / notice. - pub notice: String, -} -/// Known deprecations for legacy DeepSeek model aliases. -fn deepseek_legacy_aliases() -> &'static [ModelDeprecation] { - use std::sync::OnceLock; - static ALIASES: OnceLock> = OnceLock::new(); - ALIASES.get_or_init(|| { - vec![ - ModelDeprecation { - alias: String::from("deepseek-chat"), - replacement: String::from("deepseek-v4-flash"), - notice: String::from("Deprecated; will be removed in a future release. Use 'deepseek-v4-flash' instead."), - }, - ModelDeprecation { - alias: String::from("deepseek-reasoner"), - replacement: String::from("deepseek-v4-flash"), - notice: String::from("Deprecated; will be removed in a future release. Use 'deepseek-v4-flash' instead."), - }, - ModelDeprecation { - alias: String::from("deepseek-r1"), - replacement: String::from("deepseek-v4-flash"), - notice: String::from("Deprecated; will be removed in a future release. Use 'deepseek-v4-flash' instead."), - }, - ModelDeprecation { - alias: String::from("deepseek-v3"), - replacement: String::from("deepseek-v4-flash"), - notice: String::from("Deprecated; will be removed in a future release. Use 'deepseek-v4-flash' instead."), - }, - ModelDeprecation { - alias: String::from("deepseek-v3.2"), - replacement: String::from("deepseek-v4-flash"), - notice: String::from("Deprecated; will be removed in a future release. Use 'deepseek-v4-flash' instead."), - }, - ] - }) -} - -/// Check if a model name is a known legacy alias and return its deprecation info. -/// -/// This matches the same list as [`canonical_model_name`] and -/// [`normalize_model_name`], returning deprecation metadata for each alias. -#[must_use] -pub fn deprecation_for_model(model: &str) -> Option<&'static ModelDeprecation> { - let lower = model.trim().to_ascii_lowercase(); - deepseek_legacy_aliases().iter().find(|d| d.alias == lower) -} /// Resolve the provider capability for a given [`ApiProvider`] and resolved /// model string. @@ -248,11 +192,6 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi // Request payload mode: all current providers use chat completions. let request_payload_mode = RequestPayloadMode::ChatCompletions; - // Deprecation: check if the original model name (before normalization) - // is a known legacy alias. We check the resolved model since that's what - // we have here; the caller should also check the user-facing model. - let deprecation = deprecation_for_model(resolved_model); - ProviderCapability { provider, resolved_model: resolved_model.to_string(), @@ -261,30 +200,29 @@ pub fn provider_capability(provider: ApiProvider, resolved_model: &str) -> Provi thinking_supported, cache_telemetry_supported, request_payload_mode, - deprecation: deprecation.cloned(), } } /// Canonicalize common model aliases to stable DeepSeek IDs. /// -/// Legacy `deepseek-chat` / `deepseek-reasoner` remain silent aliases for the -/// current fast V4 model. +/// v4-pro/v4-flash provide canonical forms; v-series snapshots pass through +/// unchanged. Legacy aliases (deepseek-chat, etc.) are no longer folded — +/// DeepSeek's own `/v1/models` endpoint is the source of truth. #[must_use] pub fn canonical_model_name(model: &str) -> Option<&'static str> { match model.trim().to_ascii_lowercase().as_str() { "deepseek-v4-pro" | "deepseek-v4pro" => Some("deepseek-v4-pro"), "deepseek-v4-flash" | "deepseek-v4flash" => Some("deepseek-v4-flash"), - "deepseek-chat" | "deepseek-reasoner" | "deepseek-r1" | "deepseek-v3" | "deepseek-v3.2" => { - Some("deepseek-v4-flash") - } _ => None, } } /// Normalize a configured/runtime model name. /// -/// Accepts known aliases plus any valid `deepseek*` model ID so future -/// DeepSeek releases work without code changes. +/// Trims whitespace and lowercases. v-series snapshots (deepseek-v4-flash-20260423) +/// pass through unchanged so users can pin dated variants. Non-DeepSeek or +/// malformed names return `None`; DeepSeek's `/v1/models` endpoint is the +/// authority on valid model IDs. #[must_use] pub fn normalize_model_name(model: &str) -> Option { let trimmed = model.trim(); @@ -3246,27 +3184,36 @@ api_key = "old-openrouter-key" } #[test] - fn normalize_model_name_handles_aliases_and_future_ids() { + fn normalize_model_name_preserves_v_series_snapshots() { + // v4 canonical forms still resolve assert_eq!( - normalize_model_name("deepseek-v3.2").as_deref(), - Some("deepseek-v4-flash") + normalize_model_name("deepseek-v4-pro").as_deref(), + Some("deepseek-v4-pro") ); assert_eq!( - normalize_model_name("deepseek-r1").as_deref(), - Some("deepseek-v4-flash") + normalize_model_name("deepseek-v4pro").as_deref(), + Some("deepseek-v4-pro") ); + // v-series dated snapshots pass through unchanged assert_eq!( - normalize_model_name("DeepSeek-V4").as_deref(), - Some("deepseek-v4") + normalize_model_name("deepseek-v4-flash-20260423").as_deref(), + Some("deepseek-v4-flash-20260423") ); + // future v-series identities pass through + assert_eq!( + normalize_model_name("deepseek-v5-pro-20270101").as_deref(), + Some("deepseek-v5-pro-20270101") + ); + // legacy names pass through unchanged — server decides + assert_eq!( + normalize_model_name("deepseek-chat").as_deref(), + Some("deepseek-chat") + ); + // cross-provider names still normalize assert_eq!( normalize_model_name("deepseek-ai/deepseek-v4-pro").as_deref(), Some("deepseek-ai/deepseek-v4-pro") ); - assert_eq!( - normalize_model_name("deepseek-ai/deepseek-v4-flash").as_deref(), - Some("deepseek-ai/deepseek-v4-flash") - ); } #[test] @@ -3338,13 +3285,14 @@ api_key = "old-openrouter-key" // Safety: test-only environment mutation guarded by a global mutex. unsafe { - env::set_var("DEEPSEEK_MODEL", "deepseek-chat"); + env::set_var("DEEPSEEK_MODEL", "deepseek-v4-flash-20260423"); } let config = Config::load(None, None)?; + // v-series snapshots pass through unchanged — no alias folding assert_eq!( config.default_text_model.as_deref(), - Some("deepseek-v4-flash") + Some("deepseek-v4-flash-20260423") ); Ok(()) } @@ -3950,41 +3898,6 @@ model = "deepseek-v4-pro" // Provider Capability Matrix tests // ======================================================================== - #[test] - fn deprecation_for_model_returns_notice_for_legacy_aliases() { - let cases = &[ - "deepseek-chat", - "deepseek-reasoner", - "deepseek-r1", - "deepseek-v3", - "deepseek-v3.2", - ]; - for alias in cases { - let dep = deprecation_for_model(alias); - assert!(dep.is_some(), "expected deprecation for '{alias}'"); - let dep = dep.unwrap(); - assert_eq!(dep.alias, *alias); - assert_eq!(dep.replacement, "deepseek-v4-flash"); - assert!(dep.notice.contains("Deprecated")); - assert!(dep.notice.contains("deepseek-v4-flash")); - } - } - - #[test] - fn deprecation_for_model_returns_none_for_current_models() { - assert!(deprecation_for_model("deepseek-v4-pro").is_none()); - assert!(deprecation_for_model("deepseek-v4-flash").is_none()); - assert!(deprecation_for_model("deepseek-ai/deepseek-v4-pro").is_none()); - } - - #[test] - fn deprecation_for_model_is_case_insensitive() { - let dep = deprecation_for_model("DeepSeek-Chat").unwrap(); - assert_eq!(dep.alias, "deepseek-chat"); - let dep = deprecation_for_model("DEEPSEEK-REASONER").unwrap(); - assert_eq!(dep.alias, "deepseek-reasoner"); - } - #[test] fn provider_capability_deepseek_v4_pro_has_1m_window_and_thinking() { let cap = provider_capability(ApiProvider::Deepseek, "deepseek-v4-pro"); @@ -3999,7 +3912,6 @@ model = "deepseek-v4-pro" cap.request_payload_mode, RequestPayloadMode::ChatCompletions ); - assert!(cap.deprecation.is_none()); } #[test] @@ -4106,21 +4018,6 @@ model = "deepseek-v4-pro" assert!(!cap.thinking_supported); } - #[test] - fn provider_capability_legacy_alias_shows_deprecation() { - let cap = provider_capability(ApiProvider::Deepseek, "deepseek-chat"); - assert!(cap.deprecation.is_some()); - let dep = cap.deprecation.unwrap(); - assert_eq!(dep.alias, "deepseek-chat"); - assert_eq!(dep.replacement, "deepseek-v4-flash"); - assert!(dep.notice.contains("Deprecated")); - // Even though deprecated, it still resolves as a V4 model with 1M window. - assert_eq!( - cap.context_window, - crate::models::DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS - ); - } - #[test] fn provider_capability_roundtrip_serialization() { let cap = provider_capability(ApiProvider::Deepseek, "deepseek-v4-pro"); @@ -4129,15 +4026,4 @@ model = "deepseek-v4-pro" assert_eq!(cap, deserialized); } - #[test] - fn deprecation_serialization_roundtrip() { - let dep = ModelDeprecation { - alias: "deepseek-chat".to_string(), - replacement: "deepseek-v4-flash".to_string(), - notice: "Test notice".to_string(), - }; - let json = serde_json::to_value(&dep).unwrap(); - let deserialized: ModelDeprecation = serde_json::from_value(json).unwrap(); - assert_eq!(dep, deserialized); - } } diff --git a/crates/tui/src/core/capacity.rs b/crates/tui/src/core/capacity.rs index 61c0b73e..819e76ad 100644 --- a/crates/tui/src/core/capacity.rs +++ b/crates/tui/src/core/capacity.rs @@ -719,22 +719,14 @@ mod tests { } #[test] - fn normalize_v3_and_reasoner_unchanged() { + fn normalize_v4_and_fallback_prior_keys() { assert_eq!( - normalize_model_prior_key("deepseek-chat"), - "deepseek_v3_2_chat" + normalize_model_prior_key("deepseek-v4-pro"), + "deepseek_v4_pro" ); assert_eq!( - normalize_model_prior_key("deepseek-v3-chat"), - "deepseek_v3_2_chat" - ); - assert_eq!( - normalize_model_prior_key("deepseek-reasoner"), - "deepseek_v3_2_reasoner" - ); - assert_eq!( - normalize_model_prior_key("deepseek-r1"), - "deepseek_v3_2_reasoner" + normalize_model_prior_key("deepseek-v4-flash"), + "deepseek_v4_flash" ); assert_eq!( normalize_model_prior_key("unknown-model"), diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 739f2df2..d7f48d21 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -2185,31 +2185,16 @@ fn run_doctor_json( /// Build the `capability` section for the machine-readable doctor report. /// /// Returns a JSON value with the resolved provider, resolved model, context -/// window, max output, thinking support, cache telemetry support, request -/// payload mode, and any deprecation notice for legacy aliases. +/// window, max output, thinking support, cache telemetry support, and request +/// payload mode. fn provider_capability_report(config: &Config) -> serde_json::Value { use serde_json::json; let provider = config.api_provider(); let model = config.default_model(); - // Detect deprecation for the raw model name (before provider-specific mapping). - let raw_model = config - .default_text_model - .as_deref() - .unwrap_or(DEFAULT_TEXT_MODEL); - let raw_deprecation = crate::config::deprecation_for_model(raw_model); - let cap = crate::config::provider_capability(provider, &model); - let deprecation = raw_deprecation.map(|d| { - json!({ - "alias": d.alias, - "replacement": d.replacement, - "notice": d.notice, - }) - }); - json!({ "resolved_provider": provider.as_str(), "resolved_model": cap.resolved_model, @@ -2218,7 +2203,6 @@ 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(), - "deprecation": deprecation, }) } diff --git a/crates/tui/src/models.rs b/crates/tui/src/models.rs index 8576cf8a..5aae3bb0 100644 --- a/crates/tui/src/models.rs +++ b/crates/tui/src/models.rs @@ -218,7 +218,7 @@ pub fn context_window_for_model(model: &str) -> Option { if let Some(explicit_window) = deepseek_context_window_hint(&lower) { return Some(explicit_window); } - if lower.contains("v4") || is_current_deepseek_v4_alias(&lower) { + if lower.contains("v4") { return Some(DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS); } return Some(LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS); @@ -229,13 +229,6 @@ pub fn context_window_for_model(model: &str) -> Option { None } -fn is_current_deepseek_v4_alias(model_lower: &str) -> bool { - matches!( - model_lower, - "deepseek-chat" | "deepseek-reasoner" | "deepseek-r1" | "deepseek-v3" | "deepseek-v3.2" - ) -} - fn deepseek_context_window_hint(model_lower: &str) -> Option { let bytes = model_lower.as_bytes(); let mut i = 0usize; @@ -376,21 +369,14 @@ mod tests { use super::*; #[test] - fn current_deepseek_aliases_map_to_v4_1m_context_window() { + fn v4_snapshots_preserve_context_window() { + // v-series snapshots get 1M context since they contain "v4" assert_eq!( - context_window_for_model("deepseek-reasoner"), + context_window_for_model("deepseek-v4-flash-20260423"), Some(DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS) ); assert_eq!( - context_window_for_model("deepseek-chat"), - Some(DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS) - ); - assert_eq!( - context_window_for_model("deepseek-v3"), - Some(DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS) - ); - assert_eq!( - context_window_for_model("deepseek-v3.2"), + context_window_for_model("deepseek-v4-pro-20260423"), Some(DEEPSEEK_V4_CONTEXT_WINDOW_TOKENS) ); } diff --git a/crates/tui/src/pricing.rs b/crates/tui/src/pricing.rs index b7cc9187..96c599d3 100644 --- a/crates/tui/src/pricing.rs +++ b/crates/tui/src/pricing.rs @@ -50,8 +50,7 @@ fn pricing_for_model_at(model: &str, now: DateTime) -> Option output_per_million: 3.48, }) } else { - // deepseek-v4-flash and legacy aliases (deepseek-chat, deepseek-reasoner, - // deepseek-v3*) all price as v4-flash. + // deepseek-v4-flash pricing. Some(ModelPricing { input_cache_hit_per_million: 0.0028, input_cache_miss_per_million: 0.14,