Spaces:
Running
Running
Upload 4 files
Browse files
README.md
CHANGED
|
@@ -10,4 +10,4 @@ license: mit
|
|
| 10 |
---
|
| 11 |
|
| 12 |
# ZAI API — chat.z.ai
|
| 13 |
-
OpenAI-compatible API
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
# ZAI API — chat.z.ai
|
| 13 |
+
OpenAI-compatible API: GLM-5.1 / GLM-5-Turbo / GLM-5V-Turbo via Playwright.
|
main.py
CHANGED
|
@@ -6,13 +6,12 @@ import threading
|
|
| 6 |
import json
|
| 7 |
import re
|
| 8 |
from typing import Optional
|
| 9 |
-
from fastapi import FastAPI,
|
| 10 |
from fastapi.responses import JSONResponse
|
| 11 |
|
| 12 |
|
| 13 |
-
API_SECRET_KEY = os.getenv("API_SECRET_KEY", "
|
| 14 |
|
| 15 |
-
# الموديلات المتاحة في chat.z.ai
|
| 16 |
ZAI_MODELS = ["GLM-5.1", "GLM-5-Turbo", "GLM-5V-Turbo"]
|
| 17 |
|
| 18 |
|
|
@@ -55,9 +54,9 @@ class AsyncBrowserThread(threading.Thread):
|
|
| 55 |
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
| 56 |
viewport={'width': 1920, 'height': 1080}
|
| 57 |
)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
page = await context.new_page()
|
| 62 |
|
| 63 |
try:
|
|
@@ -65,30 +64,45 @@ class AsyncBrowserThread(threading.Thread):
|
|
| 65 |
await page.goto("https://chat.z.ai/", wait_until="domcontentloaded")
|
| 66 |
await asyncio.sleep(3)
|
| 67 |
|
| 68 |
-
# Input:
|
| 69 |
await page.wait_for_selector("textarea#chat-input", timeout=60000)
|
| 70 |
await page.fill("textarea#chat-input", prompt)
|
| 71 |
await asyncio.sleep(0.5)
|
| 72 |
await page.press("textarea#chat-input", "Enter")
|
|
|
|
| 73 |
|
| 74 |
-
#
|
| 75 |
await asyncio.sleep(2)
|
| 76 |
await page.wait_for_selector("#response-content-container", timeout=120000)
|
| 77 |
|
|
|
|
| 78 |
last_text = ""
|
| 79 |
unchanged_count = 0
|
| 80 |
while unchanged_count < 5:
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
await asyncio.sleep(1.0)
|
| 90 |
|
| 91 |
-
|
|
|
|
| 92 |
|
| 93 |
except Exception as e:
|
| 94 |
print(f"[ZAI-SERVER] Error: {e}")
|
|
@@ -100,30 +114,10 @@ class AsyncBrowserThread(threading.Thread):
|
|
| 100 |
def process_request(self, prompt: str):
|
| 101 |
if not self.ready_event.wait(timeout=60):
|
| 102 |
raise Exception("Error From Browser")
|
| 103 |
-
|
| 104 |
future = asyncio.run_coroutine_threadsafe(self._talk_to_zai(prompt), self.loop)
|
| 105 |
return future.result(timeout=120)
|
| 106 |
|
| 107 |
|
| 108 |
-
def _clean_zai_response(text: str) -> str:
|
| 109 |
-
"""
|
| 110 |
-
Z.ai بيعرض thinking block فوق الرد الحقيقي.
|
| 111 |
-
نشيل كل حاجة قبل أول سطر فارغ (الـ thinking)
|
| 112 |
-
ونرجع الرد الفعلي فقط.
|
| 113 |
-
"""
|
| 114 |
-
lines = text.splitlines()
|
| 115 |
-
result_lines = []
|
| 116 |
-
found_blank = False
|
| 117 |
-
for i, line in enumerate(lines):
|
| 118 |
-
if not found_blank and line.strip() == "" and i > 0:
|
| 119 |
-
found_blank = True
|
| 120 |
-
continue
|
| 121 |
-
if found_blank:
|
| 122 |
-
result_lines.append(line)
|
| 123 |
-
cleaned = "\n".join(result_lines).strip()
|
| 124 |
-
return cleaned if cleaned else text.strip()
|
| 125 |
-
|
| 126 |
-
|
| 127 |
browser_engine = AsyncBrowserThread()
|
| 128 |
browser_engine.start()
|
| 129 |
|
|
@@ -173,7 +167,9 @@ def format_prompt(messages, tools=None):
|
|
| 173 |
tc_descriptions = []
|
| 174 |
for tc in tool_calls_in_msg:
|
| 175 |
func = tc.get("function", {})
|
| 176 |
-
tc_descriptions.append(
|
|
|
|
|
|
|
| 177 |
assistant_content += "\n[Previous tool calls: " + "; ".join(tc_descriptions) + "]"
|
| 178 |
if assistant_content.strip():
|
| 179 |
parts.append(f"[Assistant]: {assistant_content}")
|
|
@@ -219,15 +215,12 @@ def format_tools_instruction(tools, user_question=""):
|
|
| 219 |
instruction += "You MUST use one of the tools below to answer this question.\n"
|
| 220 |
instruction += "Do NOT answer directly. Do NOT say you don't have information.\n"
|
| 221 |
instruction += "You MUST respond with ONLY a JSON object to call the tool.\n\n"
|
| 222 |
-
|
| 223 |
instruction += "RESPONSE FORMAT - respond with ONLY this JSON, nothing else:\n"
|
| 224 |
instruction += '{"tool_calls": [{"name": "TOOL_NAME", "arguments": {"param": "value"}}]}\n\n'
|
| 225 |
-
|
| 226 |
instruction += "RULES:\n"
|
| 227 |
instruction += "- Your ENTIRE response must be valid JSON only\n"
|
| 228 |
instruction += "- No markdown, no code blocks, no explanation\n"
|
| 229 |
instruction += "- No text before or after the JSON\n\n"
|
| 230 |
-
|
| 231 |
instruction += "Available tools:\n\n"
|
| 232 |
|
| 233 |
for tool in tools:
|
|
@@ -235,10 +228,7 @@ def format_tools_instruction(tools, user_question=""):
|
|
| 235 |
name = func.get("name", "unknown")
|
| 236 |
desc = func.get("description", "No description")
|
| 237 |
params = func.get("parameters", {})
|
| 238 |
-
|
| 239 |
-
instruction += f"Tool: {name}\n"
|
| 240 |
-
instruction += f"Description: {desc}\n"
|
| 241 |
-
|
| 242 |
if params.get("properties"):
|
| 243 |
instruction += "Parameters:\n"
|
| 244 |
required_params = params.get("required", [])
|
|
@@ -250,14 +240,11 @@ def format_tools_instruction(tools, user_question=""):
|
|
| 250 |
instruction += "\n"
|
| 251 |
|
| 252 |
instruction += "=== END OF TOOLS ===\n\n"
|
| 253 |
-
|
| 254 |
first_tool = tools[0] if tools else {}
|
| 255 |
first_func = first_tool.get("function", first_tool)
|
| 256 |
first_name = first_func.get("name", "tool")
|
| 257 |
-
|
| 258 |
instruction += f'EXAMPLE: If the user asks a question, respond with:\n'
|
| 259 |
instruction += '{"tool_calls": [{"name": "' + first_name + '", "arguments": {"input": "the user question here"}}]}\n\n'
|
| 260 |
-
|
| 261 |
instruction += "Now respond with the JSON to call the appropriate tool:\n\n"
|
| 262 |
return instruction
|
| 263 |
|
|
@@ -308,6 +295,11 @@ def parse_tool_calls(response_text):
|
|
| 308 |
app = FastAPI(title="zai_api for n8n")
|
| 309 |
|
| 310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
@app.post("/v1/chat/completions")
|
| 312 |
async def chat_completions(request: Request):
|
| 313 |
try:
|
|
@@ -315,8 +307,7 @@ async def chat_completions(request: Request):
|
|
| 315 |
except Exception:
|
| 316 |
return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
|
| 317 |
|
| 318 |
-
|
| 319 |
-
if not authorization or authorization.replace("Bearer ", "").strip() != API_SECRET_KEY:
|
| 320 |
return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
|
| 321 |
|
| 322 |
messages = data.get("messages", [])
|
|
@@ -331,9 +322,7 @@ async def chat_completions(request: Request):
|
|
| 331 |
response_text = browser_engine.process_request(prompt)
|
| 332 |
p_tokens = len(prompt.split())
|
| 333 |
c_tokens = len(response_text.split())
|
| 334 |
-
tool_calls = None
|
| 335 |
-
if tools:
|
| 336 |
-
tool_calls = parse_tool_calls(response_text)
|
| 337 |
|
| 338 |
if tool_calls:
|
| 339 |
return {
|
|
@@ -346,17 +335,16 @@ async def chat_completions(request: Request):
|
|
| 346 |
"usage": {"prompt_tokens": p_tokens, "completion_tokens": c_tokens,
|
| 347 |
"total_tokens": p_tokens + c_tokens}
|
| 348 |
}
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
"
|
| 356 |
-
|
| 357 |
-
"
|
| 358 |
-
|
| 359 |
-
}
|
| 360 |
except Exception as e:
|
| 361 |
return JSONResponse(status_code=500, content={"error": str(e)})
|
| 362 |
|
|
@@ -368,8 +356,7 @@ async def responses(request: Request):
|
|
| 368 |
except Exception:
|
| 369 |
return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
|
| 370 |
|
| 371 |
-
|
| 372 |
-
if not authorization or authorization.replace("Bearer ", "").strip() != API_SECRET_KEY:
|
| 373 |
return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
|
| 374 |
|
| 375 |
input_data = data.get("input", "")
|
|
@@ -393,9 +380,7 @@ async def responses(request: Request):
|
|
| 393 |
response_text = browser_engine.process_request(prompt)
|
| 394 |
p_tokens = len(prompt.split())
|
| 395 |
c_tokens = len(response_text.split())
|
| 396 |
-
tool_calls = None
|
| 397 |
-
if tools:
|
| 398 |
-
tool_calls = parse_tool_calls(response_text)
|
| 399 |
|
| 400 |
if tool_calls:
|
| 401 |
output_items = []
|
|
@@ -418,18 +403,17 @@ async def responses(request: Request):
|
|
| 418 |
"usage": {"input_tokens": p_tokens, "output_tokens": c_tokens,
|
| 419 |
"total_tokens": p_tokens + c_tokens}
|
| 420 |
}
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
"
|
| 429 |
-
|
| 430 |
-
"
|
| 431 |
-
|
| 432 |
-
}
|
| 433 |
except Exception as e:
|
| 434 |
return JSONResponse(status_code=500, content={"error": str(e)})
|
| 435 |
|
|
|
|
| 6 |
import json
|
| 7 |
import re
|
| 8 |
from typing import Optional
|
| 9 |
+
from fastapi import FastAPI, Request
|
| 10 |
from fastapi.responses import JSONResponse
|
| 11 |
|
| 12 |
|
| 13 |
+
API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-secret-key-2026")
|
| 14 |
|
|
|
|
| 15 |
ZAI_MODELS = ["GLM-5.1", "GLM-5-Turbo", "GLM-5V-Turbo"]
|
| 16 |
|
| 17 |
|
|
|
|
| 54 |
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
| 55 |
viewport={'width': 1920, 'height': 1080}
|
| 56 |
)
|
| 57 |
+
await context.add_init_script(
|
| 58 |
+
"Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
|
| 59 |
+
)
|
| 60 |
page = await context.new_page()
|
| 61 |
|
| 62 |
try:
|
|
|
|
| 64 |
await page.goto("https://chat.z.ai/", wait_until="domcontentloaded")
|
| 65 |
await asyncio.sleep(3)
|
| 66 |
|
| 67 |
+
# ── Input: confirmed from DOM ─────────────────────────────
|
| 68 |
await page.wait_for_selector("textarea#chat-input", timeout=60000)
|
| 69 |
await page.fill("textarea#chat-input", prompt)
|
| 70 |
await asyncio.sleep(0.5)
|
| 71 |
await page.press("textarea#chat-input", "Enter")
|
| 72 |
+
print(f"[ZAI-SERVER] Sent ({len(prompt)} chars)")
|
| 73 |
|
| 74 |
+
# ── Wait for response container ───────────────────────────
|
| 75 |
await asyncio.sleep(2)
|
| 76 |
await page.wait_for_selector("#response-content-container", timeout=120000)
|
| 77 |
|
| 78 |
+
# ── Wait until response stabilizes ────────────────────────
|
| 79 |
last_text = ""
|
| 80 |
unchanged_count = 0
|
| 81 |
while unchanged_count < 5:
|
| 82 |
+
current_text = await page.evaluate("""
|
| 83 |
+
() => {
|
| 84 |
+
const prose = document.querySelector(
|
| 85 |
+
'#response-content-container .markdown-prose'
|
| 86 |
+
);
|
| 87 |
+
if (!prose) return '';
|
| 88 |
+
const clone = prose.cloneNode(true);
|
| 89 |
+
|
| 90 |
+
// ✅ شيل كل direct div children (thinking blocks)
|
| 91 |
+
// الرد الحقيقي كله في <p> مش <div>
|
| 92 |
+
clone.querySelectorAll(':scope > div').forEach(el => el.remove());
|
| 93 |
+
|
| 94 |
+
return clone.innerText.trim();
|
| 95 |
+
}
|
| 96 |
+
""")
|
| 97 |
+
if current_text == last_text and current_text.strip():
|
| 98 |
+
unchanged_count += 1
|
| 99 |
+
else:
|
| 100 |
+
last_text = current_text
|
| 101 |
+
unchanged_count = 0
|
| 102 |
await asyncio.sleep(1.0)
|
| 103 |
|
| 104 |
+
print(f"[ZAI-SERVER] Response: {len(last_text)} chars")
|
| 105 |
+
return last_text.strip()
|
| 106 |
|
| 107 |
except Exception as e:
|
| 108 |
print(f"[ZAI-SERVER] Error: {e}")
|
|
|
|
| 114 |
def process_request(self, prompt: str):
|
| 115 |
if not self.ready_event.wait(timeout=60):
|
| 116 |
raise Exception("Error From Browser")
|
|
|
|
| 117 |
future = asyncio.run_coroutine_threadsafe(self._talk_to_zai(prompt), self.loop)
|
| 118 |
return future.result(timeout=120)
|
| 119 |
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
browser_engine = AsyncBrowserThread()
|
| 122 |
browser_engine.start()
|
| 123 |
|
|
|
|
| 167 |
tc_descriptions = []
|
| 168 |
for tc in tool_calls_in_msg:
|
| 169 |
func = tc.get("function", {})
|
| 170 |
+
tc_descriptions.append(
|
| 171 |
+
f"Called '{func.get('name', '?')}' with: {func.get('arguments', '{}')}"
|
| 172 |
+
)
|
| 173 |
assistant_content += "\n[Previous tool calls: " + "; ".join(tc_descriptions) + "]"
|
| 174 |
if assistant_content.strip():
|
| 175 |
parts.append(f"[Assistant]: {assistant_content}")
|
|
|
|
| 215 |
instruction += "You MUST use one of the tools below to answer this question.\n"
|
| 216 |
instruction += "Do NOT answer directly. Do NOT say you don't have information.\n"
|
| 217 |
instruction += "You MUST respond with ONLY a JSON object to call the tool.\n\n"
|
|
|
|
| 218 |
instruction += "RESPONSE FORMAT - respond with ONLY this JSON, nothing else:\n"
|
| 219 |
instruction += '{"tool_calls": [{"name": "TOOL_NAME", "arguments": {"param": "value"}}]}\n\n'
|
|
|
|
| 220 |
instruction += "RULES:\n"
|
| 221 |
instruction += "- Your ENTIRE response must be valid JSON only\n"
|
| 222 |
instruction += "- No markdown, no code blocks, no explanation\n"
|
| 223 |
instruction += "- No text before or after the JSON\n\n"
|
|
|
|
| 224 |
instruction += "Available tools:\n\n"
|
| 225 |
|
| 226 |
for tool in tools:
|
|
|
|
| 228 |
name = func.get("name", "unknown")
|
| 229 |
desc = func.get("description", "No description")
|
| 230 |
params = func.get("parameters", {})
|
| 231 |
+
instruction += f"Tool: {name}\nDescription: {desc}\n"
|
|
|
|
|
|
|
|
|
|
| 232 |
if params.get("properties"):
|
| 233 |
instruction += "Parameters:\n"
|
| 234 |
required_params = params.get("required", [])
|
|
|
|
| 240 |
instruction += "\n"
|
| 241 |
|
| 242 |
instruction += "=== END OF TOOLS ===\n\n"
|
|
|
|
| 243 |
first_tool = tools[0] if tools else {}
|
| 244 |
first_func = first_tool.get("function", first_tool)
|
| 245 |
first_name = first_func.get("name", "tool")
|
|
|
|
| 246 |
instruction += f'EXAMPLE: If the user asks a question, respond with:\n'
|
| 247 |
instruction += '{"tool_calls": [{"name": "' + first_name + '", "arguments": {"input": "the user question here"}}]}\n\n'
|
|
|
|
| 248 |
instruction += "Now respond with the JSON to call the appropriate tool:\n\n"
|
| 249 |
return instruction
|
| 250 |
|
|
|
|
| 295 |
app = FastAPI(title="zai_api for n8n")
|
| 296 |
|
| 297 |
|
| 298 |
+
def _auth(request: Request) -> bool:
|
| 299 |
+
auth = request.headers.get("authorization", "")
|
| 300 |
+
return auth.replace("Bearer ", "").strip() == API_SECRET_KEY
|
| 301 |
+
|
| 302 |
+
|
| 303 |
@app.post("/v1/chat/completions")
|
| 304 |
async def chat_completions(request: Request):
|
| 305 |
try:
|
|
|
|
| 307 |
except Exception:
|
| 308 |
return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
|
| 309 |
|
| 310 |
+
if not _auth(request):
|
|
|
|
| 311 |
return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
|
| 312 |
|
| 313 |
messages = data.get("messages", [])
|
|
|
|
| 322 |
response_text = browser_engine.process_request(prompt)
|
| 323 |
p_tokens = len(prompt.split())
|
| 324 |
c_tokens = len(response_text.split())
|
| 325 |
+
tool_calls = parse_tool_calls(response_text) if tools else None
|
|
|
|
|
|
|
| 326 |
|
| 327 |
if tool_calls:
|
| 328 |
return {
|
|
|
|
| 335 |
"usage": {"prompt_tokens": p_tokens, "completion_tokens": c_tokens,
|
| 336 |
"total_tokens": p_tokens + c_tokens}
|
| 337 |
}
|
| 338 |
+
return {
|
| 339 |
+
"id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
|
| 340 |
+
"object": "chat.completion",
|
| 341 |
+
"created": int(start_time),
|
| 342 |
+
"model": data.get("model", "GLM-5.1"),
|
| 343 |
+
"choices": [{"index": 0, "message": {"role": "assistant", "content": response_text},
|
| 344 |
+
"finish_reason": "stop"}],
|
| 345 |
+
"usage": {"prompt_tokens": p_tokens, "completion_tokens": c_tokens,
|
| 346 |
+
"total_tokens": p_tokens + c_tokens}
|
| 347 |
+
}
|
|
|
|
| 348 |
except Exception as e:
|
| 349 |
return JSONResponse(status_code=500, content={"error": str(e)})
|
| 350 |
|
|
|
|
| 356 |
except Exception:
|
| 357 |
return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
|
| 358 |
|
| 359 |
+
if not _auth(request):
|
|
|
|
| 360 |
return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
|
| 361 |
|
| 362 |
input_data = data.get("input", "")
|
|
|
|
| 380 |
response_text = browser_engine.process_request(prompt)
|
| 381 |
p_tokens = len(prompt.split())
|
| 382 |
c_tokens = len(response_text.split())
|
| 383 |
+
tool_calls = parse_tool_calls(response_text) if tools else None
|
|
|
|
|
|
|
| 384 |
|
| 385 |
if tool_calls:
|
| 386 |
output_items = []
|
|
|
|
| 403 |
"usage": {"input_tokens": p_tokens, "output_tokens": c_tokens,
|
| 404 |
"total_tokens": p_tokens + c_tokens}
|
| 405 |
}
|
| 406 |
+
return {
|
| 407 |
+
"id": f"resp-{uuid.uuid4().hex[:29]}",
|
| 408 |
+
"object": "response",
|
| 409 |
+
"created_at": int(start_time),
|
| 410 |
+
"model": data.get("model", "GLM-5.1"),
|
| 411 |
+
"status": "completed",
|
| 412 |
+
"output": [{"type": "message", "role": "assistant",
|
| 413 |
+
"content": [{"type": "output_text", "text": response_text}]}],
|
| 414 |
+
"usage": {"input_tokens": p_tokens, "output_tokens": c_tokens,
|
| 415 |
+
"total_tokens": p_tokens + c_tokens}
|
| 416 |
+
}
|
|
|
|
| 417 |
except Exception as e:
|
| 418 |
return JSONResponse(status_code=500, content={"error": str(e)})
|
| 419 |
|