返回提交历史
Modified
etc/unittest/client.py
+3
-2
Modified
etc/unittest/models.py
+14
-4
Modified
g4f/Provider/Cloudflare.py
+133
-69
Added
g4f/Provider/DeepInfra.py
+197
-0
Modified
g4f/Provider/GptFree.py
+9
-7
Added
g4f/Provider/Miklium.py
+59
-0
Added
g4f/Provider/Perchance.py
+145
-0
Added
g4f/Provider/Surfsense.py
+73
-0
Modified
g4f/Provider/__init__.py
+16
-2
Deleted
g4f/Provider/deepinfra/__init__.py
+0
-143
Deleted
g4f/Provider/deepinfra/turnstile.py
+0
-289
Modified
g4f/Provider/hf_space/__init__.py
+7
-4
Modified
g4f/gui/server/website.py
+19
-3
Modified
g4f/models.py
+9
-2
Modified
g4f/providers/any_model_map.py
+13
-2
Modified
g4f/providers/any_provider.py
+11
-3
Modified
g4f/requests/__init__.py
+49
-0
Added
g4f/requests/cdp.py
+668
-0
Modified
scripts/build-nuitka.sh
+1
-0
XFEstudio/gpt4free
feat: add Perchance, MIKLIUM, Surfsense & fix DeepInfra/CDP
fbb55395
代码差异
19 个文件
+1426
-530
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
3
3
import unittest
4
4
5
from g4f.errors import ModelNotFoundError
5
import g4f
6
from g4f.errors import ModelNotFoundError, ResponseError
6
7
from g4f.client import Client, AsyncClient, ChatCompletion, ChatCompletionChunk
7
8
from g4f.client.service import get_model_and_provider
8
9
from g4f.providers.types import BaseProvider
@@ -123,7 +124,7 @@ class TestPassModel(unittest.TestCase):
123
124
def run_exception():
124
125
client = Client()
125
126
client.chat.completions.create(DEFAULT_MESSAGES, "Hello")
126
self.assertRaises(ModelNotFoundError, run_exception)
127
self.assertRaises((ModelNotFoundError, ResponseError), run_exception)
127
128
128
129
def test_best_provider(self):
129
130
not_default_model = "gpt-4o"
@@ -14,13 +14,18 @@ class TestProviderHasModel(unittest.TestCase):
14
14
for model, providers in __models__.values():
15
15
for provider in providers:
16
16
if isinstance(provider, str):
17
provider = __getattr__(provider)
17
try:
18
provider = __getattr__(provider)
19
except AttributeError:
20
continue
21
if provider is None:
22
continue
18
23
if getattr(provider, "needs_auth", False):
19
24
continue
20
25
if issubclass(provider, ProviderModelMixin):
21
26
try:
22
27
provider.get_models(timeout=5) # Update models
23
if model.name in provider.model_aliases:
28
if provider.model_aliases and model.name in provider.model_aliases:
24
29
model_name = provider.model_aliases[model.name]
25
30
else:
26
31
model_name = model.get_long_name()
@@ -35,12 +40,17 @@ class TestProviderHasModel(unittest.TestCase):
35
40
except (MissingRequirementsError, MissingAuthError):
36
41
return
37
42
if self.cache[provider.__name__]:
38
if not model in provider.model_aliases:
43
if not provider.model_aliases or model not in provider.model_aliases:
39
44
self.assertIn(model, self.cache[provider.__name__], provider.__name__)
40
45
41
46
def test_all_providers_working(self):
42
47
for model, providers in __models__.values():
43
48
for provider in providers:
44
49
if isinstance(provider, str):
45
provider = __getattr__(provider)
50
try:
51
provider = __getattr__(provider)
52
except AttributeError:
53
continue
54
if provider is None:
55
continue
46
56
self.assertTrue(provider.working, f"{provider.__name__} in {model.name}")
@@ -1,13 +1,11 @@
1
1
from __future__ import annotations
2
2
3
import asyncio
3
4
import json
4
5
5
6
from ..typing import AsyncResult, Messages
6
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin
7
from ..requests import StreamSession, get_args_from_nodriver, raise_for_status, merge_cookies
8
from ..requests import DEFAULT_HEADERS, has_nodriver, has_curl_cffi
9
from ..providers.response import FinishReason, Usage
10
from ..errors import ResponseStatusError, ModelNotFoundError
7
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
8
from ..errors import ModelNotFoundError
11
9
from .. import debug
12
10
from .helper import render_messages
13
11
@@ -27,37 +25,41 @@ def clean_name(name: str) -> str:
27
25
"qwen-", "qwen").replace(
28
26
"qwen", "qwen-")
29
27
30
# models = []
31
# model_aliases = {clean_name(m.get("name")): m.get("name") for m in models}
32
# open(__file__, "a").write(f"""# Generated by g4f.models.cloudflare.py
33
# model_aliases = {model_aliases}
34
# """)
35
36
class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
28
class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin):
37
29
label = "Cloudflare AI"
38
30
url = "https://playground.ai.cloudflare.com"
39
working = False
40
use_nodriver = True
31
working = True
32
use_nodriver = False
41
33
active_by_default = True
42
api_endpoint = "https://playground.ai.cloudflare.com/api/inference"
43
models_url = "https://playground.ai.cloudflare.com/api/models"
44
34
supports_stream = True
45
35
supports_system_message = True
46
36
supports_message_history = True
47
37
default_model = 'llama-3.3-70b'
48
38
model_aliases = {
49
'deepseek-coder-6.7b-base': '@hf/thebloke/deepseek-coder-6.7b-base-awq',
50
39
'deepseek-coder-6.7b': '@hf/thebloke/deepseek-coder-6.7b-instruct-awq',
51
'deepseek-math-7b': '@cf/deepseek-ai/deepseek-math-7b-instruct',
40
'deepseek-coder-6.7b-base': '@hf/thebloke/deepseek-coder-6.7b-base-awq',
52
41
'deepseek-distill-qwen-32b': '@cf/deepseek-ai/deepseek-r1-distill-qwen-32b',
42
'deepseek-math-7b': '@cf/deepseek-ai/deepseek-math-7b-instruct',
53
43
'discolm-german-7b-v1': '@cf/thebloke/discolm-german-7b-v1-awq',
54
44
'falcon-7b': '@cf/tiiuae/falcon-7b-instruct',
45
'gemma-2b-lora': '@cf/google/gemma-2b-it-lora',
55
46
'gemma-3-12b': '@cf/google/gemma-3-12b-it',
47
'gemma-4-26b-a4b-it': '@cf/google/gemma-4-26b-a4b-it',
56
48
'gemma-7b': '@hf/google/gemma-7b-it',
49
'gemma-7b-it-lora': '@cf/google/gemma-7b-it-lora',
50
'gemma-sea-lion-v4-27b': '@cf/aisingapore/gemma-sea-lion-v4-27b-it',
51
'glm-4.7-flash': '@cf/zai-org/glm-4.7-flash',
52
'glm-5.2': '@cf/zai-org/glm-5.2',
53
'gpt-oss-120b': '@cf/openai/gpt-oss-120b',
54
'gpt-oss-20b': '@cf/openai/gpt-oss-20b',
55
'granite-4.0-h-micro': '@cf/ibm-granite/granite-4.0-h-micro',
57
56
'hermes-2-pro-mistral-7b': '@hf/nousresearch/hermes-2-pro-mistral-7b',
57
'kimi-k2.6': '@cf/moonshotai/kimi-k2.6',
58
'kimi-k2.7-code': '@cf/moonshotai/kimi-k2.7-code',
58
59
'llama-2-13b': '@hf/thebloke/llama-2-13b-chat-awq',
59
'llama-2-7b-fp16': '@cf/meta/llama-2-7b-chat-fp16',
60
60
'llama-2-7b': '@cf/meta/llama-2-7b-chat-int8',
61
'llama-2-7b-fp16': '@cf/meta/llama-2-7b-chat-fp16',
62
'llama-2-7b-lora': '@cf/meta-llama/llama-2-7b-chat-hf-lora',
61
63
'llama-3-8b': '@hf/meta-llama/meta-llama-3-8b-instruct',
62
64
'llama-3.1-8b': '@cf/meta/llama-3.1-8b-instruct-fp8',
63
65
'llama-3.2-11b-vision': '@cf/meta/llama-3.2-11b-vision-instruct',
@@ -69,16 +71,19 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
69
71
'llamaguard-7b': '@hf/thebloke/llamaguard-7b-awq',
70
72
'mistral-7b-v0.1': '@hf/thebloke/mistral-7b-instruct-v0.1-awq',
71
73
'mistral-7b-v0.2': '@hf/mistral/mistral-7b-instruct-v0.2',
74
'mistral-7b-v0.2-lora': '@cf/mistral/mistral-7b-instruct-v0.2-lora',
72
75
'mistral-small-3.1-24b': '@cf/mistralai/mistral-small-3.1-24b-instruct',
76
'nemotron-3-120b': '@cf/nvidia/nemotron-3-120b-a12b',
73
77
'neural-7b-v3-1': '@hf/thebloke/neural-chat-7b-v3-1-awq',
74
78
'openchat-3.5-0106': '@cf/openchat/openchat-3.5-0106',
75
79
'openhermes-2.5-mistral-7b': '@hf/thebloke/openhermes-2.5-mistral-7b-awq',
76
80
'phi-2': '@cf/microsoft/phi-2',
77
'qwen1.5-0.5b': '@cf/qwen/qwen1.5-0.5b-chat',
78
81
'qwen-1.5-1.8b': '@cf/qwen/qwen1.5-1.8b-chat',
79
82
'qwen-1.5-14b': '@cf/qwen/qwen1.5-14b-chat-awq',
80
83
'qwen-1.5-7b': '@cf/qwen/qwen1.5-7b-chat-awq',
81
84
'qwen-2.5-coder-32b': '@cf/qwen/qwen2.5-coder-32b-instruct',
85
'qwen1.5-0.5b': '@cf/qwen/qwen1.5-0.5b-chat',
86
'qwen3-30b-a3b': '@cf/qwen/qwen3-30b-a3b-fp8',
82
87
'qwq-32b': '@cf/qwen/qwq-32b',
83
88
'sqlcoder-7b-2': '@cf/defog/sqlcoder-7b-2',
84
89
'starling-lm-7b-beta': '@hf/nexusflow/starling-lm-7b-beta',
@@ -87,7 +92,6 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
87
92
'zephyr-7b-beta': '@hf/thebloke/zephyr-7b-beta-awq'
88
93
}
89
94
models = list(model_aliases.keys())
90
_args: dict = None
91
95
92
96
@classmethod
93
97
async def create_async_generator(
@@ -98,56 +102,116 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
98
102
max_tokens: int = 2048,
99
103
**kwargs
100
104
) -> AsyncResult:
101
cache_file = cls.get_cache_file()
102
if cls._args is None:
103
headers = DEFAULT_HEADERS.copy()
104
headers["referer"] = f"{cls.url}"
105
headers["origin"] = cls.url
106
if cache_file.exists():
107
with cache_file.open("r") as f:
108
cls._args = json.load(f)
109
elif has_nodriver:
110
try:
111
cls._args = await get_args_from_nodriver(cls.url, proxy=proxy)
112
except (RuntimeError, FileNotFoundError) as e:
113
debug.log(f"Cloudflare: Nodriver is not available:", e)
114
cls._args = {"headers": headers, "cookies": {}, "impersonate": "chrome"}
115
else:
116
cls._args = {"headers": headers, "cookies": {}, "impersonate": "chrome"}
105
try:
106
from ..requests.cdp import CDPSession
107
except ImportError:
108
raise RuntimeError("CDP module is required for Cloudflare provider. Please ensure g4f.requests.cdp is available.")
109
117
110
try:
118
111
model = cls.get_model(model)
119
112
except ModelNotFoundError:
120
113
pass
121
data = {
122
"messages": [{
123
**message,
124
"parts": [{"type":"text", "text": message["content"]}]} for message in render_messages(messages)],
125
"lora": None,
126
"model": model,
127
"max_tokens": max_tokens,
128
"stream": True,
129
"system_message":"You are a helpful assistant",
130
"tools":[]
131
}
132
async with StreamSession(**cls._args) as session:
133
async with session.post(
134
cls.api_endpoint,
135
json=data,
136
) as response:
137
cls._args["cookies"] = merge_cookies(cls._args["cookies"] , response)
114
115
debug.log("Cloudflare: Starting CDPSession...")
116
session = CDPSession(headless=False)
117
await session.start()
118
119
try:
120
await session.navigate(cls.url)
121
122
# Wait for Cloudflare validation to pass and React to load
123
await asyncio.sleep(5)
124
125
for _ in range(30):
126
title = await session.evaluate_js("document.title") or ""
127
content = await session.evaluate_js("document.body.innerText") or ""
128
if title and "Just a moment" not in title and "Attention Required" not in title and "cf-browser-verification" not in content:
129
break
130
await asyncio.sleep(1)
131
132
# Setup event queue for console logs
133
q = asyncio.Queue()
134
session.add_event_handler("Runtime.consoleAPICalled", q)
135
136
# Render messages to the format expected by Cloudflare UI
137
cf_messages = [{"role": msg["role"], "parts": [{"type": "text", "text": msg["content"]}], "id": f"msg_{i}"} for i, msg in enumerate(render_messages(messages))]
138
139
# Inject JS to handle the WebSocket stream
140
js_code = f"""
141
async function runChat() {{
142
const pk = crypto.randomUUID();
143
const agentId = 'playground-' + Math.random().toString(36).substring(2, 15);
144
const modelStr = {json.dumps(model)};
145
146
const wsUrl = `wss://playground.ai.cloudflare.com/agents/playground/${{agentId}}?_pk=${{pk}}&model=${{encodeURIComponent(modelStr)}}`;
147
const ws = new WebSocket(wsUrl);
148
149
ws.onopen = () => {{
150
ws.send(JSON.stringify({{"type":"cf_agent_stream_resume_request"}}));
151
ws.send(JSON.stringify({{"name":agentId,"agent":"playground","type":"cf_agent_identity"}}));
152
ws.send(JSON.stringify({{"state":{{"model":modelStr,"temperature":1,"stream":true,"system":"You are a helpful assistant.","useExternalProvider":false,"externalProvider":"openai","generativeUI":false}},"type":"cf_agent_state"}}));
153
ws.send(JSON.stringify({{"mcp":{{"prompts":[],"resources":[],"servers":{{}},"tools":[]}},"type":"cf_agent_mcp_servers"}}));
154
ws.send(JSON.stringify({{"type":"cf_agent_stream_resume_none"}}));
155
156
setTimeout(() => {{
157
const req = {{
158
"id": "req_" + Math.random().toString(36).substring(2, 10),
159
"init": {{
160
"method": "POST",
161
"body": JSON.stringify({{
162
"messages": {json.dumps(cf_messages)},
163
"trigger": "submit-message"
164
}})
165
}},
166
"type": "cf_agent_use_chat_request"
167
}};
168
ws.send(JSON.stringify(req));
169
}}, 1000);
170
}};
171
172
ws.onmessage = (event) => {{
173
try {{
174
const data = JSON.parse(event.data);
175
if (data.type === 'cf_agent_use_chat_response') {{
176
const body = JSON.parse(data.body);
177
if (body.type === 'text-delta') {{
178
console.log("CF_CHUNK: " + body.delta);
179
}} else if (body.type === 'finish-step' || body.type === 'finish') {{
180
console.log("CF_DONE");
181
ws.close();
182
}}
183
}}
184
}} catch (e) {{}}
185
}};
186
187
ws.onerror = () => console.log("CF_ERROR");
188
ws.onclose = () => console.log("CF_DONE");
189
}}
190
runChat();
191
"""
192
193
debug.log("Cloudflare: Injecting WebSocket streaming JS...")
194
await session.evaluate_js(js_code)
195
196
# Yield from the queue
197
last_val = None
198
while True:
138
199
try:
139
await raise_for_status(response)
140
except ResponseStatusError:
141
cls._args = None
142
if cache_file.exists():
143
cache_file.unlink()
144
raise
145
async for line in response.iter_lines():
146
if line.startswith(b'0:'):
147
yield json.loads(line[2:])
148
elif line.startswith(b'e:'):
149
finish = json.loads(line[2:])
150
yield Usage(**finish.get("usage"))
151
yield FinishReason(finish.get("finishReason"))
152
with cache_file.open("w") as f:
153
json.dump(cls._args, f)
200
event = await asyncio.wait_for(q.get(), timeout=30.0)
201
args = event.get("args", [])
202
if args and args[0].get("type") == "string":
203
val = args[0].get("value", "")
204
if val == last_val:
205
continue
206
last_val = val
207
if val.startswith("CF_CHUNK: "):
208
yield val[10:]
209
elif val == "CF_DONE":
210
break
211
elif val == "CF_ERROR":
212
raise RuntimeError("WebSocket error inside Cloudflare session")
213
except asyncio.TimeoutError:
214
raise TimeoutError("Timeout waiting for Cloudflare response")
215
finally:
216
session.remove_event_handler("Runtime.consoleAPICalled", q)
217
await session.close()
@@ -0,0 +1,197 @@
1
from __future__ import annotations
2
3
import asyncio
4
import requests
5
6
from ..requests.cdp import SyncCDPSession
7
from .. import debug
8
from .template import OpenaiTemplate
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
"""
18
Synchronous Turnstile token retrieval using SyncCDPSession with retries.
19
Uses a blocking recv() loop — no async timeouts, waits as long as needed.
20
Designed to be run via asyncio.run_in_executor() from async context.
21
"""
22
import time
23
24
for attempt in range(3):
25
port = find_free_port()
26
session = SyncCDPSession(port=port, headless=False)
27
session.start_chrome()
28
29
try:
30
url = f"https://deepinfra.com/{model}"
31
debug.log(f"[DeepInfra] Navigating to {url} (Attempt {attempt + 1}/3)...")
32
session.navigate(url)
33
34
# Inject completions request blocker
35
fetch_blocker_js = """
36
const origFetch = window.fetch;
37
window.fetch = async function(...args) {
38
let url = args[0];
39
if (typeof url === 'string' && url.includes('/chat/completions')) {
40
return new Response('{}', {status: 200});
41
}
42
return origFetch.apply(this, args);
43
};
44
"""
45
session.evaluate_js(fetch_blocker_js)
46
47
# Try to click "Accept" on cookies consent popup if present
48
session.evaluate_js("""
49
(() => {
50
const btn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Accept');
51
if (btn) btn.click();
52
})()
53
""")
54
55
# Click on an empty page area to give window focus — signals Cloudflare that
56
# a real user is present, which speeds up Turnstile token generation significantly.
57
session.click(200, 400)
58
59
# Wait for textarea readiness, then focus and input text
60
debug.log("[DeepInfra] Waiting for active textarea...")
61
text_entered = False
62
for _ in range(80): # Up to 40 seconds
63
try:
64
ready = session.evaluate_js("""
65
(() => {
66
const ta = document.querySelector('textarea');
67
const ts = document.querySelector('[name=cf-turnstile-response]');
68
if (!ta) return 'no_textarea';
69
if (ta.disabled) return 'disabled';
70
if (!ts) return 'no_turnstile';
71
ta.click();
72
ta.focus();
73
ta.scrollIntoView({ block: 'center' });
74
return 'ready';
75
})()
76
""")
77
78
if ready == 'ready':
79
debug.log("[DeepInfra] Textarea and Turnstile found, focusing and entering text...")
80
81
# Retrieve textarea nodeId for native focusing
82
doc = session.call('DOM.getDocument')
83
root_id = doc['root']['nodeId']
84
textarea = session.call('DOM.querySelector', nodeId=root_id, selector='textarea')
85
86
# Native focus via CDP
87
session.call('DOM.focus', nodeId=textarea['nodeId'])
88
89
# Enter text via native CDP command
90
import random
91
test_prompt = random.choice(["Hello", "Hi", "Hey there", "Testing", "Ping", "What's up?", "Can you hear me?"])
92
session.call("Input.insertText", text=test_prompt)
93
94
time.sleep(0.5)
95
96
# Simulate Enter keypress
97
session.call("Input.dispatchKeyEvent",
98
type="keyDown",
99
windowsVirtualKeyCode=13,
100
key="Enter", code="Enter",
101
text="\r", unmodifiedText="\r")
102
session.call("Input.dispatchKeyEvent",
103
type="keyUp",
104
windowsVirtualKeyCode=13,
105
key="Enter", code="Enter",
106
text="\r", unmodifiedText="\r")
107
108
text_entered = True
109
break
110
except Exception:
111
pass
112
time.sleep(0.5)
113
114
if not text_entered:
115
debug.log("[DeepInfra] Textarea/Turnstile not ready or failed to submit, retrying attempt...")
116
session.close()
117
continue
118
119
# Poll page for Turnstile token
120
debug.log("[DeepInfra] Waiting for Cloudflare Turnstile solve...")
121
token_js = "document.querySelector('[name=cf-turnstile-response]') ? document.querySelector('[name=cf-turnstile-response]').value : ''"
122
token = ""
123
for i in range(240): # Up to 120 seconds per attempt
124
try:
125
token = session.evaluate_js(token_js)
126
if token:
127
debug.log(f"[DeepInfra] Token generated on check {i+1}!")
128
return token
129
except Exception:
130
pass
131
time.sleep(0.5)
132
133
except Exception as e:
134
debug.log(f"[DeepInfra] Error on attempt {attempt + 1}: {e}")
135
finally:
136
session.close()
137
138
return ""
139
140
async def get_turnstile_token_async(model: str = None) -> str:
141
"""Run the synchronous Turnstile solver in a thread pool executor."""
142
if model is None:
143
model = DeepInfra.default_model
144
loop = asyncio.get_running_loop()
145
return await loop.run_in_executor(None, _get_turnstile_token_sync, model)
146
147
class DeepInfra(OpenaiTemplate):
148
url = "https://deepinfra.com"
149
login_url = "https://deepinfra.com/dash/api_keys"
150
base_url = "https://api.deepinfra.com/v1/openai"
151
152
working = True
153
active_by_default = True
154
155
default_model = "zai-org/GLM-5.2"
156
157
@classmethod
158
async def get_quota(cls, **kwargs):
159
return {}
160
161
@classmethod
162
def get_models(cls, **kwargs):
163
if not cls.models:
164
url = 'https://api.deepinfra.com/models/featured'
165
response = requests.get(url)
166
models = response.json()
167
168
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"}
169
cls.image_models = [model["model_name"] for model in models if model.get("reported_type") == "text-to-image"]
170
if cls.live == 0 and cls.models:
171
cls.live += 1
172
173
return cls.models
174
175
@classmethod
176
async def create_async_generator(cls, model, messages, api_key=None, headers=None, **kwargs):
177
if not api_key:
178
# Generate a Turnstile token for each request (required without an API key)
179
token = await get_turnstile_token_async(model)
180
if token:
181
if headers is None:
182
headers = {}
183
headers["X-DeepInfra-Turnstile"] = token
184
else:
185
raise ValueError("Failed to obtain Turnstile token for DeepInfra request.")
186
187
async for chunk in super().create_async_generator(model, messages, api_key=api_key, headers=headers, **kwargs):
188
yield chunk
189
190
@classmethod
191
def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict:
192
headers = super().get_headers(stream, api_key, headers)
193
if not api_key:
194
headers["X-Deepinfra-Source"] = "web-page"
195
headers["Origin"] = "https://deepinfra.com"
196
headers["Referer"] = "https://deepinfra.com/"
197
return headers
@@ -91,17 +91,17 @@ class GptFree(AsyncGeneratorProvider, ProviderModelMixin):
91
91
"images": images,
92
92
"history": history
93
93
}
94
95
94
firebase_api_key = "AIzaSyBdU-Np8RSh1tPSsPOWg3qIm6PnVK5PQb4"
96
95
97
96
async with ClientSession() as session:
98
auth_url = f"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key={firebase_api_key}"
99
async with session.post(auth_url, json={"returnSecureToken": True}, proxy=proxy) as auth_resp:
100
auth_resp.raise_for_status()
101
auth_data = await auth_resp.json()
102
id_token = auth_data["idToken"]
97
if not hasattr(cls, '_id_token'):
98
auth_url = f"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key={firebase_api_key}"
99
async with session.post(auth_url, json={"returnSecureToken": True}, proxy=proxy) as auth_resp:
100
auth_resp.raise_for_status()
101
auth_data = await auth_resp.json()
102
cls._id_token = auth_data["idToken"]
103
103
104
headers["Authorization"] = f"Bearer {id_token}"
104
headers["Authorization"] = f"Bearer {cls._id_token}"
105
105
106
106
async with session.post(
107
107
cls.api_endpoint,
@@ -109,6 +109,8 @@ class GptFree(AsyncGeneratorProvider, ProviderModelMixin):
109
109
json=payload,
110
110
proxy=proxy
111
111
) as response:
112
if response.status == 401 and hasattr(cls, '_id_token'):
113
delattr(cls, '_id_token')
112
114
response.raise_for_status()
113
115
114
116
async for line in response.content:
@@ -0,0 +1,59 @@
1
from __future__ import annotations
2
3
from typing import Any
4
5
from ..typing import AsyncResult, Messages
6
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
7
from .helper import format_prompt
8
from ..requests import StreamSession
9
10
class Miklium(AsyncGeneratorProvider, ProviderModelMixin):
11
label = "Miklium"
12
url = "https://miklium.vercel.app"
13
api_endpoint = "/api/chatbot"
14
15
working = True
16
needs_auth = False
17
supports_stream = False
18
supports_system_message = True
19
supports_message_history = True
20
21
default_model = 'miklium'
22
models = ['miklium', 'personalityless', 'male', 'female', 'all']
23
24
@classmethod
25
async def create_async_generator(
26
cls,
27
model: str,
28
messages: Messages,
29
proxy: str | None = None,
30
**kwargs: Any
31
) -> AsyncResult:
32
model = cls.get_model(model)
33
34
headers = {
35
"accept": "*/*",
36
"accept-language": "en-US,en;q=0.9",
37
"content-type": "application/json",
38
"origin": cls.url,
39
"referer": f"{cls.url}/"
40
}
41
42
prompt = format_prompt(messages)
43
44
data_payload = {
45
"message": prompt,
46
"response_stacking": kwargs.get("response_stacking", 4),
47
"personality": model
48
}
49
50
async with StreamSession(headers=headers, impersonate="chrome") as session:
51
async with session.post(f"{cls.url}{cls.api_endpoint}", json=data_payload, proxy=proxy) as response:
52
response.raise_for_status()
53
json_data = await response.json()
54
55
if str(json_data.get("success")).lower() == "true":
56
yield json_data.get("response", "")
57
else:
58
error_msg = json_data.get("error", "Unknown error")
59
raise RuntimeError(f"Miklium error: {error_msg}")
@@ -0,0 +1,145 @@
1
from __future__ import annotations
2
3
import asyncio
4
import random
5
import json
6
from g4f.requests.cdp import CDPSession
7
from ..typing import AsyncResult, Messages
8
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
9
from .helper import format_prompt
10
from ..requests import StreamSession
11
12
class Perchance(AsyncGeneratorProvider, ProviderModelMixin):
13
url = "https://perchance.org/ai-chat"
14
api_endpoint = "https://text-generation.perchance.org/api/generate"
15
verify_url = "https://text-generation.perchance.org/embed?thread=0"
16
working = True
17
supports_stream = True
18
supports_system_message = True
19
supports_message_history = True
20
default_model = "perchance"
21
22
# Class-level cache for credentials to avoid restarting browser for every request
23
_user_key: str | None = None
24
_cookies: dict | None = None
25
_headers: dict | None = None
26
27
@classmethod
28
async def _get_user_key(cls, proxy: str = None) -> tuple[str, dict, dict]:
29
"""Runs CDP to solve Turnstile and extract the userKey and session cookies."""
30
session = CDPSession(headless=True)
31
await session.start()
32
try:
33
await session.navigate(cls.verify_url)
34
35
# Setup message capture in page context
36
setup_js = """
37
window.collectedMessages = [];
38
window.addEventListener("message", (e) => {
39
if (e.data && (e.data.type.startsWith("stream") || e.data.type === "verified")) {
40
window.collectedMessages.push(e.data);
41
}
42
});
43
"""
44
await session.evaluate_js(setup_js)
45
46
# Always trigger verifyUser to ensure Turnstile solving/checking is executed
47
verify_js = """
48
window.postMessage({type: "verifyUser"}, window.location.origin);
49
"""
50
await session.evaluate_js(verify_js)
51
52
# Poll for userKey-0
53
user_key = None
54
for _ in range(30):
55
user_key = await session.evaluate_js("localStorage.getItem('userKey-0')")
56
if user_key:
57
break
58
await asyncio.sleep(1)
59
60
if not user_key:
61
raise RuntimeError("Failed to verify user/solve Turnstile on Perchance.")
62
63
cookies = await session.get_cookies()
64
user_agent = await session.get_user_agent()
65
66
headers = {
67
"user-agent": user_agent,
68
"Accept": "*/*",
69
"Origin": "https://text-generation.perchance.org",
70
"Referer": "https://text-generation.perchance.org/embed",
71
}
72
73
return user_key, cookies, headers
74
finally:
75
await session.close()
76
77
@classmethod
78
async def create_async_generator(
79
cls,
80
model: str,
81
messages: Messages,
82
proxy: str = None,
83
**kwargs
84
) -> AsyncResult:
85
# Check if we need to fetch credentials (cold start)
86
if not cls._user_key or not cls._cookies or not cls._headers:
87
cls._user_key, cls._cookies, cls._headers = await cls._get_user_key(proxy=proxy)
88
89
# Approximate token count (chars divided by ~4)
90
instruction = format_prompt(messages)
91
instruction_token_count = max(1, len(instruction) // 4)
92
93
payload = {
94
"instruction": instruction,
95
"startWith": "",
96
"stopSequences": ["\n\n", "\nAnon:", "\nBot:"],
97
"generatorName": "ai-chat",
98
"startWithTokenCount": 0,
99
"instructionTokenCount": instruction_token_count
100
}
101
102
try:
103
async with StreamSession(
104
headers=cls._headers,
105
cookies=cls._cookies,
106
proxies={"all": proxy} if proxy else None,
107
impersonate="chrome"
108
) as session:
109
req_id = f"aiTextCompletion{random.random()}"
110
gen_url = f"{cls.api_endpoint}?userKey={cls._user_key}&thread=0&requestId={req_id}&__cacheBust={random.random()}"
111
112
async with session.post(gen_url, json=payload, proxy=proxy) as response:
113
# If Cloudflare blocks or token has expired, raise auth_failed to trigger a refresh
114
if response.status in (401, 403):
115
raise RuntimeError("auth_failed")
116
117
if response.status != 200:
118
text = await response.text()
119
if "invalid_key" in text or "failed_verification" in text:
120
raise RuntimeError("auth_failed")
121
raise RuntimeError(f"generate failed: {response.status} {text}")
122
123
# Parse custom SSE format
124
async for chunk_bytes in response.iter_content():
125
if chunk_bytes:
126
chunk_str = chunk_bytes.decode(errors="ignore")
127
for line in chunk_str.split("\n"):
128
line = line.strip()
129
if line.startswith('t:'):
130
try:
131
# Parse the text chunk safely via JSON loads (handles escapes)
132
text_val = json.loads(line[2:])
133
if text_val:
134
yield text_val
135
except Exception:
136
pass
137
138
except RuntimeError as e:
139
if str(e) == "auth_failed":
140
# Clear cached credentials and retry once with fresh verification
141
cls._user_key = cls._cookies = cls._headers = None
142
async for chunk in cls.create_async_generator(model, messages, proxy, **kwargs):
143
yield chunk
144
else:
145
raise e
@@ -0,0 +1,73 @@
1
from __future__ import annotations
2
3
import json
4
from typing import Any
5
6
from ..typing import AsyncResult, Messages
7
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
8
from ..requests import StreamSession
9
10
class Surfsense(AsyncGeneratorProvider, ProviderModelMixin):
11
label = "Surfsense"
12
url = "https://www.surfsense.com"
13
api_endpoint = "https://api.surfsense.com/api/v1/public/anon-chat/stream"
14
15
working = True
16
needs_auth = False
17
supports_stream = True
18
supports_system_message = True
19
supports_message_history = True
20
21
default_model = 'gpt-o4-mini-no-login'
22
models = ['gpt-o4-mini-no-login', 'gpt-5.4-mini-no-login']
23
24
model_aliases = {
25
"o4-mini": "gpt-o4-mini-no-login",
26
"gpt-4o-mini": "gpt-o4-mini-no-login",
27
"gpt-4.5-mini": "gpt-5.4-mini-no-login"
28
}
29
30
@classmethod
31
async def create_async_generator(
32
cls,
33
model: str,
34
messages: Messages,
35
proxy: str | None = None,
36
**kwargs: Any
37
) -> AsyncResult:
38
model = cls.get_model(model)
39
40
headers = {
41
"accept": "*/*",
42
"accept-language": "en-US,en;q=0.9",
43
"content-type": "application/json",
44
"origin": cls.url,
45
"referer": f"{cls.url}/",
46
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
47
}
48
49
data_payload = {
50
"model_slug": model,
51
"messages": messages
52
}
53
54
async with StreamSession(headers=headers, impersonate="safari15_3") as session:
55
async with session.post(cls.api_endpoint, json=data_payload, proxy=proxy) as response:
56
response.raise_for_status()
57
58
async for chunk in response.iter_lines():
59
if not chunk:
60
continue
61
chunk = chunk.decode("utf-8")
62
if chunk.startswith("data: "):
63
chunk = chunk[6:]
64
if chunk == "[DONE]":
65
break
66
try:
67
json_data = json.loads(chunk)
68
if json_data.get("type") == "text-delta":
69
delta = json_data.get("delta")
70
if delta:
71
yield delta
72
except json.JSONDecodeError:
73
pass
@@ -32,6 +32,8 @@ def _resolve_provider(name: str) -> ProviderType:
32
32
from g4f.Provider.needs_auth.Cerebras import Cerebras; return Cerebras
33
33
elif name == "Claude":
34
34
from g4f.Provider.needs_auth.Claude import Claude; return Claude
35
elif name == "Cloudflare":
36
from g4f.Provider.Cloudflare import Cloudflare; return Cloudflare
35
37
elif name == "Cohere":
36
38
from g4f.Provider.needs_auth.Cohere import Cohere; return Cohere
37
39
elif name == "CohereForAI_C4AI_Command":
@@ -47,7 +49,7 @@ def _resolve_provider(name: str) -> ProviderType:
47
49
elif name == "Custom":
48
50
from g4f.Provider.needs_auth.Custom import Custom; return Custom
49
51
elif name == "DeepInfra":
50
from g4f.Provider.deepinfra import DeepInfra; return DeepInfra
52
from .DeepInfra import DeepInfra; return DeepInfra
51
53
elif name == "DeepSeek":
52
54
from g4f.Provider.needs_auth.DeepSeek import DeepSeek; return DeepSeek
53
55
elif name == "DeepSeekAPI":
@@ -78,8 +80,9 @@ def _resolve_provider(name: str) -> ProviderType:
78
80
from g4f.Provider.needs_auth.GlhfChat import GlhfChat; return GlhfChat
79
81
elif name == "GoogleSearch":
80
82
from g4f.Provider.search.GoogleSearch import GoogleSearch; return GoogleSearch
83
81
84
elif name == "GradientNetwork":
82
from g4f.Provider.GradientNetwork import GradientNetwork; return GradientNetwork
85
from .GradientNetwork import GradientNetwork; return GradientNetwork
83
86
elif name == "Grok":
84
87
from g4f.Provider.needs_auth.Grok import Grok; return Grok
85
88
elif name == "Groq":
@@ -110,6 +113,8 @@ def _resolve_provider(name: str) -> ProviderType:
110
113
from g4f.Provider.needs_auth.MetaAIAccount import MetaAIAccount; return MetaAIAccount
111
114
elif name == "MicrosoftDesigner":
112
115
from g4f.Provider.needs_auth.MicrosoftDesigner import MicrosoftDesigner; return MicrosoftDesigner
116
elif name == "Miklium":
117
from g4f.Provider.Miklium import Miklium; return Miklium
113
118
elif name == "MiniMax":
114
119
from g4f.Provider.needs_auth.mini_max.MiniMax import MiniMax; return MiniMax
115
120
elif name == "Nvidia":
@@ -136,6 +141,8 @@ def _resolve_provider(name: str) -> ProviderType:
136
141
from g4f.Provider.template.OpenaiTemplate import OpenaiTemplate; return OpenaiTemplate
137
142
elif name == "OperaAria":
138
143
from g4f.Provider.OperaAria import OperaAria; return OperaAria
144
elif name == "Perchance":
145
from g4f.Provider.Perchance import Perchance; return Perchance
139
146
elif name == "Perplexity":
140
147
from g4f.Provider.Perplexity import Perplexity; return Perplexity
141
148
elif name == "PerplexityApi":
@@ -164,6 +171,8 @@ def _resolve_provider(name: str) -> ProviderType:
164
171
from g4f.Provider.search.SearXNG import SearXNG; return SearXNG
165
172
elif name == "StabilityAI_SD35Large":
166
173
from g4f.Provider.hf_space.StabilityAI_SD35Large import StabilityAI_SD35Large; return StabilityAI_SD35Large
174
elif name == "Surfsense":
175
from g4f.Provider.Surfsense import Surfsense; return Surfsense
167
176
elif name == "TeachAnything":
168
177
from g4f.Provider.TeachAnything import TeachAnything; return TeachAnything
169
178
elif name == "ThebApi":
@@ -203,6 +212,7 @@ _provider_names = [
203
212
"CachedSearch",
204
213
"Cerebras",
205
214
"Claude",
215
"Cloudflare",
206
216
"Cohere",
207
217
"CohereForAI_C4AI_Command",
208
218
"Copilot",
@@ -225,6 +235,7 @@ _provider_names = [
225
235
"GithubCopilotAPI",
226
236
"GlhfChat",
227
237
"GoogleSearch",
238
228
239
"GradientNetwork",
229
240
"Grok",
230
241
"Groq",
@@ -241,6 +252,7 @@ _provider_names = [
241
252
"MetaAI",
242
253
"MetaAIAccount",
243
254
"MicrosoftDesigner",
255
"Miklium",
244
256
"MiniMax",
245
257
"Nvidia",
246
258
"Ollama",
@@ -253,6 +265,7 @@ _provider_names = [
253
265
"OpenaiChat",
254
266
"OpenaiTemplate",
255
267
"OperaAria",
268
"Perchance",
256
269
"Perplexity",
257
270
"PerplexityApi",
258
271
"PhindAi",
@@ -267,6 +280,7 @@ _provider_names = [
267
280
"Replicate",
268
281
"SearXNG",
269
282
"StabilityAI_SD35Large",
283
"Surfsense",
270
284
"TeachAnything",
271
285
"ThebApi",
272
286
"Together",
@@ -1,143 +0,0 @@
1
from __future__ import annotations
2
3
import asyncio
4
import requests
5
import time
6
7
from ..template import OpenaiTemplate
8
from ...requests import BrowserConfig
9
from .turnstile import Turnstile
10
11
def find_free_port() -> int:
12
import socket
13
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
14
s.bind(('', 0))
15
return s.getsockname()[1]
16
17
def get_turnstile_token_sync(model: str) -> str:
18
"""Get Turnstile token using Turnstile with a fallback to DrissionPage."""
19
# 1. Try our lightweight CDP client first
20
first_error = None
21
try:
22
if not BrowserConfig.port:
23
import os
24
import tempfile
25
port = find_free_port()
26
# Use a persistent temp directory to preserve browser session cookies and Turnstile reputation
27
user_data_dir = os.path.join(tempfile.gettempdir(), "g4f_chrome_profile_light")
28
client = Turnstile(port=port, user_data_dir=user_data_dir)
29
else:
30
client = Turnstile(port=BrowserConfig.port, host=BrowserConfig.host)
31
client.connect() # Ensure connection if using a specified port
32
try:
33
token = client.get_token(model)
34
if token:
35
return token
36
print("[DeepInfra] Turnstile failed to obtain token. Trying fallback option (DrissionPage)...")
37
except:
38
raise
39
finally:
40
client.close()
41
except Exception as e:
42
first_error = e
43
44
# 2. Fallback option: try original DrissionPage if installed
45
try:
46
from DrissionPage import ChromiumPage, ChromiumOptions
47
co = ChromiumOptions()
48
co.set_argument('--window-position=-2000,-2000')
49
co.set_argument('--window-size=1024,768')
50
co.set_argument('--log-level=3')
51
page = ChromiumPage(co)
52
try:
53
page.get(f'https://deepinfra.com/{model}')
54
55
# Block completions requests
56
js_block_fetch = """
57
const origFetch = window.fetch;
58
window.fetch = async function(...args) {
59
let url = args[0];
60
if (typeof url === 'string' && url.includes('/chat/completions')) {
61
return new Response('{}', {status: 200});
62
}
63
return origFetch.apply(this, args);
64
};
65
"""
66
page.run_js(js_block_fetch)
67
68
textarea = page.ele('tag:textarea', timeout=15)
69
if textarea:
70
textarea.input('Test Prompt')
71
textarea.input('\n')
72
73
token_input = page.ele('@name=cf-turnstile-response', timeout=20)
74
if token_input:
75
for _ in range(40):
76
token = token_input.attr('value')
77
if token:
78
return token
79
time.sleep(0.5)
80
finally:
81
try:
82
page.quit()
83
except:
84
pass
85
except:
86
raise first_error if first_error else Exception("Failed to obtain Turnstile token using both methods.")
87
return ""
88
89
async def get_turnstile_token_async() -> str:
90
loop = asyncio.get_running_loop()
91
return await loop.run_in_executor(None, get_turnstile_token_sync, DeepInfra.default_model)
92
93
class DeepInfra(OpenaiTemplate):
94
url = "https://deepinfra.com"
95
login_url = "https://deepinfra.com/dash/api_keys"
96
base_url = "https://api.deepinfra.com/v1/openai"
97
98
working = True
99
active_by_default = True
100
101
default_model = "zai-org/GLM-5.2"
102
103
@classmethod
104
async def get_quota(cls, **kwargs):
105
return {}
106
107
@classmethod
108
def get_models(cls, **kwargs):
109
if not cls.models:
110
url = 'https://api.deepinfra.com/models/featured'
111
response = requests.get(url)
112
models = response.json()
113
114
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"}
115
cls.image_models = [model["model_name"] for model in models if model.get("reported_type") == "text-to-image"]
116
if cls.live == 0 and cls.models:
117
cls.live += 1
118
119
return cls.models
120
121
@classmethod
122
async def create_async_generator(cls, model, messages, api_key=None, headers=None, **kwargs):
123
if not api_key:
124
# Generate a Turnstile token for each request (required without an API key)
125
token = await get_turnstile_token_async()
126
if token:
127
if headers is None:
128
headers = {}
129
headers["X-DeepInfra-Turnstile"] = token
130
else:
131
raise ValueError("Failed to obtain Turnstile token for DeepInfra request.")
132
133
async for chunk in super().create_async_generator(model, messages, api_key=api_key, headers=headers, **kwargs):
134
yield chunk
135
136
@classmethod
137
def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict:
138
headers = super().get_headers(stream, api_key, headers)
139
if not api_key:
140
headers["X-Deepinfra-Source"] = "model-embed"
141
headers["Origin"] = "https://deepinfra.com"
142
headers["Referer"] = "https://deepinfra.com/"
143
return headers
@@ -42,10 +42,13 @@ class HuggingSpace(AsyncGeneratorProvider, ProviderModelMixin):
42
42
cls.model_aliases = {}
43
43
for provider in cls.providers:
44
44
models.extend(provider.get_models(**kwargs))
45
models.extend(provider.model_aliases.keys())
46
image_models.extend(provider.image_models)
47
vision_models.extend(provider.vision_models)
48
cls.model_aliases.update(provider.model_aliases)
45
if provider.model_aliases:
46
models.extend(provider.model_aliases.keys())
47
cls.model_aliases.update(provider.model_aliases)
48
if provider.image_models:
49
image_models.extend(provider.image_models)
50
if provider.vision_models:
51
vision_models.extend(provider.vision_models)
49
52
models = list(set(models))
50
53
models.sort()
51
54
cls.models = models
@@ -55,17 +55,25 @@ def render(filename = "home", download_url: str = GITHUB_URL):
55
55
try:
56
56
response = requests.get(f"{download_url}{filename}")
57
57
response.raise_for_status()
58
except requests.exceptions.SSLError:
59
response = requests.get(f"{download_url}{filename}", verify=False)
60
response.raise_for_status()
58
61
except requests.RequestException:
59
62
try:
60
63
response = requests.get(f"{DOWNLOAD_URL}{filename}")
61
64
response.raise_for_status()
65
except requests.exceptions.SSLError:
66
response = requests.get(f"{DOWNLOAD_URL}{filename}", verify=False)
67
response.raise_for_status()
62
68
except requests.RequestException:
63
69
found = None
64
70
for root, _, files in os.walk(cache_dir):
65
71
for file in files:
66
72
if file.startswith(secure_filename(filename)):
67
73
found = os.path.abspath(root), file
68
break
74
break
75
if found:
76
break
69
77
if found:
70
78
return send_from_directory(found[0], found[1], max_age=31536000)
71
79
else:
@@ -189,11 +197,19 @@ class Website:
189
197
try:
190
198
response = requests.get(f"{PLAYGROUND_URL}{filename}", timeout=10)
191
199
response.raise_for_status()
200
except requests.exceptions.SSLError:
201
try:
202
response = requests.get(f"{PLAYGROUND_URL}{filename}", timeout=10, verify=False)
203
response.raise_for_status()
204
except requests.RequestException:
205
pass
206
except requests.RequestException:
207
pass
208
209
if 'response' in locals() and response.status_code == 200:
192
210
with open(safe_path, 'wb') as f:
193
211
f.write(response.content)
194
212
return send_from_directory(os.path.dirname(safe_path), os.path.basename(safe_path), max_age=31536000)
195
except requests.RequestException:
196
pass
197
213
# SPA fallback: serve index.html for unknown sub-paths
198
214
index_path = os.path.join(cache_dir, "index.html")
199
215
if os.path.isfile(index_path):