Skip to content

fix(zai): comprehensive Z.AI/GLM provider support - #24915

Open
nibzard wants to merge 1 commit into
NousResearch:mainfrom
nibzard:fix/zai-provider-comprehensive
Open

fix(zai): comprehensive Z.AI/GLM provider support#24915
nibzard wants to merge 1 commit into
NousResearch:mainfrom
nibzard:fix/zai-provider-comprehensive

Conversation

@nibzard

@nibzard nibzard commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes multiple issues with Z.AI (api.z.ai) GLM models that caused timeouts, empty responses, and missing reasoning support.

Root Causes Identified

  1. tool_stream not sent — 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.
  2. thinking parameter missing — Z.AI uses thinking: {type: "enabled/disabled"} (not OpenRouter's reasoning key) for extended thinking. Without this, GLM-5+ models put all content into reasoning_content with empty content field.
  3. No Coding Plan endpoint support — The zai provider only knew about api.z.ai/api/paas/v4 (standard API), not /api/coding/paas/v4 (Coding Plan).

Changes

File Change
plugins/model-providers/zai/__init__.py Replace bare ProviderProfile with ZaiProfile subclass. Register 4 provider variants: zai (Global), zai-cn (China), zai-coding-global, zai-coding-cn — each with correct base URLs, env vars, and X-Title headers for Coding Plan endpoints.
agent/transports/chat_completions.py In both legacy and profile paths: detect Z.AI endpoints (by provider name or base_url), inject tool_stream=true when tools present, inject thinking parameter. This ensures provider: custom configs pointing at api.z.ai also work.
run_agent.py Add _is_zai detection in _build_api_kwargs, pass is_zai flag to legacy path. Add "z-ai/" to _supports_reasoning_extra_body prefixes. Add new provider names to _anthropic_preserve_dots allowlist.
tests/agent/transports/test_chat_completions.py 15 new tests covering all Z.AI transport behavior (legacy + profile paths, tool_stream, thinking, URL detection).

Provider Variants

Hermes ID Endpoint Env Var
zai api.z.ai/api/paas/v4 ZAI_API_KEY
zai-cn open.bigmodel.cn/api/paas/v4 GLM_API_KEY
zai-coding-global api.z.ai/api/coding/paas/v4 ZAI_CODING_API_KEY
zai-coding-cn open.bigmodel.cn/api/coding/paas/v4 GLM_CODING_API_KEY

Testing

  • 122 tests pass (81 transport + 41 provider profiles)
  • 15 new Z.AI-specific tests covering:
    • Legacy path: tool_stream with/without tools, thinking enabled/disabled, URL-based detection (z.ai, bigmodel.cn), all 4 provider names
    • Profile path: tool_stream, thinking enabled/disabled, all profile variants
    • Negative: non-Z.AI providers don't get tool_stream

Related 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/v4 should switch to:

model:
  provider: zai-coding-global
  api_key: <your-key>

Or set ZAI_CODING_API_KEY env var and use provider: auto.

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
@DeamonDev888

DeamonDev888 commented Jul 9, 2026

Copy link
Copy Markdown

👋 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 (zai / zai-cn / zai-coding-global / zai-coding-cn) is a valid approach. #62080 takes a different one: instead of adding 4 provider entries to the registry, it extends ZAI_ENDPOINTS with 6 candidates (added anthropic-global and anthropic-cn to the existing 4) and lets detect_zai_endpoint() pick the right wire format automatically.

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.

DeamonDev888 pushed a commit to DeamonDev888/hermes-agent that referenced this pull request Jul 11, 2026
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
@DeamonDev888

Copy link
Copy Markdown

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.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for identifying the Z.AI tool-streaming gap. Current main already implements the thinking portion through ZaiProfile.build_api_kwargs_extras() (plugins/model-providers/zai/__init__.py:88-108) and covers omitted preferences, unsupported GLM models, and GLM-5.2 effort mapping in tests/plugins/model_providers/test_zai_profile.py:38-203.

Problems

  • The proposed ZaiProfile.build_extra_body() always enables thinking when no preference is provided and does not gate by model version. That conflicts with current behavior that intentionally omits the field without a preference and avoids unsupported pre-4.5 GLM models (plugins/model-providers/zai/__init__.py:94-101).
  • The proposed build_api_kwargs_extras() returns empty values, which would remove the current GLM-5.2 native reasoning_effort mapping (plugins/model-providers/zai/__init__.py:103-106).

Suggested changes

  • Salvage only tool_stream into the current profile-path extra-body assembly at agent/transports/chat_completions.py:577-610; that path currently receives tools at :537-541 but has no Z.AI tool-stream insertion.
  • Add profile-path and custom-base-URL regression tests while retaining the existing Z.AI reasoning contract.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026

@GottZ GottZ left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/zai ZAI provider sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants