feat(llm): support Z.ai structured output - #372
Conversation
Use the coding API by default, preserve JSON-object mode through LiteLLM, and enable the live-verified GLM-5.2 extraction tool loop while rejecting incompatible structured fallback ladders.
📝 WalkthroughWalkthroughChangesAdds Z.ai coding-endpoint routing and structured-output transport handling, including prompt-based JSON schema instructions, fallback strategy validation, stricter empty-response parsing, and exact tool-calling overrides. Documentation and unit tests cover the new behavior. Z.ai structured-output support
Sequence Diagram(s)sequenceDiagram
participant Client
participant _build_completion_params
participant StructuredOutputMixin
participant ZaiCodingEndpoint
Client->>_build_completion_params: provide model, messages, and response schema
_build_completion_params->>StructuredOutputMixin: select and apply transport strategy
StructuredOutputMixin->>ZaiCodingEndpoint: route request with JSON-object format and schema instruction
ZaiCodingEndpoint-->>Client: return structured completion
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
reflexio/server/llm/_litellm_text_generation.py (1)
424-441: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftZ.ai’s forced
api_baseshould not leak into cross-provider fallbacks.params["api_base"] = _ZAI_CODING_API_BASEis set once on the shared params dict, and LiteLLM plain-string fallbacks reuse that same kwargs set for every rung. Azai/*primary with anopenai/*fallback will send the fallback attempt to the Z.ai endpoint instead of the target provider, defeating failover. Use per-fallback overrides or avoid forcingapi_basewhen the ladder spans other providers.🤖 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 `@reflexio/server/llm/_litellm_text_generation.py` around lines 424 - 441, Prevent the Z.ai-specific api_base in the shared params from being reused by cross-provider fallback attempts. Update the fallback handling around fallback_models and the shared params construction so each fallback receives its provider-appropriate api_base, or omit the forced Z.ai api_base whenever fallback_models includes a different provider such as openai/*. Preserve the Z.ai endpoint for the primary and same-provider fallbacks.
🧹 Nitpick comments (1)
tests/server/llm/test_litellm_client_unit.py (1)
1056-1061: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winLGTM! Solid coverage for the new zai transport and fallback-compatibility behavior. One gap worth closing:
test_text_fallback_does_not_require_matching_transport(Lines 1406-1418) only asserts the returned fallback list, notparams["api_base"]— it doesn't catch the cross-providerapi_baseleak described in the_litellm_text_generation.pyreview (a zai primary forcesapi_baseonto the whole ladder, including this exact fallback scenario).Also applies to: 1276-1419
🤖 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 `@tests/server/llm/test_litellm_client_unit.py` around lines 1056 - 1061, Extend test_text_fallback_does_not_require_matching_transport to assert params["api_base"] for the fallback request, ensuring a zai primary does not leak its api_base to the text fallback ladder. Preserve the existing fallback-list assertions and cover the expected cross-provider behavior.
🤖 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.
Outside diff comments:
In `@reflexio/server/llm/_litellm_text_generation.py`:
- Around line 424-441: Prevent the Z.ai-specific api_base in the shared params
from being reused by cross-provider fallback attempts. Update the fallback
handling around fallback_models and the shared params construction so each
fallback receives its provider-appropriate api_base, or omit the forced Z.ai
api_base whenever fallback_models includes a different provider such as
openai/*. Preserve the Z.ai endpoint for the primary and same-provider
fallbacks.
---
Nitpick comments:
In `@tests/server/llm/test_litellm_client_unit.py`:
- Around line 1056-1061: Extend
test_text_fallback_does_not_require_matching_transport to assert
params["api_base"] for the fallback request, ensuring a zai primary does not
leak its api_base to the text fallback ladder. Preserve the existing
fallback-list assertions and cover the expected cross-provider behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 745e691c-d50a-49a3-876c-63c07cedeae8
📒 Files selected for processing (7)
reflexio/server/README.mdreflexio/server/llm/_litellm_structured_output.pyreflexio/server/llm/_litellm_text_generation.pyreflexio/server/llm/llm_utils.pyreflexio/server/llm/tools.pytests/server/llm/test_litellm_client_unit.pytests/server/llm/test_tools.py
## Why `main` is red. `test_consolidator_calls_litellm_with_fallback_configured` has been failing since #372 landed: ``` E ValueError: Structured-output fallback models must use the same transport strategy as 'gpt-test' (pydantic_passthrough); incompatible fallbacks: ['gpt-5.4-mini'] ``` ## Root cause `_build_real_client_consolidator` patched the generation site var to a fake `"gpt-test"`, so the `LiteLLMConfig(model=...)` each test passed never reached the request — `PlaybookConsolidator` reads its request model from the site var (`deduplication_utils.py:191`), not from the client config, and passes it as `model=self.model_name` (`components/consolidator.py:743`). #372 added `_validate_structured_fallback_strategies`, which resolves a transport strategy per model and rejects a ladder whose fallbacks disagree with the primary. Measured strategies: | model | provider | strategy | |---|---|---| | `gpt-test` | *(unresolvable)* | `pydantic_passthrough` | | `gpt-5.4-mini` | openai | `native_json_schema` | | `minimax/MiniMax-M3` | minimax | `native_json_schema` | The fake model made the pairing artificially incompatible. The guard itself is correct — the test was asserting against a model it never intended to use. ## Fix Pin the patched site var to `config.model`. The fallback-configured case now exercises a realistic production pairing (MiniMax primary + OpenAI fallback, both `native_json_schema`). The second test (`claude-code/...`, no fallbacks) never reaches the guard and is unaffected. Also included: a `style:` commit applying `ruff format` to `client/client.py`, the only file in the package `ruff format --check` flagged. No behavior change. ## Verification - `pytest tests/server/services/playbook/test_playbook_consolidator_integration.py` — 14 passed (1 failed before) - `ruff format --check reflexio tests` — 821 files already formatted - `ruff check` + `pyright` clean on both touched files <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved filtering of outdated operation status updates, helping prevent stale results from being processed. * **Tests** * Updated integration coverage to validate structured-output fallback behavior using the configured primary model. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Summary
zai/*completions to the Z.ai coding API while preserving custom-endpoint and per-call overrides.zai/glm-5.2extraction-agent tool loop despite LiteLLM 1.82.2's stale capability result.Changes
Z.ai request strategy
response_format={"type":"json_object"}through LiteLLM'sdrop_params=Truebehavior.Endpoint and capability handling
https://api.z.ai/api/coding/paas/v4as the built-inzai/*completion endpoint.zai/glm-5.2function-calling override backed by dependent live tool-call evidence.Safety and regression coverage
Test Plan
uv run ruff checkon changed Python sources and testsuv run pyrighton changed LLM sources and testsuv run pytest -q --no-cov tests/server/llm/test_litellm_client_unit.py tests/server/llm/test_tools.py -k 'SupportsToolCallingOverrides or MaybeParseStructuredOutput or StrictStructuredOutputRequest'— 39 passedzai/glm-5.2, omitting--base-url— single tool call and dependentget_weather -> convert_temperature -> parsed terminussequence both passedSummary by CodeRabbit
New Features
Bug Fixes