fix(zai): comprehensive Z.AI/GLM provider support - #24915
Conversation
Fixes multiple issues with Z.AI (api.z.ai) GLM models that caused
timeouts, empty responses, and missing reasoning support.
Root causes identified:
- Z.AI has a server-side 30s idle timeout. When tool_call arguments
are generated in a single batch (no tool_stream), the connection
goes silent and gets killed, causing ReadTimeout errors.
- Z.AI uses thinking: {type: enabled/disabled} (not OpenRouter's
reasoning key) for extended thinking. Without this, models put
all content into reasoning_content with empty content field.
- Coding Plan endpoints (api.z.ai/api/coding/paas/v4) require
X-Title header for proper routing.
Changes:
- plugins/model-providers/zai/__init__.py: Replace bare ProviderProfile
with ZaiProfile subclass that injects thinking parameter. Register 4
provider variants: zai (Global), zai-cn (China), zai-coding-global,
zai-coding-cn with correct base URLs, env vars, and X-Title headers.
- agent/transports/chat_completions.py: In BOTH legacy and profile
paths, detect Z.AI endpoints and inject tool_stream=true when tools
are present. Legacy path also injects thinking parameter for
provider:custom users who point base_url at z.ai.
- run_agent.py: Add _is_zai detection in _build_api_kwargs, pass
is_zai flag to legacy transport path. Add z-ai/ to
_supports_reasoning_extra_body reasoning_model_prefixes. Add
new provider names to _anthropic_preserve_dots allowlist.
- tests: 15 new tests covering all Z.AI-specific transport behavior
in both legacy and profile paths.
Closes: NousResearch#12758, NousResearch#14619, NousResearch#16592, NousResearch#11494, NousResearch#18863
Supersedes: NousResearch#13911 (provider split approach)
Relates-to: NousResearch#16479
👋 Superseded — please see the new PR@nibzard thanks for the comprehensive 4-variant proposal. #62080 is the new rebased PR that closes PR #61575. Your 4-variant provider split ( Both approaches are valid. If maintainers prefer the 4-variant split approach from this PR (#24915) over the unified probe approach in #62080, that's a maintainer design decision — both achieve the same routing correctness. Either way, end users get the same result. |
End-to-end fix for the Z.AI credential pool cascade bug discovered
during a live audit of 8 production keys. Five coordinated layers
ensure the right endpoint is selected, the wrong error is not treated
as a payment error, and the user-configured overrides work end-to-end.
== Problem statement ==
Z.AI Coding Plan subscriptions (id.secret.* format) authenticate on
/api/coding/paas/v4 but the metered /api/paas/v4 endpoint also returns
HTTP 200 for glm-5. Without explicit routing, the metered endpoint
gets cached in auth.json and Coding Plan traffic silently draws from
the metered billing pool instead of the subscription.
A second symptom: Z.AI Coding Plan error codes 1113 (Insufficient
balance or no resource package) and 1308 (Usage limit reached for 5
hour) are per-key rolling quotas. They were misclassified as payment
errors via substring traps intended for Vertex AI (resource exhausted)
and Nous Portal (reached your session usage limit). A single exhausted
key cascade-marked every other key in the pool, taking the whole
provider offline.
A third gap: keys on the Anthropic Messages wire (/api/anthropic) were
not in ZAI_ENDPOINTS at all, so detect_zai_endpoint() returned the
wrong URL for any Anthropic-wire Coding Plan key.
== Solution overview ==
Five layers, all coordinated, all tested end-to-end:
Layer 1 - _is_payment_error() exemption for Z.AI Coding Plan
Detects api.z.ai / z.ai/api/coding / zhipuai+coding context plus
codes 1113, 1308 and message patterns. Returns False so the
credential pool rotation layer handles per-key 5h rolling quotas
correctly. Real Z.AI payment errors (e.g. code 1311 plan-block)
continue to flow through the normal payment-fallback path.
Layer 2 - Vision auto-detect zai_openai_urls ordering
The vision helper list now probes Coding Plan endpoints (global +
China) before the metered paas/v4 fallbacks. Coding Plan keys
authenticate on first vision call. Corrects an indentation bug in
the original PR NousResearch#55116.
Layer 3 - Runtime pool base_url re-resolution
_resolve_api_key_provider() now re-invokes _resolve_zai_base_url()
for provider_id == zai so manual-pool entries added via
her mes auth add honor the cached detected_endpoint state in
auth.json and the GLM_BASE_URL env override. Brings manual-pool
parity with the env-seeded path fixed in commit 9e84416.
Probe failures are caught and logged so they never break pool
selection.
Layer 4 - config.yaml model.base_url precedence
New _configured_zai_base_url() helper reads model.base_url from
config.yaml when model.provider is a Z.AI alias (zai / glm / z-ai /
z.ai / zhipu). The precedence chain in _resolve_zai_base_url() is:
1. GLM_BASE_URL env var (highest)
2. model.base_url from config.yaml (when provider is Z.AI)
3. cached detected_endpoint in auth.json
4. live probe of all candidate endpoints
5. registry default
The provider alias guard prevents leakage from non-Z.AI configs.
Layer 5 - Anthropic-wire endpoints in ZAI_ENDPOINTS
Extended from 4 to 6 candidates with anthropic-global and
anthropic-cn. coding-global is probed FIRST (99% of keys that
accept coding endpoint also accept metered, so probing coding
first caches the right URL on first try). anthropic-global is
position 2 (fallthrough for pure Anthropic-wire keys).
New _zai_probe_path() and _zai_probe_body() helpers dispatch the
right HTTP body shape: Anthropic Messages for anthropic-* ids
(/v1/messages, no stream field, anthropic-version header) and
OpenAI chat completions for everything else.
== Audit results ==
Validated against 8 real production keys (see
tests/agent/test_zai_8keys_audit.py):
- 6/8 keys: pure Anthropic-wire subscribers (Anthropic Messages)
- 1/8 keys: pure OpenAI-wire (standard paas/v4)
- 1/8 keys: full-stack (works on all 3 endpoints)
Live test audit (tests/agent/test_zai_live_audit.py) covers every
Z.AI error category against the real api.z.ai API: 200, 1305, 1308,
1113, 401, 402 - all classified correctly.
Before fix: detect_zai_endpoint() returned the wrong endpoint for
6/8 keys. After fix: 9/9 keys correctly routed and verified working.
== Test coverage ==
249 unit + integration tests, 21 live tests (opt-in via env var):
tests/hermes_cli/test_zai_5gateway_extension.py (29 tests)
ZAI_ENDPOINTS structure, dispatch helpers, signature stability,
backward compat, probe order pinning
tests/hermes_cli/test_zai_config_yaml_precedence.py (8 tests)
GLM_BASE_URL > model.base_url > cached > probe > default
tests/agent/test_zai_manual_pool_routing.py (7 tests)
Runtime re-resolution, GLM_BASE_URL forwarding, probe failure
tolerance, non-zai provider leak guard
tests/agent/test_auxiliary_client_zai_payment_classification.py
(14 tests)
_is_payment_error Z.AI Coding Plan exemption, verbatim Z.AI
response bodies, negative cases for Vertex/Bedrock/OpenRouter
tests/agent/test_zai_e2e_pool.py + test_zai_e2e_pool_rotation.py
(8 tests)
Real HTTP round-trip via local mock Z.AI server, 5-key
round_robin, revoked key, probe failure recovery
tests/agent/test_zai_live.py + test_zai_live_audit.py +
test_zai_8keys_audit.py (21 tests)
Live against api.z.ai, opt-in via HERMES_RUN_LIVE=1 or
GLM_AUDIT_KEYS env var. Keys masked to 8-char prefix in all
output. File is safe to commit.
== Related work ==
PR NousResearch#61492 - _is_payment_error() Z.AI exemption (cloned in Layer 1)
PR NousResearch#55116 - vision helper coding endpoint (Layer 2)
PR NousResearch#58088 - config.yaml base_url precedence (Layer 4)
PR NousResearch#24915 - 4-variant provider split (intentionally NOT adopted:
orthogonal refactor, deferred)
PR NousResearch#54643 - /api/anthropic to /api/coding/paas/v4 rewrite
(orthogonal; this PR probes the right wire, NousResearch#54643 rewrites at
runtime if a user forces the OpenAI wire via config)
PR NousResearch#55007 - stream probes without reading bodies (orthogonal;
this PR can refactor _zai_probe_path to use httpx.stream() once
NousResearch#55007 lands)
PR NousResearch#61333 - skip /anthropic to /v1 rewrite (complementary)
PR NousResearch#60753 - preserve /anthropic for custom vision (complementary)
PR NousResearch#32174 - add zai-coding provider (different design choice)
PR NousResearch#60034 - Z.AI Coding overload adaptive backoff (already merged)
== Related issues ==
NousResearch#61487 - cascade _is_payment_error (Layer 1 closes)
NousResearch#61563 - manual-pool routing audit (this PR implements)
NousResearch#47970 - GLM-5.2 context_length fallback (Layer 5 helps)
NousResearch#55112 - auxiliary vision hardcoded zai (Layer 5 helps)
NousResearch#47685 - Hermes Agent prompt block on Z.ai (orthogonal)
== Test commands ==
Unit + integration (no network):
pytest tests/hermes_cli/test_api_key_providers.py \
tests/hermes_cli/test_zai_config_yaml_precedence.py \
tests/hermes_cli/test_zai_5gateway_extension.py \
tests/agent/test_auxiliary_client.py::TestIsPaymentError \
tests/agent/test_auxiliary_client_zai_payment_classification.py \
tests/agent/test_zai_manual_pool_routing.py \
tests/agent/test_zai_e2e_pool.py \
tests/agent/test_zai_e2e_pool_rotation.py -v
Live (consumes Z.AI quota, requires HERMES_RUN_LIVE=1):
pytest tests/agent/test_zai_live.py \
tests/agent/test_zai_live_audit.py \
tests/agent/test_zai_8keys_audit.py -v --runlive
Cross-platform check:
scripts/check-windows-footguns.py --diff upstream/main
== Security ==
- All live test keys read from env var (GLM_TEST_KEYS, GLM_AUDIT_KEYS,
GLM_WORKING_KEY) NEVER from files. Files are safe to commit
publicly.
- All keys masked to 8-char prefix in any printed output.
- No hardcoded credentials, no path leaks, no LAN IPs.
- Privacy scan: 0 secrets, 0 private paths in diff.
Co-authored-by: Hermes triage bot <bot@nousresearch.com>
Refs: NousResearch#61487, NousResearch#61563, PR NousResearch#61492, PR NousResearch#55116, PR NousResearch#58088, PR NousResearch#24915,
PR NousResearch#54643, PR NousResearch#55007, PR NousResearch#61333, PR NousResearch#60753, PR NousResearch#32174,
PR NousResearch#60034, NousResearch#47970, NousResearch#55112, NousResearch#47685
|
Related: our PR #62467 takes a different approach to the same root cause. Instead of splitting Z.AI into 4 provider variants (zai, zai-cn, zai-coding-global, zai-coding-cn), we use a single unified resolver that detects the correct endpoint per-key at runtime. Both approaches are valid; ours avoids adding provider-registry bloat. Would welcome your feedback on #62467. |
|
Thanks for identifying the Z.AI tool-streaming gap. Current main already implements the thinking portion through Problems
Suggested changes
Automated hermes-sweeper review. |
GottZ
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Summary
Four PRs address overlapping Z.AI failures: #24915 covers tool-call streaming, thinking behavior, and endpoint variants; #46615/#46666 target missing picker models and the Coding Plan URL; #46724 targets regional model discovery and omitted free Flash models. Current main already provides Z.AI thinking handling, resolved-endpoint fetching, and live-plus-curated catalog merging, leaving tool_stream and any independently verified missing model IDs as the substantive gaps.
Related pull requests
- #24915
related— (+324/-9) — salvage and narrow: the diff uniquely addresses the reported 30-second tool-call timeout by adding tool_stream when Z.AI tools are present, including custom Z.AI URLs. Keep the contributor keep_open review in force, but remove the provider split and thinking replacement because they conflict with current ZaiProfile model gating, omitted-preference behavior, and GLM-5.2 reasoning_effort mapping. - #46615 [closed]
related— (+8711/-1221) — superseded: this closed PR remains relevant as the original model-picker proposal, but its Z.AI change is buried among thousands of unrelated stale-branch changes. The contributor explicitly rejected that scope, and the clean continuation is #46666. - #46666
related— (+47/-2) — mostly obsolete, with limited salvage: its current override accepts and forwards base_url, but current main already performs live-plus-curated merging and resolved-endpoint discovery, while changing the profile base_url does not change the runtime default. Despite the keep_open review on #46666, the diff now shows only candidate missing IDs such as glm-4.5-air/glm-4.6 remain worth consolidating after verification. - #46724
related— (+27/-1) — salvage verified free-model IDs only: resolved China-endpoint fetching has already landed on main, while this diff's monkey-patched fetch_models wrapper neither accepts nor forwards base_url and would regress authenticated discovery. Despite the keep_open review on #46724, its five verified free Flash IDs can be folded into a proper ZaiProfile.fetch_models implementation; the wrapper and removal of glm-5.2 should not merge.
Duplicates
#46615 and #46666 are successive versions of substantially the same curated-model/base-URL change; #46724 overlaps their catalog and endpoint work but contributes a narrower free-model allowlist. #24915 overlaps endpoint/provider handling but uniquely contains the tool_stream fix.
Suggested consolidation
Merge #24915 only after narrowing it to the contributor-requested tool_stream fix on top of the current ZaiProfile, preserving existing thinking and GLM-5.2 reasoning behavior; optionally fold in only independently verified missing IDs from #46666/#46724 through the existing catalog path. Then #46615 can remain closed as superseded, and #46666 plus #46724 can be closed as consolidated duplicates despite their keep_open reviews because current main already contains their endpoint and primary picker fixes, while their remaining useful model IDs are separable from the obsolete implementations.
Cross-PR triage: Reviewed 4 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 631 kB of PR diffs, 8 kB of issue/PR text, 8 kB of discussion (9 comments), 1 verify verdict. verdicts reflect diff content, not PR titles. Part of an automated triage batch.
Summary
Fixes multiple issues with Z.AI (api.z.ai) GLM models that caused timeouts, empty responses, and missing reasoning support.
Root Causes Identified
tool_streamnot sent — Z.AI has a server-side 30s idle timeout. Whentool_callarguments are generated in a single batch (notool_stream), the connection goes silent and gets killed, causingReadTimeouterrors.thinkingparameter missing — Z.AI usesthinking: {type: "enabled/disabled"}(not OpenRouter'sreasoningkey) for extended thinking. Without this, GLM-5+ models put all content intoreasoning_contentwith emptycontentfield.zaiprovider only knew aboutapi.z.ai/api/paas/v4(standard API), not/api/coding/paas/v4(Coding Plan).Changes
plugins/model-providers/zai/__init__.pyProviderProfilewithZaiProfilesubclass. Register 4 provider variants:zai(Global),zai-cn(China),zai-coding-global,zai-coding-cn— each with correct base URLs, env vars, andX-Titleheaders for Coding Plan endpoints.agent/transports/chat_completions.pytool_stream=truewhen tools present, injectthinkingparameter. This ensuresprovider: customconfigs pointing atapi.z.aialso work.run_agent.py_is_zaidetection in_build_api_kwargs, passis_zaiflag to legacy path. Add"z-ai/"to_supports_reasoning_extra_bodyprefixes. Add new provider names to_anthropic_preserve_dotsallowlist.tests/agent/transports/test_chat_completions.pyProvider Variants
zaiapi.z.ai/api/paas/v4ZAI_API_KEYzai-cnopen.bigmodel.cn/api/paas/v4GLM_API_KEYzai-coding-globalapi.z.ai/api/coding/paas/v4ZAI_CODING_API_KEYzai-coding-cnopen.bigmodel.cn/api/coding/paas/v4GLM_CODING_API_KEYTesting
tool_streamwith/without tools,thinkingenabled/disabled, URL-based detection (z.ai,bigmodel.cn), all 4 provider namestool_stream,thinkingenabled/disabled, all profile variantstool_streamRelated PRs
Closes #12758 (tool_stream fix), #14619 (tool_stream in transport), #16592 (thinking injection), #11494 (thinking support)
Relates-to #13911 (4-plan provider split), #16479 (preserved thinking), #18863
Config Migration
Users currently on
provider: custom+base_url: https://api.z.ai/api/coding/paas/v4should switch to:Or set
ZAI_CODING_API_KEYenv var and useprovider: auto.