infinityonline commited on
Commit
800c574
·
verified ·
1 Parent(s): 5c90c6e

Upload 4 files

Browse files
Files changed (2) hide show
  1. README.md +1 -1
  2. main.py +65 -81
README.md CHANGED
@@ -10,4 +10,4 @@ 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.
 
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, Header, HTTPException, Request
10
  from fastapi.responses import JSONResponse
11
 
12
 
13
- API_SECRET_KEY = os.getenv("API_SECRET_KEY", "2026-2026")
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
- await context.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
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: 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}")
@@ -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(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}")
@@ -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
- 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", [])
@@ -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
- 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
 
@@ -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
- 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", "")
@@ -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
- 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
 
 
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