Skip to content

feat(llm): transport-aware per-rung LLM fallback walk + GLM (glm-5.2) support - #375

Merged
guangyu-reflexio merged 10 commits into
mainfrom
feat/transport-aware-llm-fallback-glm
Jul 21, 2026
Merged

feat(llm): transport-aware per-rung LLM fallback walk + GLM (glm-5.2) support#375
guangyu-reflexio merged 10 commits into
mainfrom
feat/transport-aware-llm-fallback-glm

Conversation

@guangyu-reflexio

@guangyu-reflexio guangyu-reflexio commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Make the LLM fallback ladder transport-aware so a fallback can mix providers whose structured-output transports differ (e.g. MiniMax native_json_schema primary → Z.ai GLM prompt_json_object fallback), and default zai to the live-verified flagship glm-5.2.

Previously reflexio delegated the fallback ladder to LiteLLM's native fallbacks=[...], which reuses a single request-params dict across the ladder — so the primary's response_format was sent verbatim to every fallback, and a guard hard-rejected mixed-transport ladders. That blocked using GLM as a fallback behind a MiniMax primary.

What changed

  • Reflexio-owned per-rung walk (_litellm_text_generation.py): reflexio walks [primary, *fallbacks] itself, rebuilding request params per rung via the existing _build_completion_params (transport/api_base/timeout are already keyed on the model). fallbacks is never handed to litellm.completion. The transport-compatibility guard (_validate_structured_fallback_strategies) is deleted — mixed-transport ladders are safe by construction.
  • Both paths, one walker: the plain path and the validator/repair path route through the same walk. Each rung is entered at most once; a fallback rung reached via repair gets the original prompt, not the primary's cross-model repair conversation.
  • Behavior change (fleet-wide): a parse/validation-exhausted primary now advances to the fallback (previously it raised without trying the fallback).
  • Per-rung timeout: each rung gets its own single-attempt hard timeout with num_retries=0, preserving the PYTHON-FASTAPI-62 property (a hung primary can't block the fallback) per rung.
  • Observability: loop-index-driven event=llm_fallback_used signal preserving the existing Sentry tag keys (llm.fallback_used/llm.primary_model/llm.fallback_model) + a new reason (transport_error | parse_exhausted | cap_saturated).
  • Fail-closed concurrency cap (_provider_concurrency.py): REFLEXIO_LLM_FAIL_CLOSED_PROVIDERS makes a provider raise on cap saturation (protects the fixed-quota GLM coding plan from a fallback stampede); REFLEXIO_LLM_PROVIDER_MAX_CONCURRENCY_OVERRIDES sets per-provider caps. Default = unchanged fail-open.
  • Boot-time fallback-key check (model_defaults.py): fails loud at startup when a known key-based fallback provider has no key; warns (doesn't refuse boot) for litellm providers reflexio can't key-validate (bedrock/vertex/etc.).
  • zai default glm-5.1 → glm-5.2 (only 5.2 is live-verified).

Verification

  • Contract tests for the invariant (each rung gets its own transport; no fallbacks to litellm; parse-exhaustion advances; entered-once + original-prompt-to-fallback; per-rung timeout; cap-saturation advances; observability fires).
  • Real-API capability probe: check_model_support.py --model zai/glm-5.2 = 17/18 (all 13 structured-output ops + tool-calling + embedding; only optional vision unsupported on the coding endpoint).
  • Committed opt-in real-API e2e (@skip_low_priority): GLM-as-primary pipeline + prod-shape MiniMax→GLM fallback via the faithful publish path (3/3 passed).
  • Unit tier 3712-pass, integration (llm+services) 1030-pass, ruff/pyright clean.

Caveat

GLM-5.2 on the Z.ai coding endpoint rejects image inputs — an image-bearing interaction that hits the GLM fallback (MiniMax down + image content) would fail that rung. Narrow; documented.

Summary by CodeRabbit

  • New Features

    • Added per-provider LLM concurrency caps with configurable fail-open or fail-closed behavior.
    • Enhanced fallback handling to progress rung-by-rung, including structured-output repair and improved timeout budgeting.
    • Added startup validation for configured fallback model providers.
    • Updated ZAI default models to GLM-5.2.
  • Bug Fixes

    • Improved fallback observability, including clearer fallback reason reporting and provider capacity saturation handling.
  • Documentation

    • Updated configuration and fallback behavior docs for concurrency caps and fallback ladder semantics.
  • Tests

    • Added/updated unit and e2e coverage for real fallback behavior and cap saturation.

…ability

Replace litellm-native fallback with a reflexio-owned walk in `_make_request`.
Both the plain and validator/repair paths walk `[primary, *fallbacks]` one rung
at a time via `_resolve_ladder`, rebuilding transport params per rung (structured
-output strategy, api_base, and a PER-SINGLE-ATTEMPT hard timeout — not the old
`(1+len(fallbacks))×per_attempt` ladder-wide budget). No `fallbacks` kwarg is ever
handed to `litellm.completion` on any path. `num_retries=0` per rung preserves the
PYTHON-FASTAPI-62 fix (a hung primary is abandoned after one attempt, then advances).

- Delete the transport guard `_validate_structured_fallback_strategies`: mixed
  -strategy ladders are now safe because each rung builds its own transport.
- Per-rung repair: the primary keeps one same-model parse-retry (plain) or one
  same-model corrective turn (validator). A fallback rung reached via repair gets
  the ORIGINAL prompt, never the prior rung's repair conversation. Delete
  `_repair_target_kwargs`'s cross-model escalation (the walk owns advancement).
- Error taxonomy: transport errors, StructuredOutput{Parse,Repair}Error, and
  ProviderCapSaturatedError (Task 1) are all advance-worthy; the final rung
  preserves the typed repair error and surfaces others as LiteLLMClientError.
- Observability: replace `_emit_fallback_observability`/`_same_observed_model`
  (which relied on litellm rewriting response.model and never fired post-change)
  with a loop-driven `_emit_fallback_signal` + `_rung_reason`. Preserves the
  `event=llm_fallback_used` log + Sentry tag keys; adds `llm.fallback_reason`.
- Advisory ladder wall-clock budget warning.

Guard deletion + owned walk are atomic (Global Constraint). Tests rewritten by
class: delete 3 guard tests + ladder-wide-timeout scaling; rewrite forwarding /
escalation / sentry / hard-timeout tests to the owned walk; add L1 contract class
TestOwnedFallbackWalk (transport/advance/slot/cap/signal); consolidator E2E updated.
…cuit ladder under custom endpoint; doc fail-closed cap

Addresses review-loop findings: boot-time validation no longer refuses boot for
litellm providers outside _ENV_TO_PROVIDER (bedrock/vertex/etc.) — warns instead;
a configured custom endpoint short-circuits the fallback ladder to one rung
(no wasted rungs, no false llm_fallback_used signal); README concurrency-cap
bullet documents REFLEXIO_LLM_FAIL_CLOSED_PROVIDERS + overrides.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a1c0dfce-7475-4e5a-a541-45a4d8761bea

📥 Commits

Reviewing files that changed from the base of the PR and between 60b5ef8 and 2cdfbd6.

📒 Files selected for processing (3)
  • tests/e2e_tests/test_glm_fallback_real_llm.py
  • tests/server/llm/test_litellm_client_unit.py
  • tests/server/llm/test_model_defaults.py

📝 Walkthrough

Walkthrough

Refactors LiteLLM fallback handling into a Reflexio-owned per-rung ladder with structured-output repair, per-rung timeouts, and explicit fallback telemetry. Adds configurable provider concurrency caps, fail-closed saturation errors, startup fallback-provider validation, GLM 5.2 defaults, and expanded unit, integration, and e2e coverage.

Changes

LLM fallback and provider resilience

Layer / File(s) Summary
Provider capacity policies
reflexio/server/llm/_provider_concurrency.py, reflexio/server/llm/README.md, tests/server/llm/test_provider_concurrency.py
Provider slots now support per-provider caps, fail-open behavior by default, fail-closed providers, and ProviderCapSaturatedError.
Owned fallback ladder
reflexio/server/llm/_litellm_text_generation.py, reflexio/server/llm/_litellm_types.py, tests/server/llm/test_litellm_client_unit.py, tests/server/services/playbook/test_playbook_consolidator_integration.py
Fallbacks are resolved and executed rung-by-rung without LiteLLM delegation, with num_retries=0, per-rung timeouts, same-model parse/repair attempts, failure classification, and authoritative telemetry.
Startup model validation
reflexio/server/llm/model_defaults.py, tests/server/llm/test_model_defaults.py
ZAI defaults use zai/glm-5.2, and configured fallback providers are validated during startup while local embedding fallback handling remains supported.
Real pipeline fallback validation
tests/e2e_tests/test_glm_fallback_real_llm.py
Opt-in tests verify direct GLM generation and MiniMax-to-GLM transport fallback through the real Reflexio pipeline.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Reflexio
  participant TextGenerationMixin
  participant ProviderSlot
  participant LiteLLM
  Reflexio->>TextGenerationMixin: Submit generation request
  TextGenerationMixin->>ProviderSlot: Acquire provider slot
  ProviderSlot->>LiteLLM: Execute current rung
  LiteLLM-->>TextGenerationMixin: Response or classified failure
  TextGenerationMixin->>TextGenerationMixin: Retry or repair within rung
  TextGenerationMixin->>ProviderSlot: Acquire next provider slot
  ProviderSlot->>LiteLLM: Execute fallback rung
  LiteLLM-->>TextGenerationMixin: Fallback response
  TextGenerationMixin-->>Reflexio: Return generated result
Loading

Possibly related PRs

Suggested reviewers: yilu331

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: a per-rung transport-aware LLM fallback walk and Z.ai glm-5.2 support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/transport-aware-llm-fallback-glm

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🤖 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 `@tests/e2e_tests/test_glm_fallback_real_llm.py`:
- Around line 182-202: Update test_glm_as_primary_pipeline to clear
REFLEXIO_LLM_FALLBACK_MODELS before reflexio_instance is constructed, then
create or obtain the instance within that cleared-configuration scope so client
initialization cannot inherit fallback models. Preserve the existing GLM
forcing, publish, tagging drain, and row assertions.

In `@tests/server/llm/test_litellm_client_unit.py`:
- Line 3428: Update the match pattern in the pytest.raises assertion for
LiteLLMClientError to use a raw string and escape the dot in “zai/glm-5.2”,
ensuring the regex matches the exact model name and satisfies RUF043.

In `@tests/server/llm/test_model_defaults.py`:
- Line 369: Update the pytest.raises match patterns in the affected assertions
to raw string literals, including the assertions near the fallback model checks,
while preserving the existing regex text and matching behavior.
- Around line 363-402: Update the shared test environment cleanup helper
_clean_env to remove REFLEXIO_LLM_FALLBACK_MODELS alongside the other
LLM-related environment variables. Ensure every availability test starts without
a stale configured fallback while preserving the existing cleanup behavior.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f008b65-d1db-45eb-977e-db52f57e3343

📥 Commits

Reviewing files that changed from the base of the PR and between 5f78279 and 60b5ef8.

📒 Files selected for processing (10)
  • reflexio/server/README.md
  • reflexio/server/llm/_litellm_text_generation.py
  • reflexio/server/llm/_litellm_types.py
  • reflexio/server/llm/_provider_concurrency.py
  • reflexio/server/llm/model_defaults.py
  • tests/e2e_tests/test_glm_fallback_real_llm.py
  • tests/server/llm/test_litellm_client_unit.py
  • tests/server/llm/test_model_defaults.py
  • tests/server/llm/test_provider_concurrency.py
  • tests/server/services/playbook/test_playbook_consolidator_integration.py

Comment thread tests/e2e_tests/test_glm_fallback_real_llm.py
Comment thread tests/server/llm/test_litellm_client_unit.py Outdated
Comment thread tests/server/llm/test_model_defaults.py
Comment thread tests/server/llm/test_model_defaults.py Outdated
@guangyu-reflexio
guangyu-reflexio merged commit a4229ef into main Jul 21, 2026
1 check was pending
@guangyu-reflexio
guangyu-reflexio deleted the feat/transport-aware-llm-fallback-glm branch July 22, 2026 00:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant