返回提交历史
Deleted
g4f/Provider/DeepInfra.py
+0
-162
Modified
g4f/Provider/__init__.py
+1
-1
Added
g4f/Provider/deepinfra/__init__.py
+130
-0
Added
g4f/Provider/deepinfra/turnstile.py
+284
-0
XFEstudio/gpt4free
feat(deepinfra): wip custom cdp turnstile bypass
f7b00a69
代码差异
4 个文件
+415
-163
@@ -1,162 +0,0 @@
1
from __future__ import annotations
2
3
import time
4
import asyncio
5
import requests
6
7
from ..requests import get_nodriver_session
8
from ..errors import MissingRequirementsError
9
from .template import OpenaiTemplate
10
11
async def get_turnstile_token_async() -> str:
12
try:
13
import zendriver as zd
14
except ImportError:
15
return None
16
17
async with get_nodriver_session() as session:
18
# Generate a token on any model's page; it is valid for the entire domain
19
tab = await session.get('https://deepinfra.com/' + DeepInfra.default_model)
20
21
# Inject JS to block the original request
22
js_block_fetch = """
23
const origFetch = window.fetch;
24
window.fetch = async function(...args) {
25
let url = args[0];
26
if (typeof url === 'string' && url.includes('/chat/completions')) {
27
return new Response('{}', {status: 200});
28
}
29
return origFetch.apply(this, args);
30
};
31
"""
32
await tab.evaluate(js_block_fetch)
33
34
# Initiate Turnstile
35
textarea = await tab.find('textarea', timeout=15)
36
if not textarea:
37
return ""
38
39
await textarea.send_keys('Test\n')
40
41
# Wait for the challenge to be solved
42
token = ""
43
for _ in range(40):
44
token = await tab.evaluate(
45
"(document.querySelector('[name=cf-turnstile-response]') || {value: ''}).value"
46
)
47
if token:
48
break
49
await asyncio.sleep(0.5)
50
51
return token
52
53
def get_turnstile_token() -> str:
54
"""
55
Opens the DeepInfra page using DrissionPage to obtain a Turnstile token.
56
Raises MissingRequirementsError if DrissionPage is not installed.
57
"""
58
try:
59
from DrissionPage import ChromiumPage, ChromiumOptions
60
except ImportError:
61
raise MissingRequirementsError('Install "DrissionPage" package to use DeepInfra without an API key | pip install DrissionPage')
62
63
co = ChromiumOptions()
64
# Hide the window off-screen
65
co.set_argument('--window-position=-2000,-2000')
66
co.set_argument('--window-size=800,600')
67
co.set_argument('--log-level=3')
68
69
page = ChromiumPage(co)
70
71
try:
72
# Generate a token on any model's page; it is valid for the entire domain
73
page.get('https://deepinfra.com/' + DeepInfra.default_model)
74
75
# Inject JS to block the original request
76
js_block_fetch = """
77
const origFetch = window.fetch;
78
window.fetch = async function(...args) {
79
let url = args[0];
80
if (typeof url === 'string' && url.includes('/chat/completions')) {
81
return new Response('{}', {status: 200});
82
}
83
return origFetch.apply(this, args);
84
};
85
"""
86
page.run_js(js_block_fetch)
87
88
# Initiate Turnstile
89
textarea = page.ele('tag:textarea', timeout=15)
90
if not textarea:
91
return ""
92
93
textarea.input('Test')
94
textarea.input('\n')
95
96
# Wait for the challenge to be solved
97
token_input = page.ele('@name=cf-turnstile-response', timeout=20)
98
99
if not token_input:
100
return ""
101
102
token = ""
103
for _ in range(40):
104
token = token_input.attr('value')
105
if token:
106
break
107
time.sleep(0.5)
108
109
return token
110
finally:
111
try:
112
page.quit()
113
except:
114
pass
115
116
class DeepInfra(OpenaiTemplate):
117
url = "https://deepinfra.com"
118
login_url = "https://deepinfra.com/dash/api_keys"
119
base_url = "https://api.deepinfra.com/v1/openai"
120
121
working = True
122
active_by_default = True
123
124
default_model = "zai-org/GLM-5.2"
125
126
@classmethod
127
def get_models(cls, **kwargs):
128
if not cls.models:
129
url = 'https://api.deepinfra.com/models/featured'
130
response = requests.get(url)
131
models = response.json()
132
133
cls.models = {model["model_name"]: {"id": model["model_name"], **model} for model in models if model.get("type") == "text-generation" or model.get("reported_type") == "text-to-image"}
134
cls.image_models = [model["model_name"] for model in models if model.get("reported_type") == "text-to-image"]
135
if cls.live == 0 and cls.models:
136
cls.live += 1
137
return cls.models
138
139
@classmethod
140
async def create_async_generator(cls, model, messages, api_key=None, headers=None, **kwargs):
141
if not api_key:
142
# Generate a Turnstile token for each request (required without an API key)
143
token = await get_turnstile_token_async()
144
if token:
145
if headers is None:
146
headers = {}
147
headers["X-DeepInfra-Turnstile"] = token
148
async for chunk in super().create_async_generator(model, messages, api_key=api_key, headers=headers, **kwargs):
149
yield chunk
150
151
@classmethod
152
def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict:
153
headers = super().get_headers(stream, api_key, headers)
154
if not api_key:
155
headers["X-Deepinfra-Source"] = "model-embed"
156
headers["Origin"] = "https://deepinfra.com"
157
headers["Referer"] = "https://deepinfra.com/"
158
if not headers.get("X-DeepInfra-Turnstile"):
159
token = get_turnstile_token()
160
if token:
161
headers["X-DeepInfra-Turnstile"] = token
162
return headers
@@ -33,7 +33,7 @@ __map_paths__ = {
33
33
"CopilotSession": "g4f.Provider.CopilotSession",
34
34
"CreateImagesProvider": "g4f.providers.create_images",
35
35
"Custom": "g4f.Provider.needs_auth.Custom",
36
"DeepInfra": "g4f.Provider.DeepInfra",
36
"DeepInfra": "g4f.Provider.deepinfra",
37
37
"DeepSeek": "g4f.Provider.needs_auth.DeepSeek",
38
38
"DeepSeekAPI": "g4f.Provider.needs_auth.DeepSeekAPI",
39
39
"EasyChat": "g4f.Provider.EasyChat",
@@ -0,0 +1,130 @@
1
from __future__ import annotations
2
3
import asyncio
4
import requests
5
import time
6
7
from ..template import OpenaiTemplate
8
from .turnstile import Turnstile
9
10
def find_free_port() -> int:
11
import socket
12
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
13
s.bind(('', 0))
14
return s.getsockname()[1]
15
16
def get_turnstile_token_sync(model: str) -> str:
17
"""Get Turnstile token using Turnstile with a fallback to DrissionPage."""
18
# 1. Try our lightweight CDP client first
19
try:
20
import os
21
import tempfile
22
port = find_free_port()
23
# Use a persistent temp directory to preserve browser session cookies and Turnstile reputation
24
user_data_dir = os.path.join(tempfile.gettempdir(), "g4f_chrome_profile_light")
25
client = Turnstile(port=port, user_data_dir=user_data_dir)
26
try:
27
token = client.get_token(model)
28
if token:
29
return token
30
print("[DeepInfra] Turnstile failed to obtain token. Trying fallback option (DrissionPage)...")
31
finally:
32
client.close()
33
except Exception as e:
34
print(f"[DeepInfra] Turnstile error: {e}. Trying fallback option (DrissionPage)...")
35
36
# 2. Fallback option: try original DrissionPage if installed
37
try:
38
from DrissionPage import ChromiumPage, ChromiumOptions
39
co = ChromiumOptions()
40
co.set_argument('--window-position=-2000,-2000')
41
co.set_argument('--window-size=1024,768')
42
co.set_argument('--log-level=3')
43
page = ChromiumPage(co)
44
try:
45
page.get(f'https://deepinfra.com/{model}')
46
47
# Block completions requests
48
js_block_fetch = """
49
const origFetch = window.fetch;
50
window.fetch = async function(...args) {
51
let url = args[0];
52
if (typeof url === 'string' && url.includes('/chat/completions')) {
53
return new Response('{}', {status: 200});
54
}
55
return origFetch.apply(this, args);
56
};
57
"""
58
page.run_js(js_block_fetch)
59
60
textarea = page.ele('tag:textarea', timeout=15)
61
if textarea:
62
textarea.input('Test Prompt')
63
textarea.input('\n')
64
65
token_input = page.ele('@name=cf-turnstile-response', timeout=20)
66
if token_input:
67
for _ in range(40):
68
token = token_input.attr('value')
69
if token:
70
return token
71
time.sleep(0.5)
72
finally:
73
try:
74
page.quit()
75
except:
76
pass
77
except Exception as e:
78
print(f"[DeepInfra] Fallback DrissionPage method error: {e}")
79
80
return ""
81
82
async def get_turnstile_token_async() -> str:
83
loop = asyncio.get_running_loop()
84
return await loop.run_in_executor(None, get_turnstile_token_sync, DeepInfra.default_model)
85
86
class DeepInfra(OpenaiTemplate):
87
url = "https://deepinfra.com"
88
login_url = "https://deepinfra.com/dash/api_keys"
89
base_url = "https://api.deepinfra.com/v1/openai"
90
91
working = True
92
active_by_default = True
93
94
default_model = "zai-org/GLM-5.2"
95
96
@classmethod
97
def get_models(cls, **kwargs):
98
if not cls.models:
99
url = 'https://api.deepinfra.com/models/featured'
100
response = requests.get(url)
101
models = response.json()
102
103
cls.models = {model["model_name"]: {"id": model["model_name"], **model} for model in models if model.get("type") == "text-generation" or model.get("reported_type") == "text-to-image"}
104
cls.image_models = [model["model_name"] for model in models if model.get("reported_type") == "text-to-image"]
105
if cls.live == 0 and cls.models:
106
cls.live += 1
107
108
return cls.models
109
110
@classmethod
111
async def create_async_generator(cls, model, messages, api_key=None, headers=None, **kwargs):
112
if not api_key:
113
# Generate a Turnstile token for each request (required without an API key)
114
token = await get_turnstile_token_async()
115
if token:
116
if headers is None:
117
headers = {}
118
headers["X-DeepInfra-Turnstile"] = token
119
120
async for chunk in super().create_async_generator(model, messages, api_key=api_key, headers=headers, **kwargs):
121
yield chunk
122
123
@classmethod
124
def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict:
125
headers = super().get_headers(stream, api_key, headers)
126
if not api_key:
127
headers["X-Deepinfra-Source"] = "model-embed"
128
headers["Origin"] = "https://deepinfra.com"
129
headers["Referer"] = "https://deepinfra.com/"
130
return headers
@@ -0,0 +1,284 @@
1
import os
2
import shutil
3
import platform
4
import subprocess
5
import tempfile
6
import time
7
import json
8
import urllib.request
9
import urllib.error
10
11
def find_chrome_path():
12
"""Search for Google Chrome or Chromium binary depending on OS."""
13
# Respect g4f's custom BrowserConfig.executable_path if configured
14
try:
15
from g4f.cookies import BrowserConfig
16
if BrowserConfig.executable_path and os.path.exists(BrowserConfig.executable_path):
17
return BrowserConfig.executable_path
18
except ImportError:
19
pass
20
21
# First, search using shutil.which
22
for name in ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome']:
23
path = shutil.which(name)
24
if path:
25
return path
26
27
# System default paths
28
sys_name = platform.system().lower()
29
if sys_name == 'linux':
30
for path in ['/usr/bin/google-chrome', '/opt/google/chrome/google-chrome', '/usr/bin/chromium-browser']:
31
if os.path.exists(path):
32
return path
33
elif sys_name in ('macos', 'darwin'):
34
path = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
35
if os.path.exists(path):
36
return path
37
elif sys_name == 'windows':
38
paths = [
39
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
40
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
41
]
42
for path in paths:
43
if os.path.exists(path):
44
return path
45
return None
46
47
class Turnstile:
48
def __init__(self, port=9222, user_data_dir=None):
49
self.port = port
50
51
# Respect g4f's central cookies/cache directory if available
52
if user_data_dir is None:
53
try:
54
from g4f.cookies import get_cookies_dir
55
cookies_dir = get_cookies_dir()
56
if cookies_dir:
57
user_data_dir = os.path.join(cookies_dir, "chrome_profile_light")
58
except ImportError:
59
pass
60
61
# Default fallback to a persistent directory in temp folder
62
if user_data_dir is None:
63
user_data_dir = os.path.join(tempfile.gettempdir(), "g4f_chrome_profile_light")
64
65
self.user_data_dir = user_data_dir
66
self.process = None
67
self.ws = None
68
self.id_counter = 0
69
70
def start_chrome(self):
71
"""Launch Chrome with CDP remote debugging port."""
72
chrome_path = find_chrome_path()
73
if not chrome_path:
74
raise RuntimeError("Google Chrome / Chromium executable not found.")
75
76
print(f"[Turnstile] Launching Chrome: {chrome_path} on port {self.port}")
77
78
# Create an isolated profile directory
79
os.makedirs(self.user_data_dir, exist_ok=True)
80
81
# Launch arguments matching DrissionPage to minimize detection
82
cmd = [
83
chrome_path,
84
f"--remote-debugging-port={self.port}",
85
f"--user-data-dir={self.user_data_dir}",
86
"--window-position=-2000,-2000",
87
"--window-size=1024,768",
88
"--no-default-browser-check",
89
"--disable-suggestions-ui",
90
"--no-first-run",
91
"--disable-infobars",
92
"--disable-popup-blocking",
93
"--hide-crash-restore-bubble",
94
"--disable-features=PrivacySandboxSettings4",
95
"--remote-allow-origins=*"
96
]
97
98
self.process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
99
100
# Wait for CDP port readiness and retrieve the WebSocket URL
101
ws_url = None
102
for i in range(40): # Up to 20 seconds
103
time.sleep(0.5)
104
try:
105
with urllib.request.urlopen(f"http://127.0.0.1:{self.port}/json", timeout=2) as req:
106
targets = json.loads(req.read().decode('utf-8'))
107
for target in targets:
108
if target.get('type') in ('page', 'webview'):
109
ws_url = target.get('webSocketDebuggerUrl')
110
break
111
if ws_url:
112
break
113
except (urllib.error.URLError, ConnectionResetError, ConnectionRefusedError):
114
pass
115
116
if not ws_url:
117
self.close()
118
raise RuntimeError(f"Failed to connect to Chrome debugging port 127.0.0.1:{self.port}")
119
120
print(f"[Turnstile] Connected to CDP WebSocket: {ws_url}")
121
try:
122
from websocket import create_connection
123
except ImportError:
124
from g4f.errors import MissingRequirementsError
125
raise MissingRequirementsError('Install "websocket-client" package | pip install websocket-client')
126
127
self.ws = create_connection(ws_url)
128
129
# Enable necessary CDP domains
130
self.call_cdp("Page.enable")
131
self.call_cdp("DOM.enable")
132
self.call_cdp("Runtime.enable")
133
self.call_cdp("Emulation.setFocusEmulationEnabled", enabled=True)
134
135
def call_cdp(self, method, **params):
136
"""Call CDP method and wait for response."""
137
self.id_counter += 1
138
payload = {
139
"id": self.id_counter,
140
"method": method,
141
"params": params
142
}
143
self.ws.send(json.dumps(payload))
144
145
while True:
146
response = json.loads(self.ws.recv())
147
if response.get("id") == self.id_counter:
148
if "error" in response:
149
raise RuntimeError(f"CDP Error calling {method}: {response['error']}")
150
return response.get("result", {})
151
152
def evaluate_js(self, expression):
153
"""Execute JS code on the page and return the result."""
154
res = self.call_cdp("Runtime.evaluate", expression=expression, returnByValue=True)
155
return res.get("result", {}).get("value")
156
157
def get_token(self, model: str) -> str:
158
"""Retrieve Turnstile token for the model."""
159
if not self.ws:
160
self.start_chrome()
161
162
target_url = f"https://deepinfra.com/{model}"
163
print(f"[Turnstile] Navigating to {target_url}...")
164
self.call_cdp("Page.navigate", url=target_url)
165
166
# Give some time to load
167
time.sleep(2.0)
168
169
# Inject completions request blocker ONLY in main frame (to avoid tampering with fetch inside Cloudflare's iframe)
170
fetch_blocker_js = """
171
const origFetch = window.fetch;
172
window.fetch = async function(...args) {
173
let url = args[0];
174
if (typeof url === 'string' && url.includes('/chat/completions')) {
175
return new Response('{}', {status: 200});
176
}
177
return origFetch.apply(this, args);
178
};
179
"""
180
self.evaluate_js(fetch_blocker_js)
181
182
# Try to click "Accept" on cookies consent popup if present
183
self.evaluate_js("""
184
(() => {
185
const btn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Accept');
186
if (btn) btn.click();
187
})()
188
""")
189
190
# Wait for textarea readiness, focus and input text
191
print("[Turnstile] Waiting for active textarea...")
192
text_entered = False
193
for _ in range(40): # Up to 20 seconds
194
try:
195
ready = self.evaluate_js("""
196
(() => {
197
const ta = document.querySelector('textarea');
198
if (!ta) return 'no_textarea';
199
if (ta.disabled) return 'disabled';
200
ta.click();
201
ta.focus();
202
ta.scrollIntoView({ block: 'center' });
203
return 'ready';
204
})()
205
""")
206
207
if ready == 'ready':
208
print("[Turnstile] Textarea found, focusing and entering text...")
209
210
# Retrieve textarea nodeId for native focusing
211
doc = self.call_cdp('DOM.getDocument')
212
root_id = doc['root']['nodeId']
213
textarea = self.call_cdp('DOM.querySelector', nodeId=root_id, selector='textarea')
214
215
# Native focus via CDP
216
self.call_cdp('DOM.focus', nodeId=textarea['nodeId'])
217
218
# Enter text via native CDP command
219
self.call_cdp("Input.insertText", text="Test Prompt")
220
221
# Give React some time to process input before pressing Enter
222
time.sleep(0.5)
223
224
# Simulate Enter keypress via native CDP events (matching DrissionPage implementation)
225
self.call_cdp("Input.dispatchKeyEvent",
226
type="keyDown",
227
windowsVirtualKeyCode=13,
228
key="Enter",
229
code="Enter",
230
text="\r",
231
unmodifiedText="\r")
232
self.call_cdp("Input.dispatchKeyEvent",
233
type="keyUp",
234
windowsVirtualKeyCode=13,
235
key="Enter",
236
code="Enter",
237
text="\r",
238
unmodifiedText="\r")
239
240
text_entered = True
241
break
242
except Exception:
243
pass
244
time.sleep(0.5)
245
246
if not text_entered:
247
print("[-] Turnstile initiation error: textarea not found or disabled.")
248
return ""
249
250
# Poll page for Turnstile token presence
251
print("[Turnstile] Waiting for Cloudflare Turnstile solve...")
252
token_js = "document.querySelector('[name=cf-turnstile-response]') ? document.querySelector('[name=cf-turnstile-response]').value : ''"
253
token = ""
254
for i in range(120): # Up to 60 seconds
255
try:
256
token = self.evaluate_js(token_js)
257
if token:
258
print(f"[Turnstile] Token generated on check {i+1}!")
259
break
260
except Exception:
261
pass
262
time.sleep(0.5)
263
264
return token
265
266
def close(self):
267
"""Close connection and terminate Chrome process."""
268
if self.ws:
269
try:
270
self.ws.close()
271
except Exception:
272
pass
273
self.ws = None
274
275
if self.process:
276
try:
277
self.process.terminate()
278
self.process.wait(timeout=5)
279
except Exception:
280
try:
281
self.process.kill()
282
except Exception:
283
pass
284
self.process = None