v3 M3: z.ai (GLM) provider — Anthropic-compatible poll from a pasted API key

ZaiProvider replaces the zai stub. It reaches z.ai's Anthropic-compatible
endpoint (default https://api.z.ai/api/anthropic) exactly like Claude Code with
ANTHROPIC_BASE_URL pointed at z.ai: a /v1/messages POST with a Bearer key. The
key + base URL come from the config the control panel's z.ai field writes
(providers.zai.base_url/.api_key) — no OAuth, no local files.

Two z.ai-specific cares: (1) static API key, no self-refresh; (2) the GLM Coding
Plan meters by prompts, so a 60s poll would drain quota — the provider
self-throttles to one network call per poll_interval_s (default 15 min) and
serves a cached status in between. Rate-limit parsing assumes z.ai proxies
Anthropic's unified headers (5h->s/sr, 7d->w/wr); if a real response lacks them
it still reports connected and logs the limit-ish headers it did return, so the
mapping can be finalized against a live key.

tools/probe_zai.py dumps a real z.ai response's headers (key via env, never
printed) to pin the exact header names. Graceful without a key: selecting z.ai
shows the blue theme + "no key", never crashes the loop. Registry: all three
providers now real; unknown ids fall back to StubProvider. +7 z.ai tests (mocked
transport: header mapping, auth, throttle, fallback); 120 passed, 2 Linux-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wenil
2026-07-10 07:27:34 +03:00
co-authored by Claude Opus 4.8
parent b63a6a62ae
commit ac0742d92c
6 changed files with 347 additions and 17 deletions
+6 -9
View File
@@ -10,12 +10,7 @@ from __future__ import annotations
from .base import Provider, ProviderStatus, StubProvider
from .anthropic import AnthropicProvider
from .openai_codex import OpenAICodexProvider
# Brand accents / labels for the not-yet-implemented providers, so the stub still
# carries the right theme hint to the watch once firmware theming lands.
_STUB_META = {
"zai": ("z.ai GLM", "3859ff"),
}
from .zai import ZaiProvider
def get_provider(pid: str) -> Provider:
@@ -23,11 +18,13 @@ def get_provider(pid: str) -> Provider:
return AnthropicProvider()
if pid == "openai":
return OpenAICodexProvider()
label, accent = _STUB_META.get(pid, (pid, "d97757"))
return StubProvider(pid, label=label, accent=accent)
if pid == "zai":
return ZaiProvider()
# Unknown id (a config from a newer version, say) — a safe placeholder.
return StubProvider(pid, label=pid, accent="d97757")
__all__ = [
"Provider", "ProviderStatus", "StubProvider",
"AnthropicProvider", "OpenAICodexProvider", "get_provider",
"AnthropicProvider", "OpenAICodexProvider", "ZaiProvider", "get_provider",
]