infinityonline commited on
Commit
f343084
·
verified ·
1 Parent(s): bf8aefb

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +41 -0
  2. README.md +5 -5
  3. main.py +621 -0
  4. requirements.txt +6 -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,10 @@
1
  ---
2
- title: Dz
3
- emoji: 🏆
4
- colorFrom: green
5
  colorTo: purple
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: DZ Serve
3
+ emoji: 🤖
4
+ colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
 
 
main.py ADDED
@@ -0,0 +1,621 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import time
4
+ import asyncio
5
+ import threading
6
+ import json
7
+ import re
8
+ import httpx
9
+ from typing import Optional
10
+ from fastapi import FastAPI, Request
11
+ from fastapi.responses import JSONResponse, StreamingResponse
12
+
13
+ # ====================================================================
14
+ # Configuration
15
+ # ====================================================================
16
+ API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
17
+
18
+ # ── Duck.ai models (HTTP مباشر - بدون Playwright) ──────────────────
19
+ DUCK_MODELS = {
20
+ "gpt-4o-mini": "gpt-4o-mini",
21
+ "gpt-5-mini": "gpt-5-mini",
22
+ "o3-mini": "o3-mini",
23
+ "gpt-oss-120b": "gpt-oss-120b",
24
+ "claude-haiku-4-5": "claude-haiku-4-5",
25
+ "claude-3-haiku-20240307": "claude-3-haiku-20240307",
26
+ "llama-4-scout": "meta-llama/Llama-4-Scout-17B-16E-Instruct",
27
+ "meta-llama/Llama-3.3-70B-Instruct-Turbo": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
28
+ "mistral-small-4": "mistralai/Mistral-Small-3.1-24B-Instruct-2503",
29
+ "mistralai/Mistral-Small-24B-Instruct-2501": "mistralai/Mistral-Small-24B-Instruct-2501",
30
+ "mistralai/Mixtral-8x7B-Instruct-v0.1": "mistralai/Mixtral-8x7B-Instruct-v0.1",
31
+ }
32
+
33
+ # ── ZAI models (Playwright - browser scraping) ─────────────────────
34
+ ZAI_MODELS = [
35
+ "GLM-5.1", "GLM-5-Turbo", "GLM-5V-Turbo",
36
+ "GLM-5", "GLM-4.7", "GLM-4.6V", "GLM-4.5-Air"
37
+ ]
38
+
39
+ ALL_MODELS = list(DUCK_MODELS.keys()) + ZAI_MODELS
40
+
41
+ DUCK_STATUS_URL = "https://duckduckgo.com/duckchat/v1/status"
42
+ DUCK_CHAT_URL = "https://duckduckgo.com/duckchat/v1/chat"
43
+
44
+ DUCK_HEADERS_BASE = {
45
+ "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",
46
+ "Accept-Language": "en-US,en;q=0.9",
47
+ "Origin": "https://duckduckgo.com",
48
+ "Referer": "https://duckduckgo.com/",
49
+ }
50
+
51
+ # ====================================================================
52
+ # Duck.ai HTTP Client
53
+ # ====================================================================
54
+
55
+ async def _get_vqd_token() -> str:
56
+ """الحصول على رمز VQD من duck.ai - مطلوب لكل محادثة جديدة"""
57
+ headers = {**DUCK_HEADERS_BASE, "x-vqd-accept": "1"}
58
+ async with httpx.AsyncClient(timeout=30) as client:
59
+ r = await client.get(DUCK_STATUS_URL, headers=headers)
60
+ token = r.headers.get("x-vqd-4", "")
61
+ if not token:
62
+ raise Exception("فشل الحصول على VQD token من duck.ai")
63
+ return token
64
+
65
+
66
+ def _build_duck_messages(messages: list) -> list:
67
+ """تحويل messages إلى الصيغة التي تقبلها duck.ai"""
68
+ result = []
69
+ for m in messages:
70
+ role = m.get("role", "user")
71
+ # duck.ai تدعم user و assistant فقط - system يتحول لـ user
72
+ if role == "system":
73
+ role = "user"
74
+ if role not in ("user", "assistant"):
75
+ continue
76
+ content = _extract_content(m)
77
+ if content.strip():
78
+ result.append({"role": role, "content": content})
79
+ return result
80
+
81
+
82
+ def _extract_content(msg: dict) -> str:
83
+ content = msg.get("content", "")
84
+ if isinstance(content, list):
85
+ parts = []
86
+ for item in content:
87
+ if isinstance(item, dict):
88
+ parts.append(item.get("text", item.get("content", str(item))))
89
+ else:
90
+ parts.append(str(item))
91
+ return "\n".join(parts)
92
+ return str(content) if content else ""
93
+
94
+
95
+ async def duck_chat_complete(model: str, messages: list) -> str:
96
+ """استدعاء duck.ai وإرجاع النص الكامل"""
97
+ duck_model = DUCK_MODELS.get(model, "gpt-4o-mini")
98
+ vqd_token = await _get_vqd_token()
99
+
100
+ payload = {
101
+ "model": duck_model,
102
+ "messages": _build_duck_messages(messages),
103
+ }
104
+ headers = {
105
+ **DUCK_HEADERS_BASE,
106
+ "Content-Type": "application/json",
107
+ "Accept": "text/event-stream",
108
+ "x-vqd-4": vqd_token,
109
+ }
110
+
111
+ async with httpx.AsyncClient(timeout=120) as client:
112
+ r = await client.post(DUCK_CHAT_URL, json=payload, headers=headers)
113
+ if r.status_code == 429:
114
+ raise Exception("duck.ai rate limit - حاول مرة أخرى بعد قليل")
115
+ r.raise_for_status()
116
+
117
+ full_text = ""
118
+ for line in r.text.splitlines():
119
+ if line.startswith("data: "):
120
+ data = line[6:].strip()
121
+ if data == "[DONE]":
122
+ break
123
+ try:
124
+ chunk = json.loads(data)
125
+ full_text += chunk.get("message", "")
126
+ except Exception:
127
+ pass
128
+ return full_text.strip()
129
+
130
+
131
+ async def duck_chat_stream(model: str, messages: list):
132
+ """استدعاء duck.ai بوضع streaming - يُرجع generator"""
133
+ duck_model = DUCK_MODELS.get(model, "gpt-4o-mini")
134
+ vqd_token = await _get_vqd_token()
135
+
136
+ payload = {
137
+ "model": duck_model,
138
+ "messages": _build_duck_messages(messages),
139
+ }
140
+ headers = {
141
+ **DUCK_HEADERS_BASE,
142
+ "Content-Type": "application/json",
143
+ "Accept": "text/event-stream",
144
+ "x-vqd-4": vqd_token,
145
+ }
146
+
147
+ async with httpx.AsyncClient(timeout=120) as client:
148
+ async with client.stream("POST", DUCK_CHAT_URL, json=payload, headers=headers) as r:
149
+ if r.status_code == 429:
150
+ raise Exception("duck.ai rate limit")
151
+ r.raise_for_status()
152
+ async for line in r.aiter_lines():
153
+ if line.startswith("data: "):
154
+ data = line[6:].strip()
155
+ if data == "[DONE]":
156
+ return
157
+ try:
158
+ chunk = json.loads(data)
159
+ token = chunk.get("message", "")
160
+ if token:
161
+ yield token
162
+ except Exception:
163
+ pass
164
+
165
+
166
+ # ====================================================================
167
+ # Playwright Browser Engine (ZAI models)
168
+ # ====================================================================
169
+
170
+ class AsyncBrowserThread(threading.Thread):
171
+ def __init__(self):
172
+ super().__init__(daemon=True)
173
+ self.loop = asyncio.new_event_loop()
174
+ self.ready_event = threading.Event()
175
+ self.browser = None
176
+ self.playwright = None
177
+
178
+ def run(self):
179
+ asyncio.set_event_loop(self.loop)
180
+ self.loop.run_until_complete(self._start_browser())
181
+ self.ready_event.set()
182
+ print("[ZAI-SERVER] Browser is ready!")
183
+ self.loop.run_forever()
184
+
185
+ async def _start_browser(self):
186
+ from playwright.async_api import async_playwright
187
+ print("[ZAI-SERVER] Starting Chrome...")
188
+ self.playwright = await async_playwright().start()
189
+ self.browser = await self.playwright.chromium.launch(
190
+ headless=True,
191
+ channel="chrome",
192
+ args=[
193
+ "--disable-blink-features=AutomationControlled",
194
+ "--no-sandbox",
195
+ "--disable-gpu",
196
+ "--disable-dev-shm-usage",
197
+ "--disable-setuid-sandbox",
198
+ "--single-process",
199
+ "--no-zygote",
200
+ ],
201
+ )
202
+ print("[ZAI-SERVER] Chrome launched!")
203
+
204
+ async def _talk_to_zai(self, prompt: str):
205
+ context = await self.browser.new_context(
206
+ 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",
207
+ viewport={"width": 1920, "height": 1080},
208
+ )
209
+ await context.add_init_script(
210
+ "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
211
+ )
212
+ page = await context.new_page()
213
+ try:
214
+ page.set_default_timeout(120000)
215
+ await page.goto("https://chat.z.ai/", wait_until="domcontentloaded")
216
+ await asyncio.sleep(3)
217
+ await page.wait_for_selector("textarea#chat-input", timeout=60000)
218
+ await page.fill("textarea#chat-input", prompt)
219
+ await asyncio.sleep(0.5)
220
+ await page.press("textarea#chat-input", "Enter")
221
+ print(f"[ZAI-SERVER] Sent ({len(prompt)} chars)")
222
+ await asyncio.sleep(2)
223
+ await page.wait_for_selector("#response-content-container", timeout=120000)
224
+ last_text = ""
225
+ unchanged_cnt = 0
226
+ while unchanged_cnt < 5:
227
+ current_text = await page.evaluate("""
228
+ () => {
229
+ const prose = document.querySelector(
230
+ '#response-content-container .markdown-prose'
231
+ );
232
+ if (!prose) return '';
233
+ const clone = prose.cloneNode(true);
234
+ clone.querySelectorAll(':scope > div').forEach(el => el.remove());
235
+ return clone.innerText.trim();
236
+ }
237
+ """)
238
+ if current_text == last_text and current_text.strip():
239
+ unchanged_cnt += 1
240
+ else:
241
+ last_text = current_text
242
+ unchanged_cnt = 0
243
+ await asyncio.sleep(1.0)
244
+ print(f"[ZAI-SERVER] Response: {len(last_text)} chars")
245
+ return last_text.strip()
246
+ except Exception as e:
247
+ print(f"[ZAI-SERVER] Error: {e}")
248
+ raise e
249
+ finally:
250
+ await page.close()
251
+ await context.close()
252
+
253
+ def process_request(self, prompt: str):
254
+ if not self.ready_event.wait(timeout=60):
255
+ raise Exception("Browser not ready")
256
+ future = asyncio.run_coroutine_threadsafe(self._talk_to_zai(prompt), self.loop)
257
+ return future.result(timeout=120)
258
+
259
+
260
+ browser_engine = AsyncBrowserThread()
261
+ browser_engine.start()
262
+
263
+
264
+ # ====================================================================
265
+ # Prompt Builder (للـ ZAI models - محفوظ بالكامل)
266
+ # ====================================================================
267
+
268
+ def format_prompt(messages, tools=None):
269
+ parts = []
270
+ system_parts = []
271
+ has_tool_results = False
272
+ user_question = ""
273
+
274
+ for msg in messages:
275
+ role = msg.get("role", "")
276
+ msg_type = msg.get("type", "")
277
+ content = msg.get("content", "")
278
+
279
+ if isinstance(content, list):
280
+ text_parts = []
281
+ for item in content:
282
+ if isinstance(item, dict):
283
+ text_parts.append(item.get("text", item.get("content", str(item))))
284
+ else:
285
+ text_parts.append(str(item))
286
+ content = "\n".join(text_parts)
287
+
288
+ if role == "system":
289
+ system_parts.append(content)
290
+ elif role == "tool":
291
+ has_tool_results = True
292
+ tool_name = msg.get("name", "tool")
293
+ parts.append(f"[TOOL RESULT from '{tool_name}']:\n{content}")
294
+ elif msg_type == "function_call_output":
295
+ has_tool_results = True
296
+ call_id = msg.get("call_id", "")
297
+ output_content = msg.get("output", content)
298
+ parts.append(f"[TOOL RESULT (call_id: {call_id})]:\n{output_content}")
299
+ elif msg_type == "function_call":
300
+ func_name = msg.get("name", "?")
301
+ func_args = msg.get("arguments", "{}")
302
+ parts.append(f"[PREVIOUS TOOL CALL: Called '{func_name}' with arguments: {func_args}]")
303
+ elif role == "assistant":
304
+ assistant_content = content if content else ""
305
+ tool_calls_in_msg = msg.get("tool_calls", [])
306
+ if tool_calls_in_msg:
307
+ tc_desc = []
308
+ for tc in tool_calls_in_msg:
309
+ func = tc.get("function", {})
310
+ tc_desc.append(f"Called '{func.get('name','?')}' with: {func.get('arguments','{}')}")
311
+ assistant_content += "\n[Previous tool calls: " + "; ".join(tc_desc) + "]"
312
+ if assistant_content.strip():
313
+ parts.append(f"[Assistant]: {assistant_content}")
314
+ elif role == "user" or (msg_type == "message" and role != "system"):
315
+ user_question = content
316
+ has_tool_results = False
317
+ parts.append(content)
318
+ elif content:
319
+ parts.append(content)
320
+
321
+ final = ""
322
+ if system_parts:
323
+ if tools and not has_tool_results:
324
+ final += "=== YOUR ROLE ===\n" + "\n\n".join(system_parts) + "\n=== END OF ROLE ===\n\n"
325
+ else:
326
+ final += "=== SYSTEM INSTRUCTIONS (FOLLOW STRICTLY) ===\n" + "\n\n".join(system_parts) + "\n=== END OF INSTRUCTIONS ===\n\n"
327
+
328
+ if tools and not has_tool_results:
329
+ final += format_tools_instruction(tools, user_question)
330
+
331
+ if has_tool_results:
332
+ final += "=== CONTEXT FROM TOOLS ===\nThe following information was retrieved by the tools you requested.\nUse ONLY this information to answer the user's question.\n\n"
333
+
334
+ if parts:
335
+ final += "\n".join(parts)
336
+
337
+ if has_tool_results:
338
+ final += "\n\n=== INSTRUCTION ===\nNow answer the user's question based ONLY on the tool results above.\n"
339
+
340
+ return final
341
+
342
+
343
+ def format_tools_instruction(tools, user_question=""):
344
+ instruction = "\n=== MANDATORY TOOL USAGE ===\n"
345
+ instruction += "You MUST use one of the tools below to answer this question.\n"
346
+ instruction += "Do NOT answer directly. Do NOT say you don't have information.\n"
347
+ instruction += "You MUST respond with ONLY a JSON object to call the tool.\n\n"
348
+ instruction += 'RESPONSE FORMAT - respond with ONLY this JSON, nothing else:\n'
349
+ instruction += '{"tool_calls": [{"name": "TOOL_NAME", "arguments": {"param": "value"}}]}\n\n'
350
+ instruction += "RULES:\n- Your ENTIRE response must be valid JSON only\n- No markdown, no code blocks, no explanation\n- No text before or after the JSON\n\n"
351
+ instruction += "Available tools:\n\n"
352
+
353
+ for tool in tools:
354
+ func = tool.get("function", tool)
355
+ name = func.get("name", "unknown")
356
+ desc = func.get("description", "No description")
357
+ params = func.get("parameters", {})
358
+ instruction += f"Tool: {name}\nDescription: {desc}\n"
359
+ if params.get("properties"):
360
+ instruction += "Parameters:\n"
361
+ required_params = params.get("required", [])
362
+ for pname, pinfo in params["properties"].items():
363
+ ptype = pinfo.get("type", "string")
364
+ pdesc = pinfo.get("description", "")
365
+ req = "required" if pname in required_params else "optional"
366
+ instruction += f" - {pname} ({ptype}, {req}): {pdesc}\n"
367
+ instruction += "\n"
368
+
369
+ instruction += "=== END OF TOOLS ===\n\n"
370
+ first_func = (tools[0] if tools else {}).get("function", tools[0] if tools else {})
371
+ first_name = first_func.get("name", "tool")
372
+ instruction += f'EXAMPLE:\n{{"tool_calls": [{{"name": "{first_name}", "arguments": {{"input": "the user question here"}}}}]}}\n\n'
373
+ instruction += "Now respond with the JSON to call the appropriate tool:\n\n"
374
+ return instruction
375
+
376
+
377
+ def parse_tool_calls(response_text):
378
+ cleaned = response_text.strip()
379
+ if "```" in cleaned:
380
+ m = re.search(r'```(?:json)?\s*\n?(.*?)\n?\s*```', cleaned, re.DOTALL)
381
+ if m:
382
+ cleaned = m.group(1).strip()
383
+
384
+ candidates = [cleaned]
385
+ m2 = re.search(r'\{[\s\S]*"tool_calls"[\s\S]*\}', cleaned)
386
+ if m2:
387
+ candidates.append(m2.group(0))
388
+
389
+ for candidate in candidates:
390
+ try:
391
+ parsed = json.loads(candidate)
392
+ if isinstance(parsed, dict) and "tool_calls" in parsed:
393
+ raw_calls = parsed["tool_calls"]
394
+ if isinstance(raw_calls, list) and raw_calls:
395
+ formatted = []
396
+ for call in raw_calls:
397
+ tool_name = call.get("name", "")
398
+ arguments = call.get("arguments", {})
399
+ arguments_str = json.dumps(arguments, ensure_ascii=False) if isinstance(arguments, dict) else str(arguments)
400
+ formatted.append({
401
+ "id": f"call_{uuid.uuid4().hex[:24]}",
402
+ "type": "function",
403
+ "function": {"name": tool_name, "arguments": arguments_str},
404
+ })
405
+ return formatted
406
+ except (json.JSONDecodeError, TypeError, KeyError):
407
+ continue
408
+ return None
409
+
410
+
411
+ # ====================================================================
412
+ # Helpers
413
+ # ====================================================================
414
+
415
+ def _is_duck_model(model: str) -> bool:
416
+ return model in DUCK_MODELS
417
+
418
+
419
+ def _auth(request: Request) -> bool:
420
+ auth = request.headers.get("authorization", "")
421
+ return auth.replace("Bearer ", "").strip() == API_SECRET_KEY
422
+
423
+
424
+ def _build_response(start_time, model, response_text, messages, tools, is_duck):
425
+ p_tokens = sum(len(_extract_content(m).split()) for m in messages)
426
+ c_tokens = len(response_text.split())
427
+ tool_calls = parse_tool_calls(response_text) if tools else None
428
+
429
+ if tool_calls:
430
+ return {
431
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
432
+ "object": "chat.completion",
433
+ "created": int(start_time),
434
+ "model": model,
435
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": None,
436
+ "tool_calls": tool_calls}, "finish_reason": "tool_calls"}],
437
+ "usage": {"prompt_tokens": p_tokens, "completion_tokens": c_tokens,
438
+ "total_tokens": p_tokens + c_tokens},
439
+ }
440
+ return {
441
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
442
+ "object": "chat.completion",
443
+ "created": int(start_time),
444
+ "model": model,
445
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": response_text},
446
+ "finish_reason": "stop"}],
447
+ "usage": {"prompt_tokens": p_tokens, "completion_tokens": c_tokens,
448
+ "total_tokens": p_tokens + c_tokens},
449
+ }
450
+
451
+
452
+ # ====================================================================
453
+ # FastAPI App
454
+ # ====================================================================
455
+ app = FastAPI(title="ZAI + DuckAI API Server")
456
+
457
+
458
+ # ── POST /v1/chat/completions ──────────────────────────────────────
459
+ @app.post("/v1/chat/completions")
460
+ async def chat_completions(request: Request):
461
+ try:
462
+ data = await request.json()
463
+ except Exception:
464
+ return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
465
+
466
+ if not _auth(request):
467
+ return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
468
+
469
+ messages = data.get("messages", [])
470
+ if not messages:
471
+ return JSONResponse(status_code=400, content={"error": {"message": "messages field is required"}})
472
+
473
+ model = data.get("model", "gpt-4o-mini")
474
+ tools = data.get("tools", None)
475
+ do_stream = data.get("stream", False)
476
+ start_time = time.time()
477
+
478
+ try:
479
+ # ── Duck.ai path ──────────────────────────────────────────
480
+ if _is_duck_model(model):
481
+ if do_stream:
482
+ chunk_id = f"chatcmpl-{uuid.uuid4().hex[:29]}"
483
+
484
+ async def event_stream():
485
+ async for token in duck_chat_stream(model, messages):
486
+ chunk = {
487
+ "id": chunk_id, "object": "chat.completion.chunk",
488
+ "created": int(start_time), "model": model,
489
+ "choices": [{"index": 0, "delta": {"content": token}, "finish_reason": None}],
490
+ }
491
+ yield f"data: {json.dumps(chunk)}\n\n"
492
+ final = {
493
+ "id": chunk_id, "object": "chat.completion.chunk",
494
+ "created": int(start_time), "model": model,
495
+ "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
496
+ }
497
+ yield f"data: {json.dumps(final)}\n\n"
498
+ yield "data: [DONE]\n\n"
499
+
500
+ return StreamingResponse(event_stream(), media_type="text/event-stream")
501
+
502
+ response_text = await duck_chat_complete(model, messages)
503
+ return _build_response(start_time, model, response_text, messages, tools, is_duck=True)
504
+
505
+ # ── ZAI (Playwright) path ─────────────────────────────────
506
+ else:
507
+ prompt = format_prompt(messages, tools=tools)
508
+ print(f"[ZAI-SERVER] Processing ({len(prompt)} chars)")
509
+ response_text = browser_engine.process_request(prompt)
510
+ return _build_response(start_time, model, response_text, messages, tools, is_duck=False)
511
+
512
+ except Exception as e:
513
+ return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
514
+
515
+
516
+ # ── POST /v1/responses ────────────────────────────────────────────
517
+ @app.post("/v1/responses")
518
+ async def responses(request: Request):
519
+ try:
520
+ data = await request.json()
521
+ except Exception:
522
+ return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
523
+
524
+ if not _auth(request):
525
+ return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
526
+
527
+ input_data = data.get("input", "")
528
+ if isinstance(input_data, str):
529
+ messages = [{"role": "user", "content": input_data}]
530
+ elif isinstance(input_data, list):
531
+ messages = input_data
532
+ else:
533
+ messages = data.get("messages", [])
534
+
535
+ if not messages:
536
+ return JSONResponse(status_code=400, content={"error": {"message": "input field is required"}})
537
+
538
+ model = data.get("model", "gpt-4o-mini")
539
+ tools = data.get("tools", None)
540
+ instructions = data.get("instructions", "")
541
+ if instructions:
542
+ messages.insert(0, {"role": "system", "content": instructions})
543
+
544
+ start_time = time.time()
545
+
546
+ try:
547
+ if _is_duck_model(model):
548
+ response_text = await duck_chat_complete(model, messages)
549
+ else:
550
+ prompt = format_prompt(messages, tools=tools)
551
+ response_text = browser_engine.process_request(prompt)
552
+
553
+ p_tokens = sum(len(_extract_content(m).split()) for m in messages)
554
+ c_tokens = len(response_text.split())
555
+ tool_calls = parse_tool_calls(response_text) if tools else None
556
+
557
+ if tool_calls:
558
+ output_items = []
559
+ for tc in tool_calls:
560
+ output_items.append({
561
+ "type": "function_call",
562
+ "id": tc["id"],
563
+ "call_id": tc["id"],
564
+ "name": tc["function"]["name"],
565
+ "arguments": tc["function"]["arguments"],
566
+ "status": "completed",
567
+ })
568
+ return {
569
+ "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
570
+ "created_at": int(start_time), "model": model, "status": "completed",
571
+ "output": output_items,
572
+ "usage": {"input_tokens": p_tokens, "output_tokens": c_tokens,
573
+ "total_tokens": p_tokens + c_tokens},
574
+ }
575
+
576
+ return {
577
+ "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
578
+ "created_at": int(start_time), "model": model, "status": "completed",
579
+ "output": [{"type": "message", "role": "assistant",
580
+ "content": [{"type": "output_text", "text": response_text}]}],
581
+ "usage": {"input_tokens": p_tokens, "output_tokens": c_tokens,
582
+ "total_tokens": p_tokens + c_tokens},
583
+ }
584
+
585
+ except Exception as e:
586
+ return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
587
+
588
+
589
+ # ── GET /v1/models ────────────────────────────────────────────────
590
+ @app.get("/v1/models")
591
+ async def list_models(request: Request):
592
+ if not _auth(request):
593
+ return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
594
+ return {
595
+ "object": "list",
596
+ "data": [
597
+ {
598
+ "id": m,
599
+ "object": "model",
600
+ "owned_by": "duck.ai" if m in DUCK_MODELS else "zai",
601
+ }
602
+ for m in ALL_MODELS
603
+ ],
604
+ }
605
+
606
+
607
+ # ── GET /health & GET / ─────────────────────────────────────────
608
+ @app.get("/health")
609
+ @app.get("/")
610
+ async def health_check():
611
+ return {
612
+ "status": "running",
613
+ "message": "ZAI + DuckAI API Server is active!",
614
+ "duck_models": list(DUCK_MODELS.keys()),
615
+ "zai_models": ZAI_MODELS,
616
+ }
617
+
618
+
619
+ if __name__ == "__main__":
620
+ import uvicorn
621
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
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
6
+ httpx==0.27.0