Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions agent/transports/chat_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,30 @@ def build_kwargs(
provider_name = str(params.get("provider_name") or "").strip().lower()
base_url = params.get("base_url")

# Z.AI / BigModel (GLM): stream tool-call args incrementally to avoid
# long silent gaps that trigger the server-side 30s idle timeout.
# Also inject thinking parameter for extended reasoning support.
_is_zai = (
provider_name == "zai"
or provider_name == "zai-cn"
or provider_name == "zai-coding-global"
or provider_name == "zai-coding-cn"
or ("z.ai" in str(base_url or "").lower())
or ("bigmodel.cn" in str(base_url or "").lower())
)

if _is_zai and tools:
extra_body.setdefault("tool_stream", True)

if _is_zai:
_zai_thinking_enabled = True
if reasoning_config and isinstance(reasoning_config, dict):
if reasoning_config.get("enabled") is False:
_zai_thinking_enabled = False
extra_body["thinking"] = {
"type": "enabled" if _zai_thinking_enabled else "disabled",
}

provider_prefs = params.get("provider_preferences")
if provider_prefs and is_openrouter:
extra_body["provider"] = provider_prefs
Expand Down Expand Up @@ -483,6 +507,12 @@ def _build_kwargs_from_profile(self, profile, model, sanitized, tools, params):
if profile_body:
extra_body.update(profile_body)

# Z.AI / GLM: tool_stream must be set when tools are present to
# avoid 30s idle timeouts on the server side.
_zai_profile_names = ("zai", "zai-cn", "zai-coding-global", "zai-coding-cn")
if profile.name in _zai_profile_names and tools:
extra_body["tool_stream"] = True

# Profile's reasoning/thinking extra_body entries
if extra_body_from_profile:
extra_body.update(extra_body_from_profile)
Expand Down
120 changes: 112 additions & 8 deletions plugins/model-providers/zai/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,125 @@
"""ZAI / GLM provider profile."""
"""ZAI / GLM provider profiles.

Z.AI (GLM) — api.z.ai (Global) and open.bigmodel.cn (China)
Both support Coding Plan endpoints at /api/coding/paas/v4.
"""

from typing import Any

from providers import register_provider
from providers.base import ProviderProfile

zai = ProviderProfile(

class ZaiProfile(ProviderProfile):
"""Z.AI / GLM — thinking parameter, tool_stream, Coding Plan headers."""

def build_extra_body(
self, *, session_id: str | None = None, **context: Any
) -> dict[str, Any]:
"""Inject thinking parameter for Z.AI/GLM models."""
reasoning_config = context.get("reasoning_config")
body: dict[str, Any] = {}

if reasoning_config and isinstance(reasoning_config, dict):
if reasoning_config.get("enabled") is False:
body["thinking"] = {"type": "disabled"}
else:
body["thinking"] = {"type": "enabled"}
else:
# Default: thinking enabled (GLM-5+ models support it)
body["thinking"] = {"type": "enabled"}

return body

def build_api_kwargs_extras(
self,
*,
reasoning_config: dict | None = None,
**context: Any,
) -> tuple[dict[str, Any], dict[str, Any]]:
"""Z.AI returns no provider-specific top-level kwargs beyond defaults."""
return {}, {}


# ── Global ──────────────────────────────────────────────────────────────
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",
aliases=("z-ai", "z.ai"),
env_vars=("ZAI_API_KEY", "Z_AI_API_KEY"),
display_name="Z.AI",
description="Z.AI (GLM)api.z.ai",
signup_url="https://z.ai/",
base_url="https://api.z.ai/api/paas/v4",
hostname="api.z.ai",
default_headers={
"X-Title": "Hermes-Agent",
},
default_aux_model="glm-4.5-flash",
fallback_models=(
"glm-5",
"glm-4-9b",
"glm-5-turbo",
"glm-4.7",
),
base_url="https://api.z.ai/api/paas/v4",
)

# ── China ───────────────────────────────────────────────────────────────
zai_cn = ZaiProfile(
name="zai-cn",
aliases=("glm", "zhipu", "bigmodel"),
env_vars=("GLM_API_KEY",),
display_name="Zhipu AI",
description="Zhipu AI (GLM) — open.bigmodel.cn",
signup_url="https://open.bigmodel.cn/",
base_url="https://open.bigmodel.cn/api/paas/v4",
hostname="open.bigmodel.cn",
default_aux_model="glm-4.5-flash",
fallback_models=(
"glm-5",
"glm-5-turbo",
"glm-4.7",
),
)

# ── Global Coding Plan ─────────────────────────────────────────────────
zai_coding_global = ZaiProfile(
name="zai-coding-global",
aliases=("glm-coding-global", "z-ai-coding"),
env_vars=("ZAI_CODING_API_KEY",),
display_name="Z.AI Coding Plan",
description="Z.AI Coding Plan — api.z.ai/api/coding",
signup_url="https://z.ai/pricing",
base_url="https://api.z.ai/api/coding/paas/v4",
hostname="api.z.ai",
default_headers={
"X-Title": "Hermes-Agent",
},
default_aux_model="glm-4.7",
fallback_models=(
"glm-5",
"glm-5-turbo",
"glm-4.7",
),
)

# ── China Coding Plan ──────────────────────────────────────────────────
zai_coding_cn = ZaiProfile(
name="zai-coding-cn",
aliases=("glm-coding-cn",),
env_vars=("GLM_CODING_API_KEY",),
display_name="Zhipu AI Coding Plan",
description="Zhipu AI Coding Plan — open.bigmodel.cn/api/coding",
signup_url="https://open.bigmodel.cn/",
base_url="https://open.bigmodel.cn/api/coding/paas/v4",
hostname="open.bigmodel.cn",
default_aux_model="glm-4.7",
fallback_models=(
"glm-5",
"glm-5-turbo",
"glm-4.7",
),
)

register_provider(zai)
register_provider(zai_cn)
register_provider(zai_coding_global)
register_provider(zai_coding_cn)
8 changes: 7 additions & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9359,7 +9359,7 @@ def _anthropic_preserve_dots(self) -> bool:
if (getattr(self, "provider", "") or "").lower() in {
"alibaba", "minimax", "minimax-cn",
"opencode-go", "opencode-zen",
"zai", "bedrock",
"zai", "zai-cn", "zai-coding-global", "zai-coding-cn", "bedrock",
"xiaomi",
}:
return True
Expand Down Expand Up @@ -9530,6 +9530,10 @@ def _build_api_kwargs(self, api_messages: list) -> dict:
)
_is_tokenhub = base_url_host_matches(self._base_url_lower, "tokenhub.tencentmaas.com")
_is_lmstudio = (self.provider or "").strip().lower() == "lmstudio"
_is_zai = (
base_url_host_matches(self._base_url_lower, "z.ai")
or base_url_host_matches(self._base_url_lower, "open.bigmodel.cn")
)

