infinityonline commited on
Commit
9b00487
ยท
verified ยท
1 Parent(s): d04ae9a

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +222 -259
main.py CHANGED
@@ -5,31 +5,27 @@ import asyncio
5
  import threading
6
  import json
7
  import re
8
- from typing import Optional
9
  from fastapi import FastAPI, Request
10
- from fastapi.responses import JSONResponse, StreamingResponse
11
 
12
  # ====================================================================
13
  # Configuration
14
  # ====================================================================
15
  API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
16
 
17
- # โ”€โ”€ Duck.ai models โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
18
- # ุงู„ู€ key ู‡ูˆ ู…ุง ูŠุฑุณู„ู‡ ุงู„ู…ุณุชุฎุฏู…ุŒ ุงู„ู€ value ู‡ูˆ ID ุงู„ู†ู…ูˆุฐุฌ ููŠ duck.ai
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": "claude-3-haiku-20240307",
26
- "llama-4-scout": "meta-llama/Llama-4-Scout-17B-16E-Instruct",
27
- "llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
28
- "mistral-small-4": "mistralai/Mistral-Small-3.1-24B-Instruct-2503",
29
- "mistral-small": "mistralai/Mistral-Small-24B-Instruct-2501",
30
  }
31
 
32
- # โ”€โ”€ ZAI models (Playwright โ†’ chat.z.ai) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
33
  ZAI_MODELS = [
34
  "GLM-5.1", "GLM-5-Turbo", "GLM-5V-Turbo",
35
  "GLM-5", "GLM-4.7", "GLM-4.6V", "GLM-4.5-Air"
@@ -37,11 +33,8 @@ ZAI_MODELS = [
37
 
38
  ALL_MODELS = list(DUCK_MODELS.keys()) + ZAI_MODELS
39
 
40
-
41
  # ====================================================================
42
- # Shared Browser Engine (Playwright)
43
- # ูŠุณุชุฎุฏู…ู‡ ZAI ุนุจุฑ chat.z.ai
44
- # ูˆูŠุณุชุฎุฏู…ู‡ Duck.ai ุนุจุฑ duck.ai (ู…ุชุตูุญ ุญู‚ูŠู‚ูŠ ูŠุชุฌุงูˆุฒ ุงู„ุญู…ุงูŠุฉ)
45
  # ====================================================================
46
 
47
  class AsyncBrowserThread(threading.Thread):
@@ -78,15 +71,16 @@ class AsyncBrowserThread(threading.Thread):
78
  )
79
  print("[SERVER] Chrome launched!")
80
 
81
- # โ”€โ”€ Duck.ai via Playwright โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
82
  async def _talk_to_duck(self, model_id: str, messages: list) -> str:
83
- """
84
- ูŠูุชุญ duck.ai ููŠ ุงู„ู…ุชุตูุญ ุงู„ุญู‚ูŠู‚ูŠ ูˆูŠุฑุณู„ ุงู„ุฑุณุงู„ุฉ
85
- model_id: ุงู„ู‚ูŠู…ุฉ ู…ู† DUCK_MODELS dict
86
- messages: ู‚ุงุฆู…ุฉ OpenAI messages
87
- """
88
  context = await self.browser.new_context(
89
- 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",
 
 
 
 
90
  viewport={"width": 1920, "height": 1080},
91
  )
92
  await context.add_init_script(
@@ -95,95 +89,115 @@ class AsyncBrowserThread(threading.Thread):
95
  page = await context.new_page()
96
  try:
97
  page.set_default_timeout(120000)
98
-
99
- # โ”€โ”€ ุจู†ุงุก ุงู„ู€ prompt ู…ู† messages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
100
  prompt = _build_duck_prompt(messages)
101
 
102
- # โ”€โ”€ ูุชุญ duck.ai โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
103
- await page.goto("https://duck.ai/", wait_until="domcontentloaded")
104
- await asyncio.sleep(4)
105
 
106
- # โ”€โ”€ ู‚ุจูˆู„ ุงู„ุดุฑูˆุท ุฅู† ุธู‡ุฑุช โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
107
  try:
108
- accept_btn = page.locator("button:has-text('Accept'), button:has-text('Get Started'), button:has-text('Start chatting')")
109
- if await accept_btn.count() > 0:
110
- await accept_btn.first.click()
111
- await asyncio.sleep(2)
 
 
 
 
 
 
 
112
  except Exception:
113
  pass
114
 
115
- # โ”€โ”€ ุงุฎุชูŠุงุฑ ุงู„ู†ู…ูˆุฐุฌ ุงู„ู…ุทู„ูˆุจ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
116
- # duck.ai ูŠุนุฑุถ ู†ู…ูˆุฐุฌุงู‹ ุงูุชุฑุงุถูŠุงู‹ - ู†ุญุชุงุฌ ุชุบูŠูŠุฑู‡ ุฅุฐุง ูƒุงู† ู…ุฎุชู„ูุงู‹
117
- try:
118
- model_btn = page.locator("[data-testid='model-selector'], button[aria-label*='model'], button[aria-label*='Model'], .model-selector")
119
- if await model_btn.count() > 0:
120
- await model_btn.first.click()
121
- await asyncio.sleep(1)
122
- # ุงู„ุจุญุซ ุนู† ุงู„ู†ู…ูˆุฐุฌ ุงู„ู…ุทู„ูˆุจ ููŠ ุงู„ู‚ุงุฆู…ุฉ
123
- model_option = page.locator(f"[data-value='{model_id}'], [value='{model_id}'], li:has-text('{model_id.split('/')[-1]}')")
124
- if await model_option.count() > 0:
125
- await model_option.first.click()
126
- await asyncio.sleep(1)
127
- else:
128
- # ุฅุบู„ุงู‚ ุงู„ู‚ุงุฆู…ุฉ ุฅุฐุง ู„ู… ูŠุฌุฏ ุงู„ู†ู…ูˆุฐุฌ
129
- await page.keyboard.press("Escape")
130
- except Exception as e:
131
- print(f"[DUCK] Model selection skipped: {e}")
132
-
133
- # โ”€โ”€ ุฅุฑุณุงู„ ุงู„ุฑุณุงู„ุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
134
- textarea = page.locator("textarea, [contenteditable='true'], [role='textbox']").first
135
- await textarea.wait_for(state="visible", timeout=30000)
136
  await textarea.click()
137
  await textarea.fill(prompt)
138
  await asyncio.sleep(0.5)
139
- await page.keyboard.press("Enter")
140
- print(f"[DUCK] Sent ({len(prompt)} chars)")
141
 
142
- # โ”€โ”€ ุงู†ุชุธุงุฑ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
143
  await asyncio.sleep(3)
144
 
145
- # ุงู†ุชุธุฑ ุฃู† ูŠุจุฏุฃ ุงู„ุฑุฏ
146
- response_selector = "[data-testid='message-assistant'], .message-content, .chat-message-content, [class*='AssistantMessage'], [class*='assistant-message']"
147
- await page.wait_for_selector(response_selector, timeout=60000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
- # ุงู†ุชุธุฑ ุญุชู‰ ูŠุชูˆู‚ู ุงู„ุฑุฏ ุนู† ุงู„ุชุบูŠู‘ุฑ
150
- last_text = ""
151
- unchanged_cnt = 0
152
- while unchanged_cnt < 6:
153
- current_text = await page.evaluate("""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  () => {
155
- // ู…ุญุงูˆู„ุฉ ุนุฏุฉ selectors
156
- const selectors = [
157
- '[data-testid="message-assistant"]:last-child',
158
- '.chat-message--assistant:last-child',
159
- '[class*="AssistantMessage"]:last-child',
160
- '[class*="assistant"]:last-child [class*="content"]',
161
- ];
162
- for (const sel of selectors) {
163
- const el = document.querySelector(sel);
164
- if (el && el.innerText && el.innerText.trim().length > 0) {
165
- return el.innerText.trim();
166
  }
167
  }
168
- // fallback: ุขุฎุฑ ุฑุณุงู„ุฉ ููŠ ุงู„ู…ุญุงุฏุซุฉ
169
- const allMsgs = document.querySelectorAll(
170
- '[class*="message"]:not([class*="user"]):not([class*="User"])'
171
- );
172
- if (allMsgs.length > 0) {
173
- return allMsgs[allMsgs.length - 1].innerText.trim();
174
- }
175
- return '';
176
  }
177
  """)
178
- if current_text == last_text and current_text.strip():
179
- unchanged_cnt += 1
180
- else:
181
- last_text = current_text
182
- unchanged_cnt = 0
183
- await asyncio.sleep(1.2)
184
 
185
- print(f"[DUCK] Response: {len(last_text)} chars")
186
- return last_text.strip()
187
 
188
  except Exception as e:
189
  print(f"[DUCK] Error: {e}")
@@ -192,10 +206,16 @@ class AsyncBrowserThread(threading.Thread):
192
  await page.close()
193
  await context.close()
194
 
195
- # โ”€โ”€ ZAI via Playwright โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
196
  async def _talk_to_zai(self, prompt: str) -> str:
197
  context = await self.browser.new_context(
198
- 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",
 
 
 
 
199
  viewport={"width": 1920, "height": 1080},
200
  )
201
  await context.add_init_script(
@@ -242,18 +262,22 @@ class AsyncBrowserThread(threading.Thread):
242
  await page.close()
243
  await context.close()
244
 
245
- # โ”€โ”€ Public methods โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
246
- def process_duck(self, model_id: str, messages: list) -> str:
 
 
247
  if not self.ready_event.wait(timeout=60):
248
- raise Exception("Browser not ready")
 
 
 
249
  future = asyncio.run_coroutine_threadsafe(
250
  self._talk_to_duck(model_id, messages), self.loop
251
  )
252
  return future.result(timeout=180)
253
 
254
  def process_zai(self, prompt: str) -> str:
255
- if not self.ready_event.wait(timeout=60):
256
- raise Exception("Browser not ready")
257
  future = asyncio.run_coroutine_threadsafe(self._talk_to_zai(prompt), self.loop)
258
  return future.result(timeout=120)
259
 
@@ -280,10 +304,6 @@ def _extract_content(msg: dict) -> str:
280
 
281
 
282
  def _build_duck_prompt(messages: list) -> str:
283
- """
284
- ูŠุจู†ูŠ prompt ู†ุตูŠ ู…ู† messages ู„ุฅุฑุณุงู„ู‡ ู„ู€ duck.ai
285
- ูŠุฏู…ุฌ system + history + ุงู„ุณุคุงู„ ุงู„ุฃุฎูŠุฑ
286
- """
287
  parts = []
288
  for msg in messages:
289
  role = msg.get("role", "user")
@@ -291,23 +311,19 @@ def _build_duck_prompt(messages: list) -> str:
291
  if not content.strip():
292
  continue
293
  if role == "system":
294
- parts.append(f"[INSTRUCTIONS]: {content}")
295
  elif role == "assistant":
296
- parts.append(f"[Previous AI response]: {content}")
297
  else:
298
  parts.append(content)
299
-
300
- # ุงู„ุฑุณุงู„ุฉ ุงู„ุฃุฎูŠุฑุฉ ูู‚ุท ู‡ูŠ ุงู„ุณุคุงู„ ุงู„ูุนู„ูŠ
301
  if len(parts) > 1:
302
- context = "\n\n".join(parts[:-1])
303
- question = parts[-1]
304
- return f"{context}\n\n---\n\n{question}"
305
  return "\n\n".join(parts)
306
 
307
 
308
  def format_prompt(messages, tools=None):
309
- parts = []
310
- system_parts = []
311
  has_tool_results = False
312
  user_question = ""
313
 
@@ -320,28 +336,20 @@ def format_prompt(messages, tools=None):
320
  system_parts.append(content)
321
  elif role == "tool":
322
  has_tool_results = True
323
- tool_name = msg.get("name", "tool")
324
- parts.append(f"[TOOL RESULT from '{tool_name}']:\n{content}")
325
  elif msg_type == "function_call_output":
326
  has_tool_results = True
327
- call_id = msg.get("call_id", "")
328
- output_content = msg.get("output", content)
329
- parts.append(f"[TOOL RESULT (call_id: {call_id})]:\n{output_content}")
330
  elif msg_type == "function_call":
331
- func_name = msg.get("name", "?")
332
- func_args = msg.get("arguments", "{}")
333
- parts.append(f"[PREVIOUS TOOL CALL: Called '{func_name}' with arguments: {func_args}]")
334
  elif role == "assistant":
335
- assistant_content = content
336
- tool_calls_in_msg = msg.get("tool_calls", [])
337
- if tool_calls_in_msg:
338
- tc_desc = []
339
- for tc in tool_calls_in_msg:
340
- func = tc.get("function", {})
341
- tc_desc.append(f"Called '{func.get('name','?')}' with: {func.get('arguments','{}')}")
342
- assistant_content += "\n[Previous tool calls: " + "; ".join(tc_desc) + "]"
343
- if assistant_content.strip():
344
- parts.append(f"[Assistant]: {assistant_content}")
345
  elif role == "user" or (msg_type == "message" and role != "system"):
346
  user_question = content
347
  has_tool_results = False
@@ -355,52 +363,36 @@ def format_prompt(messages, tools=None):
355
  final += "=== YOUR ROLE ===\n" + "\n\n".join(system_parts) + "\n=== END OF ROLE ===\n\n"
356
  else:
357
  final += "=== SYSTEM INSTRUCTIONS (FOLLOW STRICTLY) ===\n" + "\n\n".join(system_parts) + "\n=== END OF INSTRUCTIONS ===\n\n"
358
-
359
  if tools and not has_tool_results:
360
  final += _format_tools_instruction(tools, user_question)
361
-
362
  if has_tool_results:
363
- 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"
364
-
365
  if parts:
366
  final += "\n".join(parts)
367
-
368
  if has_tool_results:
369
- final += "\n\n=== INSTRUCTION ===\nNow answer the user's question based ONLY on the tool results above.\n"
370
-
371
  return final
372
 
373
 
374
  def _format_tools_instruction(tools, user_question=""):
375
- instruction = "\n=== MANDATORY TOOL USAGE ===\n"
376
- instruction += "You MUST use one of the tools below to answer this question.\n"
377
- instruction += "Do NOT answer directly. Do NOT say you don't have information.\n"
378
- instruction += "You MUST respond with ONLY a JSON object to call the tool.\n\n"
379
- instruction += 'RESPONSE FORMAT - respond with ONLY this JSON, nothing else:\n'
380
- instruction += '{"tool_calls": [{"name": "TOOL_NAME", "arguments": {"param": "value"}}]}\n\n'
381
- 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"
382
- instruction += "Available tools:\n\n"
383
  for tool in tools:
384
- func = tool.get("function", tool)
385
- name = func.get("name", "unknown")
386
- desc = func.get("description", "No description")
387
  params = func.get("parameters", {})
388
- instruction += f"Tool: {name}\nDescription: {desc}\n"
389
  if params.get("properties"):
390
- instruction += "Parameters:\n"
391
- required_params = params.get("required", [])
392
- for pname, pinfo in params["properties"].items():
393
- ptype = pinfo.get("type", "string")
394
- pdesc = pinfo.get("description", "")
395
- req = "required" if pname in required_params else "optional"
396
- instruction += f" - {pname} ({ptype}, {req}): {pdesc}\n"
397
- instruction += "\n"
398
- instruction += "=== END OF TOOLS ===\n\n"
399
- first_func = (tools[0] if tools else {}).get("function", tools[0] if tools else {})
400
- first_name = first_func.get("name", "tool")
401
- instruction += f'EXAMPLE:\n{{"tool_calls": [{{"name": "{first_name}", "arguments": {{"input": "the user question here"}}}}]}}\n\n'
402
- instruction += "Now respond with the JSON to call the appropriate tool:\n\n"
403
- return instruction
404
 
405
 
406
  def parse_tool_calls(response_text):
@@ -413,71 +405,62 @@ def parse_tool_calls(response_text):
413
  m2 = re.search(r'\{[\s\S]*"tool_calls"[\s\S]*\}', cleaned)
414
  if m2:
415
  candidates.append(m2.group(0))
416
- for candidate in candidates:
417
  try:
418
- parsed = json.loads(candidate)
419
  if isinstance(parsed, dict) and "tool_calls" in parsed:
420
- raw_calls = parsed["tool_calls"]
421
- if isinstance(raw_calls, list) and raw_calls:
422
- formatted = []
423
- for call in raw_calls:
424
- tool_name = call.get("name", "")
425
- arguments = call.get("arguments", {})
426
- arguments_str = json.dumps(arguments, ensure_ascii=False) if isinstance(arguments, dict) else str(arguments)
427
- formatted.append({
428
- "id": f"call_{uuid.uuid4().hex[:24]}",
429
- "type": "function",
430
- "function": {"name": tool_name, "arguments": arguments_str},
431
- })
432
- return formatted
433
  except (json.JSONDecodeError, TypeError, KeyError):
434
  continue
435
  return None
436
 
437
 
438
  # ====================================================================
439
- # Auth & Response Builder
440
  # ====================================================================
441
 
442
  def _auth(request: Request) -> bool:
443
- auth = request.headers.get("authorization", "")
444
- return auth.replace("Bearer ", "").strip() == API_SECRET_KEY
445
 
446
 
447
- def _is_duck_model(model: str) -> bool:
448
  return model in DUCK_MODELS
449
 
450
 
451
- def _make_completion(start_time, model, response_text, messages, tools):
452
- p_tokens = sum(len(_extract_content(m).split()) for m in messages)
453
- c_tokens = len(response_text.split())
454
- tool_calls = parse_tool_calls(response_text) if tools else None
455
-
456
- if tool_calls:
457
  return {
458
- "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
459
- "object": "chat.completion",
460
- "created": int(start_time),
461
- "model": model,
462
  "choices": [{"index": 0, "message": {"role": "assistant", "content": None,
463
- "tool_calls": tool_calls}, "finish_reason": "tool_calls"}],
464
- "usage": {"prompt_tokens": p_tokens, "completion_tokens": c_tokens,
465
- "total_tokens": p_tokens + c_tokens},
466
  }
467
  return {
468
- "id": f"chatcmpl-{uuid.uuid4().hex[:29]}",
469
- "object": "chat.completion",
470
- "created": int(start_time),
471
- "model": model,
472
- "choices": [{"index": 0, "message": {"role": "assistant", "content": response_text},
473
  "finish_reason": "stop"}],
474
- "usage": {"prompt_tokens": p_tokens, "completion_tokens": c_tokens,
475
- "total_tokens": p_tokens + c_tokens},
476
  }
477
 
478
 
479
  # ====================================================================
480
- # FastAPI App
481
  # ====================================================================
482
  app = FastAPI(title="ZAI + DuckAI API Server")
483
 
@@ -487,37 +470,34 @@ async def chat_completions(request: Request):
487
  try:
488
  data = await request.json()
489
  except Exception:
490
- return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
491
-
492
  if not _auth(request):
493
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
494
 
495
  messages = data.get("messages", [])
496
  if not messages:
497
- return JSONResponse(status_code=400, content={"error": {"message": "messages field is required"}})
498
 
499
  model = data.get("model", "gpt-4o-mini")
500
  tools = data.get("tools", None)
501
  start_time = time.time()
502
 
503
  try:
504
- if _is_duck_model(model):
505
- duck_model_id = DUCK_MODELS[model]
506
- print(f"[SERVER] Duck.ai request โ†’ {duck_model_id}")
507
- response_text = await asyncio.get_event_loop().run_in_executor(
508
- None, browser_engine.process_duck, duck_model_id, messages
509
  )
510
  else:
511
  prompt = format_prompt(messages, tools=tools)
512
- print(f"[SERVER] ZAI request ({len(prompt)} chars)")
513
- response_text = await asyncio.get_event_loop().run_in_executor(
514
  None, browser_engine.process_zai, prompt
515
  )
516
-
517
- return _make_completion(start_time, model, response_text, messages, tools)
518
-
519
  except Exception as e:
520
- print(f"[SERVER] Error: {e}")
521
  return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
522
 
523
 
@@ -526,8 +506,7 @@ async def responses(request: Request):
526
  try:
527
  data = await request.json()
528
  except Exception:
529
- return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON payload"}})
530
-
531
  if not _auth(request):
532
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
533
 
@@ -538,9 +517,8 @@ async def responses(request: Request):
538
  messages = input_data
539
  else:
540
  messages = data.get("messages", [])
541
-
542
  if not messages:
543
- return JSONResponse(status_code=400, content={"error": {"message": "input field is required"}})
544
 
545
  model = data.get("model", "gpt-4o-mini")
546
  tools = data.get("tools", None)
@@ -549,49 +527,36 @@ async def responses(request: Request):
549
  messages.insert(0, {"role": "system", "content": instructions})
550
 
551
  start_time = time.time()
552
-
553
  try:
554
- if _is_duck_model(model):
555
- duck_model_id = DUCK_MODELS[model]
556
- response_text = await asyncio.get_event_loop().run_in_executor(
557
- None, browser_engine.process_duck, duck_model_id, messages
558
  )
559
  else:
560
- prompt = format_prompt(messages, tools=tools)
561
- response_text = await asyncio.get_event_loop().run_in_executor(
562
- None, browser_engine.process_zai, prompt
563
  )
564
 
565
- p_tokens = sum(len(_extract_content(m).split()) for m in messages)
566
- c_tokens = len(response_text.split())
567
- tool_calls = parse_tool_calls(response_text) if tools else None
568
-
569
- if tool_calls:
570
- output_items = [{
571
- "type": "function_call",
572
- "id": tc["id"],
573
- "call_id": tc["id"],
574
- "name": tc["function"]["name"],
575
- "arguments": tc["function"]["arguments"],
576
- "status": "completed",
577
- } for tc in tool_calls]
578
  return {
579
  "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
580
  "created_at": int(start_time), "model": model, "status": "completed",
581
- "output": output_items,
582
- "usage": {"input_tokens": p_tokens, "output_tokens": c_tokens,
583
- "total_tokens": p_tokens + c_tokens},
 
584
  }
585
-
586
  return {
587
  "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
588
  "created_at": int(start_time), "model": model, "status": "completed",
589
  "output": [{"type": "message", "role": "assistant",
590
- "content": [{"type": "output_text", "text": response_text}]}],
591
- "usage": {"input_tokens": p_tokens, "output_tokens": c_tokens,
592
- "total_tokens": p_tokens + c_tokens},
593
  }
594
-
595
  except Exception as e:
596
  return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
597
 
@@ -602,17 +567,15 @@ async def list_models(request: Request):
602
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
603
  return {
604
  "object": "list",
605
- "data": [
606
- {"id": m, "object": "model",
607
- "owned_by": "duck.ai" if m in DUCK_MODELS else "zai"}
608
- for m in ALL_MODELS
609
- ],
610
  }
611
 
612
 
613
  @app.get("/health")
614
  @app.get("/")
615
- async def health_check():
616
  return {
617
  "status": "running",
618
  "message": "ZAI + DuckAI API Server is active!",
 
5
  import threading
6
  import json
7
  import re
 
8
  from fastapi import FastAPI, Request
9
+ from fastapi.responses import JSONResponse
10
 
11
  # ====================================================================
12
  # Configuration
13
  # ====================================================================
14
  API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
15
 
 
 
16
  DUCK_MODELS = {
17
+ "gpt-4o-mini": "gpt-4o-mini",
18
+ "gpt-5-mini": "gpt-5-mini",
19
+ "o3-mini": "o3-mini",
20
+ "gpt-oss-120b": "gpt-oss-120b",
21
+ "claude-haiku-4-5": "claude-haiku-4-5",
22
+ "claude-3-haiku": "claude-3-haiku-20240307",
23
+ "llama-4-scout": "meta-llama/Llama-4-Scout-17B-16E-Instruct",
24
+ "llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
25
+ "mistral-small-4": "mistralai/Mistral-Small-3.1-24B-Instruct-2503",
26
+ "mistral-small": "mistralai/Mistral-Small-24B-Instruct-2501",
27
  }
28
 
 
29
  ZAI_MODELS = [
30
  "GLM-5.1", "GLM-5-Turbo", "GLM-5V-Turbo",
31
  "GLM-5", "GLM-4.7", "GLM-4.6V", "GLM-4.5-Air"
 
33
 
34
  ALL_MODELS = list(DUCK_MODELS.keys()) + ZAI_MODELS
35
 
 
36
  # ====================================================================
37
+ # Browser Engine
 
 
38
  # ====================================================================
39
 
40
  class AsyncBrowserThread(threading.Thread):
 
71
  )
72
  print("[SERVER] Chrome launched!")
73
 
74
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
75
+ # Duck.ai
76
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
77
  async def _talk_to_duck(self, model_id: str, messages: list) -> str:
 
 
 
 
 
78
  context = await self.browser.new_context(
79
+ user_agent=(
80
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
81
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
82
+ "Chrome/124.0.0.0 Safari/537.36"
83
+ ),
84
  viewport={"width": 1920, "height": 1080},
85
  )
86
  await context.add_init_script(
 
89
  page = await context.new_page()
90
  try:
91
  page.set_default_timeout(120000)
 
 
92
  prompt = _build_duck_prompt(messages)
93
 
94
+ # โ”€โ”€ 1. ูุชุญ ุงู„ุตูุญุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
95
+ await page.goto("https://duckduckgo.com/aichat", wait_until="domcontentloaded")
96
+ await asyncio.sleep(5)
97
 
98
+ # โ”€โ”€ 2. ู‚ุจูˆู„ ุงู„ุดุฑูˆุท ุฅู† ุธู‡ุฑุช โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
99
  try:
100
+ btns = ["button:has-text('Got it')",
101
+ "button:has-text('Accept')",
102
+ "button:has-text('Get started')",
103
+ "button:has-text('I agree')",
104
+ "[data-testid='accept-terms']"]
105
+ for btn_sel in btns:
106
+ btn = page.locator(btn_sel)
107
+ if await btn.count() > 0:
108
+ await btn.first.click()
109
+ await asyncio.sleep(2)
110
+ break
111
  except Exception:
112
  pass
113
 
114
+ # โ”€โ”€ 3. ุงู„ุงู†ุชุธุงุฑ ุญุชู‰ ูŠุธู‡ุฑ ุตู†ุฏูˆู‚ ุงู„ูƒุชุงุจุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
115
+ # ุงู„ู€ selector ุงู„ู…ุซุจุช ู…ู† ู…ุตุงุฏุฑ ุญู‚ูŠู‚ูŠุฉ
116
+ await page.wait_for_selector("textarea[name='user-prompt']", timeout=30000)
117
+
118
+ # โ”€โ”€ 4. ุฅุฑุณุงู„ ุงู„ุฑุณุงู„ุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
119
+ textarea = page.locator("textarea[name='user-prompt']")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  await textarea.click()
121
  await textarea.fill(prompt)
122
  await asyncio.sleep(0.5)
 
 
123
 
124
+ # ุฅูŠุฌุงุฏ ุฒุฑ ุงู„ุฅุฑุณุงู„ ูˆุงู„ุถุบุท ุนู„ูŠู‡
125
+ send_btn = page.locator("button[type='submit'], button[aria-label*='Send'], button[aria-label*='send']")
126
+ if await send_btn.count() > 0:
127
+ await send_btn.first.click()
128
+ else:
129
+ await page.keyboard.press("Enter")
130
+
131
+ print(f"[DUCK] Sent prompt ({len(prompt)} chars)")
132
+
133
+ # โ”€โ”€ 5. ุงู†ุชุธุงุฑ ุจุฏุก ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
134
  await asyncio.sleep(3)
135
 
136
+ # ุงู†ุชุธุฑ ูˆู‚ู ุงู„ู…ุคุดุฑ ุงู„ูˆุงู…ุถ (ูŠุนู†ูŠ ุงู†ุชู‡ู‰ ุงู„ุฑุฏ)
137
+ # duck.ai ูŠุถูŠู data-copyairesponse ุนู„ู‰ ุฒุฑ ุงู„ู†ุณุฎ ุนู†ุฏ ุงูƒุชู…ุงู„ ุงู„ุฑุฏ
138
+ try:
139
+ await page.wait_for_selector(
140
+ "button[data-copyairesponse='true']",
141
+ timeout=90000
142
+ )
143
+ await asyncio.sleep(1)
144
+ except Exception:
145
+ # fallback: ุงู†ุชุธุฑ ุซุงุจุช
146
+ await asyncio.sleep(15)
147
+
148
+ # โ”€โ”€ 6. ุงุณุชุฎุฑุงุฌ ุงู„ู†ุต โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
149
+ response_text = await page.evaluate("""
150
+ () => {
151
+ // duck.ai ูŠุถุน ุงู„ุฑุฏูˆุฏ ููŠ ุนู†ุงุตุฑ ุจุฌุงู†ุจ ุฒุฑ ุงู„ู†ุณุฎ
152
+ // ู†ุฌูŠุจ ุขุฎุฑ ุฑุฏ ููŠ ุงู„ุตูุญุฉ
153
+ const copyBtns = document.querySelectorAll("button[data-copyairesponse='true']");
154
+ if (copyBtns.length > 0) {
155
+ const lastCopyBtn = copyBtns[copyBtns.length - 1];
156
+ // ุงู„ุฑุฏ ููŠ ุงู„ู€ parent container ู‚ุจู„ ุฒุฑ ุงู„ู†ุณุฎ
157
+ const msgContainer = lastCopyBtn.closest('[class*="message"], [class*="Message"], [class*="response"], [class*="Response"]');
158
+ if (msgContainer) return msgContainer.innerText.trim();
159
+ }
160
 
161
+ // fallback: ูƒู„ ุงู„ุนู†ุงุตุฑ ุงู„ุชูŠ ุชุจุฏูˆ ูƒุฑุฏูˆุฏ AI
162
+ const allArticles = document.querySelectorAll('article, [role="article"]');
163
+ if (allArticles.length > 0) {
164
+ const last = allArticles[allArticles.length - 1];
165
+ return last.innerText.trim();
166
+ }
167
+
168
+ // ุขุฎุฑ ุญู„: ุฃุฎุฐ ูƒู„ ุงู„ู†ุตูˆุต ุงู„ุทูˆูŠู„ุฉ
169
+ const divs = [...document.querySelectorAll('div')].filter(d =>
170
+ d.children.length < 5 &&
171
+ d.innerText &&
172
+ d.innerText.trim().length > 100
173
+ );
174
+ if (divs.length > 0) return divs[divs.length - 1].innerText.trim();
175
+ return '';
176
+ }
177
+ """)
178
+
179
+ # ุฅุฐุง ูƒุงู† ุงู„ู†ุต ูุงุฑุบุงู‹ุŒ ุงู†ุชุธุฑ ุฃูƒุซุฑ ูˆุญุงูˆู„ ู…ุฑุฉ ุซุงู†ูŠุฉ
180
+ if not response_text or len(response_text.strip()) < 5:
181
+ await asyncio.sleep(8)
182
+ response_text = await page.evaluate("""
183
  () => {
184
+ const copyBtns = document.querySelectorAll("button[data-copyairesponse='true']");
185
+ if (copyBtns.length > 0) {
186
+ const last = copyBtns[copyBtns.length - 1];
187
+ let el = last.parentElement;
188
+ for (let i = 0; i < 5; i++) {
189
+ if (el && el.innerText && el.innerText.trim().length > 30) {
190
+ return el.innerText.trim();
191
+ }
192
+ el = el ? el.parentElement : null;
 
 
193
  }
194
  }
195
+ return document.body.innerText.slice(0, 3000);
 
 
 
 
 
 
 
196
  }
197
  """)
 
 
 
 
 
 
198
 
199
+ print(f"[DUCK] Response: {len(response_text)} chars")
200
+ return response_text.strip()
201
 
202
  except Exception as e:
203
  print(f"[DUCK] Error: {e}")
 
206
  await page.close()
207
  await context.close()
208
 
209
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€๏ฟฝ๏ฟฝ๏ฟฝโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
210
+ # ZAI (ู…ุญููˆุธ ูƒู…ุง ู‡ูˆ ุจุงู„ูƒุงู…ู„)
211
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
212
  async def _talk_to_zai(self, prompt: str) -> str:
213
  context = await self.browser.new_context(
214
+ user_agent=(
215
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
216
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
217
+ "Chrome/124.0.0.0 Safari/537.36"
218
+ ),
219
  viewport={"width": 1920, "height": 1080},
220
  )
221
  await context.add_init_script(
 
262
  await page.close()
263
  await context.close()
264
 
265
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
266
+ # Public blocking methods
267
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
268
+ def _wait_ready(self):
269
  if not self.ready_event.wait(timeout=60):
270
+ raise Exception("Browser not ready after 60s")
271
+
272
+ def process_duck(self, model_id: str, messages: list) -> str:
273
+ self._wait_ready()
274
  future = asyncio.run_coroutine_threadsafe(
275
  self._talk_to_duck(model_id, messages), self.loop
276
  )
277
  return future.result(timeout=180)
278
 
279
  def process_zai(self, prompt: str) -> str:
280
+ self._wait_ready()
 
281
  future = asyncio.run_coroutine_threadsafe(self._talk_to_zai(prompt), self.loop)
282
  return future.result(timeout=120)
283
 
 
304
 
305
 
306
  def _build_duck_prompt(messages: list) -> str:
 
 
 
 
307
  parts = []
308
  for msg in messages:
309
  role = msg.get("role", "user")
 
311
  if not content.strip():
312
  continue
313
  if role == "system":
314
+ parts.append(f"[INSTRUCTIONS]:\n{content}")
315
  elif role == "assistant":
316
+ parts.append(f"[Previous AI response]:\n{content}")
317
  else:
318
  parts.append(content)
 
 
319
  if len(parts) > 1:
320
+ return "\n\n---\n\n".join(parts)
 
 
321
  return "\n\n".join(parts)
322
 
323
 
324
  def format_prompt(messages, tools=None):
325
+ parts = []
326
+ system_parts = []
327
  has_tool_results = False
328
  user_question = ""
329
 
 
336
  system_parts.append(content)
337
  elif role == "tool":
338
  has_tool_results = True
339
+ parts.append(f"[TOOL RESULT from '{msg.get('name','tool')}']:\n{content}")
 
340
  elif msg_type == "function_call_output":
341
  has_tool_results = True
342
+ parts.append(f"[TOOL RESULT (call_id: {msg.get('call_id','')})]:\n{msg.get('output', content)}")
 
 
343
  elif msg_type == "function_call":
344
+ parts.append(f"[PREVIOUS TOOL CALL: Called '{msg.get('name','?')}' with arguments: {msg.get('arguments','{}')}]")
 
 
345
  elif role == "assistant":
346
+ ac = content
347
+ tc_list = msg.get("tool_calls", [])
348
+ if tc_list:
349
+ td = [f"Called '{t.get('function',{}).get('name','?')}' with: {t.get('function',{}).get('arguments','{}')}" for t in tc_list]
350
+ ac += "\n[Previous tool calls: " + "; ".join(td) + "]"
351
+ if ac.strip():
352
+ parts.append(f"[Assistant]: {ac}")
 
 
 
353
  elif role == "user" or (msg_type == "message" and role != "system"):
354
  user_question = content
355
  has_tool_results = False
 
363
  final += "=== YOUR ROLE ===\n" + "\n\n".join(system_parts) + "\n=== END OF ROLE ===\n\n"
364
  else:
365
  final += "=== SYSTEM INSTRUCTIONS (FOLLOW STRICTLY) ===\n" + "\n\n".join(system_parts) + "\n=== END OF INSTRUCTIONS ===\n\n"
 
366
  if tools and not has_tool_results:
367
  final += _format_tools_instruction(tools, user_question)
 
368
  if has_tool_results:
369
+ final += "=== CONTEXT FROM TOOLS ===\nUse ONLY this information to answer.\n\n"
 
370
  if parts:
371
  final += "\n".join(parts)
 
372
  if has_tool_results:
373
+ final += "\n\n=== INSTRUCTION ===\nAnswer based ONLY on tool results above.\n"
 
374
  return final
375
 
376
 
377
  def _format_tools_instruction(tools, user_question=""):
378
+ i = "\n=== MANDATORY TOOL USAGE ===\n"
379
+ i += "You MUST use one of the tools below. Respond with ONLY valid JSON.\n\n"
380
+ i += 'FORMAT: {"tool_calls": [{"name": "TOOL_NAME", "arguments": {"param": "value"}}]}\n\n'
381
+ i += "Available tools:\n\n"
 
 
 
 
382
  for tool in tools:
383
+ func = tool.get("function", tool)
384
+ i += f"Tool: {func.get('name','unknown')}\nDescription: {func.get('description','')}\n"
 
385
  params = func.get("parameters", {})
 
386
  if params.get("properties"):
387
+ i += "Parameters:\n"
388
+ req = params.get("required", [])
389
+ for pn, pi in params["properties"].items():
390
+ i += f" - {pn} ({pi.get('type','string')}, {'required' if pn in req else 'optional'}): {pi.get('description','')}\n"
391
+ i += "\n"
392
+ i += "=== END OF TOOLS ===\n\n"
393
+ fn = (tools[0] if tools else {}).get("function", tools[0] if tools else {}).get("name", "tool")
394
+ i += f'Now respond with JSON to call the tool:\n'
395
+ return i
 
 
 
 
 
396
 
397
 
398
  def parse_tool_calls(response_text):
 
405
  m2 = re.search(r'\{[\s\S]*"tool_calls"[\s\S]*\}', cleaned)
406
  if m2:
407
  candidates.append(m2.group(0))
408
+ for c in candidates:
409
  try:
410
+ parsed = json.loads(c)
411
  if isinstance(parsed, dict) and "tool_calls" in parsed:
412
+ raw = parsed["tool_calls"]
413
+ if isinstance(raw, list) and raw:
414
+ return [{
415
+ "id": f"call_{uuid.uuid4().hex[:24]}",
416
+ "type": "function",
417
+ "function": {
418
+ "name": call.get("name", ""),
419
+ "arguments": json.dumps(call.get("arguments", {}), ensure_ascii=False)
420
+ if isinstance(call.get("arguments"), dict)
421
+ else str(call.get("arguments", "{}"))
422
+ }
423
+ } for call in raw]
 
424
  except (json.JSONDecodeError, TypeError, KeyError):
425
  continue
426
  return None
427
 
428
 
429
  # ====================================================================
430
+ # Auth & Shared response builder
431
  # ====================================================================
432
 
433
  def _auth(request: Request) -> bool:
434
+ return request.headers.get("authorization", "").replace("Bearer ", "").strip() == API_SECRET_KEY
 
435
 
436
 
437
+ def _is_duck(model: str) -> bool:
438
  return model in DUCK_MODELS
439
 
440
 
441
+ def _completion_response(start_time, model, text, messages, tools):
442
+ p = sum(len(_extract_content(m).split()) for m in messages)
443
+ c = len(text.split())
444
+ tc = parse_tool_calls(text) if tools else None
445
+ if tc:
 
446
  return {
447
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}", "object": "chat.completion",
448
+ "created": int(start_time), "model": model,
 
 
449
  "choices": [{"index": 0, "message": {"role": "assistant", "content": None,
450
+ "tool_calls": tc}, "finish_reason": "tool_calls"}],
451
+ "usage": {"prompt_tokens": p, "completion_tokens": c, "total_tokens": p + c},
 
452
  }
453
  return {
454
+ "id": f"chatcmpl-{uuid.uuid4().hex[:29]}", "object": "chat.completion",
455
+ "created": int(start_time), "model": model,
456
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": text},
 
 
457
  "finish_reason": "stop"}],
458
+ "usage": {"prompt_tokens": p, "completion_tokens": c, "total_tokens": p + c},
 
459
  }
460
 
461
 
462
  # ====================================================================
463
+ # FastAPI
464
  # ====================================================================
465
  app = FastAPI(title="ZAI + DuckAI API Server")
466
 
 
470
  try:
471
  data = await request.json()
472
  except Exception:
473
+ return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON"}})
 
474
  if not _auth(request):
475
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
476
 
477
  messages = data.get("messages", [])
478
  if not messages:
479
+ return JSONResponse(status_code=400, content={"error": {"message": "messages required"}})
480
 
481
  model = data.get("model", "gpt-4o-mini")
482
  tools = data.get("tools", None)
483
  start_time = time.time()
484
 
485
  try:
486
+ if _is_duck(model):
487
+ model_id = DUCK_MODELS[model]
488
+ print(f"[SERVER] duck.ai โ†’ {model_id}")
489
+ text = await asyncio.get_event_loop().run_in_executor(
490
+ None, browser_engine.process_duck, model_id, messages
491
  )
492
  else:
493
  prompt = format_prompt(messages, tools=tools)
494
+ print(f"[SERVER] zai โ†’ {len(prompt)} chars")
495
+ text = await asyncio.get_event_loop().run_in_executor(
496
  None, browser_engine.process_zai, prompt
497
  )
498
+ return _completion_response(start_time, model, text, messages, tools)
 
 
499
  except Exception as e:
500
+ print(f"[SERVER] ERROR: {e}")
501
  return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
502
 
503
 
 
506
  try:
507
  data = await request.json()
508
  except Exception:
509
+ return JSONResponse(status_code=400, content={"error": {"message": "Invalid JSON"}})
 
510
  if not _auth(request):
511
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
512
 
 
517
  messages = input_data
518
  else:
519
  messages = data.get("messages", [])
 
520
  if not messages:
521
+ return JSONResponse(status_code=400, content={"error": {"message": "input required"}})
522
 
523
  model = data.get("model", "gpt-4o-mini")
524
  tools = data.get("tools", None)
 
527
  messages.insert(0, {"role": "system", "content": instructions})
528
 
529
  start_time = time.time()
 
530
  try:
531
+ if _is_duck(model):
532
+ text = await asyncio.get_event_loop().run_in_executor(
533
+ None, browser_engine.process_duck, DUCK_MODELS[model], messages
 
534
  )
535
  else:
536
+ text = await asyncio.get_event_loop().run_in_executor(
537
+ None, browser_engine.process_zai, format_prompt(messages, tools=tools)
 
538
  )
539
 
540
+ p = sum(len(_extract_content(m).split()) for m in messages)
541
+ c = len(text.split())
542
+ tc = parse_tool_calls(text) if tools else None
543
+
544
+ if tc:
 
 
 
 
 
 
 
 
545
  return {
546
  "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
547
  "created_at": int(start_time), "model": model, "status": "completed",
548
+ "output": [{"type": "function_call", "id": t["id"], "call_id": t["id"],
549
+ "name": t["function"]["name"], "arguments": t["function"]["arguments"],
550
+ "status": "completed"} for t in tc],
551
+ "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
552
  }
 
553
  return {
554
  "id": f"resp-{uuid.uuid4().hex[:29]}", "object": "response",
555
  "created_at": int(start_time), "model": model, "status": "completed",
556
  "output": [{"type": "message", "role": "assistant",
557
+ "content": [{"type": "output_text", "text": text}]}],
558
+ "usage": {"input_tokens": p, "output_tokens": c, "total_tokens": p + c},
 
559
  }
 
560
  except Exception as e:
561
  return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
562
 
 
567
  return JSONResponse(status_code=401, content={"error": {"message": "Invalid API Key"}})
568
  return {
569
  "object": "list",
570
+ "data": [{"id": m, "object": "model",
571
+ "owned_by": "duck.ai" if m in DUCK_MODELS else "zai"}
572
+ for m in ALL_MODELS],
 
 
573
  }
574
 
575
 
576
  @app.get("/health")
577
  @app.get("/")
578
+ async def health():
579
  return {
580
  "status": "running",
581
  "message": "ZAI + DuckAI API Server is active!",