返回提交历史
Deleted
g4f/Provider/EasyChat.py
+0
-112
Deleted
g4f/Provider/Felo.py
+0
-150
Deleted
g4f/Provider/GptFree.py
+0
-135
Deleted
g4f/Provider/GradientNetwork.py
+0
-107
Deleted
g4f/Provider/Miklium.py
+0
-59
Deleted
g4f/Provider/OllamaSwarm.py
+0
-669
Modified
g4f/Provider/OperaAria.py
+0
-6
Deleted
g4f/Provider/Perchance.py
+0
-237
Deleted
g4f/Provider/Surfsense.py
+0
-73
Modified
g4f/Provider/__init__.py
+0
-17
Modified
g4f/cli/__init__.py
+115
-4
Added
g4f/mcp/pa_downloader.py
+292
-0
Modified
g4f/providers/any_provider.py
+7
-4
Modified
g4f/tools/run_tools.py
+1
-1
XFEstudio/gpt4free
Move providers to pa providers
bf804659
代码差异
14 个文件
+415
-1574
@@ -1,112 +0,0 @@
1
from __future__ import annotations
2
3
import json
4
import base64
5
import hashlib
6
from aiohttp import ClientSession
7
8
from ..typing import AsyncResult, Messages
9
from ..config import DEFAULT_MODEL
10
from ..providers.base_provider import ProviderModelMixin
11
from .template import OpenaiTemplate
12
from .. import debug
13
14
class EasyChat(OpenaiTemplate, ProviderModelMixin):
15
url = "https://chat3.eqing.tech"
16
base_url = f"{url}/api/openai/v1"
17
api_endpoint = f"{base_url}/chat/completions"
18
working = True
19
active_by_default = True
20
use_model_names = True
21
22
default_model = DEFAULT_MODEL.split("/")[-1]
23
model_aliases = {
24
DEFAULT_MODEL: f"{default_model}-free",
25
}
26
27
captchaToken: str = None
28
29
@classmethod
30
def get_models(cls, **kwargs) -> list[str]:
31
if not cls.models:
32
models = super().get_models(**kwargs)
33
models = {m.replace("-free", ""): m for m in models if m.endswith("-free")}
34
cls.model_aliases.update(models)
35
cls.models = list(models)
36
return cls.models
37
38
@classmethod
39
async def _solve_altcha(cls, proxy: str = None) -> str:
40
debug.log("EasyChat: Solving Altcha...")
41
async with ClientSession() as session:
42
async with session.get(f"{cls.url}/api/altcaptcha/challenge", proxy=proxy, headers={
43
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
44
"Accept": "application/json"
45
}) as response:
46
response.raise_for_status()
47
data = await response.json()
48
49
salt = data['salt']
50
challenge = data['challenge']
51
maxnumber = data['maxnumber']
52
algorithm = data['algorithm']
53
signature = data['signature']
54
55
for n in range(maxnumber + 1):
56
text = f"{salt}{n}".encode('utf-8')
57
if algorithm == "SHA-512":
58
h = hashlib.sha512(text).hexdigest()
59
elif algorithm == "SHA-256":
60
h = hashlib.sha256(text).hexdigest()
61
else:
62
raise ValueError(f"Unknown Altcha algorithm: {algorithm}")
63
64
if h == challenge:
65
payload = {
66
"algorithm": algorithm,
67
"challenge": challenge,
68
"number": n,
69
"salt": salt,
70
"signature": signature
71
}
72
token = base64.b64encode(json.dumps(payload).encode()).decode()
73
debug.log(f"EasyChat: Altcha solved (n={n})")
74
return token
75
raise ValueError("Failed to solve Altcha")
76
77
@classmethod
78
async def create_async_generator(
79
cls,
80
model: str,
81
messages: Messages,
82
stream: bool = True,
83
proxy: str = None,
84
extra_body: dict = None,
85
**kwargs
86
) -> AsyncResult:
87
model = cls.get_model(model.replace("-free", ""))
88
89
# Always solve Altcha fresh to avoid expiration
90
cls.captchaToken = await cls._solve_altcha(proxy=proxy)
91
92
if extra_body is None:
93
extra_body = {}
94
extra_body["captchaToken"] = cls.captchaToken
95
96
try:
97
last_chunk = None
98
async for chunk in super().create_async_generator(
99
model=model,
100
messages=messages,
101
stream=stream,
102
extra_body=extra_body,
103
proxy=proxy,
104
**kwargs
105
):
106
# Remove provided by
107
if last_chunk == "\n" and chunk == "\n":
108
break
109
last_chunk = chunk
110
yield chunk
111
except Exception as e:
112
raise e
@@ -1,150 +0,0 @@
1
from __future__ import annotations
2
3
import json
4
import uuid
5
import re
6
7
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
8
from ..typing import AsyncResult, Messages
9
from ..requests.raise_for_status import raise_for_status
10
from ..requests import StreamSession
11
12
class Felo(AsyncGeneratorProvider, ProviderModelMixin):
13
"""
14
Provider for felo.ai.
15
"""
16
label = "Felo"
17
url = "https://felo.ai"
18
working = True
19
needs_auth = False
20
supports_stream = True
21
supports_system_message = False
22
supports_message_history = False
23
24
default_model = "felo-chat"
25
26
# Mapping of g4f model names to Felo categories
27
model_aliases = {
28
"felo-chat": "chat",
29
"felo-search": "google",
30
"felo-scholar": "scholar",
31
"felo-social": "social",
32
"felo-document": "document"
33
}
34
35
models = list(model_aliases.keys())
36
37
@classmethod
38
async def create_async_generator(
39
cls,
40
model: str,
41
messages: Messages,
42
proxy: str = None,
43
**kwargs
44
) -> AsyncResult:
45
prompt = messages[-1]["content"]
46
search_uuid = str(uuid.uuid4())
47
48
# Determine the category based on the requested model
49
category = cls.model_aliases.get(model, "chat")
50
51
headers = {
52
"Accept": "*/*",
53
"Content-Type": "application/json",
54
"Origin": cls.url,
55
"Referer": f"{cls.url}/search?q=hello",
56
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"
57
}
58
59
payload = {
60
"query": prompt,
61
"search_uuid": search_uuid,
62
"lang": "",
63
"agent_lang": "en",
64
"search_options": {"langcode": "en-US"},
65
"search_video": True,
66
"query_from": "default",
67
"category": category,
68
"model": "",
69
"auto_routing": True,
70
"mode": "concise",
71
"device_id": str(uuid.uuid4().hex),
72
"source_message_rid": "",
73
"documents": [],
74
"document_action": "",
75
"slides_source": {"type": "ask_question", "files": {}},
76
"slide_template_uid": "",
77
"selected_resource_ids": [],
78
"process_id": search_uuid,
79
"stream_protocol": "message_center_v1",
80
"enable_task_state": True
81
}
82
83
async with StreamSession(
84
headers=headers,
85
impersonate="chrome",
86
proxy=proxy,
87
timeout=120
88
) as session:
89
# 1. Start the thread and get the stream key
90
threads_url = f"{cls.url}/api-proxy/main/search/threads"
91
async with session.post(threads_url, json=payload) as response:
92
await raise_for_status(response)
93
try:
94
res_json = await response.json()
95
except Exception:
96
raise RuntimeError(f"Failed to parse JSON response from {threads_url}")
97
98
stream_key = res_json.get("stream_key")
99
if not stream_key:
100
raise RuntimeError("Failed to get stream_key from Felo response")
101
102
# 2. Connect to the SSE stream
103
stream_url = f"{cls.url}/api/message/v1/stream/{stream_key}?offset=0"
104
async with session.get(stream_url) as stream_res:
105
await raise_for_status(stream_res)
106
107
previous_text = ""
108
sources_to_yield = None
109
async for line in stream_res.iter_lines():
110
line = line.decode("utf-8").strip()
111
if line.startswith("data:{"):
112
try:
113
# Safely load the JSON inside the data block
114
data = json.loads(line[5:])
115
if "content" in data:
116
content_str = data["content"]
117
if isinstance(content_str, str):
118
content_json = json.loads(content_str)
119
120
# Handle answer types
121
if content_json.get("data", {}).get("type") == "answer":
122
text = content_json.get("data", {}).get("data", {}).get("text", "")
123
124
if text.startswith(previous_text):
125
# Yield only the new part of the text
126
new_part = text[len(previous_text):]
127
if new_part:
128
yield new_part
129
previous_text = text
130
else:
131
# If it doesn't strictly start with previous text, yield the whole text and update
132
yield text
133
previous_text = text
134
135
# Handle sources for Web UI formatting
136
elif content_json.get("data", {}).get("type") == "final_contexts":
137
sources_list = content_json.get("data", {}).get("data", {}).get("sources", [])
138
if sources_list:
139
# format to match g4f standard sources
140
formatted_sources = [{"url": s.get("link"), "title": s.get("title")} for s in sources_list if s.get("link")]
141
if formatted_sources:
142
from ..providers.response import Sources
143
sources_to_yield = Sources(formatted_sources)
144
except Exception:
145
# Ignore malformed JSON or other parsing errors in the stream
146
continue
147
148
# Yield sources at the very end so they appear at the bottom of the response
149
if sources_to_yield:
150
yield sources_to_yield
@@ -1,135 +0,0 @@
1
import json
2
from aiohttp import ClientSession
3
from typing import AsyncGenerator
4
5
from ..typing import AsyncResult, Messages
6
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
7
from ..image import to_data_uri
8
9
class GptFree(AsyncGeneratorProvider, ProviderModelMixin):
10
url = "https://gptfree.com"
11
api_endpoint = "https://us-central1-gptfree-2.cloudfunctions.net/agent_stream"
12
working = True
13
supports_message_history = True
14
supports_system_message = False
15
16
default_model = ""
17
18
_id_token: str = None
19
20
@classmethod
21
async def create_async_generator(
22
cls,
23
model: str,
24
messages: Messages,
25
proxy: str = None,
26
**kwargs
27
) -> AsyncGenerator:
28
headers = {
29
"accept": "*/*",
30
"accept-language": "en-US,en;q=0.9",
31
"cache-control": "no-cache",
32
"content-type": "application/json",
33
"origin": "https://gptfree.com",
34
"pragma": "no-cache",
35
"referer": "https://gptfree.com/",
36
"sec-ch-ua": '"Not;A=Brand";v="8", "Chromium";v="150", "Google Chrome";v="150"',
37
"sec-ch-ua-mobile": "?0",
38
"sec-ch-ua-platform": '"Linux"',
39
"sec-fetch-dest": "empty",
40
"sec-fetch-mode": "cors",
41
"sec-fetch-site": "cross-site",
42
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
43
}
44
45
history = []
46
current_message = ""
47
images = []
48
49
for msg in messages:
50
content = msg["content"]
51
52
# Handle multimodal messages (where content is a list of dicts)
53
if isinstance(content, list):
54
text_parts = []
55
for part in content:
56
if part.get("type") == "text":
57
text_parts.append(part.get("text", ""))
58
elif part.get("type") == "image_url":
59
img_url = part.get("image_url", {}).get("url")
60
if img_url:
61
images.append(img_url)
62
content = "\n".join(text_parts)
63
64
if msg["role"] == "system":
65
history.append({"type": "user", "content": content})
66
elif msg["role"] == "user":
67
current_message = content
68
history.append({"type": "user", "content": content})
69
elif msg["role"] == "assistant":
70
history.append({"type": "agent", "content": content})
71
72
# Process images from kwargs
73
if "image" in kwargs and kwargs["image"]:
74
images.append(to_data_uri(kwargs["image"]))
75
if "images" in kwargs and kwargs["images"]:
76
for img in kwargs["images"]:
77
images.append(to_data_uri(img[0] if isinstance(img, tuple) else img))
78
if "media" in kwargs and kwargs["media"]:
79
for m in kwargs["media"]:
80
images.append(to_data_uri(m[0]))
81
82
# The last message is the current one
83
if history and history[-1]["type"] == "user":
84
current_message = history.pop()["content"]
85
86
if not current_message.strip():
87
if history and history[-1]["type"] == "user":
88
current_message = history.pop()["content"]
89
else:
90
current_message = "Analyze this image" if images else "Hello"
91
92
payload = {
93
"message": current_message,
94
"images": images,
95
"history": history
96
}
97
firebase_api_key = "AIzaSyBdU-Np8RSh1tPSsPOWg3qIm6PnVK5PQb4"
98
99
async with ClientSession() as session:
100
for attempt in range(2):
101
if not cls._id_token:
102
auth_url = f"https://identitytoolkit.googleapis.com/v1/accounts:signUp?key={firebase_api_key}"
103
async with session.post(auth_url, json={"returnSecureToken": True}, proxy=proxy) as auth_resp:
104
auth_resp.raise_for_status()
105
auth_data = await auth_resp.json()
106
cls._id_token = auth_data["idToken"]
107
108
headers["Authorization"] = f"Bearer {cls._id_token}"
109
110
async with session.post(
111
cls.api_endpoint,
112
headers=headers,
113
json=payload,
114
proxy=proxy
115
) as response:
116
if response.status == 401:
117
# Token expired or invalid, clear it and retry
118
cls._id_token = None
119
continue
120
121
response.raise_for_status()
122
123
async for line in response.content:
124
line = line.decode('utf-8').strip()
125
if line.startswith('data: '):
126
data_str = line[6:]
127
if data_str == '{}':
128
continue
129
try:
130
data = json.loads(data_str)
131
if "response" in data:
132
yield data["response"]
133
except json.JSONDecodeError:
134
pass
135
break
@@ -1,107 +0,0 @@
1
from __future__ import annotations
2
3
import json
4
5
from ..typing import AsyncResult, Messages
6
from ..providers.response import Reasoning, JsonResponse
7
from ..requests import StreamSession
8
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
9
10
11
class GradientNetwork(AsyncGeneratorProvider, ProviderModelMixin):
12
"""
13
Provider for chat.gradient.network
14
Supports streaming text generation with Qwen and GPT OSS models.
15
"""
16
label = "Gradient Network"
17
url = "https://chat.gradient.network"
18
api_endpoint = "https://chat.gradient.network/api/generate"
19
20
working = False
21
needs_auth = False
22
supports_stream = True
23
supports_system_message = True
24
supports_message_history = True
25
26
default_model = "GPT OSS 120B"
27
models = [
28
default_model,
29
"Qwen3 235B",
30
]
31
model_aliases = {
32
"qwen-3-235b": "Qwen3 235B",
33
"qwen3-235b": "Qwen3 235B",
34
"gpt-oss-120b": "GPT OSS 120B",
35
}
36
37
@classmethod
38
async def create_async_generator(
39
cls,
40
model: str,
41
messages: Messages,
42
proxy: str = None,
43
enable_thinking: bool = True,
44
**kwargs
45
) -> AsyncResult:
46
"""
47
Create an async generator for streaming chat responses.
48
49
Args:
50
model: The model name to use
51
messages: List of message dictionaries
52
proxy: Optional proxy URL
53
enable_thinking: Enable the thinking/analysis channel (maps to enableThinking in API)
54
**kwargs: Additional arguments
55
56
Yields:
57
str: Content chunks from the response
58
Reasoning: Reasoning content when enable_thinking is True
59
"""
60
model = cls.get_model(model)
61
62
headers = {
63
"Accept": "application/x-ndjson",
64
"Content-Type": "application/json",
65
"Origin": cls.url,
66
"Referer": f"{cls.url}/",
67
}
68
69
payload = {
70
"clusterMode": "nvidia" if "GPT OSS" in model else "hybrid",
71
"model": model,
72
"messages": messages,
73
}
74
if enable_thinking:
75
payload["enableThinking"] = enable_thinking
76
async with StreamSession(headers=headers, proxy=proxy, impersonate="chrome") as session:
77
async with session.post(
78
cls.api_endpoint,
79
json=payload,
80
) as response:
81
response.raise_for_status()
82
83
async for line in response.iter_lines():
84
if not line:
85
continue
86
87
try:
88
data = json.loads(line)
89
yield JsonResponse.from_dict(data)
90
msg_type = data.get("type")
91
92
if msg_type == "reply":
93
# Response chunks with content or reasoningContent
94
reply_data = data.get("data", {})
95
content = reply_data.get("content")
96
reasoning_content = reply_data.get("reasoningContent")
97
98
if reasoning_content:
99
yield Reasoning(reasoning_content)
100
if content:
101
yield content
102
103
# Skip clusterInfo and blockUpdate GPU visualization messages
104
105
except json.JSONDecodeError:
106
# Skip non-JSON lines (may be partial data or empty)
107
raise
@@ -1,59 +0,0 @@
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}")
@@ -72,12 +72,6 @@ class OperaAria(AsyncGeneratorProvider, ProviderModelMixin):
72
72
_user_agent_v1 = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Mobile Safari/537.36 OPR/89.0.0.0"
73
73
_user_agent_v2 = "Mozilla/5.0 (Linux; U; Android 14; Pixel 8 Pro Build/UQ1A.240205.004; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/138.0.7204.179 Mobile Safari/537.36 OPR/99.0.2254.81922"
74
74
75
@classmethod
76
def get_model(cls, model: str) -> str:
77
if not model:
78
return cls.default_model
79
return cls.model_aliases.get(model, model if model in cls.models else cls.default_model)
80
81
75
@classmethod
82
76
def _get_api_version(cls, model: str) -> str:
83
77
return cls._model_to_version.get(model, 'v2')
@@ -1,237 +0,0 @@
1
from __future__ import annotations
2
3
import asyncio
4
import random
5
import json
6
from aiohttp import ClientSession
7
from g4f.requests.cdp import CDPSession
8
from ..typing import AsyncResult, Messages
9
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
10
from .helper import format_prompt, format_media_prompt
11
from ..requests import StreamSession
12
from ..providers.response import ImageResponse
13
14
class Perchance(AsyncGeneratorProvider, ProviderModelMixin):
15
url = "https://perchance.org/ai-chat"
16
api_endpoint = "https://text-generation.perchance.org/api/generate"
17
image_api_endpoint = "https://image-generation.perchance.org/api/generate"
18
verify_url = "https://text-generation.perchance.org/embed?thread=0"
19
working = True
20
supports_stream = True
21
supports_system_message = True
22
supports_message_history = True
23
default_model = "perchance"
24
default_image_model = "perchance-image"
25
image_models = ["perchance-image"]
26
models = [default_model] + image_models
27
28
# Class-level cache for credentials to avoid restarting browser for every request
29
_user_key: str | None = None
30
_cookies: dict | None = None
31
_headers: dict | None = None
32
33
_image_user_key: str | None = None
34
_image_cookies: dict | None = None
35
_image_headers: dict | None = None
36
37
@classmethod
38
async def _get_user_key(cls, is_image: bool = False, proxy: str = None) -> tuple[str, dict, dict]:
39
"""Runs CDP to solve Turnstile and extract the userKey and session cookies."""
40
session = CDPSession(headless=False)
41
await session.start()
42
try:
43
if is_image:
44
import urllib.parse
45
hash_data = {"prompt": "a", "resolution": "512x512", "guidanceScale": 7, "channel": "ai-text-to-image-generator", "subChannel": "public", "seed": -1, "requestId": "a"}
46
hash_str = urllib.parse.quote(json.dumps(hash_data))
47
verify_url = f"https://image-generation.perchance.org/embed#{hash_str}"
48
origin = "https://image-generation.perchance.org"
49
else:
50
verify_url = cls.verify_url
51
origin = "https://text-generation.perchance.org"
52
53
await session.navigate(verify_url)
54
55
# Setup message capture in page context
56
setup_js = """
57
window.collectedMessages = [];
58
window.addEventListener("message", (e) => {
59
if (e.data && (e.data.type.startsWith("stream") || e.data.type === "verified")) {
60
window.collectedMessages.push(e.data);
61
}
62
});
63
"""
64
await session.evaluate_js(setup_js)
65
66
# Always trigger verifyUser to ensure Turnstile solving/checking is executed
67
if is_image:
68
verify_js = "if(typeof start === 'function') start({reloadPageOnFail: false});"
69
else:
70
verify_js = 'window.postMessage({type: "verifyUser"}, window.location.origin);'
71
await session.evaluate_js(verify_js)
72
73
# Anti-detect: warm up the browser and disable debugger overhead DURING Turnstile check
74
await session.bypass_turnstile()
75
76
# Poll for userKey-0
77
user_key = None
78
for _ in range(30):
79
user_key = await session.evaluate_js("localStorage.getItem('userKey-0')")
80
if user_key:
81
break
82
await asyncio.sleep(1)
83
84
if not user_key:
85
raise RuntimeError("Failed to verify user/solve Turnstile on Perchance.")
86
87
cookies = await session.get_cookies()
88
user_agent = await session.get_user_agent()
89
90
headers = {
91
"user-agent": user_agent,
92
"Accept": "*/*",
93
"Origin": origin,
94
"Referer": verify_url,
95
}
96
97
return user_key, cookies, headers
98
finally:
99
await session.close()
100
101
@classmethod
102
async def get_ad_access_code(cls, proxy: str = None) -> str:
103
cache_bust = random.randint(1000000, 9999999)
104
url = f"https://perchance.org/api/getAccessCodeForAdPoweredStuff?__cacheBust={cache_bust}"
105
async with ClientSession() as session:
106
async with session.get(url, proxy=proxy) as response:
107
if response.status == 200:
108
return await response.text()
109
return ""
110
111
@classmethod
112
async def create_async_generator(
113
cls,
114
model: str,
115
messages: Messages,
116
prompt: str = None,
117
proxy: str = None,
118
**kwargs
119
) -> AsyncResult:
120
# Check if we need to fetch credentials (cold start)
121
if not cls._user_key or not cls._cookies or not cls._headers:
122
cls._user_key, cls._cookies, cls._headers = await cls._get_user_key(proxy=proxy)
123
124
# Image Generation Flow
125
if model in cls.image_models or kwargs.get("image"):
126
if not cls._image_user_key or not cls._image_cookies or not cls._image_headers:
127
cls._image_user_key, cls._image_cookies, cls._image_headers = await cls._get_user_key(is_image=True, proxy=proxy)
128
129
if not prompt:
130
prompt = format_media_prompt(messages)
131
132
ad_access_code = await cls.get_ad_access_code(proxy=proxy)
133
req_id = random.random()
134
135
payload = {
136
"prompt": prompt,
137
"negativePrompt": "",
138
"seed": -1,
139
"resolution": "512x512",
140
"guidanceScale": 7,
141
"channel": "ai-text-to-image-generator",
142
"subChannel": "public",
143
"userKey": cls._image_user_key,
144
"adAccessCode": ad_access_code,
145
"requestId": str(req_id)
146
}
147
148
gen_url = f"{cls.image_api_endpoint}?userKey={cls._image_user_key}&requestId={req_id}&adAccessCode={ad_access_code}&__cacheBust={random.random()}&bdf={random.random()}"
149
150
headers = cls._image_headers.copy()
151
headers["Content-Type"] = "text/plain;charset=UTF-8"
152
153
async with StreamSession(
154
headers=headers,
155
cookies=cls._image_cookies,
156
proxies={"all": proxy} if proxy else None,
157
impersonate="chrome"
158
) as session:
159
async with session.post(gen_url, data=json.dumps(payload), proxy=proxy) as response:
160
if response.status in (401, 403):
161
cls._image_user_key = cls._image_cookies = cls._image_headers = None
162
raise RuntimeError("auth_failed")
163
if response.status != 200:
164
text = await response.text()
165
if "invalid_key" in text or "failed_verification" in text:
166
cls._image_user_key = cls._image_cookies = cls._image_headers = None
167
raise RuntimeError("auth_failed")
168
raise RuntimeError(f"image generation failed: {response.status} {text}")
169
170
data = await response.json()
171
if data.get("status") == "success":
172
image_url = data.get("imageDownloadUrl")
173
if image_url:
174
if not image_url.startswith("http"):
175
image_url = "https://image-generation.perchance.org" + image_url
176
yield ImageResponse(image_url, prompt)
177
return
178
raise RuntimeError(f"Failed to generate image: {data}")
179
return
180
181
# Text Generation Flow
182
instruction = format_prompt(messages)
183
instruction_token_count = max(1, len(instruction) // 4)
184
185
payload = {
186
"instruction": instruction,
187
"startWith": "",
188
"stopSequences": ["\n\n", "\nAnon:", "\nBot:"],
189
"generatorName": "ai-chat",
190
"startWithTokenCount": 0,
191
"instructionTokenCount": instruction_token_count
192
}
193
194
try:
195
async with StreamSession(
196
headers=cls._headers,
197
cookies=cls._cookies,
198
proxies={"all": proxy} if proxy else None,
199
impersonate="chrome"
200
) as session:
201
req_id = f"aiTextCompletion{random.random()}"
202
gen_url = f"{cls.api_endpoint}?userKey={cls._user_key}&thread=0&requestId={req_id}&__cacheBust={random.random()}"
203
204
async with session.post(gen_url, json=payload, proxy=proxy) as response:
205
# If Cloudflare blocks or token has expired, raise auth_failed to trigger a refresh
206
if response.status in (401, 403):
207
raise RuntimeError("auth_failed")
208
209
if response.status != 200:
210
text = await response.text()
211
if "invalid_key" in text or "failed_verification" in text:
212
raise RuntimeError("auth_failed")
213
raise RuntimeError(f"generate failed: {response.status} {text}")
214
215
# Parse custom SSE format
216
async for chunk_bytes in response.iter_content():
217
if chunk_bytes:
218
chunk_str = chunk_bytes.decode(errors="ignore")
219
for line in chunk_str.split("\n"):
220
line = line.strip()
221
if line.startswith('t:'):
222
try:
223
# Parse the text chunk safely via JSON loads (handles escapes)
224
text_val = json.loads(line[2:])
225
if text_val:
226
yield text_val
227
except Exception:
228
pass
229
230
except RuntimeError as e:
231
if str(e) == "auth_failed":
232
# Clear cached credentials and retry once with fresh verification
233
cls._user_key = cls._cookies = cls._headers = None
234
async for chunk in cls.create_async_generator(model, messages, prompt, proxy, **kwargs):
235
yield chunk
236
else:
237
raise e
@@ -1,73 +0,0 @@
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
@@ -52,12 +52,8 @@ def _resolve_provider(name: str) -> ProviderType:
52
52
from .DeepInfra import DeepInfra; return DeepInfra
53
53
elif name == "DeepSeek" or name == "DeepSeekAPI":
54
54
from g4f.Provider.needs_auth.DeepSeek import DeepSeek; return DeepSeek
55
elif name == "EasyChat":
56
from g4f.Provider.EasyChat import EasyChat; return EasyChat
57
55
elif name == "EdgeTTS":
58
56
from g4f.Provider.audio.EdgeTTS import EdgeTTS; return EdgeTTS
59
elif name == "Felo":
60
from g4f.Provider.Felo import Felo; return Felo
61
57
elif name == "FenayAI":
62
58
from g4f.Provider.needs_auth.FenayAI import FenayAI; return FenayAI
63
59
elif name == "GLM":
@@ -78,9 +74,6 @@ def _resolve_provider(name: str) -> ProviderType:
78
74
from g4f.Provider.needs_auth.GlhfChat import GlhfChat; return GlhfChat
79
75
elif name == "GoogleSearch":
80
76
from g4f.Provider.search.GoogleSearch import GoogleSearch; return GoogleSearch
81
82
elif name == "GradientNetwork":
83
from .GradientNetwork import GradientNetwork; return GradientNetwork
84
77
elif name == "Grok":
85
78
from g4f.Provider.needs_auth.Grok import Grok; return Grok
86
79
elif name == "Groq":
@@ -111,20 +104,14 @@ def _resolve_provider(name: str) -> ProviderType:
111
104
from g4f.Provider.needs_auth.MetaAIAccount import MetaAIAccount; return MetaAIAccount
112
105
elif name == "MicrosoftDesigner":
113
106
from g4f.Provider.needs_auth.MicrosoftDesigner import MicrosoftDesigner; return MicrosoftDesigner
114
elif name == "Miklium":
115
from g4f.Provider.Miklium import Miklium; return Miklium
116
107
elif name == "MiniMax":
117
108
from g4f.Provider.needs_auth.mini_max.MiniMax import MiniMax; return MiniMax
118
109
elif name == "Nvidia":
119
110
from g4f.Provider.needs_auth.Nvidia import Nvidia; return Nvidia
120
111
elif name == "Ollama":
121
112
from g4f.Provider.local.Ollama import Ollama; return Ollama
122
elif name == "OllamaSwarm":
123
from g4f.Provider.OllamaSwarm import OllamaSwarm; return OllamaSwarm
124
113
elif name == "OpenAIFM":
125
114
from g4f.Provider.audio.OpenAIFM import OpenAIFM; return OpenAIFM
126
elif name == "OllamaSwarm":
127
from g4f.Provider.OllamaSwarm import OllamaSwarm; return OllamaSwarm
128
115
elif name == "OpenRouter":
129
116
from g4f.Provider.needs_auth.OpenRouter import OpenRouter; return OpenRouter
130
117
elif name == "OpenRouterFree":
@@ -139,8 +126,6 @@ def _resolve_provider(name: str) -> ProviderType:
139
126
from g4f.Provider.template.OpenaiTemplate import OpenaiTemplate; return OpenaiTemplate
140
127
elif name == "OperaAria":
141
128
from g4f.Provider.OperaAria import OperaAria; return OperaAria
142
elif name == "Perchance":
143
from g4f.Provider.Perchance import Perchance; return Perchance
144
129
elif name == "Perplexity":
145
130
from g4f.Provider.Perplexity import Perplexity; return Perplexity
146
131
elif name == "PerplexityApi":
@@ -169,8 +154,6 @@ def _resolve_provider(name: str) -> ProviderType:
169
154
from g4f.Provider.search.SearXNG import SearXNG; return SearXNG
170
155
elif name == "StabilityAI_SD35Large":
171
156
from g4f.Provider.hf_space.StabilityAI_SD35Large import StabilityAI_SD35Large; return StabilityAI_SD35Large
172
elif name == "Surfsense":
173
from g4f.Provider.Surfsense import Surfsense; return Surfsense
174
157
elif name == "TeachAnything":
175
158
from g4f.Provider.TeachAnything import TeachAnything; return TeachAnything
176
159
elif name == "ThebApi":
@@ -10,7 +10,7 @@ from ..providers.retry_provider import RotatedProvider
10
10
from ..providers.config_provider import RouterConfig, ConfigModelProvider
11
11
from ..client.factory import AbstractClientFactory
12
12
from ..Provider import __getattr__
13
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
13
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin, get_async_provider_method
14
14
from .. import Provider
15
15
from .. import models
16
16
from .. import debug
@@ -462,9 +462,12 @@ class AnyProvider(AsyncGeneratorProvider, AnyModelProviderMixin):
462
462
provider, submodel = model.split(":", maxsplit=1)
463
463
if hasattr(Provider, provider):
464
464
provider = getattr(Provider, provider)
465
if provider.working and provider.get_parent() not in ignored:
466
providers.append(provider)
467
model = submodel
465
method = get_async_provider_method(provider)
466
async for chunk in method(
467
submodel, messages, stream=stream, media=media, api_key=api_key, **kwargs
468
):
469
yield chunk
470
return
468
471
else:
469
472
if model not in cls.model_map:
470
473
if model in cls.model_aliases:
@@ -75,7 +75,7 @@ def _messages_cache_key(messages: Messages, model: str) -> Optional[str]:
75
75
exclude = {idx for idx in (last_user_idx, last_assistant_idx) if idx is not None}
76
76
if len(exclude) >= len(messages):
77
77
return None
78
parts = [model]
78
parts = [model] if model else []
79
79
for i, msg in enumerate(messages):
80
80
if i in exclude:
81
81
continue