返回提交历史
Modified
README.md
+1
-0
Modified
g4f/Provider/hf/HuggingFaceInference.py
+38
-23
Modified
g4f/Provider/needs_auth/Gemini.py
+94
-12
Modified
g4f/cookies.py
+2
-2
Modified
g4f/gui/client/demo.html
+12
-2
Modified
g4f/gui/client/index.html
+20
-6
Modified
g4f/gui/client/static/css/style.css
+39
-7
Modified
g4f/gui/client/static/js/chat.v1.js
+54
-29
Modified
g4f/models.py
+2
-2
Modified
g4f/providers/asyncio.py
+1
-2
Modified
g4f/providers/response.py
+13
-4
Modified
g4f/tools/run_tools.py
+16
-3
Modified
g4f/tools/web_search.py
+34
-13
XFEstudio/gpt4free
Add new media selection in UI Add HuggingFace provider provider Auto refresh Google Gemini cookies Add sources to search results
1d3a139a
代码差异
13 个文件
+326
-105
@@ -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
@@ -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")
@@ -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
@@ -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}")
@@ -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>
@@ -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>
@@ -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;
@@ -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('<i class="', '<i class="')
79
82
.replaceAll('"></i>', '"></i>')
83
.replaceAll('<iframe type="text/html" src="', '<iframe type="text/html" frameborder="0" src="')
84
.replaceAll('"></iframe>', `?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();
@@ -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
@@ -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()
@@ -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 ""
@@ -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