fix(deepseek): remap retired chat/reasoner aliases to v4-flash

DeepSeek cut off deepseek-chat and deepseek-reasoner on 2026-07-24.
Sending those IDs now returns HTTP 400; rewrite them (and fuzzy
reasoner names) to deepseek-v4-flash so saved configs keep working.
This commit is contained in:
HexLab98 2026-07-25 12:33:16 +07:00 committed by Teknium
parent 148497f6d6
commit cc7c418b33
2 changed files with 58 additions and 25 deletions

View file

@ -12,9 +12,11 @@ Different LLM providers expect model identifiers in different formats:
model IDs, but Claude still uses hyphenated native names like
``claude-sonnet-4-6``.
- **OpenCode Go** preserves dots in model names: ``minimax-m2.7``.
- **DeepSeek** accepts ``deepseek-chat`` (V3), ``deepseek-reasoner``
(R1-family), and the first-class V-series IDs (``deepseek-v4-pro``,
``deepseek-v4-flash``, and any future ``deepseek-v<N>-*``). Older
- **DeepSeek** accepts only the first-class V-series IDs
(``deepseek-v4-pro``, ``deepseek-v4-flash``, and any future
``deepseek-v<N>-*``). The legacy aliases ``deepseek-chat`` and
``deepseek-reasoner`` were retired on 2026-07-24 and are remapped to
``deepseek-v4-flash`` (official non-thinking / thinking shims). Older
Hermes revisions folded every non-reasoner input into
``deepseek-chat``, which on aggregators routes to V3 so a user
picking V4 Pro was silently downgraded.
@ -118,8 +120,9 @@ _LOWERCASE_MODEL_PROVIDERS: frozenset[str] = frozenset({
# ---------------------------------------------------------------------------
# DeepSeek special handling
# ---------------------------------------------------------------------------
# DeepSeek's API only recognises exactly two model identifiers. We map
# common aliases and patterns to the canonical names.
# DeepSeek's direct API only accepts first-class V-series IDs after the
# 2026-07-24 cut-off. Legacy aliases and fuzzy names are remapped here so
# saved configs / picker leftovers cannot keep sending retired IDs.
_DEEPSEEK_REASONER_KEYWORDS: frozenset[str] = frozenset({
"reasoner",
@ -129,9 +132,15 @@ _DEEPSEEK_REASONER_KEYWORDS: frozenset[str] = frozenset({
"cot",
})
# Retired on 2026-07-24 15:59 UTC. Official docs: both aliases mapped to
# deepseek-v4-flash (chat = non-thinking, reasoner = thinking). Thinking
# mode itself is controlled by extra_body.thinking on the DeepSeek profile.
_DEEPSEEK_RETIRED_ALIASES: frozenset[str] = frozenset({
"deepseek-chat",
"deepseek-reasoner",
})
_DEEPSEEK_CANONICAL_MODELS: frozenset[str] = frozenset({
"deepseek-chat", # V3 on DeepSeek direct and most aggregators
"deepseek-reasoner", # R1-family reasoning model
"deepseek-v4-pro", # V4 Pro — first-class model ID
"deepseek-v4-flash", # V4 Flash — first-class model ID
})
@ -149,13 +158,15 @@ def _normalize_for_deepseek(model_name: str) -> str:
"""Map a model input to a DeepSeek-accepted identifier.
Rules:
- Already a known canonical (``deepseek-chat``/``deepseek-reasoner``/
``deepseek-v4-pro``/``deepseek-v4-flash``) -> pass through.
- Retired aliases ``deepseek-chat`` / ``deepseek-reasoner`` (cut off
2026-07-24) -> ``deepseek-v4-flash``.
- Already a known canonical (``deepseek-v4-pro``/``deepseek-v4-flash``)
-> pass through.
- Matches the V-series pattern ``deepseek-v<digit>...`` -> pass through
(covers future ``deepseek-v5-*`` and dated variants without a release).
- Contains a reasoner keyword (r1, think, reasoning, cot, reasoner)
-> ``deepseek-reasoner``.
- Everything else -> ``deepseek-chat``.
-> ``deepseek-v4-flash``.
- Everything else -> ``deepseek-v4-flash``.
Args:
model_name: The bare model name (vendor prefix already stripped).
@ -165,6 +176,11 @@ def _normalize_for_deepseek(model_name: str) -> str:
"""
bare = _strip_vendor_prefix(model_name).lower()
# Retired aliases must rewrite — DeepSeek returns HTTP 400 after the
# 2026-07-24 cut-off if these IDs are sent on the wire.
if bare in _DEEPSEEK_RETIRED_ALIASES:
return "deepseek-v4-flash"
if bare in _DEEPSEEK_CANONICAL_MODELS:
return bare
@ -175,9 +191,9 @@ def _normalize_for_deepseek(model_name: str) -> str:
# Check for reasoner-like keywords anywhere in the name
for keyword in _DEEPSEEK_REASONER_KEYWORDS:
if keyword in bare:
return "deepseek-reasoner"
return "deepseek-v4-flash"
return "deepseek-chat"
return "deepseek-v4-flash"
# ---------------------------------------------------------------------------
@ -369,10 +385,13 @@ def normalize_model_for_provider(model_input: str, target_provider: str) -> str:
'minimax-m2.5-free'
>>> normalize_model_for_provider("deepseek-v3", "deepseek")
'deepseek-chat'
'deepseek-v4-flash'
>>> normalize_model_for_provider("deepseek-r1", "deepseek")
'deepseek-reasoner'
'deepseek-v4-flash'
>>> normalize_model_for_provider("deepseek-reasoner", "deepseek")
'deepseek-v4-flash'
>>> normalize_model_for_provider("my-model", "custom")
'my-model'

View file

@ -232,19 +232,33 @@ class TestDeepseekVSeriesPassThrough:
assert result == "deepseek-v4-flash"
# ── DeepSeek regressions (existing behaviour still holds) ──────────────
# ── DeepSeek post-2026-07-24 alias remapping ───────────────────────────
class TestDeepseekCanonicalAndReasonerMapping:
"""Canonical pass-through and reasoner-keyword folding stay intact."""
"""Retired aliases and fuzzy names rewrite to deepseek-v4-flash.
DeepSeek cut off ``deepseek-chat`` / ``deepseek-reasoner`` on
2026-07-24; sending them on the wire returns HTTP 400.
"""
@pytest.mark.parametrize("model,expected", [
("deepseek-chat", "deepseek-chat"),
("deepseek-reasoner", "deepseek-reasoner"),
("DEEPSEEK-CHAT", "deepseek-chat"),
("deepseek-chat", "deepseek-v4-flash"),
("deepseek-reasoner", "deepseek-v4-flash"),
("DEEPSEEK-CHAT", "deepseek-v4-flash"),
("DEEPSEEK-REASONER", "deepseek-v4-flash"),
("deepseek/deepseek-reasoner", "deepseek-v4-flash"),
("deepseek-v4-pro", "deepseek-v4-pro"),
("deepseek-v4-flash", "deepseek-v4-flash"),
])
def test_canonical_models_pass_through(self, model, expected):
def test_retired_aliases_and_canonicals(self, model, expected):
assert _normalize_for_deepseek(model) == expected
def test_provider_path_rewrites_reasoner(self):
assert (
normalize_model_for_provider("deepseek-reasoner", "deepseek")
== "deepseek-v4-flash"
)
@pytest.mark.parametrize("model", [
"deepseek-r1",
"deepseek-r1-0528",
@ -252,8 +266,8 @@ class TestDeepseekCanonicalAndReasonerMapping:
"deepseek-reasoning-preview",
"deepseek-cot-experimental",
])
def test_reasoner_keywords_map_to_reasoner(self, model):
assert _normalize_for_deepseek(model) == "deepseek-reasoner"
def test_reasoner_keywords_map_to_v4_flash(self, model):
assert _normalize_for_deepseek(model) == "deepseek-v4-flash"
@pytest.mark.parametrize("model", [
"deepseek-chat-v3.1", # 'chat' prefix, not V-series pattern
@ -261,5 +275,5 @@ class TestDeepseekCanonicalAndReasonerMapping:
"something-random",
"gpt-5", # non-DeepSeek names still fall through
])
def test_unknown_names_fall_back_to_chat(self, model):
assert _normalize_for_deepseek(model) == "deepseek-chat"
def test_unknown_names_fall_back_to_v4_flash(self, model):
assert _normalize_for_deepseek(model) == "deepseek-v4-flash"