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:

FeatureV4 FlashV4 Pro
Context window1M tokens1M tokens
Max output384K tokens384K tokens
Tool CallsYesYes
JSON OutputYesYes
Thinking modeBothBoth
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:

  1. Hardcoded model list — The built-in provider model registry only listed the two legacy names.
  2. 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:

InputBefore fixAfter fix
deepseek-v4-flashdeepseek-chat ❌deepseek-v4-flash ✅
deepseek-v4-prodeepseek-chat ❌deepseek-v4-pro ✅
deepseek-chatdeepseek-chat ✅deepseek-chat ✅
deepseek-reasonerdeepseek-reasoner ✅deepseek-reasoner ✅
deepseek-v3deepseek-chat ✅ (correct fallback)deepseek-chat ✅
deepseek-r1deepseek-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

FileChangeLines affected
~/.hermes/config.yamldefault → deepseek-v4-flash, update ctx/maxtokens4
~/.hermes/.envDEEPSEEK_API_KEY (if not already set)1
hermes_cli/models.pyReplace legacy names with V4 in _PROVIDER_MODELS["deepseek"]2
hermes_cli/model_normalize.pyAdd V4 names to _DEEPSEEK_CANONICAL_MODELS + docstring4

Total: ~11 lines changed across 4 files.


Lessons Learned

  1. Normalization layers are sneaky. The config.yaml change alone would have been silently overridden by model_normalize.py. Always trace the full pipeline from config → API call.
  2. Canonical model sets are not just documentation — they’re enforcement. DeepSeek’s normalization had a hardcoded whitelist (_DEEPSEEK_CANONICAL_MODELS) and a fallback to deepseek-chat. Any model not in the whitelist was silently remapped. This is a common pattern in model-routing frameworks.
  3. Check both the model list AND the normalization logic. In Hermes, models.py feeds the UI (model picker, autocomplete), while model_normalize.py feeds the actual API call. Both need updating.
  4. DeepSeek V4 API is backward compatible. Even after switching to V4 names, the old deepseek-chat and deepseek-reasoner identifiers 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/朝夕见闻志⚡️ 的更多信息

订阅后即可通过电子邮件收到最新文章。