# Temperature: _fixed_temperature_for_model may return OMIT_TEMPERATURE
# sentinel (temperature omitted entirely), a numeric override, or None.
Expand Down Expand Up @@ -9641,6 +9645,7 @@ def _build_api_kwargs(self, api_messages: list) -> dict:
is_kimi=_is_kimi,
is_tokenhub=_is_tokenhub,
is_lmstudio=_is_lmstudio,
is_zai=_is_zai,
is_custom_provider=self.provider == "custom",
ollama_num_ctx=self._ollama_num_ctx,
provider_preferences=_prefs or None,
Expand Down Expand Up @@ -9693,6 +9698,7 @@ def _supports_reasoning_extra_body(self) -> bool:
"anthropic/",
"openai/",
"x-ai/",
"z-ai/",
"google/gemini-2",
"qwen/qwen3",
"tencent/hy3-preview",
Expand Down
175 changes: 175 additions & 0 deletions tests/agent/transports/test_chat_completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -769,3 +769,178 @@ def test_with_cache(self, transport):
r = SimpleNamespace(usage=SimpleNamespace(prompt_tokens_details=details))
result = transport.extract_cache_stats(r)
assert result == {"cached_tokens": 500, "creation_tokens": 100}


class TestZaiTransport:
"""Tests for Z.AI / GLM provider-specific transport behavior."""

def _msgs(self):
return [{"role": "user", "content": "Hello"}]

def _tools(self):
return [{"type": "function", "function": {"name": "read_file", "parameters": {}}}]

def test_zai_legacy_tool_stream(self, transport):
"""Legacy path: provider_name=zai + tools → tool_stream in extra_body."""
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
tools=self._tools(),
provider_name="zai",
)
assert kw["extra_body"]["tool_stream"] is True

def test_zai_legacy_thinking_default(self, transport):
"""Legacy path: provider_name=zai → thinking enabled by default."""
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
provider_name="zai",
)
assert kw["extra_body"]["thinking"] == {"type": "enabled"}

