XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/gpt4free

feat: add Perchance, MIKLIUM, Surfsense & fix DeepInfra/CDP

fbb55395
kqlio67 <kqlio67@users.noreply.github.com>
提交于

代码差异

19 个文件 +1426 -530
Modified etc/unittest/client.py +3 -2
@@ -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"
Modified etc/unittest/models.py +14 -4
@@ -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}")
Modified g4f/Provider/Cloudflare.py +133 -69
@@ -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()
Added g4f/Provider/DeepInfra.py +197 -0
@@ -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
Modified g4f/Provider/GptFree.py +9 -7
@@ -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:
Added g4f/Provider/Miklium.py +59 -0
@@ -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}")
Added g4f/Provider/Perchance.py +145 -0
@@ -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
Added g4f/Provider/Surfsense.py +73 -0
@@ -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
Modified g4f/Provider/__init__.py +16 -2
@@ -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",
Deleted g4f/Provider/deepinfra/__init__.py +0 -143
@@ -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
Deleted g4f/Provider/deepinfra/turnstile.py +0 -289
Modified g4f/Provider/hf_space/__init__.py +7 -4
@@ -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
Modified g4f/gui/server/website.py +19 -3
@@ -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):
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
Modified scripts/build-nuitka.sh +1 -0