(MOT-3905) feat(provider-zai): add Z.AI (GLM) Chat Completions provider worker#443
Conversation
Fork of provider-xai targeting Z.AI's OpenAI-compatible endpoint
(api.z.ai/api/paas/v4). Z.AI exposes no models-listing endpoint, so a
curated catalog (GLM 5.2 → 4.5 text families plus vision rows, with
docs-sourced limits and pricing) replaces live discovery — reconciled
through the router and gated on a configured credential.
GLM specifics: an explicit thinking {enabled|disabled} toggle on every
current GLM model, reasoning_effort on GLM-5.2+ only, tool_stream on
GLM-4.6+, max_tokens (Z.AI does not document max_completion_tokens),
and json_object-only structured output with a report-and-continue
warning when a schema is requested.
Also hardens the inherited stream path: UTF-8-boundary-safe chunk
reassembly (CJK output split across transport chunks no longer corrupts
to U+FFFD) and a cap on wire-controlled tool_call indexes.
Registers the worker in the release workflows and the root README
modules table.
Coding Plan subscription keys are the common case and hard-fail (business code 1113) on the general pay-as-you-go endpoint, so the default api_url is now api.z.ai/api/coding/paas/v4/chat/completions. The catalog follows the resolved endpoint: the coding endpoint reconciles only the plan's models (glm-5.2, glm-5-turbo, glm-4.7); the general endpoint or a custom OpenAI-compatible server gets the full curated table. Pay-as-you-go operators override api_url in the llm-router config slice.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
skill-check — worker0 verified, 38 skipped (no docs/).
Four for four. Nicely done. |
|
Warning Review limit reached
Next review available in: 37 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughIntroduces a new Changesprovider-zai worker addition
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
clippy 1.96 denies items_after_test_module under -D warnings.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (4)
provider-zai/src/errors.rs (1)
59-61: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd a unit test for the
1113business-code mapping.This is called out as a headline feature of the PR ("Classifying Z.AI business code 1113 as a permanent error so routing fails fast"), and the code path is confirmed correct (Z.AI does send
codeas a JSON string), but no test in this file exercises it directly.✅ Suggested test
#[test] fn zai_envelope_codes_are_honored() { let body = r#"{"error":{"message":"This model's maximum context length is 400000 tokens.","type":"invalid_request_error","code":"context_length_exceeded"}}"#; assert_eq!(classify(Some(400), body), ErrorKind::ContextOverflow); let body = r#"{"error":{"message":"You exceeded your current quota.","type":"insufficient_quota","code":"insufficient_quota"}}"#; assert_eq!(classify(Some(429), body), ErrorKind::Permanent); let body = r#"{"error":{"message":"Incorrect API key provided.","type":"invalid_request_error","code":"invalid_api_key"}}"#; assert_eq!(classify(Some(401), body), ErrorKind::AuthExpired); + + // Z.AI business code 1113: insufficient balance / no resource package, + // frequently surfaced with a 429 status — must not be treated as a + // retryable rate limit. + let body = r#"{"error":{"code":"1113","message":"Insufficient balance or no resource package. Please recharge."}}"#; + assert_eq!(classify(Some(429), body), ErrorKind::Permanent); }Also applies to: 111-189
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@provider-zai/src/errors.rs` around lines 59 - 61, Add a focused unit test in the errors module that exercises the business-code mapping for Z.AI code 1113 and asserts it returns ErrorKind::Permanent. Use the existing mapping entry in the error classification logic (the match arm handling the string code) and mirror the style of any nearby tests in this file so the new test directly covers the JSON-string business code path.provider-zai/src/upstream.rs (1)
92-98: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winSSE framing assumes LF-only boundaries and a space after
data:.Two spec-robustness gaps that only bite with certain OpenAI-compatible endpoints/proxies (your tests and stubs only exercise
\n\n):
buf.find("\n\n")won't match a CRLF event boundary (\r\n\r\ncontains no adjacent\n\n), so with CRLF framing events would never dispatch and content is lost.strip_prefix("data: ")requires the single space, but per the SSE spec the space afterdata:is optional; adata:{...}line is silently dropped (and[DONE]missed).Please confirm Z.AI's wire framing; if CRLF or spaceless
data:are possible, harden both.♻️ Optional-space handling for
data_linefn data_line(block: &str) -> Option<&str> { block .lines() - .filter_map(|l| l.strip_prefix("data: ")) + .filter_map(|l| l.strip_prefix("data:").map(|r| r.strip_prefix(' ').unwrap_or(r))) .next_back() }Also applies to: 167-171
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@provider-zai/src/upstream.rs` around lines 92 - 98, The SSE parsing in upstream.rs is too strict for some OpenAI-compatible endpoints: the event splitter in the stream loop and the data_line helper only handle LF-only boundaries and require a space after data:. Update the parsing around data_line and the buffer scanning logic to accept CRLF event delimiters as well as optional whitespace after data:, while preserving the existing behavior for current tests and stubs.provider-zai/src/sse.rs (1)
1-10: 🧹 Nitpick | 🔵 TrivialCI clippy gate is red for this crate — please resolve before merge.
cargo clippy --all-targets --all-features -- -D warningsfails with 1 error in theprovider-zailib test build. I couldn't reproduce clippy in the review sandbox (no toolchain) and couldn't localize the offending lint to the three streaming files under review, so the error may live elsewhere in the crate. Please run the command locally and address it.Want me to open a tracking issue for the clippy failure?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@provider-zai/src/sse.rs` around lines 1 - 10, The crate-level clippy gate is failing in the provider-zai lib test build, and the offending lint may be outside the streaming files currently under review. Run cargo clippy --all-targets --all-features -- -D warnings locally, identify the exact warning, and fix it in the relevant symbol(s) such as the sse.rs state machine imports or related provider-zai code. If the lint is in nearby helper code, adjust the implementation to satisfy clippy without weakening the warning level.Source: Pipeline failures
provider-zai/src/register.rs (1)
63-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueBackoff/unreachable logic is correct but tightly coupled to the
0..5/attempt < 4pairing.Every arm returns except the guarded retry arm, so the loop always exits via
returnand the trailingunreachable!()is sound — but if either the range or the guard bound is changed independently later, this becomes a silent panic trap. Low risk today; consider deriving the guard from the range bound (e.g., a namedMAX_ATTEMPTSconstant) for resilience against future edits.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@provider-zai/src/register.rs` around lines 63 - 79, The retry/backoff logic in persist_registration_token is correct today, but it is tightly coupled to the hardcoded 0..5 loop and attempt < 4 guard. Refactor this to use a shared named bound (for example, a MAX_ATTEMPTS-style constant) so the retry condition and loop range stay in sync, and the trailing unreachable!() remains safe if the attempt count is changed later.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@provider-zai/src/curated.rs`:
- Around line 156-159: The Coding Plan model allowlist in CODING_PLAN_IDS is
missing glm-5.1, which causes it to be filtered out of the coding endpoint
picker. Update the curated model list in curated.rs where CODING_PLAN_IDS is
defined so it includes glm-5.1 alongside glm-5.2, glm-5-turbo, and glm-4.7,
keeping the existing coding-endpoint filtering behavior intact.
In `@provider-zai/src/discovery.rs`:
- Around line 50-87: Move make_refresh_models so it is defined before the
#[cfg(test)] mod tests block in discovery.rs; clippy’s items_after_test_module
lint is triggered by having a non-test item after the test module. Keep the
function body and signature unchanged, and place it alongside the other public
discovery helpers such as catalog_for and refresh_models so the module order
satisfies the lint.
In `@provider-zai/src/reasoning.rs`:
- Around line 36-38: The accepts_effort helper currently matches only model
names starting with glm-5.2, which does not align with the stated support for
GLM-5.2 and newer. Update accepts_effort in reasoning.rs to use a version-aware
comparison against the GLM family so later versions like glm-5.3 and glm-6.x are
also accepted, or otherwise tighten the wording/docs if the intent is only
glm-5.2.
In `@provider-zai/src/request.rs`:
- Around line 74-79: The authorization header construction in build_headers is
using cfg.credential_value directly inside format!, which Clippy rejects under
-D warnings. Introduce a local binding for the credential value in
build_headers, then use that binding in the Bearer token formatting so the
header creation compiles cleanly.
In `@provider-zai/src/wire/names.rs`:
- Around line 4-10: The tool-name helpers in encode_tool_name and
decode_tool_name are not bijective because literal double underscores collide
with encoded namespace separators. Update the names.rs logic so encode_tool_name
preserves enough information to round-trip uniquely, either by escaping literal
underscores before replacing namespace separators or by rejecting ids containing
"__", and make decode_tool_name reverse that scheme reliably.
---
Nitpick comments:
In `@provider-zai/src/errors.rs`:
- Around line 59-61: Add a focused unit test in the errors module that exercises
the business-code mapping for Z.AI code 1113 and asserts it returns
ErrorKind::Permanent. Use the existing mapping entry in the error classification
logic (the match arm handling the string code) and mirror the style of any
nearby tests in this file so the new test directly covers the JSON-string
business code path.
In `@provider-zai/src/register.rs`:
- Around line 63-79: The retry/backoff logic in persist_registration_token is
correct today, but it is tightly coupled to the hardcoded 0..5 loop and attempt
< 4 guard. Refactor this to use a shared named bound (for example, a
MAX_ATTEMPTS-style constant) so the retry condition and loop range stay in sync,
and the trailing unreachable!() remains safe if the attempt count is changed
later.
In `@provider-zai/src/sse.rs`:
- Around line 1-10: The crate-level clippy gate is failing in the provider-zai
lib test build, and the offending lint may be outside the streaming files
currently under review. Run cargo clippy --all-targets --all-features -- -D
warnings locally, identify the exact warning, and fix it in the relevant
symbol(s) such as the sse.rs state machine imports or related provider-zai code.
If the lint is in nearby helper code, adjust the implementation to satisfy
clippy without weakening the warning level.
In `@provider-zai/src/upstream.rs`:
- Around line 92-98: The SSE parsing in upstream.rs is too strict for some
OpenAI-compatible endpoints: the event splitter in the stream loop and the
data_line helper only handle LF-only boundaries and require a space after data:.
Update the parsing around data_line and the buffer scanning logic to accept CRLF
event delimiters as well as optional whitespace after data:, while preserving
the existing behavior for current tests and stubs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9e781551-e2ea-441e-90bf-e7a338e5557d
⛔ Files ignored due to path filters (1)
provider-zai/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (37)
.github/workflows/create-tag.yml.github/workflows/release.ymlREADME.mdprovider-zai/.gitignoreprovider-zai/Cargo.tomlprovider-zai/README.mdprovider-zai/build.rsprovider-zai/config.yamlprovider-zai/iii-permissions.yamlprovider-zai/iii.worker.yamlprovider-zai/prompts/identity.txtprovider-zai/src/config.rsprovider-zai/src/curated.rsprovider-zai/src/discovery.rsprovider-zai/src/errors.rsprovider-zai/src/lib.rsprovider-zai/src/main.rsprovider-zai/src/manifest.rsprovider-zai/src/reasoning.rsprovider-zai/src/register.rsprovider-zai/src/request.rsprovider-zai/src/router_client.rsprovider-zai/src/sse.rsprovider-zai/src/state.rsprovider-zai/src/stream_fn.rsprovider-zai/src/surface.rsprovider-zai/src/upstream.rsprovider-zai/src/wire/messages.rsprovider-zai/src/wire/mod.rsprovider-zai/src/wire/names.rsprovider-zai/src/wire/tools.rsprovider-zai/tests/golden/schemas/provider.zai.on_router_ready.jsonprovider-zai/tests/golden/schemas/provider.zai.refresh_models.jsonprovider-zai/tests/golden/schemas/provider.zai.stream.jsonprovider-zai/tests/integration.rsprovider-zai/tests/schemas.rsprovider-zai/tests/support/mod.rs
| pub fn build_headers(cfg: &ZaiConfig) -> Vec<(&'static str, String)> { | ||
| vec![ | ||
| ("authorization", format!("Bearer {}", cfg.credential_value)), | ||
| ("content-type", "application/json".to_string()), | ||
| ] | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Repo files of interest:\n'
git ls-files 'provider-zai/src/request.rs' 'Cargo.toml' 'provider-zai/Cargo.toml' '.cargo/config.toml' 'rust-toolchain.toml' 2>/dev/null || true
printf '\n--- provider-zai/src/request.rs ---\n'
cat -n provider-zai/src/request.rs
printf '\n--- Cargo.toml (root, if present) ---\n'
if [ -f Cargo.toml ]; then cat -n Cargo.toml; fi
printf '\n--- provider-zai/Cargo.toml (if present) ---\n'
if [ -f provider-zai/Cargo.toml ]; then cat -n provider-zai/Cargo.toml; fi
printf '\n--- .cargo/config.toml (if present) ---\n'
if [ -f .cargo/config.toml ]; then cat -n .cargo/config.toml; fi
printf '\n--- rust-toolchain.toml (if present) ---\n'
if [ -f rust-toolchain.toml ]; then cat -n rust-toolchain.toml; fiRepository: iii-hq/workers
Length of output: 10105
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for lint configuration and the exact format usage.
rg -n --hidden --glob '!target' 'uninlined_format_args|-D warnings|deny\(warnings\)|clippy::' .
printf '\n--- occurrences of format!("Bearer {}" ---\n'
rg -n --hidden --glob '!target' 'format!\("Bearer \{\}"' provider-zai .Repository: iii-hq/workers
Length of output: 3364
🌐 Web query:
Rust format string capture field access edition 2021 docs
💡 Result:
In Rust, the ability to capture variables directly in format strings was introduced in Rust 1.58.0 [1]. This feature allows you to use identifiers from the local scope inside curly braces, such as println!("Hello, {person}!");, which captures the local variable person [1]. Regarding field access (e.g., format!("{obj.field}")): this syntax is not supported as an implicit capture in standard format strings [1][2]. Format strings can only capture plain identifiers [1]. If you need to access a field, you must either assign the field value to a local variable first or use the explicit named argument style [1]: // Not supported: // println!("{person.name}"); // Recommended approach: let name = &person.name; println!("{name}"); // Alternative: println!("{}", person.name); While there have been community discussions and RFC proposals (such as RFC 3626) to extend implicit argument capturing to allow field access syntax (e.g., var.field), this has not been implemented as of July 2026 [2][3]. Note that the 2021 edition of Rust introduced other significant features like disjoint capture for closures, but implicit field access in format strings is not part of that edition's specification [4].
Citations:
- 1: https://blog.rust-lang.org/2022/01/13/Rust-1.58.0/
- 2: Extend format_args implicit arguments to allow field access rust-lang/rfcs#3626
- 3: Extend format_args implicit arguments to allow field access rust-lang/rfcs#3626
- 4: https://blog.rust-lang.org/2021/10/21/Rust-1.56.0/
Use a local binding here format!("Bearer {credential_value}") only works with a plain identifier, so cfg.credential_value can’t be captured directly. Bind it first: let credential_value = &cfg.credential_value; format!("Bearer {credential_value}"). CI runs clippy with -D warnings, so the current form blocks the build.
🧰 Tools
🪛 GitHub Actions: CI / provider-zai rust lint + test
[error] Command failed: cargo clippy --all-targets --all-features -- -D warnings (exit code 101). could not compile provider-zai (lib test) due to the clippy error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@provider-zai/src/request.rs` around lines 74 - 79, The authorization header
construction in build_headers is using cfg.credential_value directly inside
format!, which Clippy rejects under -D warnings. Introduce a local binding for
the credential value in build_headers, then use that binding in the Bearer token
formatting so the header creation compiles cleanly.
Source: Pipeline failures
| pub fn encode_tool_name(name: &str) -> String { | ||
| name.replace("::", "__") | ||
| } | ||
|
|
||
| pub fn decode_tool_name(name: &str) -> String { | ||
| name.replace("__", "::") | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether any registered iii function id contains a literal double underscore,
# which would collide with the "::" -> "__" encoding used here.
rg -nP '"[a-zA-Z0-9_-]*__[a-zA-Z0-9_-]*"' --type=rust -g '!provider-zai/**'Repository: iii-hq/workers
Length of output: 152
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find encode/decode usage and any validation/schema around tool names.
rg -n "encode_tool_name|decode_tool_name|tool[_ -]?name|function[_ -]?id|name.*64|a-zA-Z0-9_-|__" provider-zai provider-* . \
--glob '!**/target/**' --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' || trueRepository: iii-hq/workers
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== matching files ==\n'
git ls-files 'provider-zai/**' 'provider-openai-codex/src/wire/names.rs' 'provider-openai-codex/src/sse.rs' 'acp/src/handler.rs'
printf '\n== names.rs ==\n'
sed -n '1,120p' provider-openai-codex/src/wire/names.rs
printf '\n== sse decode path ==\n'
sed -n '250,290p' provider-openai-codex/src/sse.rs
printf '\n== acp __-containing function ids ==\n'
sed -n '660,690p' acp/src/handler.rsRepository: iii-hq/workers
Length of output: 5061
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== provider-zai function ids containing __ ==\n'
rg -n '"[^"]*__[^"]*"' provider-zai/src provider-zai/tests --glob '*.rs' || true
printf '\n== provider-zai wire usage ==\n'
sed -n '1,220p' provider-zai/src/wire/messages.rs
printf '\n---\n'
sed -n '1,120p' provider-zai/src/wire/tools.rs
printf '\n---\n'
sed -n '250,290p' provider-zai/src/sse.rsRepository: iii-hq/workers
Length of output: 14370
Make tool-name encoding bijective provider-zai/src/wire/names.rs:4-10 maps both web::fetch and web__fetch to web__fetch, so decode_tool_name can return the wrong function id. Escape literal underscores or reject __ in ids.
🧰 Tools
🪛 GitHub Actions: CI / provider-zai rust lint + test
[error] Command failed: cargo clippy --all-targets --all-features -- -D warnings (exit code 101). could not compile provider-zai (lib test) due to the clippy error.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@provider-zai/src/wire/names.rs` around lines 4 - 10, The tool-name helpers in
encode_tool_name and decode_tool_name are not bijective because literal double
underscores collide with encoded namespace separators. Update the names.rs logic
so encode_tool_name preserves enough information to round-trip uniquely, either
by escaping literal underscores before replacing namespace separators or by
rejecting ids containing "__", and make decode_tool_name reverse that scheme
reliably.
…ng_effort The coding-tools guide documents glm-5.1, glm-5, and glm-4.5-air as valid model codes on the coding endpoint alongside the featured GLM-5.2 / GLM-5-Turbo / GLM-4.7 tier, so the Coding Plan slice now carries all six. accepts_effort now parses the numeric GLM version instead of matching the literal glm-5.2 prefix: the docs say "GLM-5.2 and newer", so glm-5.3 / glm-6 qualify without a code change while glm-5-turbo and glm-5v-turbo stay excluded.
Summary
New
provider-zaiworker: Z.AI's GLM models (GLM-5.2, GLM-5-Turbo, GLM-4.7, and the wider lineup) behindllm-router, targeting the OpenAI-compatible Chat Completions API.api.z.ai/api/coding/paas/v4) — subscription keys work with zero config, and they hard-fail (business code 1113) on the general endpoint. Pay-as-you-go operators overrideapi_urltoapi.z.ai/api/paas/v4for the full lineup.thinking {enabled|disabled}toggle on every current GLM model,reasoning_efforton GLM-5.2+ only,tool_streamon GLM-4.6+,max_tokens(Z.AI doesn't documentmax_completion_tokens),json_object-only structured output with a report-and-continue warning when a schema is requested.tool_callindexes.permanentso the router fails fast instead of retrying.create-tag.ymloptions,release.ymltag pattern (provider-zai/v*), root README modules row.Testing
cargo fmt --check,cargo clippy --all-targets -D warningsclean; 76 unit tests (request assembly, SSE grammar incl. thinking/tool-call streaming, curated catalog invariants, error taxonomy, UTF-8 reassembly).provider::zai::*functions.router::chatstream with cost fill, upstream 401 →auth_expired, curated-catalog reconcile, re-declare after router registry loss.Fixes MOT-3905
Summary by CodeRabbit
New Features
provider-zaiworker with streaming chat support, model refresh, and router registration.Documentation
Tests