feat(models): preserve dated variant suffixes; remove legacy alias machinery
`normalize_model_name` now passes v-series snapshots through unchanged (deepseek-v4-flash-20260423 stays pinned, future v5-* matches via regex). Removes ~245 LOC of legacy alias machinery: deepseek_legacy_aliases, the chat/reasoner/r1/v3/v3.2 fold-arm, is_current_deepseek_v4_alias, v4 fallback branch, alias capacity test seeds, alias config test block. The migration from V3 → V4 is over; users on legacy names route their own request to DeepSeek and see the server actual response (404 if deprecated, success if still served). No more silent renaming. Closes #717
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
+30
-144
@@ -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<ModelDeprecation>,
|
||||
}
|
||||
|
||||
/// 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<Vec<ModelDeprecation>> = 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<String> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
+2
-18
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ pub fn context_window_for_model(model: &str) -> Option<u32> {
|
||||
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<u32> {
|
||||
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<u32> {
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,8 +50,7 @@ fn pricing_for_model_at(model: &str, now: DateTime<Utc>) -> Option<ModelPricing>
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user