Date: April 2026
Applies to: Hermes Agent (any version using model_normalize.py and models.py)
The Problem
In April 2026, DeepSeek released their V4 model family — deepseek-v4-flash and deepseek-v4-pro — and announced that the legacy deepseek-chat and deepseek-reasoner identifiers would be deprecated. The new models bring:
| Feature | V4 Flash | V4 Pro |
|---|---|---|
| Context window | 1M tokens | 1M tokens |
| Max output | 384K tokens | 384K tokens |
| Tool Calls | Yes | Yes |
| JSON Output | Yes | Yes |
| Thinking mode | Both | Both |
| Price (input, cached) | ¥0.2/M tokens | ¥1/M tokens |
| Price (input, uncached) | ¥1/M tokens | ¥12/M tokens |
| Price (output) | ¥2/M tokens | ¥24/M tokens |
However, Hermes Agent (the CLI framework we use) had two internal mechanisms that blocked adoption of the new model names out of the box:
- Hardcoded model list — The built-in provider model registry only listed the two legacy names.
- Aggressive model name normalization — A normalization function mapped any unrecognized DeepSeek model name back to
deepseek-chat, silently discarding the V4 identifier.
This tutorial documents every file change needed to make Hermes Agent fully compatible with DeepSeek V4 models.
File 1: Configuration (~/.hermes/config.yaml)
The most visible change: point your default model to the V4 name.
Before:
model:
default: deepseek-reasoner # or deepseek-chat
provider: deepseek
max_tokens: 8192 # legacy limit
context_length: 131072 # 128K, legacy limit
After:
model:
default: deepseek-v4-flash # or deepseek-v4-pro for premium
provider: deepseek
max_tokens: 384000 # new 384K output limit
context_length: 1048576 # 1M context window
The context_length and max_tokens values should reflect the V4 models’ actual capabilities. Hermes will handle compression/truncation if the conversation exceeds the model’s limit, so setting these high is safe.
File 2: Environment Variables (~/.hermes/.env)
Make sure your DeepSeek API key is set:
DEEPSEEK_API_KEY=sk-your-key-here
Note: If you were already using a DeepSeek key for the old models, no change is needed here — the same key works for V4.
File 3: Built-in Model Registry (hermes_cli/models.py)
This file maintains _PROVIDER_MODELS, a dictionary that Hermes uses for model listing, autocomplete, and validation. The "deepseek" entry originally listed only legacy names.
File location: ~/.hermes/hermes-agent/hermes_cli/models.py
Before (around line 248):
"deepseek": [
"deepseek-chat",
"deepseek-reasoner",
],
After:
"deepseek": [
"deepseek-v4-flash",
"deepseek-v4-pro",
],
This is a two-line change. The old deepseek-chat and deepseek-reasoner names are removed; the new V4 models take their place. Keep in mind that deepseek-chat and deepseek-reasoner still work if you type them manually — they’re just no longer the recommended entries in the built-in list.
Tip: If you want backward compatibility in the list, you could keep all four. We chose to cleanly swap because the old names are officially deprecated.
File 4: Model Name Normalization (hermes_cli/model_normalize.py) ⚠️
This is the critical fix. Hermes has a normalization layer that translates user-facing model names into the format each provider’s API expects. For DeepSeek, the function _normalize_for_deepseek() had a hardcoded set of only two “canonical” model names, and any unrecognized name was fallbacked to deepseek-chat.
This meant that even after updating config.yaml to use deepseek-v4-flash, the normalization layer would silently map it back to deepseek-chat — completely defeating the purpose.
File location: ~/.hermes/hermes-agent/hermes_cli/model_normalize.py
Change A: Update the module docstring (around line 106)
This is cosmetic but good practice — update the comment to reflect that V4 is now supported.
Before:
# DeepSeek's API only recognises exactly two model identifiers. We map
# common aliases and patterns to the canonical names.
After:
# DeepSeek's API now supports v4 models in addition to the legacy
# ``deepseek-chat`` and ``deepseek-reasoner`` identifiers. We map
# common aliases and patterns to the canonical names.
Change B: Add V4 names to the canonical set (around line 118)
This is the actual fix. The _DEEPSEEK_CANONICAL_MODELS frozenset acts as a whitelist — any model name found in this set passes through unchanged.
Before:
_DEEPSEEK_CANONICAL_MODELS: frozenset[str] = frozenset({
"deepseek-chat",
"deepseek-reasoner",
})
After:
_DEEPSEEK_CANONICAL_MODELS: frozenset[str] = frozenset({
"deepseek-chat",
"deepseek-reasoner",
"deepseek-v4-flash", # <-- added
"deepseek-v4-pro", # <-- added
})
How the normalization logic works (full function)
Here’s the complete _normalize_for_deepseek() function after the fix, for reference:
def _normalize_for_deepseek(model_name: str) -> str:
"""Map any model input to a canonical DeepSeek identifier.
Rules:
- Already in _DEEPSEEK_CANONICAL_MODELS -> pass through as-is.
- Contains any reasoner keyword (r1, think, reasoning, cot, reasoner)
-> ``deepseek-reasoner``.
- Everything else -> ``deepseek-chat`` (legacy fallback).
Args:
model_name: The bare model name (vendor prefix already stripped).
Returns:
One of the canonical model identifiers.
"""
bare = _strip_vendor_prefix(model_name).lower()
if bare in _DEEPSEEK_CANONICAL_MODELS:
return bare
# Check for reasoner-like keywords anywhere in the name
for keyword in _DEEPSEEK_REASONER_KEYWORDS:
if keyword in bare:
return "deepseek-reasoner"
return "deepseek-chat"
With the fix, this is what happens when each model name goes through normalization:
| Input | Before fix | After fix |
|---|---|---|
deepseek-v4-flash | deepseek-chat ❌ | deepseek-v4-flash ✅ |
deepseek-v4-pro | deepseek-chat ❌ | deepseek-v4-pro ✅ |
deepseek-chat | deepseek-chat ✅ | deepseek-chat ✅ |
deepseek-reasoner | deepseek-reasoner ✅ | deepseek-reasoner ✅ |
deepseek-v3 | deepseek-chat ✅ (correct fallback) | deepseek-chat ✅ |
deepseek-r1 | deepseek-reasoner ✅ | deepseek-reasoner ✅ |
Verification: Did It Work?
After making all changes, here’s a quick sanity check you can run from the Hermes directory:
# test_normalization.py
from hermes_cli.model_normalize import normalize_model_for_provider
test_cases = [
("deepseek-v4-flash", "deepseek-v4-flash"),
("deepseek-v4-pro", "deepseek-v4-pro"),
("deepseek-chat", "deepseek-chat"),
("deepseek-reasoner", "deepseek-reasoner"),
("deepseek-v3", "deepseek-chat"), # legacy fallback
("deepseek-r1", "deepseek-reasoner"), # legacy fallback
]
for model, expected in test_cases:
result = normalize_model_for_provider(model, "deepseek")
status = "✅" if result == expected else "❌"
print(f"{status} {model:25s} -> {result:25s} (expected: {expected})")
Expected output:
✅ deepseek-v4-flash -> deepseek-v4-flash (expected: deepseek-v4-flash)
✅ deepseek-v4-pro -> deepseek-v4-pro (expected: deepseek-v4-pro)
✅ deepseek-chat -> deepseek-chat (expected: deepseek-chat)
✅ deepseek-reasoner -> deepseek-reasoner (expected: deepseek-reasoner)
✅ deepseek-v3 -> deepseek-chat (expected: deepseek-chat)
✅ deepseek-r1 -> deepseek-reasoner (expected: deepseek-reasoner)
Also verify the built-in model list:
from hermes_cli.models import PROVIDER_MODELS
print(PROVIDER_MODELS["deepseek"])
# Should output: ['deepseek-v4-flash', 'deepseek-v4-pro']
Summary of All Changes
| File | Change | Lines affected |
|---|---|---|
~/.hermes/config.yaml | default → deepseek-v4-flash, update ctx/maxtokens | 4 |
~/.hermes/.env | DEEPSEEK_API_KEY (if not already set) | 1 |
hermes_cli/models.py | Replace legacy names with V4 in _PROVIDER_MODELS["deepseek"] | 2 |
hermes_cli/model_normalize.py | Add V4 names to _DEEPSEEK_CANONICAL_MODELS + docstring | 4 |
Total: ~11 lines changed across 4 files.
Lessons Learned
- Normalization layers are sneaky. The
config.yamlchange alone would have been silently overridden bymodel_normalize.py. Always trace the full pipeline from config → API call. - Canonical model sets are not just documentation — they’re enforcement. DeepSeek’s normalization had a hardcoded whitelist (
_DEEPSEEK_CANONICAL_MODELS) and a fallback todeepseek-chat. Any model not in the whitelist was silently remapped. This is a common pattern in model-routing frameworks. - Check both the model list AND the normalization logic. In Hermes,
models.pyfeeds the UI (model picker, autocomplete), whilemodel_normalize.pyfeeds the actual API call. Both need updating. - DeepSeek V4 API is backward compatible. Even after switching to V4 names, the old
deepseek-chatanddeepseek-reasoneridentifiers still work — they’re just deprecated, not removed. Our normalization preserves backward compatibility for anyone still using the legacy names.
Bonus: Switching Between Flash and Pro at Runtime
With the config set to deepseek-v4-flash as default, you can switch to Pro on-demand:
# Within Hermes CLI:
/model deepseek-v4-pro
# Or update config.yaml:
hermes config set model.default deepseek-v4-pro
The Pro model costs 12× more on input and 12× more on output, so we keep Flash as the daily driver and only reach for Pro when we need the extra capability.
了解 ~/jintaoblog/朝夕见闻志⚡️ 的更多信息
订阅后即可通过电子邮件收到最新文章。
