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

XFEstudio/gpt4free

Fix unittest, update model lists

ba602966
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

12 个文件 +187 -100
Modified g4f/Provider/PollinationsAI.py +2 -3
@@ -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()
Modified g4f/Provider/PollinationsImage.py +0 -1
@@ -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):
Modified g4f/Provider/needs_auth/Gemini.py +34 -4
@@ -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
Modified g4f/gui/client/index.html +1 -45
@@ -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">';
Modified g4f/gui/client/static/css/style.css +14 -0
@@ -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;
Modified g4f/gui/client/static/js/chat.v1.js +60 -41
@@ -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;
Added g4f/gui/client/static/js/photoswipe.js +63 -0
@@ -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();
Modified g4f/gui/server/api.py +4 -0
@@ -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))
Modified g4f/gui/server/backend_api.py +4 -4
@@ -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):
Modified g4f/models.py +1 -1
@@ -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(
Modified g4f/requests/curl_cffi.py +1 -0
@@ -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
Modified g4f/tools/files.py +3 -1
@@ -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}")