From fca45501b8e9c9e3566dc3a81b0aa41361765f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=81=B5=E8=B6=8A=E7=BE=BD=E6=AF=9B?= <97326386+Icather@users.noreply.github.com> Date: Fri, 21 Aug 2026 00:52:33 +0800 Subject: [PATCH] fix(zai): resolve model list for China API users (endpoint-aware) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebased onto current main. China endpoint routing (the original Bug 1) is already handled on main: provider_model_ids resolves the China base_url and forwards it into fetch_models. This PR now keeps only the universally-free models append (Bug 2), implemented as a ZaiProfile.fetch_models override instead of the previous monkey-patch (addresses review): - ZAI_FREE_MODELS (5 verified free Flash models) appended after the live API response, deduped case-insensitively - Empty/None live results (China keys rejected by the international endpoint) still return the free-model set - base_url is forwarded to super().fetch_models so the resolved China endpoint is not undone (providers/base.py endpoint selection) - keeps main's fallback_models (incl. glm-5.2) and ZaiProfile wiring Tests: tests/plugins/model_providers/test_zai_profile.py — free-model append, dedup, empty/None survival, base_url forwarding. --- plugins/model-providers/zai/__init__.py | 40 ++++++++++ .../model_providers/test_zai_profile.py | 73 +++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/plugins/model-providers/zai/__init__.py b/plugins/model-providers/zai/__init__.py index 322068617e117..5312501de0f0c 100644 --- a/plugins/model-providers/zai/__init__.py +++ b/plugins/model-providers/zai/__init__.py @@ -32,6 +32,17 @@ from providers import register_provider from providers.base import ProviderProfile +# Universally-free models available to all Z.AI users (both the +# international and China platforms), but not listed by the provider's +# /v1/models endpoint. All verified with real API calls. +ZAI_FREE_MODELS = ( + "glm-4v-flash", + "glm-4.6v-flash", + "glm-4.1v-thinking-flash", + "glm-4.5-flash", + "glm-4-flash-250414", +) + _GLM_VERSION_RE = re.compile(r"^glm-(\d+)(?:\.(\d+))?") @@ -89,6 +100,35 @@ def _glm_5_2_reasoning_effort(reasoning_config: dict | None) -> str | None: class ZaiProfile(ProviderProfile): """Z.AI / GLM — extra_body.thinking on/off + GLM-5.2 reasoning_effort.""" + def fetch_models( + self, + *, + api_key: str | None = None, + base_url: str | None = None, + timeout: float = 8.0, + ) -> list[str] | None: + """Fetch the live model list and append universally-free models. + + Z.AI's ``/v1/models`` endpoint omits a handful of Flash models that + nonetheless accept real API calls (``glm-4v-flash`` etc., verified + against both the international and China platforms). Append them + here so China API users — whose keys are rejected by the + international endpoint — still get the free models in the picker. + + ``base_url`` is forwarded so the China routing fix in + ``provider_model_ids`` (``hermes_cli/models.py``) is not undone. + """ + live = super().fetch_models( + api_key=api_key, base_url=base_url, timeout=timeout + ) + models = list(live) if live else [] + seen = {m.lower() for m in models} + for m in ZAI_FREE_MODELS: + if m.lower() not in seen: + models.append(m) + seen.add(m.lower()) + return models + def build_api_kwargs_extras( self, *, reasoning_config: dict | None = None, model: str | None = None, **context ) -> tuple[dict[str, Any], dict[str, Any]]: diff --git a/tests/plugins/model_providers/test_zai_profile.py b/tests/plugins/model_providers/test_zai_profile.py index 58915b0daae59..f952844139a0a 100644 --- a/tests/plugins/model_providers/test_zai_profile.py +++ b/tests/plugins/model_providers/test_zai_profile.py @@ -176,3 +176,76 @@ def test_glm_5_2_effort_reaches_top_level(self, zai_profile): ) assert kwargs["reasoning_effort"] == "max" assert kwargs["extra_body"]["thinking"] == {"type": "enabled"} + + +class TestZaiFetchModels: + """``fetch_models`` appends universally-free models and forwards base_url. + + Z.AI's ``/v1/models`` omits a handful of Flash models that nonetheless + accept real API calls. China API keys are rejected by the international + endpoint, so the free-model append must survive an empty/None live + result, and the resolved China ``base_url`` must reach the base class. + """ + + def test_appends_free_models_to_live(self, zai_profile, monkeypatch): + from providers.base import ProviderProfile + from plugins.model_providers.zai import ZAI_FREE_MODELS + + def fake_fetch(self, *, api_key=None, base_url=None, timeout=8.0): + return ["glm-5", "glm-4-9b"] + + monkeypatch.setattr(ProviderProfile, "fetch_models", fake_fetch) + models = zai_profile.fetch_models(api_key="k") + assert "glm-5" in models + assert "glm-4v-flash" in models + assert "glm-4.5-flash" in models + # Every verified free model is present. + assert set(ZAI_FREE_MODELS) <= set(models) + + def test_dedup_when_live_already_lists_free_model(self, zai_profile, monkeypatch): + from providers.base import ProviderProfile + + def fake_fetch(self, *, api_key=None, base_url=None, timeout=8.0): + return ["glm-5", "glm-4v-flash"] + + monkeypatch.setattr(ProviderProfile, "fetch_models", fake_fetch) + models = zai_profile.fetch_models(api_key="k") + assert models.count("glm-4v-flash") == 1 + + def test_empty_or_none_live_still_returns_free_models(self, zai_profile, monkeypatch): + from providers.base import ProviderProfile + from plugins.model_providers.zai import ZAI_FREE_MODELS + + expected = sorted(ZAI_FREE_MODELS) + + def fake_fetch_empty(self, *, api_key=None, base_url=None, timeout=8.0): + return [] + + monkeypatch.setattr(ProviderProfile, "fetch_models", fake_fetch_empty) + assert sorted(zai_profile.fetch_models(api_key="k")) == expected + + def fake_fetch_none(self, *, api_key=None, base_url=None, timeout=8.0): + return None + + monkeypatch.setattr(ProviderProfile, "fetch_models", fake_fetch_none) + # China keys: live fetch rejects → None; free models still surface. + assert sorted(zai_profile.fetch_models(api_key="china-key")) == expected + + def test_forwards_base_url_to_super(self, zai_profile, monkeypatch): + """The resolved China endpoint must reach the base implementation.""" + from providers.base import ProviderProfile + + seen = {} + + def fake_fetch(self, *, api_key=None, base_url=None, timeout=8.0): + seen["api_key"] = api_key + seen["base_url"] = base_url + return ["glm-5"] + + monkeypatch.setattr(ProviderProfile, "fetch_models", fake_fetch) + zai_profile.fetch_models( + api_key="china-key", + base_url="https://open.bigmodel.cn/api/paas/v4", + ) + assert seen["api_key"] == "china-key" + assert seen["base_url"] == "https://open.bigmodel.cn/api/paas/v4"