def test_zai_legacy_thinking_disabled(self, transport):
"""Legacy path: reasoning_config.enabled=False → thinking disabled."""
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
provider_name="zai",
reasoning_config={"enabled": False},
)
assert kw["extra_body"]["thinking"] == {"type": "disabled"}

def test_zai_legacy_no_tool_stream_without_tools(self, transport):
"""Legacy path: no tools → tool_stream not set."""
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
provider_name="zai",
)
assert "tool_stream" not in kw["extra_body"]

def test_zai_legacy_detection_by_base_url(self, transport):
"""Legacy path: detect z.ai by base_url containing z.ai."""
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
tools=self._tools(),
provider_name="custom",
base_url="https://api.z.ai/api/coding/paas/v4",
)
assert kw["extra_body"]["tool_stream"] is True
assert kw["extra_body"]["thinking"] == {"type": "enabled"}

def test_zai_legacy_detection_by_bigmodel_url(self, transport):
"""Legacy path: detect z.ai by base_url containing bigmodel.cn."""
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
tools=self._tools(),
provider_name="custom",
base_url="https://open.bigmodel.cn/api/paas/v4",
)
assert kw["extra_body"]["tool_stream"] is True

def test_zai_legacy_detection_zai_cn_provider(self, transport):
"""Legacy path: provider_name=zai-cn → tool_stream + thinking."""
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
tools=self._tools(),
provider_name="zai-cn",
)
assert kw["extra_body"]["tool_stream"] is True
assert kw["extra_body"]["thinking"] == {"type": "enabled"}

def test_zai_legacy_detection_zai_coding_global(self, transport):
"""Legacy path: provider_name=zai-coding-global → tool_stream + thinking."""
kw = transport.build_kwargs(
model="glm-5-turbo",
messages=self._msgs(),
tools=self._tools(),
provider_name="zai-coding-global",
)
assert kw["extra_body"]["tool_stream"] is True

def test_non_zai_no_tool_stream(self, transport):
"""Non-Z.AI provider: tool_stream must NOT be injected."""
kw = transport.build_kwargs(
model="gpt-4o",
messages=self._msgs(),
tools=self._tools(),
provider_name="openrouter",
base_url="https://openrouter.ai/api/v1",
)
assert "tool_stream" not in (kw.get("extra_body") or {})

def test_zai_profile_path_tool_stream(self, transport):
"""Profile path: zai profile + tools → tool_stream in extra_body."""
from providers import get_provider_profile
profile = get_provider_profile("zai")
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
tools=self._tools(),
provider_profile=profile,
)
assert kw["extra_body"]["tool_stream"] is True

def test_zai_profile_path_thinking(self, transport):
"""Profile path: zai profile → thinking enabled via profile hook."""
from providers import get_provider_profile
profile = get_provider_profile("zai")
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
provider_profile=profile,
)
assert kw["extra_body"]["thinking"] == {"type": "enabled"}

def test_zai_profile_path_thinking_disabled(self, transport):
"""Profile path: reasoning_config.enabled=False → thinking disabled."""
from providers import get_provider_profile
profile = get_provider_profile("zai")
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
provider_profile=profile,
reasoning_config={"enabled": False},
)
assert kw["extra_body"]["thinking"] == {"type": "disabled"}

def test_zai_profile_path_no_tool_stream_without_tools(self, transport):
"""Profile path: zai profile without tools → no tool_stream."""
from providers import get_provider_profile
profile = get_provider_profile("zai")
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
provider_profile=profile,
)
assert "tool_stream" not in kw["extra_body"]

def test_zai_coding_global_profile(self, transport):
"""Profile path: zai-coding-global profile works with tool_stream."""
from providers import get_provider_profile
profile = get_provider_profile("zai-coding-global")
kw = transport.build_kwargs(
model="glm-5-turbo",
messages=self._msgs(),
tools=self._tools(),
provider_profile=profile,
)
assert kw["extra_body"]["tool_stream"] is True
assert kw["extra_body"]["thinking"] == {"type": "enabled"}

def test_zai_cn_profile(self, transport):
"""Profile path: zai-cn profile works with tool_stream."""
from providers import get_provider_profile
profile = get_provider_profile("zai-cn")
kw = transport.build_kwargs(
model="glm-5",
messages=self._msgs(),
tools=self._tools(),
provider_profile=profile,
)
assert kw["extra_body"]["tool_stream"] is True
assert kw["extra_body"]["thinking"] == {"type": "enabled"}
Loading