Files
hermes-agent/plugins/model-providers/zai/__init__.py
Teknium 336b11584c fix: replay prior reasoning to every chat-completions route that can read it
Every model reasons and intends that reasoning to be replayed, but the
chat-completions wire echoed reasoning_content only to DeepSeek/Kimi/MiMo/
Ollama (plus a hardcoded Portal stealth model) and reasoning_details only to
OpenRouter; turn_context popped `reasoning` everywhere, so Ollama's /v1
(reads only `reasoning`) never saw any.

- message_sanitization owns one route->carrier table
  (_REASONING_CARRIER_ROUTES): strict schemas (Mistral, Groq, Cerebras,
  SambaNova) get nothing, Fireworks reasoning_content only, OpenRouter-format
  gateways (OpenRouter, Kilo, Vercel AI Gateway, Nous Portal) all three,
  Novita/MiniMax /v1/TokenHub + reasoning_details, local/Ollama/Upstage +
  reasoning, everyone else reasoning_content.
- The " " pad stays limited to the must-echo tier (DeepSeek, Kimi, MiMo);
  Ollama leaves it (no echo requirement) and the stealth hardcode is gone.
- Reactive recovery: a 4xx unknown-field rejection that NAMES a reasoning
  key this request carried is recorded per (provider, host, model) and the
  retry re-shapes api_messages without it. Generic upstream errors never
  count; a recorded key falls through, so no loop. History is untouched.
- Nous Portal reads reasoning_details again: the #118182 "replay budget"
  does not exist in the Portal and does not reproduce live.
- Gemini thought_signature survives on Gemini hosts/providers even when the
  model id is an alias.
- Vendor keep-reasoning flags (docs-only): GLM thinking.clear_thinking=false,
  DashScope preserve_thinking / clear_thinking, Kimi K2.x thinking.keep=all.
- stale_thinking_reaches_wire follows the same table, so compaction trigger
  and tail walks agree with what is sent.
2026-10-10 19:22:07 -07:00

82 lines
3.6 KiB
Python

"""ZAI / GLM provider profile.
GLM-4.5+ defaults to thinking ON, so ``reasoning_config`` is translated to
``extra_body.thinking``; GLM-5.2/5.3 also take a native ``reasoning_effort``.
"""
import re
from typing import Any
from agent import reasoning_effort as re_
from providers import register_provider
from providers.base import ProviderProfile
_GLM_VERSION_RE = re.compile(r"^glm-(\d+)(?:\.(\d+))?")
# Alias spellings seen on relays (Fireworks ``glm-5p2``, ``zai-org-glm-5-2``…).
_GLM_5_3_TOKENS = ("glm-5.3", "glm-5-3", "glm-5p3")
_GLM_5_2_TOKENS = ("glm-5.2", "glm-5-2", "glm-5p2") + _GLM_5_3_TOKENS
def _model_supports_thinking(model: str | None) -> bool:
"""GLM thinking-capable model families: glm-4.5 and later (4.5, 4.6, 5…)."""
match = _GLM_VERSION_RE.match((model or "").strip().lower())
return bool(match) and (int(match.group(1)), int(match.group(2) or 0)) >= (4, 5)
def _has_token(model: str | None, tokens: tuple[str, ...]) -> bool:
m = (model or "").strip().lower()
return any(token in m for token in tokens)
def _glm_5_2_reasoning_effort(reasoning_config: dict | None, *, model: str | None = None) -> str | None:
"""Hermes effort -> GLM vocabulary (5.2: high/max; 5.3: low..max). Below-floor
efforts clamp to the floor; disabled/unset leaves the server default."""
effort = re_.requested_effort(reasoning_config)
if effort is None or effort == "none":
return None
if _has_token(model, _GLM_5_3_TOKENS):
efforts, overrides, floor = re_.GLM53_EFFORTS, re_.GLM53_OVERRIDES, "low"
else:
efforts, overrides, floor = re_.GLM52_EFFORTS, re_.GLM52_OVERRIDES, "high"
clamped = re_.clamp_effort(effort, efforts, overrides)
return clamped if clamped in efforts else floor
class ZaiProfile(ProviderProfile):
"""Z.AI / GLM — extra_body.thinking on/off + GLM-5.2 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] = {}
is_5_2 = _has_token(model, _GLM_5_2_TOKENS)
if not _model_supports_thinking(model) and not is_5_2:
return extra_body, top_level
# Only emit when the user expressed a preference (server default = enabled).
if isinstance(reasoning_config, dict):
enabled = reasoning_config.get("enabled") is not False
if not enabled and _has_token(model, _GLM_5_3_TOKENS):
# GLM-5.3 rejects disabled thinking; low is its lightest supported mode.
enabled = True
top_level["reasoning_effort"] = "low"
# Preserved Thinking: with thinking on, the standard endpoint drops replayed prior-turn
# reasoning_content unless clear_thinking is false (docs.z.ai/guides/capabilities/thinking-mode).
extra_body["thinking"] = {"type": "enabled", "clear_thinking": False} if enabled else {"type": "disabled"}
if is_5_2:
effort = _glm_5_2_reasoning_effort(reasoning_config, model=model)
if effort is not None:
top_level["reasoning_effort"] = effort
return extra_body, top_level
zai = ZaiProfile(
name="zai", aliases=("glm", "z-ai", "z.ai", "zhipu"),
env_vars=("GLM_API_KEY", "ZAI_API_KEY", "Z_AI_API_KEY"), display_name="Z.AI (GLM)",
description="Z.AI / GLM — Zhipu AI models", signup_url="https://z.ai/",
fallback_models=("glm-5.2", "glm-5", "glm-4-9b"), base_url="https://api.z.ai/api/paas/v4",
default_aux_model="glm-4.5-flash",
)
register_provider(zai)