Adarshu07 commited on
Commit
596f075
Β·
verified Β·
1 Parent(s): afc9b7a

Update cloudflare_provider.py

Browse files
Files changed (1) hide show
  1. cloudflare_provider.py +169 -177
cloudflare_provider.py CHANGED
@@ -3,17 +3,15 @@
3
  β•‘ cloudflare_provider.py β•‘
4
  β•‘ Cloudflare AI Playground β€” Reverse Engineered Provider β•‘
5
  β•‘ β•‘
6
- β•‘ Connection Strategy: β•‘
7
- β•‘ 1. Try DIRECT Python WebSocket (no browser needed) β•‘
8
- β•‘ 2. If blocked β†’ launch browser with Xvfb virtual display β•‘
9
- β•‘ extract cookies β†’ reconnect with cookies via Python WS β•‘
10
- β•‘ 3. If still blocked β†’ keep browser as WS relay β•‘
 
 
11
  β•‘ β•‘
12
- β•‘ Virtual Display: β•‘
13
- β•‘ Set env var VR_DISPLAY=1 to auto-start Xvfb via β•‘
14
- β•‘ pyvirtualdisplay (required on headless Linux / HF Spaces). β•‘
15
- β•‘ β•‘
16
- β•‘ NOTE: Headless Chrome is intentionally disabled β€” β•‘
17
  β•‘ Cloudflare blocks headless user agents. β•‘
18
  β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
19
  """
@@ -48,15 +46,24 @@ import websocket as _ws_mod
48
 
49
  _HAS_BROWSER = False
50
  try:
51
- _install("DrissionPage")
52
  from DrissionPage import ChromiumPage, ChromiumOptions
53
  _HAS_BROWSER = True
54
  except Exception:
55
- pass
 
 
 
 
 
56
 
57
 
58
  # ═══════════════════════════════════════════════════════════
59
- # Β§1b β€” VIRTUAL DISPLAY (from OS env)
 
 
 
 
 
60
  # ═══════════════════════════════════════════════════════════
61
  def _parse_bool_env(key: str, default: bool = False) -> bool:
62
  val = os.environ.get(key, "").strip().lower()
@@ -64,21 +71,24 @@ def _parse_bool_env(key: str, default: bool = False) -> bool:
64
  return default
65
  return val in ("1", "true", "yes", "on", "enable", "enabled")
66
 
 
 
67
 
68
- VR_DISPLAY = _parse_bool_env("VR_DISPLAY", default=False)
 
 
69
 
70
  _HAS_VIRTUAL_DISPLAY = False
71
  _Display = None
72
 
73
- if VR_DISPLAY:
74
  try:
75
  _install("pyvirtualdisplay", "PyVirtualDisplay")
76
  from pyvirtualdisplay import Display as _Display
77
  _HAS_VIRTUAL_DISPLAY = True
78
  except Exception as _vd_err:
79
  print(
80
- f"[cloudflare] ⚠ VR_DISPLAY=1 but pyvirtualdisplay failed: {_vd_err}\n"
81
- f"[cloudflare] Make sure Xvfb is installed: sudo apt install xvfb",
82
  file=sys.stderr, flush=True,
83
  )
84
 
@@ -86,24 +96,22 @@ if VR_DISPLAY:
86
  # ═══════════════════════════════════════════════════════════
87
  # Β§2 β€” CONSTANTS
88
  # ═══════════════════════════════════════════════════════════
89
- _SITE = "https://playground.ai.cloudflare.com"
90
- _WS_BASE = "wss://playground.ai.cloudflare.com/agents/playground"
91
- _CACHE = Path(__file__).resolve().parent / "cache"
92
- _MFILE = _CACHE / "cloudflare_models.json"
93
- _CHARS = string.ascii_letters + string.digits
94
- _LOWER = string.ascii_lowercase + string.digits
95
- _UA = (
96
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
97
  "AppleWebKit/537.36 (KHTML, like Gecko) "
98
- "Chrome/146.0.0.0 Safari/537.36"
99
  )
100
-
101
- # Model cache TTL β€” 6 hours
102
- _CACHE_TTL_SECONDS = 6 * 3600
103
 
104
 
105
  # ═══════════════════════════════════════════════════════════
106
- # Β§3 β€” MODEL TABLE (short alias β†’ full @cf/@hf ID)
107
  # ═══════════════════════════════════════════════════════════
108
  _SHORT_TO_FULL: dict[str, str] = {
109
  "gpt-oss-120b": "@cf/openai/gpt-oss-120b",
@@ -277,21 +285,19 @@ class _Cache:
277
  def save(models):
278
  _CACHE.mkdir(parents=True, exist_ok=True)
279
  _MFILE.write_text(json.dumps({
280
- "ts": time.time(), # epoch for TTL checks
281
- "ts_human": time.strftime("%Y-%m-%d %H:%M:%S"),
282
- "models": models,
283
  }, indent=2, ensure_ascii=False))
284
 
285
  @staticmethod
286
- def load(ttl: int = _CACHE_TTL_SECONDS):
287
- """Load cache only if it exists and is within TTL."""
288
  if not _MFILE.exists():
289
  return None
290
  try:
291
  data = json.loads(_MFILE.read_text())
292
- age = time.time() - data.get("ts", 0)
293
- if age > ttl:
294
- return None # stale β€” force refresh
295
  return data.get("models")
296
  except Exception:
297
  return None
@@ -304,17 +310,17 @@ class _Cache:
304
 
305
  # ═══════════════════════════════════════════════════════════
306
  # Β§6b β€” VIRTUAL DISPLAY MANAGER
 
 
307
  # ═══════════════════════════════════════════════════════════
308
  class _VirtualDisplayManager:
309
- """Thread-safe singleton that manages a single Xvfb display."""
310
-
311
  _instance = None
312
  _lock = threading.Lock()
313
 
314
  def __init__(self):
315
- self._display = None
316
- self._running = False
317
- self._enabled = VR_DISPLAY and _HAS_VIRTUAL_DISPLAY
318
 
319
  @classmethod
320
  def instance(cls) -> "_VirtualDisplayManager":
@@ -330,9 +336,13 @@ class _VirtualDisplayManager:
330
 
331
  @property
332
  def running(self) -> bool:
333
- return self._running
 
334
 
335
- def start(self, width: int = 1920, height: int = 1080, depth: int = 24):
 
 
 
336
  if not self._enabled:
337
  return
338
  if self._running:
@@ -350,20 +360,16 @@ class _VirtualDisplayManager:
350
  )
351
  self._display.start()
352
  self._running = True
353
- _log_vd(f"βœ“ Virtual display started "
354
- f"({width}x{height}x{depth}) "
355
- f"on :{self._display.display}")
356
- except FileNotFoundError:
357
- _log_vd(
358
- "βœ— Xvfb binary not found! Install: sudo apt install xvfb"
359
- )
360
- self._enabled = False
361
  except Exception as exc:
362
- _log_vd(f"βœ— Failed to start virtual display: {exc}")
363
  self._enabled = False
364
 
365
  def stop(self):
366
- if not self._running:
367
  return
368
  with self._lock:
369
  if not self._running:
@@ -379,9 +385,10 @@ class _VirtualDisplayManager:
379
  self._running = False
380
 
381
  def __repr__(self):
 
 
382
  state = "running" if self._running else ("idle" if self._enabled else "disabled")
383
- disp = f" :{self._display.display}" if self._running and self._display else ""
384
- return f"VirtualDisplay({state}{disp})"
385
 
386
 
387
  def _log_vd(msg: str):
@@ -394,8 +401,6 @@ def _log_vd(msg: str):
394
 
395
  # ── 7a: Direct Python WebSocket ──────────────────────────
396
  class _DirectTransport:
397
- """Pure Python WS via websocket-client with background recv thread."""
398
-
399
  def __init__(self, debug=False):
400
  self._ws = None
401
  self._inbox = []
@@ -406,15 +411,15 @@ class _DirectTransport:
406
 
407
  def connect(self, url: str, cookies: str = "") -> bool:
408
  self._ws = _ws_mod.WebSocket()
409
- headers = [f"User-Agent: {_UA}"]
410
  if cookies:
411
  headers.append(f"Cookie: {cookies}")
412
 
413
  self._ws.connect(
414
  url,
415
- origin=_SITE,
416
- header=headers,
417
- timeout=15,
418
  )
419
 
420
  self._running = True
@@ -472,7 +477,7 @@ class _DirectTransport:
472
  self._thread = None
473
 
474
 
475
- # ── 7b: Browser-based WebSocket (fallback) ───────────────
476
  _BROWSER_JS = """
