from __future__ import annotations
"""
GLM (z.ai) provider — faithful Python port of the TypeScript reference.
Reference files:
- glm/spoofing.ts — fingerprint params, headers, signature, variables
- glm/session.ts — user validation, chat session creation
- glm/pipeline.ts — full request pipeline with captcha integration
- glm/stream.ts — three-phase SSE stream parser
- glm/captcha-solver.ts — Aliyun Captcha V3 solver
Key design decisions matching the TS reference:
- x-signature: HMAC-SHA256 with empty key over sorted URL params
(requestId, timestamp, user_id) — NOT SHA-256 of the body
- Fingerprint: Linux/Chrome 149/Africa/Cairo timezone
- x-fe-version: prod-fe-1.1.69
- Session flow: validate JWT via /api/v1/auths/, then create chat
- Body: full messages array, signature_prompt = first 500 chars
- Stream: phase-based parsing (thinking/answer/other/done)
"""
import os
import json
import time
import hmac
import hashlib
import uuid
import requests
import urllib.parse
from datetime import datetime, timezone
from ...typing import AsyncResult, Messages
from ...providers.response import Usage, Reasoning, JsonConversation
from ...requests import StreamSession, raise_for_status
from ...errors import ModelNotFoundError, ProviderException
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin
from .captcha_solver import (
get_captcha_verify_param,
invalidate_captcha_token,
is_available as captcha_solver_available,
)
GLM_BASE_URL = "https://chat.z.ai"
GLM_FE_VERSION = "prod-fe-1.1.69"
GLM_QUERY_VERSION = "0.0.1"
GLM_USER_AGENT = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
)
WEEKDAYS = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
]
class _CaptchaRequired(Exception):
"""Raised from the SSE stream when the server rejects the captcha token.
The error arrives as a ``chat:completion`` chunk with ``done: True`` and an
``error`` field containing ``FRONTEND_CAPTCHA_REQUIRED`` — not as an HTTP
403 — so it must be detected while iterating the stream.
"""
class GLM(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
url = GLM_BASE_URL
api_endpoint = f"{GLM_BASE_URL}/api/v2/chat/completions"
working = True
active_by_default = True
default_model = "GLM-4.7"
# Captcha solving requires zendriver (browser automation).
use_nodriver = captcha_solver_available()
api_key = None
auth_user_id = None
auth_user_name = None
# ── Signature (spoofing.ts: computeSignature) ──────────────────────────
@staticmethod
def _compute_signature(request_id: str, timestamp: str, user_id: str) -> str:
"""Compute x-signature from sorted URL param key=value pairs.
HMAC-SHA256 with empty key (browser's key is unknown — best effort,
matching the TS reference exactly).
"""
params = {"requestId": request_id, "timestamp": timestamp, "user_id": user_id}
sorted_str = ",".join(f"{k}={v}" for k, v in sorted(params.items()))
return hmac.new(b"", sorted_str.encode("utf-8"), hashlib.sha256).hexdigest()
# ── Fingerprint params (spoofing.ts: buildFingerprintParams) ────────────
@classmethod
def _build_url_params(cls, token: str, user_id: str) -> dict:
"""Build the fingerprint query params for GLM chat completion.
Matches the browser's URL query params exactly (spoofing.ts).
Returns a plain dict suitable for urllib.parse.urlencode().
"""
ts = str(int(time.time() * 1000))
request_id = str(uuid.uuid4())
return {
"timestamp": ts,
"requestId": request_id,
"user_id": user_id or "",
"version": GLM_QUERY_VERSION,
"platform": "web",
"token": token,
"user_agent": GLM_USER_AGENT,
"language": "en-US",
"languages": "en-US,en",
"timezone": "Africa/Cairo",
"cookie_enabled": "true",
"screen_width": "1920",
"screen_height": "1080",
"screen_resolution": "1920x1080",
"viewport_height": "947",
"viewport_width": "1920",
"viewport_size": "1920x1080",
"color_depth": "30",
"pixel_ratio": "1",
"current_url": f"{GLM_BASE_URL}/",
"pathname": "/",
"host": "chat.z.ai",
"hostname": "chat.z.ai",
"protocol": "https:",
"search": "",
"hash": "",
"referrer": "",
"title": "",
"timezone_offset": str(-int(time.timezone // 60)),
"local_time": datetime.now().strftime("%m/%d/%Y, %I:%M:%S %p"),
"utc_time": datetime.now(timezone.utc).isoformat(),
"is_mobile": "false",
"is_touch": "false",
"max_touch_points": "0",
"browser_name": "chrome",
"os_name": "linux",
"signature_timestamp": ts,
}
# ── Headers (spoofing.ts: buildGlmHeaders) ──────────────────────────────
@classmethod
def _build_headers(
cls, token: str, body_str: str, request_id: str, timestamp: str, user_id: str
) -> dict:
"""Build the full set of headers for a GLM API call.
Matches the browser's request headers exactly (spoofing.ts).
"""
signature = cls._compute_signature(request_id, timestamp, user_id)
return {
"authorization": f"Bearer {token}",
"content-type": "application/json",
"accept-language": "en-US",
"x-fe-version": GLM_FE_VERSION,
"referer": "",
"user-agent": GLM_USER_AGENT,
"x-region": "overseas",
"accept": "*/*",
"accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"host": "chat.z.ai",
"origin": GLM_BASE_URL,
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"sec-gpc": "1",
"x-signature": signature,
}
# ── Variables (spoofing.ts: buildGlmVariables) ──────────────────────────
@classmethod
def _build_variables(cls, user_name: str = "User") -> dict:
"""Build the variables object for the chat completion body.
Includes {{USER_NAME}}, {{CURRENT_DATETIME}}, etc. (spoofing.ts).
"""
now = datetime.now()
pad = lambda n: str(n).zfill(2)
return {
"{{USER_NAME}}": user_name or "User",
"{{USER_LOCATION}}": "Unknown",
"{{CURRENT_DATETIME}}": f"{now.year}-{pad(now.month)}-{pad(now.day)} {pad(now.hour)}:{pad(now.minute)}:{pad(now.second)}",
"{{CURRENT_DATE}}": f"{now.year}-{pad(now.month)}-{pad(now.day)}",
"{{CURRENT_TIME}}": f"{pad(now.hour)}:{pad(now.minute)}:{pad(now.second)}",
"{{CURRENT_WEEKDAY}}": WEEKDAYS[now.weekday()],
"{{CURRENT_TIMEZONE}}": "Africa/Cairo",
"{{USER_LANGUAGE}}": "en-US",
}
# ── Auth cache (AuthFileMixin) ──────────────────────────────────────────
@classmethod
def get_auth_from_cache(cls):
cache_file_path = cls.get_cache_file()
if cache_file_path.is_file():
file_mtime = cache_file_path.stat().st_mtime
if time.time() - file_mtime < 5 * 60:
try:
with open(cache_file_path, "r") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
try:
os.remove(cache_file_path)
except OSError:
pass
return None
@classmethod
def save_auth_to_cache(cls, data):
cache_file_path = cls.get_cache_file()
with cache_file_path.open("w") as f:
json.dump(data, f)
# ── Models ──────────────────────────────────────────────────────────────
@classmethod
def get_models(cls, **kwargs) -> list:
if not cls.models:
response = requests.get(
f"{cls.url}/api/v1/auths/", timeout=kwargs.get("timeout", 15)
)
auth_data = response.json()
cls.api_key = auth_data.get("token")
cls.auth_user_id = str(auth_data.get("id", ""))
cls.auth_user_name = (
auth_data.get("name") or auth_data.get("nickname") or "User"
)
response = requests.get(
f"{cls.url}/api/models",
headers={"Authorization": f"Bearer {cls.api_key}"},
timeout=kwargs.get("timeout", 15),
)
items = response.json().get("data", [])
cls.model_aliases = {
item.get("name", "").replace(
"\u4efb\u52a1\u4e13\u7528", "ChatGLM"
): item.get("id")
for item in items
}
cls.models = list(cls.model_aliases.keys())
return cls.models
# ── Session (session.ts: getCurrentUser, getOrCreateChatSession) ────────
@classmethod
def _get_current_user(cls, session) -> dict:
"""Validate JWT and get user info via /api/v1/auths/ (session.ts).
Returns dict with id, name, email, or raises ProviderException.
"""
async def _fetch():
async with session.get(
f"{GLM_BASE_URL}/api/v1/auths/",
headers={
"Authorization": f"Bearer {cls.api_key}",
"Content-Type": "application/json",
},
) as response:
if response.status >= 400:
raise ProviderException(
f"Cannot validate GLM account (status {response.status})"
)
data = await response.json()
user = data.get("user") or data
if not user or not user.get("id"):
raise ProviderException("No user ID in auth response")
return {
"id": str(user["id"]),
"name": user.get("name") or user.get("nickname") or "User",
"email": user.get("email", ""),
}
# Run in the async context — this is called from create_async_generator
import asyncio
return asyncio.get_event_loop().run_until_complete(_fetch())
@classmethod
async def _get_or_create_chat_session(cls, session, model: str) -> str:
"""Create a new chat session via /api/v1/chats/new (session.ts).
Returns the chat_id string.
"""
chat_id = str(uuid.uuid4())
chat_body = {
"chat": {
"id": chat_id,
"title": "New Chat",
"models": [model],
"params": {},
"history": {"messages": {}, "currentId": None},
"tags": [],
"flags": [],
"features": [],
"mcp_servers": [],
"enable_thinking": "glm-5" in model or "glm-4" in model,
"reasoning_effort": "max" if "glm-5" in model else "",
"auto_web_search": False,
"message_version": 1,
"extra": {},
"timestamp": int(time.time() * 1000),
"type": "default",
}
}
async with session.post(
f"{GLM_BASE_URL}/api/v1/chats/new",
json=chat_body,
headers={
"Authorization": f"Bearer {cls.api_key}",
"Content-Type": "application/json",
},
) as response:
if response.status >= 400:
raise ProviderException(
f"Cannot create GLM chat session (status {response.status})"
)
chat_data = await response.json()
return chat_data.get("id") or chat_data.get("chat", {}).get("id") or chat_id
# ── Captcha ─────────────────────────────────────────────────────────────
@classmethod
async def _get_captcha_verify_param(cls) -> str:
"""Resolve a captcha_verify_param, falling back to an empty string when
the browser-based solver is unavailable (e.g. zendriver not installed)."""
if not captcha_solver_available():
return ""
return await get_captcha_verify_param()
# ── Main entry point (pipeline.ts: proxyViaGlmWebChat) ──────────────────
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
reasoning_effort: str = None,
web_search: bool = False,
conversation: JsonConversation = None,
**kwargs,
) -> AsyncResult:
cls.get_models()
try:
model = cls.get_model(model)
except ModelNotFoundError:
pass
if conversation is None:
conversation = JsonConversation(
chat_id=None, message_id=None, parent_id=None, completion_id=None
)
if not cls.api_key:
raise ProviderException(
"Failed to obtain API key from Z.ai authentication endpoint"
)
conversation.parent_id = conversation.completion_id
conversation.completion_id = str(uuid.uuid4())
conversation.message_id = str(uuid.uuid4())
# signature_prompt: first 500 chars of all message contents joined (pipeline.ts)
signature_prompt = (
"\n".join(
m.get("content", "") if isinstance(m.get("content"), str) else ""
for m in [messages[-1]]
)[:500]
or ""
)
# Determine model-specific features (pipeline.ts)
is_glm5 = "glm-5" in model
glm_features = {
"image_generation": False,
"web_search": web_search,
"auto_web_search": False,
"preview_mode": True,
"flags": [],
"vlm_tools_enable": False,
"vlm_web_search_enable": False,
"vlm_website_mode": False,
"enable_thinking": reasoning_effort and reasoning_effort != "none",
"reasoning_effort": "max" if is_glm5 else "",
}
async with StreamSession(
impersonate="chrome",
proxy=proxy,
) as session:
# 1. Validate JWT and get user info (session.ts: getCurrentUser)
user_id = cls.auth_user_id or ""
user_name = cls.auth_user_name or "User"
try:
async with session.get(
f"{GLM_BASE_URL}/api/v1/auths/",
headers={
"Authorization": f"Bearer {cls.api_key}",
"Content-Type": "application/json",
},
) as response:
if response.status < 400:
auth_data = await response.json()
user = auth_data.get("user") or auth_data
if user and user.get("id"):
user_id = str(user["id"])
user_name = (
user.get("name") or user.get("nickname") or "User"
)
cls.auth_user_id = user_id
cls.auth_user_name = user_name
except Exception:
pass # Best effort — continue with cached values
# 2. Create chat session (session.ts: getOrCreateChatSession)
if conversation.chat_id is None:
conversation.chat_id = await cls._get_or_create_chat_session(
session, model
)
yield conversation
# 3. Build the request body (pipeline.ts)
data = {
"stream": True,
"model": model,
"messages": [messages[-1]],
"signature_prompt": signature_prompt,
"params": {},
"extra": {},
"features": glm_features,
"variables": cls._build_variables(user_name),
"chat_id": conversation.chat_id,
"id": conversation.completion_id,
"current_user_message_id": conversation.message_id,
"current_user_message_parent_id": conversation.parent_id,
"background_tasks": {
"title_generation": True,
"tags_generation": True,
},
"captcha_verify_param": await cls._get_captcha_verify_param(),
}
# 4. Build fingerprint query string and URL (spoofing.ts)
url_params = cls._build_url_params(cls.api_key, user_id)
query_string = urllib.parse.urlencode(url_params)
endpoint = f"{GLM_BASE_URL}/api/v2/chat/completions?{query_string}"
# Extract request_id and timestamp for signature
request_id = url_params.get("requestId", str(uuid.uuid4()))
timestamp = url_params.get("timestamp", str(int(time.time() * 1000)))
# 5. Retry loop for captcha errors (pipeline.ts + handler.ts)
max_captcha_retries = 2
for attempt in range(max_captcha_retries + 1):
body_str = json.dumps(data, separators=(",", ":"))
headers = cls._build_headers(
cls.api_key, body_str, request_id, timestamp, user_id
)
async with session.post(
endpoint,
data=body_str,
headers=headers,
) as response:
# HTTP-level captcha rejection (pipeline.ts)
if response.status == 403 and attempt < max_captcha_retries:
body = await response.text()
if "FRONTEND_CAPTCHA" in body or "captcha" in body.lower():
invalidate_captcha_token()
data[
"captcha_verify_param"
] = await cls._get_captcha_verify_param()
continue
await raise_for_status(response)
await raise_for_status(response)
# 6. Parse SSE stream (stream.ts: parseGlmSseLine)
try:
async for chunk in cls._iter_completion(response):
yield chunk
return
except _CaptchaRequired:
if attempt >= max_captcha_retries:
raise
invalidate_captcha_token()
data[
"captcha_verify_param"
] = await cls._get_captcha_verify_param()
continue
# ── SSE stream parser (stream.ts: parseGlmSseLine) ──────────────────────
@staticmethod
async def _iter_completion(response):
"""Yield Usage / Reasoning / content chunks from a chat completion SSE stream.
Implements the three-phase SSE parser from stream.ts:
- thinking → Reasoning delta
- answer → content delta
- other → usage stats
- done → completion
Raises ``_CaptchaRequired`` if the server emits a captcha verification
error inside the stream (FRONTEND_CAPTCHA_REQUIRED).
"""
usage = None
async for chunk in response.sse():
if chunk.get("type") != "chat:completion":
continue
chunk_data = chunk.get("data", {})
# Detect captcha errors embedded in the SSE stream (pipeline.ts)
error = chunk_data.get("error")
if error:
error_code = error.get("code") or error.get("error_code") or ""
if "CAPTCHA" in str(error_code):
raise _CaptchaRequired(error.get("detail", error_code))
raise ProviderException(json.dumps(error))
phase = chunk_data.get("phase", "")
delta_content = chunk_data.get("delta_content", "")
if phase == "thinking":
# Strip summary tags and yield reasoning
if delta_content:
cleaned = delta_content.split("</summary>\n>")[-1]
if cleaned:
yield Reasoning(cleaned)
elif phase == "answer":
# Yield content, stripping details tags
edit_content = chunk_data.get("edit_content")
if edit_content:
yield edit_content.split("\n</details>\n")[-1]
elif delta_content:
yield delta_content
elif phase == "other":
# Extract usage stats
if not usage and chunk_data.get("usage"):
usage = chunk_data.get("usage")
yield Usage(**usage)
elif phase == "done":
if usage:
yield Usage(**usage)
return
else:
# Unrecognized phase — emit delta_content as content if present
if delta_content:
yield delta_content
from __future__ import annotations
"""
GLM (z.ai) provider — faithful Python port of the TypeScript reference.
Reference files:
- glm/spoofing.ts — fingerprint params, headers, signature, variables
- glm/session.ts — user validation, chat session creation
- glm/pipeline.ts — full request pipeline with captcha integration
- glm/stream.ts — three-phase SSE stream parser
- glm/captcha-solver.ts — Aliyun Captcha V3 solver
Key design decisions matching the TS reference:
- x-signature: HMAC-SHA256 with empty key over sorted URL params
(requestId, timestamp, user_id) — NOT SHA-256 of the body
- Fingerprint: Linux/Chrome 149/Africa/Cairo timezone
- x-fe-version: prod-fe-1.1.69
- Session flow: validate JWT via /api/v1/auths/, then create chat
- Body: full messages array, signature_prompt = first 500 chars
- Stream: phase-based parsing (thinking/answer/other/done)
"""
import os
import json
import time
import hmac
import hashlib
import uuid
import requests
import urllib.parse
from datetime import datetime, timezone
from ...typing import AsyncResult, Messages
from ...providers.response import Usage, Reasoning, JsonConversation
from ...requests import StreamSession, raise_for_status
from ...errors import ModelNotFoundError, ProviderException
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin
from .captcha_solver import (
get_captcha_verify_param,
invalidate_captcha_token,
is_available as captcha_solver_available,
)
GLM_BASE_URL = "https://chat.z.ai"
GLM_FE_VERSION = "prod-fe-1.1.69"
GLM_QUERY_VERSION = "0.0.1"
GLM_USER_AGENT = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
)
WEEKDAYS = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
]
class _CaptchaRequired(Exception):
"""Raised from the SSE stream when the server rejects the captcha token.
The error arrives as a ``chat:completion`` chunk with ``done: True`` and an
``error`` field containing ``FRONTEND_CAPTCHA_REQUIRED`` — not as an HTTP
403 — so it must be detected while iterating the stream.
"""
class GLM(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
url = GLM_BASE_URL
api_endpoint = f"{GLM_BASE_URL}/api/v2/chat/completions"
working = True
active_by_default = True
default_model = "GLM-4.7"
# Captcha solving requires zendriver (browser automation).
use_nodriver = captcha_solver_available()
api_key = None
auth_user_id = None
auth_user_name = None
# ── Signature (spoofing.ts: computeSignature) ──────────────────────────
@staticmethod
def _compute_signature(request_id: str, timestamp: str, user_id: str) -> str:
"""Compute x-signature from sorted URL param key=value pairs.
HMAC-SHA256 with empty key (browser's key is unknown — best effort,
matching the TS reference exactly).
"""
params = {"requestId": request_id, "timestamp": timestamp, "user_id": user_id}
sorted_str = ",".join(f"{k}={v}" for k, v in sorted(params.items()))
return hmac.new(b"", sorted_str.encode("utf-8"), hashlib.sha256).hexdigest()
# ── Fingerprint params (spoofing.ts: buildFingerprintParams) ────────────
@classmethod
def _build_url_params(cls, token: str, user_id: str) -> dict:
"""Build the fingerprint query params for GLM chat completion.
Matches the browser's URL query params exactly (spoofing.ts).
Returns a plain dict suitable for urllib.parse.urlencode().
"""
ts = str(int(time.time() * 1000))
request_id = str(uuid.uuid4())
return {
"timestamp": ts,
"requestId": request_id,
"user_id": user_id or "",
"version": GLM_QUERY_VERSION,
"platform": "web",
"token": token,
"user_agent": GLM_USER_AGENT,
"language": "en-US",
"languages": "en-US,en",
"timezone": "Africa/Cairo",
"cookie_enabled": "true",
"screen_width": "1920",
"screen_height": "1080",
"screen_resolution": "1920x1080",
"viewport_height": "947",
"viewport_width": "1920",
"viewport_size": "1920x1080",
"color_depth": "30",
"pixel_ratio": "1",
"current_url": f"{GLM_BASE_URL}/",
"pathname": "/",
"host": "chat.z.ai",
"hostname": "chat.z.ai",
"protocol": "https:",
"search": "",
"hash": "",
"referrer": "",
"title": "",
"timezone_offset": str(-int(time.timezone // 60)),
"local_time": datetime.now().strftime("%m/%d/%Y, %I:%M:%S %p"),
"utc_time": datetime.now(timezone.utc).isoformat(),
"is_mobile": "false",
"is_touch": "false",
"max_touch_points": "0",
"browser_name": "chrome",
"os_name": "linux",
"signature_timestamp": ts,
}
# ── Headers (spoofing.ts: buildGlmHeaders) ──────────────────────────────
@classmethod
def _build_headers(
cls, token: str, body_str: str, request_id: str, timestamp: str, user_id: str
) -> dict:
"""Build the full set of headers for a GLM API call.
Matches the browser's request headers exactly (spoofing.ts).
"""
signature = cls._compute_signature(request_id, timestamp, user_id)
return {
"authorization": f"Bearer {token}",
"content-type": "application/json",
"accept-language": "en-US",
"x-fe-version": GLM_FE_VERSION,
"referer": "",
"user-agent": GLM_USER_AGENT,
"x-region": "overseas",
"accept": "*/*",
"accept-encoding": "gzip, deflate, br, zstd",
"connection": "keep-alive",
"host": "chat.z.ai",
"origin": GLM_BASE_URL,
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"sec-gpc": "1",
"x-signature": signature,
}
# ── Variables (spoofing.ts: buildGlmVariables) ──────────────────────────
@classmethod
def _build_variables(cls, user_name: str = "User") -> dict:
"""Build the variables object for the chat completion body.
Includes {{USER_NAME}}, {{CURRENT_DATETIME}}, etc. (spoofing.ts).
"""
now = datetime.now()
pad = lambda n: str(n).zfill(2)
return {
"{{USER_NAME}}": user_name or "User",
"{{USER_LOCATION}}": "Unknown",
"{{CURRENT_DATETIME}}": f"{now.year}-{pad(now.month)}-{pad(now.day)} {pad(now.hour)}:{pad(now.minute)}:{pad(now.second)}",
"{{CURRENT_DATE}}": f"{now.year}-{pad(now.month)}-{pad(now.day)}",
"{{CURRENT_TIME}}": f"{pad(now.hour)}:{pad(now.minute)}:{pad(now.second)}",
"{{CURRENT_WEEKDAY}}": WEEKDAYS[now.weekday()],
"{{CURRENT_TIMEZONE}}": "Africa/Cairo",
"{{USER_LANGUAGE}}": "en-US",
}
# ── Auth cache (AuthFileMixin) ──────────────────────────────────────────
@classmethod
def get_auth_from_cache(cls):
cache_file_path = cls.get_cache_file()
if cache_file_path.is_file():
file_mtime = cache_file_path.stat().st_mtime
if time.time() - file_mtime < 5 * 60:
try:
with open(cache_file_path, "r") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
try:
os.remove(cache_file_path)
except OSError:
pass
return None
@classmethod
def save_auth_to_cache(cls, data):
cache_file_path = cls.get_cache_file()
with cache_file_path.open("w") as f:
json.dump(data, f)
# ── Models ──────────────────────────────────────────────────────────────
@classmethod
def get_models(cls, **kwargs) -> list:
if not cls.models:
response = requests.get(
f"{cls.url}/api/v1/auths/", timeout=kwargs.get("timeout", 15)
)
auth_data = response.json()
cls.api_key = auth_data.get("token")
cls.auth_user_id = str(auth_data.get("id", ""))
cls.auth_user_name = (
auth_data.get("name") or auth_data.get("nickname") or "User"
)
response = requests.get(
f"{cls.url}/api/models",
headers={"Authorization": f"Bearer {cls.api_key}"},
timeout=kwargs.get("timeout", 15),
)
items = response.json().get("data", [])
cls.model_aliases = {
item.get("name", "").replace(
"\u4efb\u52a1\u4e13\u7528", "ChatGLM"
): item.get("id")
for item in items
}
cls.models = list(cls.model_aliases.keys())
return cls.models
# ── Session (session.ts: getCurrentUser, getOrCreateChatSession) ────────
@classmethod
def _get_current_user(cls, session) -> dict:
"""Validate JWT and get user info via /api/v1/auths/ (session.ts).
Returns dict with id, name, email, or raises ProviderException.
"""
async def _fetch():
async with session.get(
f"{GLM_BASE_URL}/api/v1/auths/",
headers={
"Authorization": f"Bearer {cls.api_key}",
"Content-Type": "application/json",
},
) as response:
if response.status >= 400:
raise ProviderException(
f"Cannot validate GLM account (status {response.status})"
)
data = await response.json()
user = data.get("user") or data
if not user or not user.get("id"):
raise ProviderException("No user ID in auth response")
return {
"id": str(user["id"]),
"name": user.get("name") or user.get("nickname") or "User",
"email": user.get("email", ""),
}
# Run in the async context — this is called from create_async_generator
import asyncio
return asyncio.get_event_loop().run_until_complete(_fetch())
@classmethod
async def _get_or_create_chat_session(cls, session, model: str) -> str:
"""Create a new chat session via /api/v1/chats/new (session.ts).
Returns the chat_id string.
"""
chat_id = str(uuid.uuid4())
chat_body = {
"chat": {
"id": chat_id,
"title": "New Chat",
"models": [model],
"params": {},
"history": {"messages": {}, "currentId": None},
"tags": [],
"flags": [],
"features": [],
"mcp_servers": [],
"enable_thinking": "glm-5" in model or "glm-4" in model,
"reasoning_effort": "max" if "glm-5" in model else "",
"auto_web_search": False,
"message_version": 1,
"extra": {},
"timestamp": int(time.time() * 1000),
"type": "default",
}
}
async with session.post(
f"{GLM_BASE_URL}/api/v1/chats/new",
json=chat_body,
headers={
"Authorization": f"Bearer {cls.api_key}",
"Content-Type": "application/json",
},
) as response:
if response.status >= 400:
raise ProviderException(
f"Cannot create GLM chat session (status {response.status})"
)
chat_data = await response.json()
return chat_data.get("id") or chat_data.get("chat", {}).get("id") or chat_id
# ── Captcha ─────────────────────────────────────────────────────────────
@classmethod
async def _get_captcha_verify_param(cls) -> str:
"""Resolve a captcha_verify_param, falling back to an empty string when
the browser-based solver is unavailable (e.g. zendriver not installed)."""
if not captcha_solver_available():
return ""
return await get_captcha_verify_param()
# ── Main entry point (pipeline.ts: proxyViaGlmWebChat) ──────────────────
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
reasoning_effort: str = None,
web_search: bool = False,
conversation: JsonConversation = None,
**kwargs,
) -> AsyncResult:
cls.get_models()
try:
model = cls.get_model(model)
except ModelNotFoundError:
pass
if conversation is None:
conversation = JsonConversation(
chat_id=None, message_id=None, parent_id=None, completion_id=None
)
if not cls.api_key:
raise ProviderException(
"Failed to obtain API key from Z.ai authentication endpoint"
)
conversation.parent_id = conversation.completion_id
conversation.completion_id = str(uuid.uuid4())
conversation.message_id = str(uuid.uuid4())
# signature_prompt: first 500 chars of all message contents joined (pipeline.ts)
signature_prompt = (
"\n".join(
m.get("content", "") if isinstance(m.get("content"), str) else ""
for m in [messages[-1]]
)[:500]
or ""
)
# Determine model-specific features (pipeline.ts)
is_glm5 = "glm-5" in model
glm_features = {
"image_generation": False,
"web_search": web_search,
"auto_web_search": False,
"preview_mode": True,
"flags": [],
"vlm_tools_enable": False,
"vlm_web_search_enable": False,
"vlm_website_mode": False,
"enable_thinking": reasoning_effort and reasoning_effort != "none",
"reasoning_effort": "max" if is_glm5 else "",
}
async with StreamSession(
impersonate="chrome",
proxy=proxy,
) as session:
# 1. Validate JWT and get user info (session.ts: getCurrentUser)
user_id = cls.auth_user_id or ""
user_name = cls.auth_user_name or "User"
try:
async with session.get(
f"{GLM_BASE_URL}/api/v1/auths/",
headers={
"Authorization": f"Bearer {cls.api_key}",
"Content-Type": "application/json",
},
) as response:
if response.status < 400:
auth_data = await response.json()
user = auth_data.get("user") or auth_data
if user and user.get("id"):
user_id = str(user["id"])
user_name = (
user.get("name") or user.get("nickname") or "User"
)
cls.auth_user_id = user_id
cls.auth_user_name = user_name
except Exception:
pass # Best effort — continue with cached values
# 2. Create chat session (session.ts: getOrCreateChatSession)
if conversation.chat_id is None:
conversation.chat_id = await cls._get_or_create_chat_session(
session, model
)
yield conversation
# 3. Build the request body (pipeline.ts)
data = {
"stream": True,
"model": model,
"messages": [messages[-1]],
"signature_prompt": signature_prompt,
"params": {},
"extra": {},
"features": glm_features,
"variables": cls._build_variables(user_name),
"chat_id": conversation.chat_id,
"id": conversation.completion_id,
"current_user_message_id": conversation.message_id,
"current_user_message_parent_id": conversation.parent_id,
"background_tasks": {
"title_generation": True,
"tags_generation": True,
},
"captcha_verify_param": await cls._get_captcha_verify_param(),
}
# 4. Build fingerprint query string and URL (spoofing.ts)
url_params = cls._build_url_params(cls.api_key, user_id)
query_string = urllib.parse.urlencode(url_params)
endpoint = f"{GLM_BASE_URL}/api/v2/chat/completions?{query_string}"
# Extract request_id and timestamp for signature
request_id = url_params.get("requestId", str(uuid.uuid4()))
timestamp = url_params.get("timestamp", str(int(time.time() * 1000)))
# 5. Retry loop for captcha errors (pipeline.ts + handler.ts)
max_captcha_retries = 2
for attempt in range(max_captcha_retries + 1):
body_str = json.dumps(data, separators=(",", ":"))
headers = cls._build_headers(
cls.api_key, body_str, request_id, timestamp, user_id
)
async with session.post(
endpoint,
data=body_str,
headers=headers,
) as response:
# HTTP-level captcha rejection (pipeline.ts)
if response.status == 403 and attempt < max_captcha_retries:
body = await response.text()
if "FRONTEND_CAPTCHA" in body or "captcha" in body.lower():
invalidate_captcha_token()
data[
"captcha_verify_param"
] = await cls._get_captcha_verify_param()
continue
await raise_for_status(response)
await raise_for_status(response)
# 6. Parse SSE stream (stream.ts: parseGlmSseLine)
try:
async for chunk in cls._iter_completion(response):
yield chunk
return
except _CaptchaRequired:
if attempt >= max_captcha_retries:
raise
invalidate_captcha_token()
data[
"captcha_verify_param"
] = await cls._get_captcha_verify_param()
continue
# ── SSE stream parser (stream.ts: parseGlmSseLine) ──────────────────────
@staticmethod
async def _iter_completion(response):
"""Yield Usage / Reasoning / content chunks from a chat completion SSE stream.
Implements the three-phase SSE parser from stream.ts:
- thinking → Reasoning delta
- answer → content delta
- other → usage stats
- done → completion
Raises ``_CaptchaRequired`` if the server emits a captcha verification
error inside the stream (FRONTEND_CAPTCHA_REQUIRED).
"""
usage = None
async for chunk in response.sse():
if chunk.get("type") != "chat:completion":
continue
chunk_data = chunk.get("data", {})
# Detect captcha errors embedded in the SSE stream (pipeline.ts)
error = chunk_data.get("error")
if error:
error_code = error.get("code") or error.get("error_code") or ""
if "CAPTCHA" in str(error_code):
raise _CaptchaRequired(error.get("detail", error_code))
raise ProviderException(json.dumps(error))
phase = chunk_data.get("phase", "")
delta_content = chunk_data.get("delta_content", "")
if phase == "thinking":
# Strip summary tags and yield reasoning
if delta_content:
cleaned = delta_content.split("</summary>\n>")[-1]
if cleaned:
yield Reasoning(cleaned)
elif phase == "answer":
# Yield content, stripping details tags
edit_content = chunk_data.get("edit_content")
if edit_content:
yield edit_content.split("\n</details>\n")[-1]
elif delta_content:
yield delta_content
elif phase == "other":
# Extract usage stats
if not usage and chunk_data.get("usage"):
usage = chunk_data.get("usage")
yield Usage(**usage)
elif phase == "done":
if usage:
yield Usage(**usage)
return
else:
# Unrecognized phase — emit delta_content as content if present
if delta_content:
yield delta_content