Files
hermes-agent/plugins/model-providers/deepseek/__init__.py
T
Teknium f7d90c9410 refactor: single canonical reasoning-effort vocabulary ends the per-vendor clamp drift
The #89503/#70058/#74295/#87279 bug class kept regenerating because every
transport and provider profile hand-rolled its own effort translation map
(9 sites, 4 distinct policies). New agent/reasoning_effort.py is the single
source of truth:

- EFFORT_LADDER: canonical low->high ordering (superset check against
  VALID_REASONING_EFFORTS pinned by test)
- clamp_effort(): one policy — supported passes verbatim, otherwise nearest
  WEAKER supported level (never escalate, never invert the ladder), floor
  when nothing weaker, 'none' never a degradation target, declared
  vendor-documented overrides win, bespoke names pass through
- declared wire vocabularies as data: OpenAI-compat, Codex Responses,
  xAI (4.6/legacy), Actual relays, Kimi K3/K2, TokenHub, GLM-5.2,
  DeepSeek V4, Ollama Cloud, Meta, Solar

Converted sites (all behavior-preserving except noted):
- chat_completions chokepoint, Kimi + TokenHub paths
- codex transport (backend branches now pick a declared set)
- auxiliary_client Responses path
- hermes_cli.models clamp_reasoning_effort_to_supported -> thin wrapper
- plugins: kimi-coding, zai, opencode-zen, deepseek, ollama-cloud,
  meta-ai, upstage, custom (copilot already routes via the wrapper)

Behavior fixes the shared policy surfaces:
- ollama-cloud/opencode-go 'minimal' now degrades to 'low' instead of
  being dropped (drop left the server default = MORE thinking than asked)

New tests: ladder contract (every configurable level is clamped by every
declared wire set; monotonicity across the full ladder for every set).
2026-08-19 19:29:10 -07:00

112 lines
4.1 KiB
Python

"""DeepSeek provider profile.
DeepSeek's V4 family defaults to thinking-mode ON when ``extra_body.thinking``
is unset. The API then returns ``reasoning_content`` and starts enforcing
the contract that subsequent turns echo it back; combined with how Hermes
replays history this lands on the notorious HTTP 400
``reasoning_content must be passed back`` error after the first tool call
(#15700, #17212, #17825).
This profile overrides :meth:`build_api_kwargs_extras` to mirror the Kimi /
Moonshot wire shape that DeepSeek's OpenAI-compat endpoint expects:
{"reasoning_effort": "<low|medium|high|max>",
"extra_body": {"thinking": {"type": "enabled" | "disabled"}}}
Non-thinking models (``deepseek-v3-*`` variants) are left as no-ops so we
don't perturb the V3 wire format.
The legacy aliases ``deepseek-chat`` / ``deepseek-reasoner`` were retired on
2026-07-24. Use ``deepseek-v4-flash`` or ``deepseek-v4-pro``; Hermes remaps
the retired IDs in ``hermes_cli.model_normalize``.
"""
from __future__ import annotations
from typing import Any
from providers import register_provider
from providers.base import ProviderProfile
def _model_supports_thinking(model: str | None) -> bool:
"""DeepSeek thinking-capable model families.
Currently covers the V4 family (``deepseek-v4-pro``, ``deepseek-v4-flash``,
and any future ``deepseek-v4-*`` variants). Retired aliases are remapped
before requests leave Hermes, so they are not listed here.
"""
m = (model or "").strip().lower()
if not m:
return False
if m.startswith("deepseek-v") and not m.startswith("deepseek-v3"):
# deepseek-v4-*, deepseek-v5-*, etc. — every V4+ generation has
# thinking. v3 explicitly excluded.
return True
return False
class DeepSeekProfile(ProviderProfile):
"""DeepSeek — extra_body.thinking + top-level reasoning_effort."""
def build_api_kwargs_extras(
self, *, reasoning_config: dict | None = None, model: str | None = None, **context
) -> tuple[dict[str, Any], dict[str, Any]]:
extra_body: dict[str, Any] = {}
top_level: dict[str, Any] = {}
if not _model_supports_thinking(model):
# V3 / unknown — leave wire format untouched, current behavior.
return extra_body, top_level
# Determine enabled/disabled. Default is enabled to match DeepSeek's
# API default; the API requires this to be set explicitly to avoid the
# reasoning_content echo trap on subsequent turns.
enabled = True
if isinstance(reasoning_config, dict) and reasoning_config.get("enabled") is False:
enabled = False
extra_body["thinking"] = {"type": "enabled" if enabled else "disabled"}
if not enabled:
return extra_body, top_level
# Effort mapping via the shared vocabulary in agent.reasoning_effort
# (DeepSeek V4: low/medium/high/max, xhigh rounds up to max). When no
# effort is set we omit reasoning_effort so DeepSeek applies its
# server default (currently high).
if isinstance(reasoning_config, dict):
from agent.reasoning_effort import (
DEEPSEEK_V4_EFFORTS,
DEEPSEEK_V4_OVERRIDES,
clamp_effort,
)
effort = (reasoning_config.get("effort") or "").strip().lower()
if effort and effort != "none":
clamped = clamp_effort(
effort, DEEPSEEK_V4_EFFORTS, DEEPSEEK_V4_OVERRIDES
)
if clamped in DEEPSEEK_V4_EFFORTS:
top_level["reasoning_effort"] = clamped
return extra_body, top_level
deepseek = DeepSeekProfile(
name="deepseek",
aliases=("deepseek-chat",),
env_vars=("DEEPSEEK_API_KEY",),
display_name="DeepSeek",
description="DeepSeek — native DeepSeek API",
signup_url="https://platform.deepseek.com/",
fallback_models=(
"deepseek-v4-pro",
"deepseek-v4-flash",
),
base_url="https://api.deepseek.com/v1",
default_aux_model="deepseek-v4-flash",
)
register_provider(deepseek)