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

XFEstudio/gpt4free

Add new media selection in UI Add HuggingFace provider provider Auto refresh Google Gemini cookies Add sources to search results

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

代码差异

13 个文件 +326 -105
Modified README.md +1 -0
@@ -835,6 +835,7 @@ A list of all contributors is available [here](https://github.com/xtekky/gpt4fre
835 835 - The [`Gemini.py`](https://github.com/xtekky/gpt4free/blob/main/g4f/Provider/needs_auth/Gemini.py) has input from [dsdanielpark/Gemini-API](https://github.com/dsdanielpark/Gemini-API)
836 836 - The [`MetaAI.py`](https://github.com/xtekky/gpt4free/blob/main/g4f/Provider/MetaAI.py) file contains code from [meta-ai-api](https://github.com/Strvm/meta-ai-api) by [@Strvm](https://github.com/Strvm)
837 837 - The [`proofofwork.py`](https://github.com/xtekky/gpt4free/blob/main/g4f/Provider/openai/proofofwork.py) has input from [missuo/FreeGPT35](https://github.com/missuo/FreeGPT35)
838 - The [`Gemini.py`](https://github.com/xtekky/gpt4free/blob/main/g4f/Provider/needs_auth/Gemini.py) has input from [HanaokaYuzu/Gemini-API](https://github.com/HanaokaYuzu/Gemini-API)
838 839
839 840 _Having input implies that the AI's code generation utilized it as one of many sources._
840 841
Modified g4f/Provider/hf/HuggingFaceInference.py +38 -23
@@ -14,6 +14,11 @@ from ..helper import format_image_prompt, get_last_user_message
14 14 from .models import default_model, default_image_model, model_aliases, text_models, image_models, vision_models
15 15 from ... import debug
16 16
17 provider_together_urls = {
18 "black-forest-labs/FLUX.1-dev": "https://router.huggingface.co/together/v1/images/generations",
19 "black-forest-labs/FLUX.1-schnell": "https://router.huggingface.co/together/v1/images/generations",
20 }
21
17 22 class HuggingFaceInference(AsyncGeneratorProvider, ProviderModelMixin):
18 23 url = "https://huggingface.co"
19 24 parent = "HuggingFace"
@@ -63,6 +68,7 @@ class HuggingFaceInference(AsyncGeneratorProvider, ProviderModelMixin):
63 68 messages: Messages,
64 69 stream: bool = True,
65 70 proxy: str = None,
71 timeout: int = 600,
66 72 api_base: str = "https://api-inference.huggingface.co",
67 73 api_key: str = None,
68 74 max_tokens: int = 1024,
@@ -71,6 +77,8 @@ class HuggingFaceInference(AsyncGeneratorProvider, ProviderModelMixin):
71 77 action: str = None,
72 78 extra_data: dict = {},
73 79 seed: int = None,
80 width: int = 1024,
81 height: int = 1024,
74 82 **kwargs
75 83 ) -> AsyncResult:
76 84 try:
@@ -78,36 +86,43 @@ class HuggingFaceInference(AsyncGeneratorProvider, ProviderModelMixin):
78 86 except ModelNotSupportedError:
79 87 pass
80 88 headers = {
81 'accept': '*/*',
82 'accept-language': 'en',
83 'cache-control': 'no-cache',
84 'origin': 'https://huggingface.co',
85 'pragma': 'no-cache',
86 'priority': 'u=1, i',
87 'referer': 'https://huggingface.co/chat/',
88 'sec-ch-ua': '"Not)A;Brand";v="99", "Google Chrome";v="127", "Chromium";v="127"',
89 'sec-ch-ua-mobile': '?0',
90 'sec-ch-ua-platform': '"macOS"',
91 'sec-fetch-dest': 'empty',
92 'sec-fetch-mode': 'cors',
93 'sec-fetch-site': 'same-origin',
94 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
89 'Accept-Encoding': 'gzip, deflate',
90 'Content-Type': 'application/json',
95 91 }
96 92 if api_key is not None:
97 93 headers["Authorization"] = f"Bearer {api_key}"
98 payload = None
99 params = {
100 "return_full_text": False,
101 "max_new_tokens": max_tokens,
102 "temperature": temperature,
103 **extra_data
104 }
105 do_continue = action == "continue"
106 94 async with StreamSession(
107 95 headers=headers,
108 96 proxy=proxy,
109 timeout=600
97 timeout=timeout
110 98 ) as session:
99 try:
100 if model in provider_together_urls:
101 data = {
102 "response_format": "url",
103 "prompt": format_image_prompt(messages, prompt),
104 "model": model,
105 "width": width,
106 "height": height,
107 **extra_data
108 }
109 async with session.post(provider_together_urls[model], json=data) as response:
110 if response.status == 404:
111 raise ModelNotSupportedError(f"Model is not supported: {model}")
112 await raise_for_status(response)
113 result = await response.json()
114 yield ImageResponse([item["url"] for item in result["data"]], data["prompt"])
115 return
116 except ModelNotSupportedError:
117 pass
118 payload = None
119 params = {
120 "return_full_text": False,
121 "max_new_tokens": max_tokens,
122 "temperature": temperature,
123 **extra_data
124 }
125 do_continue = action == "continue"
111 126 if payload is None:
112 127 model_data = await cls.get_model_data(session, model)
113 128 pipeline_tag = model_data.get("pipeline_tag")
Modified g4f/Provider/needs_auth/Gemini.py +94 -12
@@ -6,7 +6,10 @@ import random
6 6 import re
7 7 import base64
8 8 import asyncio
9 import time
9 10
11 from urllib.parse import quote_plus, unquote_plus
12 from pathlib import Path
10 13 from aiohttp import ClientSession, BaseConnector
11 14
12 15 try:
@@ -17,15 +20,15 @@ except ImportError:
17 20
18 21 from ... import debug
19 22 from ...typing import Messages, Cookies, ImagesType, AsyncResult, AsyncIterator
20 from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
21 from ..helper import format_prompt, get_cookies
22 from ...providers.response import JsonConversation, Reasoning, RequestLogin, ImageResponse
23 from ...providers.response import JsonConversation, Reasoning, RequestLogin, ImageResponse, YouTube
23 24 from ...requests.raise_for_status import raise_for_status
24 25 from ...requests.aiohttp import get_connector
25 26 from ...requests import get_nodriver
26 27 from ...errors import MissingAuthError
27 28 from ...image import to_bytes
28 from ..helper import get_last_user_message
29 from ...cookies import get_cookies_dir
30 from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
31 from ..helper import format_prompt, get_cookies, get_last_user_message
29 32 from ... import debug
30 33
31 34 REQUEST_HEADERS = {
@@ -52,6 +55,9 @@ UPLOAD_IMAGE_HEADERS = {
52 55 "x-goog-upload-protocol": "resumable",
53 56 "x-tenant-id": "bard-storage",
54 57 }
58 GOOGLE_COOKIE_DOMAIN = ".google.com"
59 ROTATE_COOKIES_URL = "https://accounts.google.com/RotateCookies"
60 GGOGLE_SID_COOKIE = "__Secure-1PSID"
55 61
56 62 models = {
57 63 "gemini-2.0-flash": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"f299729663a2343f"]'},
@@ -87,6 +93,10 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
87 93 _snlm0e: str = None
88 94 _sid: str = None
89 95
96 auto_refresh = True
97 refresh_interval = 540
98 rotate_tasks = {}
99
90 100 @classmethod
91 101 async def nodriver_login(cls, proxy: str = None) -> AsyncIterator[str]:
92 102 if not has_nodriver:
@@ -108,6 +118,29 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
108 118 finally:
109 119 stop_browser()
110 120
121 @classmethod
122 async def start_auto_refresh(cls, proxy: str = None) -> None:
123 """
124 Start the background task to automatically refresh cookies.
125 """
126
127 while True:
128 try:
129 new_1psidts = await rotate_1psidts(cls.url, cls._cookies, proxy)
130 except Exception as e:
131 debug.error(f"Failed to refresh cookies: {e}")
132 task = cls.rotate_tasks.get(cls._cookies[GGOGLE_SID_COOKIE])
133 if task:
134 task.cancel()
135 debug.error(
136 "Failed to refresh cookies. Background auto refresh task canceled."
137 )
138
139 debug.log(f"Gemini: Cookies refreshed. New __Secure-1PSIDTS: {new_1psidts}")
140 if new_1psidts:
141 cls._cookies["__Secure-1PSIDTS"] = new_1psidts
142 await asyncio.sleep(cls.refresh_interval)
143
111 144 @classmethod
112 145 async def create_async_generator(
113 146 cls,
@@ -122,8 +155,10 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
122 155 language: str = "en",
123 156 **kwargs
124 157 ) -> AsyncResult:
158 cls._cookies = cookies or cls._cookies or get_cookies(GOOGLE_COOKIE_DOMAIN, False, True)
159 if conversation is not None and getattr(conversation, "model", None) != model:
160 conversation = None
125 161 prompt = format_prompt(messages) if conversation is None else get_last_user_message(messages)
126 cls._cookies = cookies or cls._cookies or get_cookies(".google.com", False, True)
127 162 base_connector = get_connector(connector, proxy)
128 163
129 164 async with ClientSession(
@@ -144,6 +179,12 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
144 179 await cls.fetch_snlm0e(session, cls._cookies)
145 180 if not cls._snlm0e:
146 181 raise RuntimeError("Invalid cookies. SNlM0e not found")
182 if GGOGLE_SID_COOKIE in cls._cookies:
183 task = cls.rotate_tasks.get(cls._cookies[GGOGLE_SID_COOKIE])
184 if not task:
185 cls.rotate_tasks[cls._cookies[GGOGLE_SID_COOKIE]] = asyncio.create_task(
186 cls.start_auto_refresh()
187 )
147 188
148 189 images = await cls.upload_images(base_connector, images) if images else None
149 190 async with ClientSession(
@@ -190,7 +231,7 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
190 231 if not response_part[4]:
191 232 continue
192 233 if return_conversation:
193 yield Conversation(response_part[1][0], response_part[1][1], response_part[4][0][0])
234 yield Conversation(response_part[1][0], response_part[1][1], response_part[4][0][0], model)
194 235 def read_recusive(data):
195 236 for item in data:
196 237 if isinstance(item, list):
@@ -222,12 +263,13 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
222 263 if match:
223 264 image_prompt = match.group(1)
224 265 content = content.replace(match.group(0), '')
225 pattern = r"http://googleusercontent.com/(?:image_generation|youtube)_content/\d+"
266 pattern = r"http://googleusercontent.com/(?:image_generation|youtube|map)_content/\d+"
226 267 content = re.sub(pattern, "", content)
227 268 content = content.replace("<!-- end list -->", "")
228 content = content.replace("https://www.google.com/search?q=http://", "https://")
229 content = content.replace("https://www.google.com/search?q=https://", "https://")
230 content = content.replace("https://www.google.com/url?sa=E&source=gmail&q=http://", "http://")
269 def replace_link(match):
270 return f"(https://{quote_plus(unquote_plus(match.group(1)), '/?&=#')})"
271 content = re.sub(r"\(https://www.google.com/(?:search\?q=|url\?sa=E&source=gmail&q=)https?://(.+?)\)", replace_link, content)
272
231 273 if last_content and content.startswith(last_content):
232 274 yield content[len(last_content):]
233 275 else:
@@ -240,6 +282,13 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
240 282 yield ImageResponse(images, image_prompt, {"cookies": cls._cookies})
241 283 except (TypeError, IndexError, KeyError):
242 284 pass
285 youtube_ids = []
286 pattern = re.compile(r"http://www.youtube.com/watch\?v=(\w+)")
287 for match in pattern.finditer(content):
288 if match.group(1) not in youtube_ids:
289 youtube_ids.append(match.group(1))
290 if youtube_ids:
291 yield YouTube(youtube_ids)
243 292
244 293 @classmethod
245 294 async def synthesize(cls, params: dict, proxy: str = None) -> AsyncIterator[bytes]:
@@ -354,11 +403,13 @@ class Conversation(JsonConversation):
354 403 def __init__(self,
355 404 conversation_id: str,
356 405 response_id: str,
357 choice_id: str
406 choice_id: str,
407 model: str
358 408 ) -> None:
359 409 self.conversation_id = conversation_id
360 410 self.response_id = response_id
361 411 self.choice_id = choice_id
412 self.model = model
362 413
363 414 async def iter_filter_base64(chunks: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
364 415 search_for = b'[["wrb.fr","XqA3Ic","[\\"'
@@ -386,4 +437,35 @@ async def iter_base64_decode(chunks: AsyncIterator[bytes]) -> AsyncIterator[byte
386 437 buffer = chunk[-rest:]
387 438 yield base64.b64decode(chunk[:-rest])
388 439 if rest > 0:
389 yield base64.b64decode(buffer+rest*b"=")
440 yield base64.b64decode(buffer+rest*b"=")
441
442 async def rotate_1psidts(url, cookies: dict, proxy: str | None = None) -> str:
443 path = Path(get_cookies_dir())
444 path.mkdir(parents=True, exist_ok=True)
445 filename = f"auth_Gemini.json"
446 path = path / filename
447
448 # Check if the cache file was modified in the last minute to avoid 429 Too Many Requests
449 if not (path.is_file() and time.time() - os.path.getmtime(path) <= 60):
450 async with ClientSession(proxy=proxy) as client:
451 response = await client.post(
452 url=ROTATE_COOKIES_URL,
453 headers={
454 "Content-Type": "application/json",
455 },
456 cookies=cookies,
457 data='[000,"-0000000000000000000"]',
458 )
459 if response.status == 401:
460 raise MissingAuthError("Invalid cookies")
461 response.raise_for_status()
462 for key, c in response.cookies.items():
463 cookies[key] = c.value
464 new_1psidts = response.cookies.get("__Secure-1PSIDTS")
465 path.write_text(json.dumps([{
466 "name": k,
467 "value": v,
468 "domain": GOOGLE_COOKIE_DOMAIN,
469 } for k, v in cookies.items()]))
470 if new_1psidts:
471 return new_1psidts
Modified g4f/cookies.py +2 -2
@@ -192,5 +192,5 @@ def read_cookie_files(dirPath: str = None):
192 192 new_cookies[c["domain"]] = {}
193 193 new_cookies[c["domain"]][c["name"]] = c["value"]
194 194 for domain, new_values in new_cookies.items():
195 debug.log(f"Cookies added: {len(new_values)} from {domain}")
196 CookiesConfig.cookies[domain] = new_values
195 CookiesConfig.cookies[domain] = new_values
196 debug.log(f"Cookies added: {len(new_values)} from {domain}")
Modified g4f/gui/client/demo.html +12 -2
@@ -254,6 +254,13 @@
254 254 } catch(e) {
255 255 console.log(e);
256 256 input.setCustomValidity("Invalid Access Token.");
257 localStorage.removeItem("HuggingFace-api_key");
258 if (localStorage.getItem("oauth")) {
259 window.location.href = (await oauthLoginUrl({
260 clientId: 'ed074164-4f8d-4fb2-8bec-44952707965e',
261 scopes: ['inference-api']
262 }));
263 }
257 264 return;
258 265 }
259 266 localStorage.setItem("HuggingFace-api_key", accessToken);
@@ -289,10 +296,13 @@
289 296 window.location.reload();
290 297 }
291 298 } else {
299 localStorage.removeItem("oauth");
292 300 document.getElementById("signin").style.removeProperty("display");
293 301 document.getElementById("signin").onclick = async function() {
294 // prompt=consent to re-trigger the consent screen instead of silently redirecting
295 window.location.href = (await oauthLoginUrl({clientId: 'ed074164-4f8d-4fb2-8bec-44952707965e', scopes: ['inference-api']})) + "&prompt=consent";
302 window.location.href = (await oauthLoginUrl({
303 clientId: 'ed074164-4f8d-4fb2-8bec-44952707965e',
304 scopes: ['inference-api']
305 }));
296 306 }
297 307 }
298 308 </script>
Modified g4f/gui/client/index.html +20 -6
@@ -33,6 +33,12 @@
33 33 };
34 34 </script>
35 35 <script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async></script>
36 <script>
37 var tag = document.createElement('script');
38 tag.src = "https://www.youtube.com/iframe_api";
39 var firstScriptTag = document.getElementsByTagName('script')[0];
40 firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
41 </script>
36 42 <template>
37 43 <script type="module" src="https://cdn.jsdelivr.net/npm/mistral-tokenizer-js" async>
38 44 import mistralTokenizer from "mistral-tokenizer-js"
@@ -211,6 +217,19 @@
211 217 <div class="media_player">
212 218 <i class="fa-regular fa-x"></i>
213 219 </div>
220 <div class="media-select hidden">
221 <label class="image-select" for="image" title="">
222 <input type="file" id="image" name="image" accept="image/*" required/>
223 <i class="fa-regular fa-image"></i>
224 </label>
225 <label class="capture-camera" for="camera">
226 <input type="file" id="camera" name="camera" accept="image/*" capture="camera" required/>
227 <i class="fa-solid fa-camera"></i>
228 </label>
229 <button class="close">
230 <i class="fa-solid fa-xmark"></i>
231 </button>
232 </div>
214 233 <div class="toolbar">
215 234 <div id="input-count" class="">
216 235 <button class="hide-input">
@@ -236,14 +255,9 @@
236 255 <div class="box input-area">
237 256 <textarea id="message-input" placeholder="Ask a question" cols="30" rows="10"
238 257 style="white-space: pre-wrap;resize: none;"></textarea>
239 <label class="file-label image-label" for="image" title="">
240 <input type="file" id="image" name="image" accept="image/*" required/>
258 <label class="file-label image-label">
241 259 <i class="fa-regular fa-image"></i>
242 260 </label>
243 <label class="file-label image-label" for="camera">
244 <input type="file" id="camera" name="camera" accept="image/*" capture="camera" required/>
245 <i class="fa-solid fa-camera"></i>
246 </label>
247 261 <label class="file-label" for="file">
248 262 <input type="file" id="file" name="file" accept=".txt, .html, .xml, .json, .js, .har, .sh, .py, .php, .css, .yaml, .sql, .log, .csv, .twig, .md, .pdf, .docx, .odt, .epub, .xlsx, .zip" required multiple/>
249 263 <i class="fa-solid fa-paperclip"></i>
Modified g4f/gui/client/static/css/style.css +39 -7
@@ -384,7 +384,7 @@ body:not(.white) a:visited{
384 384 }
385 385
386 386 .message .reasoning_text.final {
387 max-height: 1000px;
387 max-height: 2000px;
388 388 transition: max-height 0.25s ease-in;
389 389 }
390 390
@@ -478,6 +478,13 @@ body:not(.white) a:visited{
478 478 padding: 0 4px;
479 479 }
480 480
481 .message .content blockquote {
482 padding: 8px 16px;
483 margin-bottom: 16px;
484 color: inherit;
485 border-left: .25em solid var(--colour-4);
486 }
487
481 488 .media_player {
482 489 display: none;
483 490 }
@@ -501,6 +508,36 @@ body:not(.white) a:visited{
501 508 cursor: pointer;
502 509 }
503 510
511 .media-select {
512 display: flex;
513 flex-direction: row;
514 }
515
516 .media-select label, .media-select img, .media-select button {
517 display: flex;
518 gap: 18px;
519 align-items: center;
520 cursor: pointer;
521 user-select: none;
522 color: var(--colour-1);
523 background: var(--colour-4);
524 border: 1px solid var(--colour-1);
525 transition: all 0.2s ease;
526 width: auto;
527 height: 60px;
528 margin: 2px;
529 }
530
531 .media-select label, .media-select button {
532 padding: 8px 12px;
533 border-radius: var(--border-radius-1);
534 }
535
536 .media-select button.close {
537 order: 1000;
538 height: 32px;
539 }
540
504 541 .count_total {
505 542 font-size: 12px;
506 543 padding-left: 25px;
@@ -691,7 +728,7 @@ input-count .text {
691 728 border-color: var(--accent);
692 729 }
693 730
694 label[for="image"] {
731 label.image-label {
695 732 top: 32px;
696 733 }
697 734
@@ -699,11 +736,6 @@ label[for="micro"] {
699 736 top: 54px;
700 737 }
701 738
702 label[for="camera"] {
703 top: 74px;
704 display: none;
705 }
706
707 739 @media (pointer:none), (pointer:coarse) {
708 740 label[for="camera"] {
709 741 display: block;
Modified g4f/gui/client/static/js/chat.v1.js +54 -29
@@ -7,7 +7,9 @@ const regenerate_button = document.querySelector(`.regenerate`);
7 7 const sidebar = document.querySelector(".conversations");
8 8 const sidebar_button = document.querySelector(".mobile-sidebar");
9 9 const sendButton = document.getElementById("send-button");
10 const imageInput = document.getElementById("image");
10 const imageInput = document.querySelector(".image-label");
11 const mediaSelect = document.querySelector(".media-select");
12 const imageSelect = document.getElementById("image");
11 13 const cameraInput = document.getElementById("camera");
12 14 const fileInput = document.getElementById("file");
13 15 const microLabel = document.querySelector(".micro-label");
@@ -40,6 +42,7 @@ let usage_storage = {};
40 42 let reasoning_storage = {};
41 43 let generate_storage = {};
42 44 let title_ids_storage = {};
45 let image_storage = {};
43 46 let is_demo = false;
44 47 let wakeLock = null;
45 48 let countTokensEnabled = true;
@@ -77,6 +80,8 @@ if (window.markdownit) {
77 80 .replaceAll('<code>', '<code class="language-plaintext">')
78 81 .replaceAll('&lt;i class=&quot;', '<i class="')
79 82 .replaceAll('&quot;&gt;&lt;/i&gt;', '"></i>')
83 .replaceAll('&lt;iframe type=&quot;text/html&quot; src=&quot;', '<iframe type="text/html" frameborder="0" src="')
84 .replaceAll('&quot;&gt;&lt;/iframe&gt;', `?enablejsapi=1&origin=${new URL(location.href).origin}` + '"></iframe>')
80 85 }
81 86 }
82 87
@@ -426,20 +431,6 @@ const handle_ask = async (do_ask_gpt = true) => {
426 431 let message_index = await add_message(window.conversation_id, "user", message);
427 432 let message_id = get_message_id();
428 433
429 let images = [];
430 if (do_ask_gpt) {
431 if (imageInput.dataset.objects) {
432 imageInput.dataset.objects.split(" ").forEach((object)=>URL.revokeObjectURL(object))
433 delete imageInput.dataset.objects;
434 }
435 const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput
436 if (input.files.length > 0) {
437 for (const file of input.files) {
438 images.push(URL.createObjectURL(file));
439 }
440 imageInput.dataset.objects = images.join(" ");
441 }
442 }
443 434 const message_el = document.createElement("div");
444 435 message_el.classList.add("message");
445 436 message_el.dataset.index = message_index;
@@ -452,7 +443,6 @@ const handle_ask = async (do_ask_gpt = true) => {
452 443 <div class="content" id="user_${message_id}">
453 444 <div class="content_inner">
454 445 ${markdown_render(message)}
455 ${images.map((object)=>`<img src="${object}" alt="Image upload">`).join("")}
456 446 </div>
457 447 <div class="count">
458 448 ${countTokensEnabled ? count_words_and_tokens(message, get_selected_model()?.value) : ""}
@@ -937,8 +927,6 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
937 927 html = markdown_render(message_storage[message_id]);
938 928 content_map.inner.innerHTML = html;
939 929 highlight(content_map.inner);
940 if (imageInput) imageInput.value = "";
941 if (cameraInput) cameraInput.value = "";
942 930 }
943 931 if (message_storage[message_id]) {
944 932 const message_provider = message_id in provider_storage ? provider_storage[message_id] : null;
@@ -1032,8 +1020,6 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
1032 1020 } else {
1033 1021 api_key = get_api_key_by_provider(provider);
1034 1022 }
1035 const input = imageInput && imageInput.files.length > 0 ? imageInput : cameraInput;
1036 const files = input && input.files.length > 0 ? input.files : null;
1037 1023 const download_images = document.getElementById("download_images")?.checked;
1038 1024 let api_base;
1039 1025 if (provider == "Custom") {
@@ -1056,7 +1042,7 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
1056 1042 api_key: api_key,
1057 1043 api_base: api_base,
1058 1044 ignored: ignored,
1059 }, files, message_id, scroll, finish_message);
1045 }, Object.values(image_storage), message_id, scroll, finish_message);
1060 1046 } catch (e) {
1061 1047 console.error(e);
1062 1048 if (e.name != "AbortError") {
@@ -1948,7 +1934,7 @@ async function on_load() {
1948 1934 chatPrompt.value = document.getElementById("systemPrompt")?.value || "";
1949 1935 say_hello();
1950 1936 } else {
1951 load_conversation(window.conversation_id);
1937 //load_conversation(window.conversation_id);
1952 1938 }
1953 1939 load_conversations();
1954 1940 }
@@ -2249,12 +2235,51 @@ async function load_version() {
2249 2235 setTimeout(load_version, 1000 * 60 * 60); // 1 hour
2250 2236 }
2251 2237
2252 [imageInput, cameraInput].forEach((el) => {
2253 el.addEventListener('click', async () => {
2254 el.value = '';
2255 if (imageInput.dataset.objects) {
2256 imageInput.dataset.objects.split(" ").forEach((object) => URL.revokeObjectURL(object));
2257 delete imageInput.dataset.objects
2238 function renderMediaSelect() {
2239 const oldImages = mediaSelect.querySelectorAll("a:has(img)");
2240 oldImages.forEach((el)=>el.remove());
2241 Object.entries(image_storage).forEach(([object_url, file]) => {
2242 const link = document.createElement("a");
2243 link.title = file.name;
2244 const img = document.createElement("img");
2245 img.src = object_url;
2246 img.onclick = () => {
2247 img.remove();
2248 delete image_storage[object_url];
2249 URL.revokeObjectURL(object_url)
2250 }
2251 img.onload = () => {
2252 link.title += `\n${img.naturalWidth}x${img.naturalHeight}`;
2253 };
2254 link.appendChild(img);
2255 mediaSelect.appendChild(link);
2256 });
2257 }
2258
2259 imageInput.onclick = () => {
2260 mediaSelect.classList.toggle("hidden");
2261 }
2262
2263 mediaSelect.querySelector(".close").onclick = () => {
2264 if (Object.values(image_storage).length) {
2265 for (key in image_storage) {
2266 URL.revokeObjectURL(key);
2267 }
2268 image_storage = {};
2269 renderMediaSelect();
2270 } else {
2271 mediaSelect.classList.add("hidden");
2272 }
2273 }
2274
2275 [imageSelect, cameraInput].forEach((el) => {
2276 el.addEventListener('change', async () => {
2277 if (el.files.length) {
2278 Array.from(el.files).forEach((file) => {
2279 image_storage[URL.createObjectURL(file)] = file;
2280 });
2281 el.value = "";
2282 renderMediaSelect();
2258 2283 }
2259 2284 });
2260 2285 });
@@ -2270,7 +2295,7 @@ cameraInput?.addEventListener("click", (e) => {
2270 2295 }
2271 2296 });
2272 2297
2273 imageInput?.addEventListener("click", (e) => {
2298 imageSelect?.addEventListener("click", (e) => {
2274 2299 if (window?.pywebview) {
2275 2300 e.preventDefault();
2276 2301 pywebview.api.choose_image();
Modified g4f/models.py +2 -2
@@ -883,8 +883,8 @@ demo_models = {
883 883 qwq_32b.name: [qwq_32b, [HuggingFace]],
884 884 llama_3_3_70b.name: [llama_3_3_70b, [HuggingFace]],
885 885 sd_3_5.name: [sd_3_5, [HuggingSpace, HuggingFace]],
886 flux_dev.name: [flux_dev, [PollinationsImage, HuggingSpace, HuggingFace, G4F]],
887 flux_schnell.name: [flux_schnell, [PollinationsImage, HuggingFace, HuggingSpace, G4F]],
886 flux_dev.name: [flux_dev, [PollinationsImage, HuggingFace, HuggingSpace]],
887 flux_schnell.name: [flux_schnell, [PollinationsImage, HuggingFace, HuggingSpace]],
888 888 }
889 889
890 890 # Create a list of all models and his providers
Modified g4f/providers/asyncio.py +1 -2
@@ -41,11 +41,10 @@ async def async_generator_to_list(generator: AsyncIterator) -> list:
41 41 return [item async for item in generator]
42 42
43 43 def to_sync_generator(generator: AsyncIterator, stream: bool = True) -> Iterator:
44 loop = get_running_loop(check_nested=False)
44 45 if not stream:
45 46 yield from asyncio.run(async_generator_to_list(generator))
46 47 return
47
48 loop = get_running_loop(check_nested=False)
49 48 new_loop = False
50 49 if loop is None:
51 50 loop = asyncio.new_event_loop()
Modified g4f/providers/response.py +13 -4
@@ -19,8 +19,7 @@ def quote_url(url: str) -> str:
19 19
20 20 def quote_title(title: str) -> str:
21 21 if title:
22 title = " ".join(title.split())
23 return title.replace('[', '').replace(']', '')
22 return " ".join(title.split())
24 23 return ""
25 24
26 25 def format_link(url: str, title: str = None) -> str:
@@ -161,11 +160,21 @@ class Sources(ResponseType):
161 160 self.list.append(source)
162 161
163 162 def __str__(self) -> str:
164 return "\n\n" + ("\n".join([
165 f"{idx+1}. {format_link(link['url'], link.get('title', None))}"
163 return "\n\n\n\n" + ("\n>\n".join([
164 f"> [{idx}] {format_link(link['url'], link.get('title', None))}"
166 165 for idx, link in enumerate(self.list)
167 166 ]))
168 167
168 class YouTube(ResponseType):
169 def __init__(self, ids: list[str]) -> None:
170 self.ids = ids
171
172 def __str__(self) -> str:
173 return "\n\n" + ("\n".join([
174 f'<iframe type="text/html" src="https://www.youtube.com/embed/{id}"></iframe>'
175 for id in self.ids
176 ]))
177
169 178 class BaseConversation(ResponseType):
170 179 def __str__(self) -> str:
171 180 return ""
Modified g4f/tools/run_tools.py +16 -3
@@ -10,7 +10,7 @@ from typing import Optional, Callable, AsyncIterator
10 10 from ..typing import Messages
11 11 from ..providers.helper import filter_none
12 12 from ..providers.asyncio import to_async_iterator
13 from ..providers.response import Reasoning
13 from ..providers.response import Reasoning, FinishReason
14 14 from ..providers.types import ProviderType
15 15 from ..cookies import get_cookies_dir
16 16 from .web_search import do_search, get_search_message
@@ -38,11 +38,12 @@ def get_api_key_file(cls) -> Path:
38 38 async def async_iter_run_tools(provider: ProviderType, model: str, messages, tool_calls: Optional[list] = None, **kwargs):
39 39 # Handle web_search from kwargs
40 40 web_search = kwargs.get('web_search')
41 sources = None
41 42 if web_search:
42 43 try:
43 44 messages = messages.copy()
44 45 web_search = web_search if isinstance(web_search, str) and web_search != "true" else None
45 messages[-1]["content"] = await do_search(messages[-1]["content"], web_search)
46 messages[-1]["content"], sources = await do_search(messages[-1]["content"], web_search)
46 47 except Exception as e:
47 48 debug.error(f"Couldn't do web search: {e.__class__.__name__}: {e}")
48 49 # Keep web_search in kwargs for provider native support
@@ -88,6 +89,8 @@ async def async_iter_run_tools(provider: ProviderType, model: str, messages, too
88 89 response = to_async_iterator(create_function(model=model, messages=messages, **kwargs))
89 90 async for chunk in response:
90 91 yield chunk
92 if sources is not None:
93 yield sources
91 94
92 95 def process_thinking_chunk(chunk: str, start_time: float = 0) -> tuple[float, list]:
93 96 """Process a thinking chunk and return timing and results."""
@@ -144,11 +147,12 @@ def iter_run_tools(
144 147 ) -> AsyncIterator:
145 148 # Handle web_search from kwargs
146 149 web_search = kwargs.get('web_search')
150 sources = None
147 151 if web_search:
148 152 try:
149 153 messages = messages.copy()
150 154 web_search = web_search if isinstance(web_search, str) and web_search != "true" else None
151 messages[-1]["content"] = asyncio.run(do_search(messages[-1]["content"], web_search))
155 messages[-1]["content"], sources = asyncio.run(do_search(messages[-1]["content"], web_search))
152 156 except Exception as e:
153 157 debug.error(f"Couldn't do web search: {e.__class__.__name__}: {e}")
154 158 # Keep web_search in kwargs for provider native support
@@ -198,6 +202,12 @@ def iter_run_tools(
198 202
199 203 thinking_start_time = 0
200 204 for chunk in iter_callback(model=model, messages=messages, provider=provider, **kwargs):
205 if isinstance(chunk, FinishReason):
206 if sources is not None:
207 yield sources
208 sources = None
209 yield chunk
210 continue
201 211 if not isinstance(chunk, str):
202 212 yield chunk
203 213 continue
@@ -206,3 +216,6 @@ def iter_run_tools(
206 216
207 217 for result in results:
208 218 yield result
219
220 if sources is not None:
221 yield sources
Modified g4f/tools/web_search.py +34 -13