Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions plugins/model-providers/zai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Current main invokes API-key profiles as fetch_models(api_key=..., base_url=...) in hermes_cli/models.py:2485. This keyword-only wrapper does not accept base_url, so salvaging it unchanged raises TypeError; add and forward that parameter.

)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Forward the resolved base_url here. Current ProviderProfile.fetch_models uses that override to select the endpoint (providers/base.py:189-194); omitting it would fall back to the profile's international default and undo the China routing fix.

_GLM_VERSION_RE = re.compile(r"^glm-(\d+)(?:\.(\d+))?")


Expand Down Expand Up @@ -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]]:
Expand Down
73 changes: 73 additions & 0 deletions tests/plugins/model_providers/test_zai_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading