infinityonline commited on
Commit
6011cd2
·
verified ·
1 Parent(s): 0970a4d

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +41 -0
  2. README.md +8 -5
  3. main.py +460 -0
  4. requirements.txt +5 -0
Dockerfile ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ RUN useradd -m -u 1000 user
4
+ ENV HOME=/home/user \
5
+ PATH=/home/user/.local/bin:$PATH \
6
+ PYTHONUNBUFFERED=1 \
7
+ PLAYWRIGHT_BROWSERS_PATH=/home/user/.cache/ms-playwright
8
+
9
+ WORKDIR /home/user/app
10
+
11
+ RUN apt-get update && apt-get install -y \
12
+ wget gnupg ca-certificates curl \
13
+ libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \
14
+ libcups2 libdrm2 libxkbcommon0 libxcomposite1 \
15
+ libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 \
16
+ libpango-1.0-0 libcairo2 libasound2 \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ COPY --chown=user requirements.txt .
20
+ RUN pip install --no-cache-dir -r requirements.txt
21
+
22
+ RUN mkdir -p /etc/apt/keyrings \
23
+ && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub \
24
+ | gpg --dearmor -o /etc/apt/keyrings/google-chrome.gpg \
25
+ && echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.gpg] \
26
+ http://dl.google.com/linux/chrome/deb/ stable main" \
27
+ > /etc/apt/sources.list.d/google-chrome.list \
28
+ && apt-get update && apt-get install -y google-chrome-stable \
29
+ && rm -rf /var/lib/apt/lists/*
30
+
31
+ RUN playwright install chromium
32
+
33
+ COPY --chown=user . .
34
+
35
+ USER user
36
+ EXPOSE 7860
37
+
38
+ HEALTHCHECK --interval=30s --timeout=15s --start-period=120s --retries=3 \
39
+ CMD curl -f http://localhost:7860/health || exit 1
40
+
41
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Zapi
3
- emoji: 📉
4
- colorFrom: yellow
5
- colorTo: green
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: ZAI API
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # ZAI API chat.z.ai
13
+ OpenAI-compatible API powered by Z.ai (GLM-5.1 / GLM-5-Turbo) via Playwright.
main.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import time
4
+ import asyncio
5
+ import threading
6
+ import json
7
+ import re
8
+ from typing import Optional
9
+ from fastapi import FastAPI, Header, HTTPException, Request
10
+ from fastapi.responses import JSONResponse
11
+
12
+
13
+ API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-secret-key-2026")
14
+
15
+ # الموديلات المتاحة في chat.z.ai
16
+ ZAI_MODELS = ["GLM-5.1", "GLM-5-Turbo"]
17
+
18
+
19
+ class AsyncBrowserThread(threading.Thread):
20
+ def __init__(self):
21
+ super().__init__(daemon=True)
22
+ self.loop = asyncio.new_event_loop()
23
+ self.ready_event = threading.Event()
24
+ self.browser = None
25
+ self.playwright = None
26
+
27
+ def run(self):
28
+ asyncio.set_event_loop(self.loop)
29
+ self.loop.run_until_complete(self._start_browser())
30
+ self.ready_event.set()
31
+ print("[ZAI-SERVER] Browser is ready!")
32
+ self.loop.run_forever()
33
+
34
+ async def _start_browser(self):
35
+ from playwright.async_api import async_playwright
36
+ print("[ZAI-SERVER] Starting Chrome...")
37
+ self.playwright = await async_playwright().start()
38
+ self.browser = await self.playwright.chromium.launch(
39
+ headless=True,
40
+ channel="chrome",
41
+ args=[
42
+ '--disable-blink-features=AutomationControlled',
43
+ '--no-sandbox',
44
+ '--disable-gpu',
45
+ '--disable-dev-shm-usage',
46
+ '--disable-setuid-sandbox',
47
+ '--single-process',
48
+ '--no-zygote',
49
+ ]
50
+ )
51
+ print("[ZAI-SERVER] Chrome launched!")
52
+
53
+ async def _talk_to_zai(self, prompt: str):
54
+ context = await self.browser.new_context(
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
+ await context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
60
+
61
+ page = await context.new_page()
62
+
63
+ try:
64
+ page.set_default_timeout(120000)
65
+ await page.goto("https://chat.z.ai/", wait_until="domcontentloaded")
66
+ await asyncio.sleep(3)
67
+
68
+ # Input: textarea#chat-input (confirmed from DOM)
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
+ # Response: #response-content-container (confirmed from DOM)
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
+ containers = await page.query_selector_all("#response-content-container")
82
+ if containers:
83
+ current_text = await containers[-1].inner_text()
84
+ if current_text == last_text and current_text.strip() != "":
85
+ unchanged_count += 1
86
+ else:
87
+ last_text = current_text
88
+ unchanged_count = 0
89
+ await asyncio.sleep(1.0)
90
+
91
+ return _clean_zai_response(last_text)
92
+
93
+ except Exception as e:
94
+ print(f"[ZAI-SERVER] Error: {e}")
95
+ raise e
96
+ finally:
97
+ await page.close()
98
+ await context.close()
99
+
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
+
130
+
131
+ # ====================================================================
132
+ # Smart Prompt Builder
133
+ # ====================================================================
134
+ def format_prompt(messages, tools=None):
135
+ parts = []
136
+ system_parts = []
137
+ has_tool_results = False
138
+ user_question = ""
139
+
140
+ for msg in messages:
141
+ role = msg.get("role", "")
142
+ msg_type = msg.get("type", "")
143
+ content = msg.get("content", "")
144
+
145
+ if isinstance(content, list):
146
+ text_parts = []
147
+ for item in content:
148
+ if isinstance(item, dict):
149
+ text_parts.append(item.get("text", item.get("content", str(item))))
150
+ else:
151
+ text_parts.append(str(item))
152
+ content = "\n".join(text_parts)
153
+
154
+ if role == "system":
155
+ system_parts.append(content)
156
+ elif role == "tool":
157
+ has_tool_results = True
158
+ tool_name = msg.get("name", "tool")
159
+ parts.append(f"[TOOL RESULT from '{tool_name}']:\n{content}")
160
+ elif msg_type == "function_call_output":
161
+ has_tool_results = True
162
+ call_id = msg.get("call_id", "")
163
+ output_content = msg.get("output", content)
164
+ parts.append(f"[TOOL RESULT (call_id: {call_id})]:\n{output_content}")
165
+ elif msg_type == "function_call":
166
+ func_name = msg.get("name", "?")
167
+ func_args = msg.get("arguments", "{}")
168
+ parts.append(f"[PREVIOUS TOOL CALL: Called '{func_name}' with arguments: {func_args}]")
169
+ elif role == "assistant":
170
+ assistant_content = content if content else ""
171
+ tool_calls_in_msg = msg.get("tool_calls", [])
172
+ if tool_calls_in_msg:
173
+ tc_descriptions = []
174
+ for tc in tool_calls_in_msg:
175
+ func = tc.get("function", {})
176
+ tc_descriptions.append(f"Called '{func.get('name', '?')}' with: {func.get('arguments', '{}')}")
177
+ assistant_content += "\n[Previous tool calls: " + "; ".join(tc_descriptions) + "]"
178
+ if assistant_content.strip():
179
+ parts.append(f"[Assistant]: {assistant_content}")
180
+ elif role == "user" or (msg_type == "message" and role != "system"):
181
+ user_question = content
182
+ parts.append(content)
183
+ has_tool_results = False
184
+ elif content:
185
+ parts.append(content)
186
+
187
+ final = ""
188
+
189
+ if system_parts:
190
+ if tools and not has_tool_results:
191
+ final += "=== YOUR ROLE ===\n"
192
+ final += "\n\n".join(system_parts)
193
+ final += "\n=== END OF ROLE ===\n\n"
194
+ else:
195
+ final += "=== SYSTEM INSTRUCTIONS (FOLLOW STRICTLY) ===\n"
196
+ final += "\n\n".join(system_parts)
197
+ final += "\n=== END OF INSTRUCTIONS ===\n\n"
198
+
199
+ if tools and not has_tool_results:
200
+ final += format_tools_instruction(tools, user_question)
201
+
202
+ if has_tool_results:
203
+ final += "=== CONTEXT FROM TOOLS ===\n"
204
+ final += "The following information was retrieved by the tools you requested.\n"
205
+ final += "Use ONLY this information to answer the user's question.\n\n"
206
+
207
+ if parts:
208
+ final += "\n".join(parts)
209
+
210
+ if has_tool_results:
211
+ final += "\n\n=== INSTRUCTION ===\n"
212
+ final += "Now answer the user's question based ONLY on the tool results above.\n"
213
+
214
+ return final
215
+
216
+
217
+ def format_tools_instruction(tools, user_question=""):
218
+ instruction = "\n=== MANDATORY TOOL USAGE ===\n"
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:
234
+ func = tool.get("function", tool)
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", [])
245
+ for param_name, param_info in params["properties"].items():
246
+ param_type = param_info.get("type", "string")
247
+ param_desc = param_info.get("description", "")
248
+ is_required = "required" if param_name in required_params else "optional"
249
+ instruction += f" - {param_name} ({param_type}, {is_required}): {param_desc}\n"
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
+
264
+
265
+ def parse_tool_calls(response_text):
266
+ cleaned = response_text.strip()
267
+ if "```" in cleaned:
268
+ code_block_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?\s*```', cleaned, re.DOTALL)
269
+ if code_block_match:
270
+ cleaned = code_block_match.group(1).strip()
271
+
272
+ json_candidates = [cleaned]
273
+ json_match = re.search(r'\{[\s\S]*"tool_calls"[\s\S]*\}', cleaned)
274
+ if json_match:
275
+ json_candidates.append(json_match.group(0))
276
+
277
+ for candidate in json_candidates:
278
+ try:
279
+ parsed = json.loads(candidate)
280
+ if isinstance(parsed, dict) and "tool_calls" in parsed:
281
+ raw_calls = parsed["tool_calls"]
282
+ if isinstance(raw_calls, list) and len(raw_calls) > 0:
283
+ formatted_calls = []
284
+ for call in raw_calls:
285
+ tool_name = call.get("name", "")
286
+ arguments = call.get("arguments", {})
287
+ if isinstance(arguments, dict):
288
+ arguments_str = json.dumps(arguments, ensure_ascii=False)
289
+ else:
290
+ arguments_str = str(arguments)
291
+ formatted_calls.append({
292
+ "id": f"call_{uuid.uuid4().hex[:24]}",
293
+ "type": "function",
294
+ "function": {
295
+ "name": tool_name,
296
+ "arguments": arguments_str
297
+ }
298
+ })
299
+ return formatted_calls
300
+ except (json.JSONDecodeError, TypeError, KeyError):
301
+ continue
302
+ return None
303
+
304
+
305
+ # ====================================================================
306
+ # FastAPI App
307
+ # ====================================================================
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:
314
+ data = await request.json()
315
+ except Exception:
316
+ return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
317
+
318
+ authorization = request.headers.get("authorization", "")
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", [])
323
+ if not messages:
324
+ return JSONResponse(status_code=400, content={"error": {"message": "messages field is required"}})
325
+
326
+ try:
327
+ tools = data.get("tools", None)
328
+ prompt = format_prompt(messages, tools=tools)
329
+ start_time = time.time()
330
+ print(f"[ZAI-SERVER] Processing request ({len(prompt)} chars)")
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 {
340
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
341
+ "object": "chat.completion",
342
+ "created": int(start_time),
343
+ "model": data.get("model", "GLM-5.1"),
344
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": None,
345
+ "tool_calls": tool_calls}, "finish_reason": "tool_calls"}],
346
+ "usage": {"prompt_tokens": p_tokens, "completion_tokens": c_tokens,
347
+ "total_tokens": p_tokens + c_tokens}
348
+ }
349
+ else:
350
+ return {
351
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
352
+ "object": "chat.completion",
353
+ "created": int(start_time),
354
+ "model": data.get("model", "GLM-5.1"),
355
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": response_text},
356
+ "finish_reason": "stop"}],
357
+ "usage": {"prompt_tokens": p_tokens, "completion_tokens": c_tokens,
358
+ "total_tokens": p_tokens + c_tokens}
359
+ }
360
+ except Exception as e:
361
+ return JSONResponse(status_code=500, content={"error": str(e)})
362
+
363
+
364
+ @app.post("/v1/responses")
365
+ async def responses(request: Request):
366
+ try:
367
+ data = await request.json()
368
+ except Exception:
369
+ return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
370
+
371
+ authorization = request.headers.get("authorization", "")
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", "")
376
+ if isinstance(input_data, str):
377
+ messages = [{"role": "user", "content": input_data}]
378
+ elif isinstance(input_data, list):
379
+ messages = input_data
380
+ else:
381
+ messages = data.get("messages", [])
382
+
383
+ if not messages:
384
+ return JSONResponse(status_code=400, content={"error": {"message": "input field is required"}})
385
+
386
+ try:
387
+ tools = data.get("tools", None)
388
+ instructions = data.get("instructions", "")
389
+ if instructions:
390
+ messages.insert(0, {"role": "system", "content": instructions})
391
+ prompt = format_prompt(messages, tools=tools)
392
+ start_time = time.time()
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 = []
402
+ for tc in tool_calls:
403
+ output_items.append({
404
+ "type": "function_call",
405
+ "id": tc["id"],
406
+ "call_id": tc["id"],
407
+ "name": tc["function"]["name"],
408
+ "arguments": tc["function"]["arguments"],
409
+ "status": "completed"
410
+ })
411
+ return {
412
+ "id": f"resp-{uuid.uuid4().hex[:29]}",
413
+ "object": "response",
414
+ "created_at": int(start_time),
415
+ "model": data.get("model", "GLM-5.1"),
416
+ "status": "completed",
417
+ "output": output_items,
418
+ "usage": {"input_tokens": p_tokens, "output_tokens": c_tokens,
419
+ "total_tokens": p_tokens + c_tokens}
420
+ }
421
+ else:
422
+ return {
423
+ "id": f"resp-{uuid.uuid4().hex[:29]}",
424
+ "object": "response",
425
+ "created_at": int(start_time),
426
+ "model": data.get("model", "GLM-5.1"),
427
+ "status": "completed",
428
+ "output": [{"type": "message", "role": "assistant",
429
+ "content": [{"type": "output_text", "text": response_text}]}],
430
+ "usage": {"input_tokens": p_tokens, "output_tokens": c_tokens,
431
+ "total_tokens": p_tokens + c_tokens}
432
+ }
433
+ except Exception as e:
434
+ return JSONResponse(status_code=500, content={"error": str(e)})
435
+
436
+
437
+ @app.get("/v1/models")
438
+ async def list_models():
439
+ return {
440
+ "object": "list",
441
+ "data": [
442
+ {"id": m, "object": "model", "owned_by": "zai"}
443
+ for m in ZAI_MODELS
444
+ ]
445
+ }
446
+
447
+
448
+ @app.get("/health")
449
+ @app.get("/")
450
+ async def health_check():
451
+ return {
452
+ "status": "running",
453
+ "message": "zai_api Server is active!",
454
+ "models": ZAI_MODELS,
455
+ }
456
+
457
+
458
+ if __name__ == "__main__":
459
+ import uvicorn
460
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ fastapi==0.110.0
2
+ uvicorn==0.27.1
3
+ playwright==1.42.0
4
+ pydantic==2.6.3
5
+ python-multipart==0.0.9