fix(deepseek): honor Flash 1M window leftovers and native vision

Users on native DeepSeek were told to pin model.context_length and
model.supports_vision in config.yaml. That is the wrong layer: the 1M
window is already in DEFAULT_CONTEXT_LENGTHS, and a global
supports_vision pin would also mark text-only deepseek-v4-pro as
multimodal.

Two catalog gaps still produced the reported symptoms:

- A leftover context_length_cache.yaml entry of 128K (the old
  ``deepseek`` catch-all) outlived the 1M catalog keys because
  deepseek-flash was missing from _PRE_CATALOG_STALE_KEYS.
- When models.dev is empty/cold, Flash has no capability record, so
  image routing falls through to lossy text. Vendor docs (2026-09-10)
  mark deepseek-flash as vision-capable and deepseek-v4-pro as not.

Discard those 128K leftovers, fill Flash (and retired Flash aliases)
via _BUILTIN_MODEL_METADATA, and leave Pro catalog-only.
This commit is contained in:
Teknium
2026-09-11 07:37:05 -07:00
parent 7469c0f2a5
commit 759024bdff
6 changed files with 46 additions and 4 deletions
+3
View File
@@ -1303,6 +1303,9 @@ _PRE_CATALOG_STALE_KEYS = frozenset({
"grok-4.3", "grok-4.6", # 1M / 500K; "grok-4" catch-all persisted 256,000
"grok-4-fast", "grok-4.20", # 2M; fell through to the 256K fallback
"qwen3.6-plus", # 1M; "qwen" catch-all persisted 131,072
# V4 / V4.1 Flash: 1M. Pre-entry builds matched the family catch-all and persisted 128K.
"deepseek-flash", "deepseek-v4.1-flash", "deepseek-v4-flash", "deepseek-v4-pro",
"deepseek-chat", "deepseek-reasoner",
})
+16
View File
@@ -525,6 +525,14 @@ _UNKNOWN_MODEL_BASE: Dict[str, Any] = {"limit": {"context": 200000, "output": 81
# Account-gated models may be usable before models.dev has indexed them. Keep
# their capabilities available for an explicitly selected/discovered model
# without adding them to any picker catalog.
_DEEPSEEK_FLASH_VISION: Dict[str, Any] = {
"limit": {"context": 1_000_000, "output": 384_000},
"modalities": {"input": ["text", "image"], "output": ["text"]},
"tool_call": True,
"reasoning": True,
"family": "deepseek-flash",
}
_BUILTIN_MODEL_METADATA: Dict[Tuple[str, str], Dict[str, Any]] = {
("openai", "gpt-6-astra"): {
"limit": {"context": 1_050_000, "output": 128_000},
@@ -533,6 +541,14 @@ _BUILTIN_MODEL_METADATA: Dict[Tuple[str, str], Dict[str, Any]] = {
"reasoning": True,
"family": "gpt-6",
},
# Native DeepSeek V4.1-Flash is multimodal (https://api-docs.deepseek.com/guides/vision).
# models.dev lagged the 2026-09-10 rename; without this, a cold/empty cache treats
# ``deepseek-flash`` as unknown → image_input_mode falls through to lossy text.
# ``deepseek-v4-pro`` stays catalog-only: vendor docs still mark it text-only.
("deepseek", "deepseek-flash"): _DEEPSEEK_FLASH_VISION,
("deepseek", "deepseek-v4-flash"): _DEEPSEEK_FLASH_VISION,
("deepseek", "deepseek-v4.1-flash"): _DEEPSEEK_FLASH_VISION,
("deepseek", "deepseek-v4-flash-vision-exp"): _DEEPSEEK_FLASH_VISION,
}
+7
View File
@@ -1720,6 +1720,13 @@ class TestGenericPreCatalogStaleGuard:
assert not _stale_pre_catalog_cache_entry("grok-4.20", 2_000_000)
# Sibling qwen slugs with legitimately small windows are untouched.
assert not _stale_pre_catalog_cache_entry("qwen3-coder", 131_072)
# DeepSeek V4 / V4.1 Flash: 1M. Pre-entry builds persisted the 128K
# ``deepseek`` catch-all; a leftover must drop, a 1M value must not.
assert _stale_pre_catalog_cache_entry("deepseek-flash", 128_000)
assert _stale_pre_catalog_cache_entry("deepseek/deepseek-flash", 128_000)
assert _stale_pre_catalog_cache_entry("deepseek-v4-pro", 128_000)
assert not _stale_pre_catalog_cache_entry("deepseek-flash", 1_000_000)
assert not _stale_pre_catalog_cache_entry("deepseek", 128_000)
def test_unknown_models_never_dropped(self):
from agent.model_metadata import _stale_pre_catalog_cache_entry
+16
View File
@@ -836,6 +836,22 @@ class TestGetModelCapabilities:
api_caps = get_model_capabilities("openai-api", "gpt-6-astra")
assert api_caps == caps
def test_deepseek_flash_builtin_vision_fills_catalog_lag(self):
"""Native Flash stays multimodal when models.dev is empty; Pro does not.
Vendor docs: deepseek-flash accepts images, deepseek-v4-pro does not.
A global model.supports_vision pin would lie about Pro.
"""
with patch("agent.models_dev.fetch_models_dev", return_value={}):
flash = get_model_capabilities("deepseek", "deepseek-flash")
alias = get_model_capabilities("deepseek", "deepseek-v4-flash")
pro = get_model_capabilities("deepseek", "deepseek-v4-pro")
assert flash is not None and flash.supports_vision is True
assert flash.context_window == 1_000_000
assert alias is not None and alias.supports_vision is True
assert pro is None
# ---------------------------------------------------------------------------
# Per-model metadata overrides (model_overrides config)
+2 -2
View File
@@ -200,8 +200,8 @@ When a user attaches an image — from the CLI clipboard, the gateway (Telegram/
| Your model | What happens to the image |
|---|---|
| **Vision-capable** (GPT-4V, Claude with vision, Gemini, Qwen-VL, MiMo-VL, etc.) | Sent as **real pixels** using the provider's native image content format above. No text summary layer. |
| **Text-only** (DeepSeek V3, smaller open-source models, older chat-only endpoints) | Routed through the `vision_analyze` auxiliary tool — an auxiliary vision model describes the image, and the text description is injected into the conversation. |
| **Vision-capable** (GPT-4V, Claude with vision, Gemini, Qwen-VL, MiMo-VL, DeepSeek Flash / V4.1-Flash, etc.) | Sent as **real pixels** using the provider's native image content format above. No text summary layer. |
| **Text-only** (DeepSeek V4 Pro, DeepSeek V3, smaller open-source models, older chat-only endpoints) | Routed through the `vision_analyze` auxiliary tool — an auxiliary vision model describes the image, and the text description is injected into the conversation. |
You don't configure this — Hermes looks up your current model's capability in the provider metadata and picks the right path automatically. The practical effect: you can switch between vision and non-vision models mid-session and image handling "just works" without changing your workflow. Text-only models get coherent context about the image rather than a broken multimodal payload they'd have to reject.
@@ -196,8 +196,8 @@ powershell.exe -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms;
| 你的模型 | 图像处理方式 |
|---|---|
| **支持视觉的模型**(GPT-4V、Claude with vision、Gemini、Qwen-VL、MiMo-VL 等) | 使用上述提供商原生图像内容格式,以**真实像素**发送。无文本摘要层。 |
| **纯文本模型**(DeepSeek V3、较小的开源模型、旧版纯对话端点) | 通过 `vision_analyze` 辅助工具路由——辅助视觉模型描述图像,文本描述注入对话。 |
| **支持视觉的模型**(GPT-4V、Claude with vision、Gemini、Qwen-VL、MiMo-VL、DeepSeek Flash / V4.1-Flash 等) | 使用上述提供商原生图像内容格式,以**真实像素**发送。无文本摘要层。 |
| **纯文本模型**(DeepSeek V4 Pro、DeepSeek V3、较小的开源模型、旧版纯对话端点) | 通过 `vision_analyze` 辅助工具路由——辅助视觉模型描述图像,文本描述注入对话。 |
无需手动配置——Hermes 在提供商元数据中查找当前模型的能力并自动选择正确路径。实际效果:你可以在会话中途切换视觉模型与非视觉模型,图像处理"开箱即用",无需更改工作流。纯文本模型会获得关于图像的连贯上下文,而不是一个会被拒绝的损坏多模态载荷。