返回提交历史
Modified
g4f/Provider/PollinationsAI.py
+2
-3
Modified
g4f/Provider/PollinationsImage.py
+0
-1
Modified
g4f/Provider/needs_auth/Gemini.py
+34
-4
Modified
g4f/gui/client/index.html
+1
-45
Modified
g4f/gui/client/static/css/style.css
+14
-0
Modified
g4f/gui/client/static/js/chat.v1.js
+60
-41
Added
g4f/gui/client/static/js/photoswipe.js
+63
-0
Modified
g4f/gui/server/api.py
+4
-0
Modified
g4f/gui/server/backend_api.py
+4
-4
Modified
g4f/models.py
+1
-1
Modified
g4f/requests/curl_cffi.py
+1
-0
Modified
g4f/tools/files.py
+3
-1
XFEstudio/gpt4free
Fix unittest, update model lists
ba602966
代码差异
12 个文件
+187
-100
@@ -39,7 +39,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
39
39
default_model = "openai"
40
40
default_image_model = "flux"
41
41
default_vision_model = "gpt-4o"
42
extra_image_models = ["flux-pro", "flux-dev", "flux-schnell", "midjourney", "dall-e-3"]
42
image_models = ["flux-pro", "flux-dev", "flux-schnell", "midjourney", "dall-e-3", "turbo"]
43
43
vision_models = [default_vision_model, "gpt-4o-mini"]
44
44
extra_text_models = ["claude", "claude-email", "deepseek-reasoner", "deepseek-r1"] + vision_models
45
45
model_aliases = {
@@ -67,7 +67,6 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
67
67
"sdxl-turbo": "turbo",
68
68
}
69
69
text_models = []
70
image_models = []
71
70
72
71
@classmethod
73
72
def get_models(cls, **kwargs):
@@ -76,7 +75,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
76
75
image_response = requests.get("https://image.pollinations.ai/models")
77
76
image_response.raise_for_status()
78
77
new_image_models = image_response.json()
79
cls.image_models = list(dict.fromkeys([*cls.extra_image_models, *new_image_models]))
78
cls.image_models = list(dict.fromkeys([*cls.image_models, *new_image_models]))
80
79
81
80
text_response = requests.get("https://text.pollinations.ai/models")
82
81
text_response.raise_for_status()
@@ -10,7 +10,6 @@ class PollinationsImage(PollinationsAI):
10
10
default_model = "flux"
11
11
default_vision_model = None
12
12
default_image_model = default_model
13
image_models = [default_image_model]
14
13
15
14
@classmethod
16
15
def get_models(cls, **kwargs):
@@ -19,7 +19,7 @@ from ... import debug
19
19
from ...typing import Messages, Cookies, ImagesType, AsyncResult, AsyncIterator
20
20
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
21
21
from ..helper import format_prompt, get_cookies
22
from ...providers.response import JsonConversation, SynthesizeData, RequestLogin, ImageResponse
22
from ...providers.response import JsonConversation, Reasoning, RequestLogin, ImageResponse
23
23
from ...requests.raise_for_status import raise_for_status
24
24
from ...requests.aiohttp import get_connector
25
25
from ...requests import get_nodriver
@@ -53,6 +53,17 @@ UPLOAD_IMAGE_HEADERS = {
53
53
"x-tenant-id": "bard-storage",
54
54
}
55
55
56
models = {
57
"gemini-2.0-flash": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"f299729663a2343f"]'},
58
"gemini-2.0-flash-exp": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"f299729663a2343f"]'},
59
"gemini-2.0-flash-thinking": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"9c17b1863f581b8a"]'},
60
"gemini-2.0-flash-thinking-with-apps": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"f8f8f5ea629f5d37"]'},
61
"gemini-2.0-exp-advanced": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"b1e46a6037e6aa9f"]'},
62
"gemini-1.5-flash": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"418ab5ea040b5c43"]'},
63
"gemini-1.5-pro": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"9d60dfae93c9ff1f"]'},
64
"gemini-1.5-pro-research": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"e5a44cb1dae2b489"]'},
65
}
66
56
67
class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
57
68
label = "Google Gemini"
58
69
url = "https://gemini.google.com"
@@ -61,11 +72,14 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
61
72
working = True
62
73
use_nodriver = True
63
74
64
default_model = 'gemini'
75
default_model = ""
65
76
default_image_model = default_model
66
77
default_vision_model = default_model
67
78
image_models = [default_image_model]
68
models = [default_model, "gemini-2.0"]
79
models = [
80
default_model, *models.keys()
81
]
82
model_aliases = {"gemini-2.0": ""}
69
83
70
84
synthesize_content_type = "audio/vnd.wav"
71
85
@@ -131,7 +145,6 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
131
145
if not cls._snlm0e:
132
146
raise RuntimeError("Invalid cookies. SNlM0e not found")
133
147
134
yield SynthesizeData(cls.__name__, {"text": messages[-1]["content"]})
135
148
images = await cls.upload_images(base_connector, images) if images else None
136
149
async with ClientSession(
137
150
cookies=cls._cookies,
@@ -158,6 +171,7 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
158
171
REQUEST_URL,
159
172
data=data,
160
173
params=params,
174
headers=models[model] if model in models else None
161
175
) as response:
162
176
await raise_for_status(response)
163
177
image_prompt = response_part = None
@@ -177,7 +191,23 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
177
191
continue
178
192
if return_conversation:
179
193
yield Conversation(response_part[1][0], response_part[1][1], response_part[4][0][0])
194
def read_recusive(data):
195
for item in data:
196
if isinstance(item, list):
197
yield from read_recusive(item)
198
elif isinstance(item, str) and not item.startswith("rc_"):
199
yield item
200
def first_str(data, skip=0):
201
for item in read_recusive(data):
202
if skip > 0:
203
skip -= 1
204
continue
205
yield item
206
reasoning = "".join(first_str(response_part[4][0], 3))
180
207
content = response_part[4][0][1][0]
208
if reasoning:
209
yield Reasoning(status="🤔")
210
yield Reasoning(reasoning)
181
211
except (ValueError, KeyError, TypeError, IndexError) as e:
182
212
debug.error(f"{cls.__name__} {type(e).__name__}: {e}")
183
213
continue
@@ -49,51 +49,7 @@
49
49
document.head.appendChild(template.content);
50
50
}
51
51
</script>
52
<script type="module" async>
53
import PhotoSwipeLightbox from 'https://cdn.jsdelivr.net/npm/photoswipe/dist/photoswipe-lightbox.esm.js';
54
const lightbox = new PhotoSwipeLightbox({
55
gallery: '#messages',
56
children: 'a:has(img)',
57
secondaryZoomLevel: 2,
58
allowPanToNext: true,
59
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe'),
60
});
61
lightbox.addFilter('itemData', (itemData, index) => {
62
const img = itemData.element.querySelector('img');
63
itemData.width = img.naturalWidth || 1024;
64
itemData.height = img.naturalHeight || 1024;
65
return itemData;
66
});
67
lightbox.on('uiRegister', function() {
68
lightbox.pswp.ui.registerElement({
69
name: 'custom-caption',
70
order: 9,
71
isButton: false,
72
appendTo: 'root',
73
html: 'Caption text',
74
onInit: (el, pswp) => {
75
lightbox.pswp.on('change', () => {
76
const currSlideElement = lightbox.pswp.currSlide.data.element;
77
if (currSlideElement) {
78
const img = currSlideElement.querySelector('img');
79
const download = document.createElement("a");
80
download.setAttribute("href", img.getAttribute('src'));
81
let extension = img.getAttribute('src').includes(".webp") ? ".webp" : ".jpg";
82
download.setAttribute("download", `${img.getAttribute('alt')} ${lightbox.pswp.currSlide.index}${extension}`);
83
download.style.float = "right";
84
download.innerHTML = '<i class="fa-solid fa-download"></i>';
85
let span = document.createElement("span");
86
span.innerText = img.getAttribute('alt');
87
el.innerHTML = '';
88
el.appendChild(download);
89
el.appendChild(span);
90
}
91
});
92
}
93
});
94
});
95
lightbox.init();
96
</script>
52
<script type="module" src="/static/js/photoswipe.js" async></script>
97
53
<script>
98
54
const user_image = '<img src="/static/img/user.png" alt="your avatar">';
99
55
const gpt_image = '<img src="/static/img/gpt.png" alt="your avatar">';
@@ -942,6 +942,20 @@ input.model:hover
942
942
color: #fff;
943
943
text-decoration: underline;
944
944
}
945
.pswp__button--playpause-button {
946
position: fixed;
947
bottom: 0;
948
left: 0;
949
margin-left: 6px;
950
}
951
.pswp__progress-bar {
952
position: fixed;
953
bottom: 0;
954
955
/* default position is "top", from the `progressBarPosition` option,
956
need to reset it for this example */
957
top: auto !important;
958
}
945
959
946
960
.slide-systemPrompt {
947
961
position: absolute;
@@ -23,6 +23,7 @@ const album = document.querySelector(".images");
23
23
const log_storage = document.querySelector(".log");
24
24
const switchInput = document.getElementById("switch");
25
25
const searchButton = document.getElementById("search");
26
const paperclip = document.querySelector(".user-input .fa-paperclip");
26
27
27
28
const optionElementsSelector = ".settings input, .settings textarea, #model, #model2, #provider";
28
29
@@ -400,6 +401,23 @@ const handle_ask = async (do_ask_gpt = true) => {
400
401
await count_input()
401
402
await add_conversation(window.conversation_id);
402
403
404
// Is message a url?
405
const expression = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi;
406
const regex = new RegExp(expression);
407
if (message.match(regex)) {
408
paperclip.classList.add("blink");
409
const blob = new Blob([JSON.stringify([{url: message}])], { type: 'application/json' });
410
const file = new File([blob], 'downloads.json', { type: 'application/json' }); // Create File object
411
let formData = new FormData();
412
formData.append('files', file); // Append as a file
413
const bucket_id = uuid();
414
await fetch(`/backend-api/v2/files/${bucket_id}`, {
415
method: 'POST',
416
body: formData
417
});
418
connectToSSE(`/backend-api/v2/files/${bucket_id}`, false, bucket_id); //Retrieve and refine
419
return;
420
}
403
421
let message_index = await add_message(window.conversation_id, "user", message);
404
422
let message_id = get_message_id();
405
423
@@ -1990,6 +2008,7 @@ async function on_api() {
1990
2008
providerSelect.innerHTML = `
1991
2009
<option value="" selected="selected">Demo Mode</option>
1992
2010
<option value="Feature">Feature Provider</option>
2011
<option value="PollinationsAI">Pollinations AI</option>
1993
2012
<option value="G4F">G4F framework</option>
1994
2013
<option value="HuggingFace">HuggingFace</option>
1995
2014
<option value="HuggingSpace">HuggingSpace</option>
@@ -2248,14 +2267,51 @@ function formatFileSize(bytes) {
2248
2267
return `${bytes.toFixed(2)} ${units[unitIndex]}`;
2249
2268
}
2250
2269
2270
function connectToSSE(url, do_refine, bucket_id) {
2271
const eventSource = new EventSource(url);
2272
eventSource.onmessage = (event) => {
2273
const data = JSON.parse(event.data);
2274
if (data.error) {
2275
inputCount.innerText = `Error: ${data.error.message}`;
2276
paperclip.classList.remove("blink");
2277
fileInput.value = "";
2278
} else if (data.action == "load") {
2279
inputCount.innerText = `Read data: ${formatFileSize(data.size)}`;
2280
} else if (data.action == "refine") {
2281
inputCount.innerText = `Refine data: ${formatFileSize(data.size)}`;
2282
} else if (data.action == "download") {
2283
inputCount.innerText = `Download: ${data.count} files`;
2284
} else if (data.action == "done") {
2285
if (do_refine) {
2286
do_refine = false;
2287
connectToSSE(`/backend-api/v2/files/${bucket_id}?refine_chunks_with_spacy=true`, do_refine, bucket_id);
2288
return;
2289
}
2290
appStorage.setItem(`bucket:${bucket_id}`, data.size);
2291
inputCount.innerText = "Files are loaded successfully";
2292
if (!messageInput.value) {
2293
messageInput.value = JSON.stringify({bucket_id: bucket_id});
2294
handle_ask(false);
2295
} else {
2296
messageInput.value += (messageInput.value ? "\n" : "") + JSON.stringify({bucket_id: bucket_id}) + "\n";
2297
paperclip.classList.remove("blink");
2298
fileInput.value = "";
2299
}
2300
}
2301
};
2302
eventSource.onerror = (event) => {
2303
eventSource.close();
2304
paperclip.classList.remove("blink");
2305
}
2306
}
2307
2251
2308
async function upload_files(fileInput) {
2252
const paperclip = document.querySelector(".user-input .fa-paperclip");
2253
2309
const bucket_id = uuid();
2254
2310
paperclip.classList.add("blink");
2255
2311
2256
2312
const formData = new FormData();
2257
2313
Array.from(fileInput.files).forEach(file => {
2258
formData.append('files[]', file);
2314
formData.append('files', file);
2259
2315
});
2260
2316
await fetch("/backend-api/v2/files/" + bucket_id, {
2261
2317
method: 'POST',
@@ -2263,44 +2319,7 @@ async function upload_files(fileInput) {
2263
2319
});
2264
2320
2265
2321
let do_refine = document.getElementById("refine")?.checked;
2266
function connectToSSE(url) {
2267
const eventSource = new EventSource(url);
2268
eventSource.onmessage = (event) => {
2269
const data = JSON.parse(event.data);
2270
if (data.error) {
2271
inputCount.innerText = `Error: ${data.error.message}`;
2272
paperclip.classList.remove("blink");
2273
fileInput.value = "";
2274
} else if (data.action == "load") {
2275
inputCount.innerText = `Read data: ${formatFileSize(data.size)}`;
2276
} else if (data.action == "refine") {
2277
inputCount.innerText = `Refine data: ${formatFileSize(data.size)}`;
2278
} else if (data.action == "download") {
2279
inputCount.innerText = `Download: ${data.count} files`;
2280
} else if (data.action == "done") {
2281
if (do_refine) {
2282
do_refine = false;
2283
connectToSSE(`/backend-api/v2/files/${bucket_id}?refine_chunks_with_spacy=true`);
2284
return;
2285
}
2286
appStorage.setItem(`bucket:${bucket_id}`, data.size);
2287
inputCount.innerText = "Files are loaded successfully";
2288
if (!messageInput.value) {
2289
messageInput.value = JSON.stringify({bucket_id: bucket_id});
2290
handle_ask(false);
2291
} else {
2292
messageInput.value += (messageInput.value ? "\n" : "") + JSON.stringify({bucket_id: bucket_id}) + "\n";
2293
paperclip.classList.remove("blink");
2294
fileInput.value = "";
2295
}
2296
}
2297
};
2298
eventSource.onerror = (event) => {
2299
eventSource.close();
2300
paperclip.classList.remove("blink");
2301
}
2302
}
2303
connectToSSE(`/backend-api/v2/files/${bucket_id}`);
2322
connectToSSE(`/backend-api/v2/files/${bucket_id}`, do_refine, bucket_id);
2304
2323
}
2305
2324
2306
2325
fileInput.addEventListener('change', async (event) => {
@@ -2416,7 +2435,7 @@ async function api(ressource, args=null, files=null, message_id=null, scroll=tru
2416
2435
if (files !== null) {
2417
2436
const formData = new FormData();
2418
2437
for (const file of files) {
2419
formData.append('files[]', file)
2438
formData.append('files', file)
2420
2439
}
2421
2440
formData.append('json', body);
2422
2441
body = formData;
@@ -0,0 +1,63 @@
1
import PhotoSwipeLightbox from "https://cdn.jsdelivr.net/npm/photoswipe@5.3.8/dist/photoswipe-lightbox.esm.min.js";
2
import PhotoSwipeVideoPlugin from "https://cdn.jsdelivr.net/gh/dimsemenov/photoswipe-video-plugin@5e32d6589df53df2887900bcd55267d72aee57a6/dist/photoswipe-video-plugin.esm.min.js";
3
import PhotoSwipeAutoHideUI from "https://cdn.jsdelivr.net/gh/arnowelzel/photoswipe-auto-hide-ui@1.0.1/photoswipe-auto-hide-ui.esm.min.js";
4
import PhotoSwipeSlideshow from "https://cdn.jsdelivr.net/gh/dpet23/photoswipe-slideshow@v2.0.0/photoswipe-slideshow.esm.min.js";
5
6
const lightbox = new PhotoSwipeLightbox({
7
gallery: '#messages',
8
children: 'a:has(img)',
9
initialZoomLevel: 'fill',
10
secondaryZoomLevel: 1,
11
maxZoomLevel: 2,
12
allowPanToNext: true,
13
doubleTapAction: 'close',
14
pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe'),
15
});
16
lightbox.addFilter('itemData', (itemData, index) => {
17
const img = itemData.element.querySelector('img');
18
itemData.width = img.naturalWidth || 1024;
19
itemData.height = img.naturalHeight || 1024;
20
return itemData;
21
});
22
lightbox.on('uiRegister', function() {
23
lightbox.pswp.ui.registerElement({
24
name: 'custom-caption',
25
order: 9,
26
isButton: false,
27
appendTo: 'root',
28
html: 'Caption text',
29
onInit: (el, pswp) => {
30
lightbox.pswp.on('change', () => {
31
const currSlideElement = lightbox.pswp.currSlide.data.element;
32
if (currSlideElement) {
33
const img = currSlideElement.querySelector('img');
34
const download = document.createElement("a");
35
download.setAttribute("href", img.getAttribute('src'));
36
let extension = img.getAttribute('src').includes(".webp") ? ".webp" : ".jpg";
37
download.setAttribute("download", `${img.getAttribute('alt')} ${lightbox.pswp.currSlide.index}${extension}`);
38
download.style.float = "right";
39
download.innerHTML = '<i class="fa-solid fa-download"></i>';
40
let span = document.createElement("span");
41
span.innerText = img.getAttribute('alt');
42
el.innerHTML = '';
43
el.appendChild(download);
44
el.appendChild(span);
45
}
46
});
47
}
48
});
49
});
50
// Add a slideshow to the PhotoSwipe gallery.
51
const _slideshowPlugin = new PhotoSwipeSlideshow(lightbox, {
52
defaultDelayMs: 7000,
53
restartOnSlideChange: true,
54
progressBarPosition: "top",
55
autoHideProgressBar: false
56
});
57
58
// Plugin to display video.
59
const _videoPlugin = new PhotoSwipeVideoPlugin(lightbox, {});
60
61
// Hide the PhotoSwipe UI after some time of inactivity.
62
const _autoHideUI = new PhotoSwipeAutoHideUI(lightbox, {});
63
lightbox.init();
@@ -19,6 +19,8 @@ from ... import version, models
19
19
from ... import ChatCompletion, get_model_and_provider
20
20
from ... import debug
21
21
22
logger = logging.getLogger(__name__)
23
22
24
conversations: dict[dict[str, BaseConversation]] = {}
23
25
24
26
class Api:
@@ -184,6 +186,7 @@ class Api:
184
186
else:
185
187
yield self._format_json("conversation_id", conversation_id)
186
188
elif isinstance(chunk, Exception):
189
logger.exception(chunk)
187
190
debug.error(chunk)
188
191
yield self._format_json('message', get_error_message(chunk), error=type(chunk).__name__)
189
192
elif isinstance(chunk, PreviewResponse):
@@ -219,6 +222,7 @@ class Api:
219
222
yield self._format_json("content", str(chunk))
220
223
yield from self._yield_logs()
221
224
except Exception as e:
225
logger.exception(e)
222
226
debug.error(e)
223
227
yield from self._yield_logs()
224
228
yield self._format_json('error', type(e).__name__, message=get_error_message(e))
@@ -124,9 +124,9 @@ class Backend_Api(Api):
124
124
Response: A Flask response object for streaming.
125
125
"""
126
126
kwargs = {}
127
if "files[]" in request.files:
127
if "files" in request.files:
128
128
images = []
129
for file in request.files.getlist('files[]'):
129
for file in request.files.getlist('files'):
130
130
if file.filename != '' and is_allowed_extension(file.filename):
131
131
images.append((to_image(file.stream, file.filename.endswith('.svg')), file.filename))
132
132
kwargs['images'] = images
@@ -135,7 +135,7 @@ class Backend_Api(Api):
135
135
else:
136
136
json_data = request.json
137
137
138
if app.demo and json_data.get("provider") not in ["Custom", "Feature", "HuggingFace", "HuggingSpace", "HuggingChat", "G4F"]:
138
if app.demo and json_data.get("provider") not in ["Custom", "Feature", "HuggingFace", "HuggingSpace", "HuggingChat", "G4F", "PollinationsAI"]:
139
139
model = json_data.get("model")
140
140
if model != "default" and model in models.demo_models:
141
141
json_data["provider"] = random.choice(models.demo_models[model][1])
@@ -329,7 +329,7 @@ class Backend_Api(Api):
329
329
bucket_dir = get_bucket_dir(bucket_id)
330
330
os.makedirs(bucket_dir, exist_ok=True)
331
331
filenames = []
332
for file in request.files.getlist('files[]'):
332
for file in request.files.getlist('files'):
333
333
try:
334
334
filename = secure_filename(file.filename)
335
335
if supports_filename(filename):
@@ -625,7 +625,7 @@ flux = ImageModel(
625
625
flux_pro = ImageModel(
626
626
name = 'flux-pro',
627
627
base_provider = 'Black Forest Labs',
628
best_provider = PollinationsAI
628
best_provider = PollinationsImage
629
629
)
630
630
631
631
flux_dev = ImageModel(
@@ -51,6 +51,7 @@ class StreamResponse:
51
51
"""Asynchronously enter the runtime context for the response object."""
52
52
inner: Response = await self.inner
53
53
self.inner = inner
54
self.url = inner.url
54
55
self.request = inner.request
55
56
self.status: int = inner.status_code
56
57
self.reason: str = inner.reason
@@ -157,7 +157,7 @@ def get_filenames(bucket_dir: Path):
157
157
def stream_read_files(bucket_dir: Path, filenames: list, delete_files: bool = False) -> Iterator[str]:
158
158
for filename in filenames:
159
159
file_path: Path = bucket_dir / filename
160
if not file_path.exists() and 0 > file_path.lstat().st_size:
160
if not file_path.exists() or file_path.lstat().st_size <= 0:
161
161
continue
162
162
extension = os.path.splitext(filename)[1][1:]
163
163
if filename.endswith(".zip"):
@@ -453,6 +453,8 @@ async def download_urls(
453
453
async for chunk in response.content.iter_chunked(4096):
454
454
if b'<link rel="canonical"' not in chunk:
455
455
f.write(chunk.replace(b'</head>', f'<link rel="canonical" href="{response.url}">\n</head>'.encode()))
456
else:
457
f.write(chunk)
456
458
return filename
457
459
except (ClientError, asyncio.TimeoutError) as e:
458
460
debug.log(f"Download failed: {e.__class__.__name__}: {e}")