477
  (function(){
478
  if(window.__cfws) return 'exists';
@@ -505,14 +510,14 @@ _BROWSER_JS = """
505
 
506
  class _BrowserTransport:
507
  """
508
- Headless-FREE Chrome WebSocket relay via Xvfb virtual display.
509
 
510
- NOTE: We intentionally do NOT use Chrome's --headless flag because
511
- Cloudflare Playground detects and blocks headless user agents.
512
- Instead we rely on pyvirtualdisplay / Xvfb to provide a real (but
513
- invisible) X11 display on servers that have no physical monitor.
514
 
515
- Set VR_DISPLAY=1 before importing to enable this behaviour.
516
  """
517
 
518
  def __init__(self, debug=False):
@@ -520,64 +525,81 @@ class _BrowserTransport:
520
  self._debug = debug
521
  self._vd_mgr = _VirtualDisplayManager.instance()
522
 
523
- def connect(self, url: str, **_) -> bool:
524
- if not _HAS_BROWSER:
525
- raise RuntimeError(
526
- "DrissionPage not available β€” cannot use browser fallback"
527
- )
 
 
 
528
 
529
- # ── Start virtual display (Xvfb) if enabled ──────
530
- if self._vd_mgr.enabled and not self._vd_mgr.running:
531
  self._vd_mgr.start()
532
- if not self._vd_mgr.running:
533
- raise RuntimeError(
534
- "Virtual display (Xvfb) failed to start. "
535
- "Install xvfb: apt-get install -y xvfb"
536
- )
 
 
 
 
 
537
 
538
- if not self._vd_mgr.running:
 
539
  raise RuntimeError(
540
- "No display available and VR_DISPLAY is not set. "
541
- "Set VR_DISPLAY=1 to use Xvfb virtual display, "
542
- "or run on a machine with a real display. "
543
- "Headless Chrome is intentionally disabled."
544
  )
545
 
 
 
546
  opts = ChromiumOptions()
547
  opts.set_argument("--disable-blink-features=AutomationControlled")
548
  opts.set_argument("--no-sandbox")
549
  opts.set_argument("--disable-dev-shm-usage")
550
  opts.set_argument("--disable-gpu")
551
  opts.set_argument("--disable-extensions")
552
- opts.set_argument("--disable-plugins")
553
  opts.set_argument("--disable-infobars")
554
  opts.set_argument("--window-size=1280,720")
555
- # ── NO headless flag β€” Cloudflare blocks headless ──
 
 
 
 
 
 
 
 
 
 
 
556
 
557
- self._page = ChromiumPage(addr_or_opts=opts)
558
  self._page.get(_SITE)
559
  time.sleep(4)
560
 
561
  self._page.run_js(_BROWSER_JS)
562
  self._page.run_js(f"window.__cfws.connect('{url}');")
563
 
564
- deadline = time.time() + 15
565
  while time.time() < deadline:
566
  if self._page.run_js("return window.__cfws.alive;"):
 
567
  return True
568
  err = self._page.run_js("return window.__cfws.error;")
569
  if err:
570
  raise ConnectionError(f"Browser WS failed: {err}")
571
- time.sleep(0.1)
572
 
573
- raise ConnectionError("Browser WS timed out waiting for connection")
574
 
575
  def send(self, data: str) -> bool:
576
  try:
577
  return bool(
578
- self._page.run_js(
579
- f"return window.__cfws.send({json.dumps(data)});"
580
- )
581
  )
582
  except Exception:
583
  return False
@@ -622,21 +644,10 @@ class CloudflareProvider:
622
  """
623
  ☁️ Cloudflare AI Playground β€” fully modular provider.
624
 
625
- Virtual Display (required on headless servers):
626
- Set VR_DISPLAY=1 before importing this module.
627
- This starts Xvfb so Chrome has a real (invisible) display.
628
- Headless Chrome is intentionally NOT used β€” Cloudflare blocks it.
629
-
630
- $ export VR_DISPLAY=1
631
- $ python server.py
632
-
633
- Usage:
634
- provider = CloudflareProvider()
635
- for chunk in provider.chat(data="Hello!"):
636
- print(chunk, end="")
637
-
638
- # non-streaming:
639
- response = provider.ask("What is 2+2?")
640
  """
641
 
642
  def __init__(
@@ -665,25 +676,19 @@ class CloudflareProvider:
665
  self.last_response: str = ""
666
  self.last_reasoning: str = ""
667
 
668
- self._sid: str = ""
669
- self._pk: str = ""
670
- self._transport = None
671
- self._mode: str = ""
672
- self._on: bool = False
673
 
674
  self._boot()
675
  atexit.register(self.close)
676
 
677
- # ─────────────────────────────────────────────────
678
- # Logging
679
- # ─────────────────────────────────────────────────
680
  def _d(self, *a):
681
  if self.debug:
682
  print("[cloudflare]", *a, file=sys.stderr, flush=True)
683
 
684
- # ─────────────────────────────────────────────────
685
- # Low-level WS
686
- # ─────────────────────────────────────────────────
687
  def _pull(self) -> list[str]:
688
  msgs = self._transport.recv()
689
  if self.debug:
@@ -698,56 +703,62 @@ class CloudflareProvider:
698
  raise RuntimeError("WebSocket send failed")
699
 
700
  # ─────────────────────────────────────────────────
701
- # Boot β€” tries direct WS first, then Xvfb browser
702
  # ─────────────────────────────────────────────────
703
  def _boot(self):
704
  self._sid = _make_sid()
705
  self._pk = _make_pk()
706
  url = _make_ws_url(self._sid, self._pk)
707
 
708
- # ── Attempt 1: direct Python WebSocket ──────
 
709
  try:
710
  self._d("Trying direct Python WebSocket...")
711
  t = _DirectTransport(debug=self.debug)
712
  t.connect(url)
 
713
 
714
- time.sleep(0.3)
715
  if t.alive:
716
  self._transport = t
717
  self._mode = "direct"
718
- self._d("βœ“ Direct connection β€” no browser needed!")
719
  else:
720
  t.close()
721
- raise ConnectionError("Direct WS not alive after connect")
722
 
723
  except Exception as e:
 
724
  self._d(f"Direct failed: {e}")
725
- self._d("Falling back to browser transport (Xvfb)...")
726
 
727
- # ── Attempt 2: Xvfb + Chrome relay ──────
 
 
728
  try:
 
729
  t = _BrowserTransport(debug=self.debug)
730
  t.connect(url)
731
  self._transport = t
732
  self._mode = "browser"
733
- self._d("βœ“ Browser transport connected")
734
- vd = _VirtualDisplayManager.instance()
735
- if vd.running:
736
- self._d(f" └─ {vd}")
737
- except Exception as e2:
 
 
738
  raise ConnectionError(
739
  f"All connection methods failed.\n"
740
- f" Direct: {e}\n"
741
- f" Browser: {e2}\n"
742
- f" Tip: ensure VR_DISPLAY=1 and xvfb is installed."
743
- ) from e2
744
 
745
  self._on = True
746
 
747
  # ── Handshake ──────────────────────────────
748
  want = {"cf_agent_identity", "cf_agent_state", "cf_agent_mcp_servers"}
749
  seen = set()
750
- deadline = time.time() + 10
751
  while time.time() < deadline and seen < want:
752
  for raw in self._pull():
753
  try:
@@ -755,14 +766,13 @@ class CloudflareProvider:
755
  except Exception:
756
  pass
757
  time.sleep(0.05)
758
-
759
  self._d(f"Handshake received: {seen}")
760
 
761
  self._push({"type": "cf_agent_stream_resume_request"})
762
  time.sleep(0.3)
763
  self._pull()
764
 
765
- # ── Models + state ─────────────────────────
766
  self._load_models()
767
  if self.max_tokens is None:
768
  self.max_tokens = self._ctx_window(self.model)
@@ -789,7 +799,6 @@ class CloudflareProvider:
789
  def _fetch_models(self):
790
  rid = str(uuid.uuid4())
791
  self._push({"args": [], "id": rid, "method": "getModels", "type": "rpc"})
792
-
793
  deadline = time.time() + 15
794
  while time.time() < deadline:
795
  for raw in self._pull():
@@ -832,20 +841,17 @@ class CloudflareProvider:
832
  return full
833
  return _SHORT_TO_FULL.get(name, name)
834
 
835
- # ─────────────────────────────────────────────────
836
- # State Sync
837
- # ─────────────────────────────────────────────────
838
  def _sync(self):
839
  self._push({
840
  "type": "cf_agent_state",
841
  "state": {
842
- "model": self.model,
843
- "temperature": self.temperature,
844
- "stream": True,
845
- "system": self.system,
846
- "useExternalProvider": False,
847
- "externalProvider": "openai",
848
- "authMethod": "provider-key",
849
  },
850
  })
851
  time.sleep(0.15)
@@ -884,9 +890,6 @@ class CloudflareProvider:
884
  def get_history(self) -> list[dict]:
885
  return _Conv.to_openai(self.history, self.system)
886
 
887
- # ─────────────────────────────────────────────────
888
- # Model listing
889
- # ─────────────────────────────────────────────────
890
  def list_models(self) -> list[dict]:
891
  return [{
892
  "name": m.get("name", ""),
@@ -914,7 +917,7 @@ class CloudflareProvider:
914
  max_tokens: int = None,
915
  ) -> Generator[str, None, None]:
916
  if not self._on:
917
- raise RuntimeError("Not connected β€” call new_session()")
918
  if not messages and not data:
919
  raise ValueError("Provide 'messages' or 'data'")
920
 
@@ -962,7 +965,6 @@ class CloudflareProvider:
962
 
963
  while not done:
964
  if not self._transport.alive:
965
- self._d("Transport died mid-stream")
966
  if not full_text:
967
  yield "[Connection lost]\n"
968
  break
@@ -971,11 +973,10 @@ class CloudflareProvider:
971
 
972
  if not msgs:
973
  elapsed = time.time() - last_data
974
- limit = self.timeout_idle if got_first else self.timeout_init
975
  if elapsed > limit:
976
- self._d(f"Timeout after {elapsed:.1f}s")
977
  if not full_text:
978
- yield "[Timeout β€” no response received]\n"
979
  break
980
  time.sleep(0.015 if got_first else 0.04)
981
  continue
@@ -988,8 +989,7 @@ class CloudflareProvider:
988
  except Exception:
989
  continue
990
 
991
- ftype = f.get("type", "")
992
- if ftype != "cf_agent_use_chat_response":
993
  continue
994
 
995
  body_str = f.get("body", "")
@@ -1049,15 +1049,9 @@ class CloudflareProvider:
1049
  self.last_response = full_text
1050
  self.last_reasoning = reasoning
1051
 
1052
- # ─────────────────────────────────────────────────
1053
- # ask() β€” non-streaming convenience
1054
- # ─────────────────────────────────────────────────
1055
  def ask(self, prompt: str, **kwargs) -> str:
1056
  return "".join(self.chat(data=prompt, **kwargs))
1057
 
1058
- # ─────────────────────────────────────────────────
1059
- # Session management
1060
- # ─────────────────────────────────────────────────
1061
  def new_session(self):
1062
  self._close_transport()
1063
  self.history.clear()
@@ -1074,9 +1068,8 @@ class CloudflareProvider:
1074
 
1075
  def close(self):
1076
  self._close_transport()
1077
- if self._mode == "browser":
1078
- vd = _VirtualDisplayManager.instance()
1079
- vd.stop()
1080
  self._d("Closed.")
1081
 
1082
  def __enter__(self):
@@ -1092,12 +1085,11 @@ class CloudflareProvider:
1092
  pass
1093
 
1094
  def __repr__(self):
1095
- s = "βœ…" if self._on else "❌"
1096
- vd = _VirtualDisplayManager.instance()
1097
- vd_info = f" vdisplay={vd}" if vd.enabled else ""
1098
  return (
1099
  f"CloudflareProvider({s} mode={self._mode!r} "
1100
- f"model={self.model!r} max_tokens={self.max_tokens}{vd_info})"
1101
  )
1102
 
1103
 
@@ -1106,9 +1098,9 @@ class CloudflareProvider:
1106
  # ═══════════════════════════════════════════════════════════
1107
  def _cleanup_virtual_display():
1108
  try:
1109
- vd = _VirtualDisplayManager.instance()
1110
- vd.stop()
1111
  except Exception:
1112
  pass
1113
 
1114
- atexit.register(_cleanup_virtual_display)
 
3
  β•‘ cloudflare_provider.py β•‘
4
  β•‘ Cloudflare AI Playground β€” Reverse Engineered Provider β•‘
5
  β•‘ β•‘
6
+ β•‘ Display modes (auto-detected, no config needed): β•‘
7
+ β•‘ XVFB_EXTERNAL=1 + DISPLAY=:99 β•‘
8
+ β•‘ β†’ entrypoint.sh already started Xvfb; just use DISPLAY β•‘
9
+ β•‘ VR_DISPLAY=1 β•‘
10
+ β•‘ β†’ pyvirtualdisplay starts its own Xvfb (dev/local use) β•‘
11
+ β•‘ Neither β•‘
12
+ β•‘ β†’ direct Python WS only; browser fallback disabled β•‘
13
  β•‘ β•‘
14
+ β•‘ NOTE: Chrome --headless is intentionally NEVER used. β•‘
 
 
 
 
15
  β•‘ Cloudflare blocks headless user agents. β•‘
16
  β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
17
  """
 
46
 
47
  _HAS_BROWSER = False
48
  try:
 
49
  from DrissionPage import ChromiumPage, ChromiumOptions
50
  _HAS_BROWSER = True
51
  except Exception:
52
+ try:
53
+ _install("DrissionPage")
54
+ from DrissionPage import ChromiumPage, ChromiumOptions
55
+ _HAS_BROWSER = True
56
+ except Exception as _br_err:
57
+ print(f"[cloudflare] ⚠ DrissionPage unavailable: {_br_err}", file=sys.stderr)
58
 
59
 
60
  # ═══════════════════════════════════════════════════════════
61
+ # Β§1b β€” DISPLAY DETECTION
62
+ #
63
+ # Three modes, detected in priority order:
64
+ # 1. XVFB_EXTERNAL=1 β†’ entrypoint.sh manages Xvfb, DISPLAY is set
65
+ # 2. VR_DISPLAY=1 β†’ we spawn pyvirtualdisplay ourselves
66
+ # 3. DISPLAY is set β†’ assume a real display exists (local dev)
67
  # ═══════════════════════════════════════════════════════════
68
  def _parse_bool_env(key: str, default: bool = False) -> bool:
69
  val = os.environ.get(key, "").strip().lower()
 
71
  return default
72
  return val in ("1", "true", "yes", "on", "enable", "enabled")
73
 
74
+ XVFB_EXTERNAL = _parse_bool_env("XVFB_EXTERNAL", default=False)
75
+ VR_DISPLAY = _parse_bool_env("VR_DISPLAY", default=False)
76
 
77
+ # Is any display available?
78
+ _DISPLAY_ENV = os.environ.get("DISPLAY", "").strip()
79
+ HAS_DISPLAY = bool(_DISPLAY_ENV) or XVFB_EXTERNAL
80
 
81
  _HAS_VIRTUAL_DISPLAY = False
82
  _Display = None
83
 
84
+ if VR_DISPLAY and not XVFB_EXTERNAL:
85
  try:
86
  _install("pyvirtualdisplay", "PyVirtualDisplay")
87
  from pyvirtualdisplay import Display as _Display
88
  _HAS_VIRTUAL_DISPLAY = True
89
  except Exception as _vd_err:
90
  print(
91
+ f"[cloudflare] ⚠ pyvirtualdisplay failed: {_vd_err}",
 
92
  file=sys.stderr, flush=True,
93
  )
94
 
 
96
  # ═══════════════════════════════════════════════════════════
97
  # Β§2 β€” CONSTANTS
98
  # ═══════════════════════════════════════════════════════════
99
+ _SITE = "https://playground.ai.cloudflare.com"
100
+ _WS_BASE = "wss://playground.ai.cloudflare.com/agents/playground"
101
+ _CACHE = Path(__file__).resolve().parent / "cache"
102
+ _MFILE = _CACHE / "cloudflare_models.json"
103
+ _CHARS = string.ascii_letters + string.digits
104
+ _LOWER = string.ascii_lowercase + string.digits
105
+ _UA = (
106
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
107
  "AppleWebKit/537.36 (KHTML, like Gecko) "
108
+ "Chrome/136.0.0.0 Safari/537.36"
109
  )
110
+ _CACHE_TTL = 6 * 3600 # 6 hours
 
 
111
 
112
 
113
  # ═══════════════════════════════════════════════════════════
114
+ # Β§3 β€” MODEL TABLE
115
  # ═══════════════════════════════════════════════════════════
116
  _SHORT_TO_FULL: dict[str, str] = {
117
  "gpt-oss-120b": "@cf/openai/gpt-oss-120b",
 
285
  def save(models):
286
  _CACHE.mkdir(parents=True, exist_ok=True)
287
  _MFILE.write_text(json.dumps({
288
+ "ts": time.time(),
289
+ "ts_human": time.strftime("%Y-%m-%d %H:%M:%S"),
290
+ "models": models,
291
  }, indent=2, ensure_ascii=False))
292
 
293
  @staticmethod
294
+ def load(ttl: int = _CACHE_TTL):
 
295
  if not _MFILE.exists():
296
  return None
297
  try:
298
  data = json.loads(_MFILE.read_text())
299
+ if time.time() - data.get("ts", 0) > ttl:
300
+ return None # stale
 
301
  return data.get("models")
302
  except Exception:
303
  return None
 
310
 
311
  # ═══════════════════════════════════════════════════════════
312
  # Β§6b β€” VIRTUAL DISPLAY MANAGER
313
+ # Only used when VR_DISPLAY=1 AND XVFB_EXTERNAL=0.
314
+ # When XVFB_EXTERNAL=1, the entrypoint.sh manages Xvfb.
315
  # ═══════════════════════════════════════════════════════════
316
  class _VirtualDisplayManager:
 
 
317
  _instance = None
318
  _lock = threading.Lock()
319
 
320
  def __init__(self):
321
+ self._display = None
322
+ self._running = False
323
+ self._enabled = VR_DISPLAY and _HAS_VIRTUAL_DISPLAY and not XVFB_EXTERNAL
324
 
325
  @classmethod
326
  def instance(cls) -> "_VirtualDisplayManager":
 
336
 
337
  @property
338
  def running(self) -> bool:
339
+ # If external Xvfb is managing the display, report as "running"
340
+ return self._running or XVFB_EXTERNAL
341
 
342
+ def start(self, width=1920, height=1080, depth=24):
343
+ if XVFB_EXTERNAL:
344
+ _log_vd("External Xvfb detected (XVFB_EXTERNAL=1) β€” skipping pyvirtualdisplay")
345
+ return
346
  if not self._enabled:
347
  return
348
  if self._running:
 
360
  )
361
  self._display.start()
362
  self._running = True
363
+ # Make sure DISPLAY env is set for child processes
364
+ if not os.environ.get("DISPLAY"):
365
+ os.environ["DISPLAY"] = f":{self._display.display}"
366
+ _log_vd(f"βœ“ Started pyvirtualdisplay on DISPLAY={os.environ.get('DISPLAY')}")
 
 
 
 
367
  except Exception as exc:
368
+ _log_vd(f"βœ— pyvirtualdisplay failed: {exc}")
369
  self._enabled = False
370
 
371
  def stop(self):
372
+ if XVFB_EXTERNAL or not self._running:
373
  return
374
  with self._lock:
375
  if not self._running:
 
385
  self._running = False
386
 
387
  def __repr__(self):
388
+ if XVFB_EXTERNAL:
389
+ return f"VirtualDisplay(external DISPLAY={_DISPLAY_ENV})"
390
  state = "running" if self._running else ("idle" if self._enabled else "disabled")
391
+ return f"VirtualDisplay({state})"
 
392
 
393
 
394
  def _log_vd(msg: str):
 
401
 
402
  # ── 7a: Direct Python WebSocket ──────────────────────────
403
  class _DirectTransport:
 
 
404
  def __init__(self, debug=False):
405
  self._ws = None
406
  self._inbox = []
 
411
 
412
  def connect(self, url: str, cookies: str = "") -> bool:
413
  self._ws = _ws_mod.WebSocket()
414
+ headers = [f"User-Agent: {_UA}"]
415
  if cookies:
416
  headers.append(f"Cookie: {cookies}")
417
 
418
  self._ws.connect(
419
  url,
420
+ origin = _SITE,
421
+ header = headers,
422
+ timeout = 15,
423
  )
424
 
425
  self._running = True
 
477
  self._thread = None
478
 
479
 
480
+ # ── 7b: Browser WebSocket (Xvfb display required) ────────
481
  _BROWSER_JS = """
482
  (function(){
483
  if(window.__cfws) return 'exists';
 
510
 
511
  class _BrowserTransport:
512
  """
513
+ Chrome WebSocket relay.
514
 
515
+ Display requirements (in priority order):
516
+ 1. XVFB_EXTERNAL=1 β€” entrypoint.sh started Xvfb, DISPLAY=:99 is set
517
+ 2. VR_DISPLAY=1 β€” pyvirtualdisplay spawns its own Xvfb
518
+ 3. DISPLAY env var β€” any real display (local dev)
519
 
520
+ Chrome --headless is NEVER used. Cloudflare blocks headless agents.
521
  """
522
 
523
  def __init__(self, debug=False):
 
525
  self._debug = debug
526
  self._vd_mgr = _VirtualDisplayManager.instance()
527
 
528
+ def _ensure_display(self):
529
+ """Make sure a display is available before launching Chrome."""
530
+ display = os.environ.get("DISPLAY", "")
531
+
532
+ if display:
533
+ # Display already set (external Xvfb or local X11)
534
+ _log_vd(f"Using existing display DISPLAY={display}")
535
+ return
536
 
537
+ # Try to start pyvirtualdisplay
538
+ if self._vd_mgr.enabled:
539
  self._vd_mgr.start()
540
+ display = os.environ.get("DISPLAY", "")
541
+ if display:
542
+ return
543
+
544
+ raise RuntimeError(
545
+ "No X display available for Chrome.\n"
546
+ " On servers: set XVFB_EXTERNAL=1 and start Xvfb before this process,\n"
547
+ " or set VR_DISPLAY=1 to let pyvirtualdisplay manage Xvfb.\n"
548
+ " Headless Chrome is intentionally disabled (Cloudflare blocks it)."
549
+ )
550
 
551
+ def connect(self, url: str, **_) -> bool:
552
+ if not _HAS_BROWSER:
553
  raise RuntimeError(
554
+ "DrissionPage not installed β€” cannot use browser fallback.\n"
555
+ "Install: pip install DrissionPage"
 
 
556
  )
557
 
558
+ self._ensure_display()
559
+
560
  opts = ChromiumOptions()
561
  opts.set_argument("--disable-blink-features=AutomationControlled")
562
  opts.set_argument("--no-sandbox")
563
  opts.set_argument("--disable-dev-shm-usage")
564
  opts.set_argument("--disable-gpu")
565
  opts.set_argument("--disable-extensions")
 
566
  opts.set_argument("--disable-infobars")
567
  opts.set_argument("--window-size=1280,720")
568
+ opts.set_argument("--disable-background-networking")
569
+ opts.set_argument("--disable-sync")
570
+ opts.set_argument("--metrics-recording-only")
571
+ opts.set_argument("--mute-audio")
572
+ # ── NO --headless flag β€” Cloudflare blocks headless ──
573
+
574
+ _log_vd(f"Launching Chrome on DISPLAY={os.environ.get('DISPLAY', 'unset')}")
575
+
576
+ try:
577
+ self._page = ChromiumPage(addr_or_opts=opts)
578
+ except Exception as e:
579
+ raise RuntimeError(f"Chrome failed to launch: {e}") from e
580
 
 
581
  self._page.get(_SITE)
582
  time.sleep(4)
583
 
584
  self._page.run_js(_BROWSER_JS)
585
  self._page.run_js(f"window.__cfws.connect('{url}');")
586
 
587
+ deadline = time.time() + 20
588
  while time.time() < deadline:
589
  if self._page.run_js("return window.__cfws.alive;"):
590
+ _log_vd("βœ“ Browser WebSocket connected")
591
  return True
592
  err = self._page.run_js("return window.__cfws.error;")
593
  if err:
594
  raise ConnectionError(f"Browser WS failed: {err}")
595
+ time.sleep(0.15)
596
 
597
+ raise ConnectionError("Browser WS timed out waiting for open state")
598
 
599
  def send(self, data: str) -> bool:
600
  try:
601
  return bool(
602
+ self._page.run_js(f"return window.__cfws.send({json.dumps(data)});")
 
 
603
  )
604
  except Exception:
605
  return False
 
644
  """
645
  ☁️ Cloudflare AI Playground β€” fully modular provider.
646
 
647
+ Display modes (auto-detected):
648
+ β€’ XVFB_EXTERNAL=1 β€” entrypoint.sh manages Xvfb on DISPLAY=:99
649
+ β€’ VR_DISPLAY=1 β€” pyvirtualdisplay spawns Xvfb internally
650
+ β€’ DISPLAY set β€” use existing display (local dev)
 
 
 
 
 
 
 
 
 
 
 
651
  """
652
 
653
  def __init__(
 
676
  self.last_response: str = ""
677
  self.last_reasoning: str = ""
678
 
679
+ self._sid: str = ""
680
+ self._pk: str = ""
681
+ self._transport = None
682
+ self._mode: str = ""
683
+ self._on: bool = False
684
 
685
  self._boot()
686
  atexit.register(self.close)
687
 
 
 
 
688
  def _d(self, *a):
689
  if self.debug:
690
  print("[cloudflare]", *a, file=sys.stderr, flush=True)
691
 
 
 
 
692
  def _pull(self) -> list[str]:
693
  msgs = self._transport.recv()
694
  if self.debug:
 
703
  raise RuntimeError("WebSocket send failed")
704
 
705
  # ─────────────────────────────────────────────────
706
+ # Boot
707
  # ─────────────────────────────────────────────────
708
  def _boot(self):
709
  self._sid = _make_sid()
710
  self._pk = _make_pk()
711
  url = _make_ws_url(self._sid, self._pk)
712
 
713
+ # ── Attempt 1: direct Python WS ─────────────
714
+ direct_err = None
715
  try:
716
  self._d("Trying direct Python WebSocket...")
717
  t = _DirectTransport(debug=self.debug)
718
  t.connect(url)
719
+ time.sleep(0.4)
720
 
 
721
  if t.alive:
722
  self._transport = t
723
  self._mode = "direct"
724
+ self._d("βœ“ Direct WebSocket connected")
725
  else:
726
  t.close()
727
+ raise ConnectionError("WS not alive after connect")
728
 
729
  except Exception as e:
730
+ direct_err = e
731
  self._d(f"Direct failed: {e}")
 
732
 
733
+ # ── Attempt 2: Browser relay via Xvfb ───────
734
+ if self._transport is None:
735
+ browser_err = None
736
  try:
737
+ self._d("Trying browser transport (Xvfb Chrome)...")
738
  t = _BrowserTransport(debug=self.debug)
739
  t.connect(url)
740
  self._transport = t
741
  self._mode = "browser"
742
+ self._d(f"βœ“ Browser transport connected (DISPLAY={os.environ.get('DISPLAY', '?')})")
743
+
744
+ except Exception as e:
745
+ browser_err = e
746
+ self._d(f"Browser failed: {e}")
747
+
748
+ if self._transport is None:
749
  raise ConnectionError(
750
  f"All connection methods failed.\n"
751
+ f" Direct: {direct_err}\n"
752
+ f" Browser: {browser_err}\n"
753
+ f" β†’ Check network connectivity and DISPLAY / XVFB_EXTERNAL env vars."
754
+ )
755
 
756
  self._on = True
757
 
758
  # ── Handshake ──────────────────────────────
759
  want = {"cf_agent_identity", "cf_agent_state", "cf_agent_mcp_servers"}
760
  seen = set()
761
+ deadline = time.time() + 12
762
  while time.time() < deadline and seen < want:
763
  for raw in self._pull():
764
  try:
 
766
  except Exception:
767
  pass
768
  time.sleep(0.05)
 
769
  self._d(f"Handshake received: {seen}")
770
 
771
  self._push({"type": "cf_agent_stream_resume_request"})
772
  time.sleep(0.3)
773
  self._pull()
774
 
775
+ # ── Models ─────────────────────────────────
776
  self._load_models()
777
  if self.max_tokens is None:
778
  self.max_tokens = self._ctx_window(self.model)
 
799
  def _fetch_models(self):
800
  rid = str(uuid.uuid4())
801
  self._push({"args": [], "id": rid, "method": "getModels", "type": "rpc"})
 
802
  deadline = time.time() + 15
803
  while time.time() < deadline:
804
  for raw in self._pull():
 
841
  return full
842
  return _SHORT_TO_FULL.get(name, name)
843
 
 
 
 
844
  def _sync(self):
845
  self._push({
846
  "type": "cf_agent_state",
847
  "state": {
848
+ "model": self.model,
849
+ "temperature": self.temperature,
850
+ "stream": True,
851
+ "system": self.system,
852
+ "useExternalProvider": False,
853
+ "externalProvider": "openai",
854
+ "authMethod": "provider-key",
855
  },
856
  })
857
  time.sleep(0.15)
 
890
  def get_history(self) -> list[dict]:
891
  return _Conv.to_openai(self.history, self.system)
892
 
 
 
 
893
  def list_models(self) -> list[dict]:
894
  return [{
895
  "name": m.get("name", ""),
 
917
  max_tokens: int = None,
918
  ) -> Generator[str, None, None]:
919
  if not self._on:
920
+ raise RuntimeError("Not connected")
921
  if not messages and not data:
922
  raise ValueError("Provide 'messages' or 'data'")
923
 
 
965
 
966
  while not done:
967
  if not self._transport.alive:
 
968
  if not full_text:
969
  yield "[Connection lost]\n"
970
  break
 
973
 
974
  if not msgs:
975
  elapsed = time.time() - last_data
976
+ limit = self.timeout_idle if got_first else self.timeout_init
977
  if elapsed > limit:
 
978
  if not full_text:
979
+ yield "[Timeout β€” no response]\n"
980
  break
981
  time.sleep(0.015 if got_first else 0.04)
982
  continue
 
989
  except Exception:
990
  continue
991
 
992
+ if f.get("type") != "cf_agent_use_chat_response":
 
993
  continue
994
 
995
  body_str = f.get("body", "")
 
1049
  self.last_response = full_text
1050
  self.last_reasoning = reasoning
1051
 
 
 
 
1052
  def ask(self, prompt: str, **kwargs) -> str:
1053
  return "".join(self.chat(data=prompt, **kwargs))
1054
 
 
 
 
1055
  def new_session(self):
1056
  self._close_transport()
1057
  self.history.clear()
 
1068
 
1069
  def close(self):
1070
  self._close_transport()
1071
+ if self._mode == "browser" and not XVFB_EXTERNAL:
1072
+ _VirtualDisplayManager.instance().stop()
 
1073
  self._d("Closed.")
1074
 
1075
  def __enter__(self):
 
1085
  pass
1086
 
1087
  def __repr__(self):
1088
+ s = "βœ…" if self._on else "❌"
1089
+ vd = _VirtualDisplayManager.instance()
 
1090
  return (
1091
  f"CloudflareProvider({s} mode={self._mode!r} "
1092
+ f"model={self.model!r} {vd})"
1093
  )
1094
 
1095
 
 
1098
  # ═══════════════════════════════════════════════════════════
1099
  def _cleanup_virtual_display():
1100
  try:
1101
+ if not XVFB_EXTERNAL:
1102
+ _VirtualDisplayManager.instance().stop()
1103
  except Exception:
1104
  pass
1105
 
1106
+ atexit.register(_cleanup_virtual_display)