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

XFEstudio/gpt4free

Update model list in OpenaiChat (o3-mini, o3-mini-high) Add Reasoning to OpenaiChat provider Check for pipeline_tag in HuggingChat providers Add image preview in PollinationsAI Add input of custom Model in GUI

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

代码差异

13 个文件 +257 -110
Modified g4f/Provider/Blackbox.py +1 -8
@@ -308,14 +308,7 @@ class Blackbox(AsyncGeneratorProvider, ProviderModelMixin):
308 308 image_url = image_url_match.group(1)
309 309 yield ImageResponse(image_url, format_image_prompt(messages, prompt))
310 310 else:
311 if "<think>" in text_to_yield and "</think>" in text_to_yield:
312 parts = text_to_yield.split('<think>', 1)
313 yield parts[0]
314 reasoning_parts = parts[1].split('</think>', 1)
315 yield Reasoning(f"<think>{reasoning_parts[0]}</think>")
316 yield reasoning_parts[1]
317 full_response = text_to_yield
318 elif "Generated by BLACKBOX.AI" in text_to_yield:
311 if "Generated by BLACKBOX.AI" in text_to_yield:
319 312 conversation.validated_value = await cls.fetch_validated(force_refresh=True)
320 313 if conversation.validated_value:
321 314 data["validated"] = conversation.validated_value
Modified g4f/Provider/PollinationsAI.py +14 -8
@@ -13,7 +13,7 @@ from ..typing import AsyncResult, Messages, ImagesType
13 13 from ..image import to_data_uri
14 14 from ..requests.raise_for_status import raise_for_status
15 15 from ..requests.aiohttp import get_connector
16 from ..providers.response import ImageResponse, FinishReason, Usage
16 from ..providers.response import ImageResponse, ImagePreview, FinishReason, Usage
17 17
18 18 DEFAULT_HEADERS = {
19 19 'Accept': '*/*',
@@ -125,7 +125,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
125 125 # Check if models
126 126 # Image generation
127 127 if model in cls.image_models:
128 yield await cls._generate_image(
128 async for chunk in cls._generate_image(
129 129 model=model,
130 130 prompt=format_image_prompt(messages, prompt),
131 131 proxy=proxy,
@@ -136,7 +136,8 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
136 136 private=private,
137 137 enhance=enhance,
138 138 safe=safe
139 )
139 ):
140 yield chunk
140 141 else:
141 142 # Text generation
142 143 async for result in cls._generate_text(
@@ -167,7 +168,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
167 168 private: bool,
168 169 enhance: bool,
169 170 safe: bool
170 ) -> ImageResponse:
171 ) -> AsyncResult:
171 172 params = {
172 173 "seed": seed,
173 174 "width": width,
@@ -178,11 +179,16 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
178 179 "enhance": enhance,
179 180 "safe": safe
180 181 }
181 params = {k: json.dumps(v) if isinstance(v, bool) else v for k, v in params.items() if v is not None}
182 params = {k: json.dumps(v) if isinstance(v, bool) else str(v) for k, v in params.items() if v is not None}
183 params = "&".join( "%s=%s" % (key, quote_plus(params[key]))
184 for key in params.keys())
185 url = f"{cls.image_api_endpoint}prompt/{quote_plus(prompt)}?{params}"
186 yield ImagePreview(url, prompt)
182 187 async with ClientSession(headers=DEFAULT_HEADERS, connector=get_connector(proxy=proxy)) as session:
183 async with session.head(f"{cls.image_api_endpoint}prompt/{quote_plus(prompt)}", params=params) as response:
184 await raise_for_status(response)
185 return ImageResponse(str(response.url), prompt)
188 async with session.head(url) as response:
189 if response.status != 500: # Server is busy
190 await raise_for_status(response)
191 yield ImageResponse(str(response.url), prompt)
186 192
187 193 @classmethod
188 194 async def _generate_text(
Modified g4f/Provider/hf/HuggingFaceAPI.py +23 -2
@@ -1,8 +1,11 @@
1 1 from __future__ import annotations
2 2
3 from ...providers.types import Messages
4 from ...typing import ImagesType
5 from ...requests import StreamSession, raise_for_status
6 from ...errors import ModelNotSupportedError
3 7 from ..template.OpenaiTemplate import OpenaiTemplate
4 8 from .models import model_aliases
5 from ...providers.types import Messages
6 9 from .HuggingChat import HuggingChat
7 10 from ... import debug
8 11
@@ -37,6 +40,10 @@ class HuggingFaceAPI(OpenaiTemplate):
37 40 api_base: str = None,
38 41 max_tokens: int = 2048,
39 42 max_inputs_lenght: int = 10000,
43 impersonate: str = None,
44 proxy: str = None,
45 timeout: int = 300,
46 images: ImagesType = None,
40 47 **kwargs
41 48 ):
42 49 if api_base is None:
@@ -44,6 +51,20 @@ class HuggingFaceAPI(OpenaiTemplate):
44 51 if model in cls.model_aliases:
45 52 model_name = cls.model_aliases[model]
46 53 api_base = f"https://api-inference.huggingface.co/models/{model_name}/v1"
54 if images is not None:
55 async with StreamSession(
56 proxy=proxy,
57 timeout=timeout,
58 impersonate=impersonate,
59 ) as session:
60 async with session.get(f"https://huggingface.co/api/models/{model}") as response:
61 if response.status == 404:
62 raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__}")
63 await raise_for_status(response)
64 model_data = await response.json()
65 pipeline_tag = model_data.get("pipeline_tag")
66 if pipeline_tag != "image-text-to-text":
67 raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__} pipeline_tag={pipeline_tag}")
47 68 start = calculate_lenght(messages)
48 69 if start > max_inputs_lenght:
49 70 if len(messages) > 6:
@@ -54,7 +75,7 @@ class HuggingFaceAPI(OpenaiTemplate):
54 75 if len(messages) > 1 and calculate_lenght(messages) > max_inputs_lenght:
55 76 messages = [messages[-1]]
56 77 debug.log(f"Messages trimmed from: {start} to: {calculate_lenght(messages)}")
57 async for chunk in super().create_async_generator(model, messages, api_base=api_base, max_tokens=max_tokens, **kwargs):
78 async for chunk in super().create_async_generator(model, messages, api_base=api_base, max_tokens=max_tokens, images=images, **kwargs):
58 79 yield chunk
59 80
60 81 def calculate_lenght(messages: Messages) -> int:
Modified g4f/Provider/hf/HuggingFaceInference.py +30 -27
@@ -78,18 +78,13 @@ class HuggingFaceInference(AsyncGeneratorProvider, ProviderModelMixin):
78 78 if api_key is not None:
79 79 headers["Authorization"] = f"Bearer {api_key}"
80 80 payload = None
81 if cls.get_models() and model in cls.image_models:
82 stream = False
83 prompt = format_image_prompt(messages, prompt)
84 payload = {"inputs": prompt, "parameters": {"seed": random.randint(0, 2**32), **extra_data}}
85 else:
86 params = {
87 "return_full_text": False,
88 "max_new_tokens": max_tokens,
89 "temperature": temperature,
90 **extra_data
91 }
92 do_continue = action == "continue"
81 params = {
82 "return_full_text": False,
83 "max_new_tokens": max_tokens,
84 "temperature": temperature,
85 **extra_data
86 }
87 do_continue = action == "continue"
93 88 async with StreamSession(
94 89 headers=headers,
95 90 proxy=proxy,
@@ -101,22 +96,30 @@ class HuggingFaceInference(AsyncGeneratorProvider, ProviderModelMixin):
101 96 raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__}")
102 97 await raise_for_status(response)
103 98 model_data = await response.json()
104 model_type = None
105 if "config" in model_data and "model_type" in model_data["config"]:
106 model_type = model_data["config"]["model_type"]
107 debug.log(f"Model type: {model_type}")
108 inputs = get_inputs(messages, model_data, model_type, do_continue)
109 debug.log(f"Inputs len: {len(inputs)}")
110 if len(inputs) > 4096:
111 if len(messages) > 6:
112 messages = messages[:3] + messages[-3:]
113 else:
114 messages = [m for m in messages if m["role"] == "system"] + [messages[-1]]
99 pipeline_tag = model_data.get("pipeline_tag")
100 if pipeline_tag == "text-to-image":
101 stream = False
102 inputs = format_image_prompt(messages, prompt)
103 payload = {"inputs": inputs, "parameters": {"seed": random.randint(0, 2**32), **extra_data}}
104 elif pipeline_tag in ("text-generation", "image-text-to-text"):
105 model_type = None
106 if "config" in model_data and "model_type" in model_data["config"]:
107 model_type = model_data["config"]["model_type"]
108 debug.log(f"Model type: {model_type}")
115 109 inputs = get_inputs(messages, model_data, model_type, do_continue)
116 debug.log(f"New len: {len(inputs)}")
117 if model_type == "gpt2" and max_tokens >= 1024:
118 params["max_new_tokens"] = 512
119 payload = {"inputs": inputs, "parameters": params, "stream": stream}
110 debug.log(f"Inputs len: {len(inputs)}")
111 if len(inputs) > 4096:
112 if len(messages) > 6:
113 messages = messages[:3] + messages[-3:]
114 else:
115 messages = [m for m in messages if m["role"] == "system"] + [messages[-1]]
116 inputs = get_inputs(messages, model_data, model_type, do_continue)
117 debug.log(f"New len: {len(inputs)}")
118 if model_type == "gpt2" and max_tokens >= 1024:
119 params["max_new_tokens"] = 512
120 payload = {"inputs": inputs, "parameters": params, "stream": stream}
121 else:
122 raise ModelNotSupportedError(f"Model is not supported: {model} in: {cls.__name__} pipeline_tag: {pipeline_tag}")
120 123
121 124 async with session.post(f"{api_base.rstrip('/')}/models/{model}", json=payload) as response:
122 125 if response.status == 404:
Modified g4f/Provider/needs_auth/OpenaiChat.py +19 -10
@@ -25,8 +25,9 @@ from ...requests import get_nodriver
25 25 from ...image import ImageResponse, ImageRequest, to_image, to_bytes, is_accepted_format
26 26 from ...errors import MissingAuthError, NoValidHarFileError
27 27 from ...providers.response import JsonConversation, FinishReason, SynthesizeData, AuthResult
28 from ...providers.response import Sources, TitleGeneration, RequestLogin, Parameters
28 from ...providers.response import Sources, TitleGeneration, RequestLogin, Parameters, Reasoning
29 29 from ..helper import format_cookies
30 from ..openai.models import default_model, default_image_model, models, image_models, text_models
30 31 from ..openai.har_file import get_request_config
31 32 from ..openai.har_file import RequestConfig, arkReq, arkose_url, start_url, conversation_url, backend_url, backend_anon_url
32 33 from ..openai.proofofwork import generate_proof_token
@@ -95,12 +96,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
95 96 supports_gpt_4 = True
96 97 supports_message_history = True
97 98 supports_system_message = True
98 default_model = "auto"
99 default_image_model = "dall-e-3"
100 image_models = [default_image_model]
101 text_models = [default_model, "gpt-4", "gpt-4o", "gpt-4o-mini", "o1", "o1-preview", "o1-mini"]
99 default_model = default_model
100 default_image_model = default_image_model
101 image_models = image_models
102 102 vision_models = text_models
103 models = text_models + image_models
103 models = models
104 104 synthesize_content_type = "audio/mpeg"
105 105
106 106 _api_key: str = None
@@ -368,9 +368,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
368 368 )
369 369 [debug.log(text) for text in (
370 370 #f"Arkose: {'False' if not need_arkose else auth_result.arkose_token[:12]+'...'}",
371 f"Proofofwork: {'False' if proofofwork is None else proofofwork[:12]+'...'}",
372 f"AccessToken: {'False' if cls._api_key is None else cls._api_key[:12]+'...'}",
371 #f"Proofofwork: {'False' if proofofwork is None else proofofwork[:12]+'...'}",
372 #f"AccessToken: {'False' if cls._api_key is None else cls._api_key[:12]+'...'}",
373 373 )]
374 if action == "continue" and conversation.message_id is None:
375 action = "next"
374 376 data = {
375 377 "action": action,
376 378 "parent_message_id": conversation.message_id,
@@ -497,7 +499,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
497 499 v = line.get("v")
498 500 if isinstance(v, str) and fields.is_recipient:
499 501 if "p" not in line or line.get("p") == "/message/content/parts/0":
500 yield v
502 yield Reasoning(token=v) if fields.is_thinking else v
501 503 elif isinstance(v, list):
502 504 for m in v:
503 505 if m.get("p") == "/message/content/parts/0" and fields.is_recipient:
@@ -508,6 +510,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
508 510 sources.add_source(link)
509 511 elif re.match(r"^/message/metadata/content_references/\d+$", m.get("p")):
510 512 sources.add_source(m.get("v"))
513 elif m.get("p") == "/message/metadata/finished_text":
514 fields.is_thinking = False
515 yield Reasoning(status=m.get("v"))
511 516 elif m.get("p") == "/message/metadata":
512 517 fields.finish_reason = m.get("v", {}).get("finish_details", {}).get("type")
513 518 break
@@ -519,6 +524,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
519 524 fields.is_recipient = m.get("recipient", "all") == "all"
520 525 if fields.is_recipient:
521 526 c = m.get("content", {})
527 if c.get("content_type") == "text" and m.get("author", {}).get("role") == "tool" and "initial_text" in m.get("metadata", {}):
528 fields.is_thinking = True
529 yield Reasoning(status=c.get("metadata", {}).get("initial_text"))
522 530 if c.get("content_type") == "multimodal_text":
523 531 generated_images = []
524 532 for element in c.get("parts"):
@@ -697,13 +705,14 @@ class Conversation(JsonConversation):
697 705 """
698 706 Class to encapsulate response fields.
699 707 """
700 def __init__(self, conversation_id: str = None, message_id: str = None, user_id: str = None, finish_reason: str = None, parent_message_id: str = None):
708 def __init__(self, conversation_id: str = None, message_id: str = None, user_id: str = None, finish_reason: str = None, parent_message_id: str = None, is_thinking: bool = False):
701 709 self.conversation_id = conversation_id
702 710 self.message_id = message_id
703 711 self.finish_reason = finish_reason
704 712 self.is_recipient = False
705 713 self.parent_message_id = message_id if parent_message_id is None else parent_message_id
706 714 self.user_id = user_id
715 self.is_thinking = is_thinking
707 716
708 717 def get_cookies(
709 718 urls: Optional[Iterator[str]] = None
Added g4f/Provider/openai/models.py +6 -0
@@ -0,0 +1,6 @@
1 default_model = "auto"
2 default_image_model = "dall-e-3"
3 image_models = [default_image_model]
4 text_models = [default_model, "gpt-4", "gpt-4o", "gpt-4o-mini", "o1", "o1-preview", "o1-mini", "o3-mini", "o3-mini-high"]
5 vision_models = text_models
6 models = text_models + image_models
Modified g4f/api/__init__.py +18 -11
@@ -10,6 +10,7 @@ from email.utils import formatdate
10 10 import os.path
11 11 import hashlib
12 12 import asyncio
13 from urllib.parse import quote_plus
13 14 from fastapi import FastAPI, Response, Request, UploadFile, Depends
14 15 from fastapi.middleware.wsgi import WSGIMiddleware
15 16 from fastapi.responses import StreamingResponse, RedirectResponse, HTMLResponse, JSONResponse
@@ -176,11 +177,11 @@ class Api:
176 177 return ErrorResponse.from_message("G4F API key required", HTTP_401_UNAUTHORIZED)
177 178 if not secrets.compare_digest(AppConfig.g4f_api_key, user_g4f_api_key):
178 179 return ErrorResponse.from_message("Invalid G4F API key", HTTP_403_FORBIDDEN)
179 elif not AppConfig.demo:
180 if user_g4f_api_key is not None and path.startswith("/images/"):
180 elif not AppConfig.demo and not path.startswith("/images/"):
181 if user_g4f_api_key is not None:
181 182 if not secrets.compare_digest(AppConfig.g4f_api_key, user_g4f_api_key):
182 183 return ErrorResponse.from_message("Invalid G4F API key", HTTP_403_FORBIDDEN)
183 elif path.startswith("/backend-api/") or path.startswith("/images/") or path.startswith("/chat/") and path != "/chat/":
184 elif path.startswith("/backend-api/") or path.startswith("/chat/") and path != "/chat/":
184 185 try:
185 186 username = await self.get_username(request)
186 187 except HTTPException as e:
@@ -551,8 +552,8 @@ class Api:
551 552 HTTP_404_NOT_FOUND: {}
552 553 })
553 554 async def get_image(filename, request: Request):
554 target = os.path.join(images_dir, filename)
555 ext = os.path.splitext(filename)[1]
555 target = os.path.join(images_dir, quote_plus(filename))
556 ext = os.path.splitext(filename)[1][1:]
556 557 stat_result = SimpleNamespace()
557 558 stat_result.st_size = 0
558 559 if os.path.isfile(target):
@@ -560,10 +561,12 @@ class Api:
560 561 stat_result.st_mtime = int(f"{filename.split('_')[0]}") if filename.startswith("1") else 0
561 562 headers = {
562 563 "cache-control": "public, max-age=31536000",
563 "content-type": f"image/{ext.replace('jpg', 'jpeg')[1:] or 'jpeg'}",
564 "content-length": str(stat_result.st_size),
564 "content-type": f"image/{ext.replace('jpg', 'jpeg') or 'jpeg'}",
565 565 "last-modified": formatdate(stat_result.st_mtime, usegmt=True),
566 566 "etag": f'"{hashlib.md5(filename.encode()).hexdigest()}"',
567 **({
568 "content-length": str(stat_result.st_size),
569 } if stat_result.st_size else {})
567 570 }
568 571 response = FileResponse(
569 572 target,
@@ -583,10 +586,14 @@ class Api:
583 586 source_url = source_url[1]
584 587 source_url = source_url.replace("%2F", "/").replace("%3A", ":").replace("%3F", "?").replace("%3D", "=")
585 588 if source_url.startswith("https://"):
586 await copy_images(
587 [source_url],
588 target=target)
589 debug.log(f"Image copied from {source_url}")
589 try:
590 await copy_images(
591 [source_url],
592 target=target)
593 debug.log(f"Image copied from {source_url}")
594 except Exception as e:
595 debug.log(f"{type(e).__name__}: Download failed: {source_url}\n{e}")
596 return RedirectResponse(url=source_url)
590 597 if not os.path.isfile(target):
591 598 return ErrorResponse.from_message("File not found", HTTP_404_NOT_FOUND)
592 599 async def stream():
Modified g4f/gui/client/index.html +16 -6
@@ -47,12 +47,13 @@
47 47 gallery: '#messages',
48 48 children: 'a:has(img)',
49 49 secondaryZoomLevel: 2,
50 allowPanToNext: true,
50 51 pswpModule: () => import('https://cdn.jsdelivr.net/npm/photoswipe'),
51 52 });
52 53 lightbox.addFilter('itemData', (itemData, index) => {
53 54 const img = itemData.element.querySelector('img');
54 itemData.width = img.naturalWidth;
55 itemData.height = img.naturalHeight;
55 itemData.width = img.naturalWidth || 1024;
56 itemData.height = img.naturalHeight || 1024;
56 57 return itemData;
57 58 });
58 59 lightbox.on('uiRegister', function() {
@@ -66,7 +67,13 @@
66 67 lightbox.pswp.on('change', () => {
67 68 const currSlideElement = lightbox.pswp.currSlide.data.element;
68 69 if (currSlideElement) {
69 el.innerText = currSlideElement.querySelector('img').getAttribute('alt');
70 const img = currSlideElement.querySelector('img');
71 el.innerText = img.getAttribute('alt');
72 const download = document.createElement("a");
73 download.setAttribute("href", img.getAttribute('src'));
74 download.setAttribute("download", `${img.getAttribute('alt')}${lightbox.pswp.currSlide.index}.jpg`);
75 download.innerHTML = '<i class="fa-solid fa-download"></i>';
76 el.appendChild(download);
70 77 }
71 78 });
72 79 }
@@ -157,8 +164,8 @@
157 164 <label for="report_error" class="toogle" title=""></label>
158 165 </div>
159 166 <div class="field box">
160 <label for="systemPrompt" class="label" title="">System prompt</label>
161 <textarea id="systemPrompt" placeholder="You are a helpful assistant."></textarea>
167 <label for="systemPrompt" class="label">System prompt</label>
168 <textarea id="systemPrompt" placeholder="You are a helpful assistant." data-value="If you need to generate images, you can use the following format: ![keywords](/generate/filename.jpg). This will enable the use of an image generation tool."></textarea>
162 169 </div>
163 170 <div class="field box">
164 171 <label for="message-input-height" class="label" title="">Input max. height</label>
@@ -269,6 +276,7 @@
269 276 <div id="send-button">
270 277 <i class="fa-solid fa-square-plus"></i>
271 278 <i class="fa-regular fa-paper-plane"></i>
279 <a href="" id="download" class="hidden"></a>
272 280 </div>
273 281 </div>
274 282 </div>
@@ -293,7 +301,8 @@
293 301 <option value="dall-e-3">dall-e-3 (Image Generation)</option>
294 302 <option disabled="disabled">----</option>
295 303 </select>
296 <select name="model2" id="model2" class="hidden"></select>
304 <select name="model2" id="model2" class="hidden model"></select>
305 <input type="text" id="model3" value="" class="hidden model" placeholder="Model:"/>
297 306 </div>
298 307 <div class="field">
299 308 <select name="provider" id="provider">
@@ -303,6 +312,7 @@
303 312 <option value="Gemini">Google Gemini</option>
304 313 <option value="DDG">DuckDuckGo AI Chat</option>
305 314 <option value="Blackbox">Blackbox AI</option>
315 <option value="Custom Model">Custom Model</option>
306 316 <option disabled="disabled">----</option>
307 317 </select>
308 318 </div>
Modified g4f/gui/client/static/css/style.css +4 -3
@@ -799,7 +799,7 @@ form input:checked+label:after {
799 799 color: var(--colour-3);
800 800 }
801 801
802 select {
802 select, input.model {
803 803 border-radius: 8px;
804 804 backdrop-filter: blur(20px);
805 805 cursor: pointer;
@@ -871,6 +871,7 @@ button.regenerate_button, button.continue_button, button.options_button {
871 871 }
872 872
873 873 select:hover,
874 input.model:hover
874 875 .buttons button:hover,
875 876 .stop_generating button:hover,
876 877 .toolbar .regenerate button:hover,
@@ -948,7 +949,7 @@ select:hover,
948 949 }
949 950
950 951 @media only screen and (min-width: 40em) {
951 select {
952 select, input.model {
952 953 width: 200px;
953 954 }
954 955 .field {
@@ -1446,7 +1447,7 @@ form .field.saved .fa-xmark {
1446 1447 max-height: 200px;
1447 1448 }
1448 1449
1449 .hidden {
1450 .hidden, input.hidden {
1450 1451 display: none;
1451 1452 }
1452 1453
Modified g4f/gui/client/static/img/site.webmanifest +1 -1
@@ -17,7 +17,7 @@
17 17 "background_color": "#ffffff",
18 18 "display": "standalone",
19 19 "share_target": {
20 "action": "/chat/",
20 "action": "/chat/share",
21 21 "method": "GET",
22 22 "enctype": "application/x-www-form-urlencoded",
23 23 "params": {
Modified g4f/gui/client/static/js/chat.v1.js +104 -25
@@ -15,6 +15,7 @@ const inputCount = document.getElementById("input-count").querySelector("
15 15 const providerSelect = document.getElementById("provider");
16 16 const modelSelect = document.getElementById("model");
17 17 const modelProvider = document.getElementById("model2");
18 const custom_model = document.getElementById("model3");
18 19 const chatPrompt = document.getElementById("chatPrompt");
19 20 const settings = document.querySelector(".settings");
20 21 const chat = document.querySelector(".conversation");
@@ -78,18 +79,28 @@ function render_reasoning(reasoning, final = false) {
78 79 </div>` : "";
79 80 return `<div class="reasoning_body">
80 81 <div class="reasoning_title">
81 <strong>Reasoning <i class="fa-solid fa-brain"></i>:</strong> ${escapeHtml(reasoning.status)}
82 <strong>Reasoning <i class="brain">🧠</i>:</strong> ${escapeHtml(reasoning.status)}
82 83 </div>
83 84 ${inner_text}
84 85 </div>`;
85 86 }
86 87
88 function render_reasoning_text(reasoning) {
89 return `Reasoning 🧠: ${reasoning.status}\n\n${reasoning.text}\n\n`;
90 }
91
87 92 function filter_message(text) {
88 93 return text.replaceAll(
89 94 /<!-- generated images start -->[\s\S]+<!-- generated images end -->/gm, ""
90 95 ).replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "");
91 96 }
92 97
98 function filter_message_content(text) {
99 return text.replaceAll(
100 /\/\]\(\/generate\//gm, "/](/images/"
101 ).replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "")
102 }
103
93 104 function fallback_clipboard (text) {
94 105 var textBox = document.createElement("textarea");
95 106 textBox.value = text;
@@ -182,6 +193,53 @@ const get_message_el = (el) => {
182 193 return message_el;
183 194 }
184 195
196 function register_message_images() {
197 message_box.querySelectorAll(`.loading-indicator`).forEach((el) => el.remove());
198 message_box.querySelectorAll(`.message img:not([alt="your avatar"])`).forEach(async (el) => {
199 if (!el.complete) {
200 const indicator = document.createElement("span");
201 indicator.classList.add("loading-indicator");
202 indicator.innerHTML = `<i class="fas fa-spinner fa-spin"></i>`;
203 el.parentElement.appendChild(indicator);
204 el.onerror = () => {
205 let indexCommand;
206 if ((indexCommand = el.src.indexOf("/generate/")) >= 0) {
207 indexCommand = indexCommand + "/generate/".length + 1;
208 let newPath = el.src.substring(indexCommand)
209 let filename = newPath.replace(/(?:\?.+?|$)/, "");
210 let seed = Math.floor(Date.now() / 1000);
211 newPath = `https://image.pollinations.ai/prompt/${newPath}?seed=${seed}&nologo=true`;
212 let downloadUrl = newPath;
213 if (document.getElementById("download_images")?.checked) {
214 downloadUrl = `/images/${filename}?url=${escapeHtml(newPath)}`;
215 }
216 const link = document.createElement("a");
217 link.setAttribute("href", newPath);
218 const newImg = document.createElement("img");
219 newImg.src = downloadUrl;
220 newImg.alt = el.alt;
221 newImg.onload = () => {
222 lazy_scroll_to_bottom();
223 indicator.remove();
224 }
225 link.appendChild(newImg);
226 el.parentElement.appendChild(link);
227 } else {
228 const span = document.createElement("span");
229 span.innerHTML = `<i class="fa-solid fa-plug"></i>${escapeHtml(el.alt)}`;
230 el.parentElement.appendChild(span);
231 }
232 el.remove();
233 indicator.remove();
234 }
235 el.onload = () => {
236 indicator.remove();
237 lazy_scroll_to_bottom();
238 }
239 }
240 });
241 }
242
185 243 const register_message_buttons = async () => {
186 244 message_box.querySelectorAll(".message .content .provider").forEach(async (el) => {
187 245 if (!("click" in el.dataset)) {
@@ -243,24 +301,22 @@ const register_message_buttons = async () => {
243 301 message_box.querySelectorAll(".message .fa-file-export").forEach(async (el) => {
244 302 if (!("click" in el.dataset)) {
245 303 el.dataset.click = "true";
304 //
246 305 el.addEventListener("click", async () => {
247 306 const elem = window.document.createElement('a');
248 307 let filename = `chat ${new Date().toLocaleString()}.md`.replaceAll(":", "-");
249 308 const conversation = await get_conversation(window.conversation_id);
250 309 let buffer = "";
251 310 conversation.items.forEach(message => {
311 buffer += render_reasoning_text(message.reasoning);
252 312 buffer += `${message.role == 'user' ? 'User' : 'Assistant'}: ${message.content.trim()}\n\n\n`;
253 313 });
254 const file = new File([buffer.trim()], 'message.md', {type: 'text/plain'});
255 const objectUrl = URL.createObjectURL(file);
256 elem.href = objectUrl;
257 elem.download = filename;
258 document.body.appendChild(elem);
259 elem.click();
260 document.body.removeChild(elem);
314 var download = document.getElementById("download");
315 download.setAttribute("href", "data:text/markdown;charset=utf-8," + encodeURIComponent(buffer.trim()));
316 download.setAttribute("download", filename);
317 download.click();
261 318 el.classList.add("clicked");
262 319 setTimeout(() => el.classList.remove("clicked"), 1000);
263 URL.revokeObjectURL(objectUrl);
264 320 })
265 321 }
266 322 });
@@ -376,7 +432,7 @@ const handle_ask = async (do_ask_gpt = true) => {
376 432 messageInput.focus();
377 433 await scroll_to_bottom();
378 434
379 let message = messageInput.value;
435 let message = messageInput.value.trim();
380 436 if (message.length <= 0) {
381 437 return;
382 438 }
@@ -755,6 +811,7 @@ async function add_message_chunk(message, message_id, provider, scroll, finish_m
755 811 if (!img.complete)
756 812 return;
757 813 content_map.inner.innerHTML = markdown_render(message.preview);
814 await register_message_images();
758 815 } else if (message.type == "content") {
759 816 message_storage[message_id] += message.content;
760 817 update_message(content_map, message_id, null, scroll);
@@ -779,7 +836,7 @@ async function add_message_chunk(message, message_id, provider, scroll, finish_m
779 836 } else if (message.type == "reasoning") {
780 837 if (!reasoning_storage[message_id]) {
781 838 reasoning_storage[message_id] = message;
782 reasoning_storage[message_id].text = message.token || "";
839 reasoning_storage[message_id].text = "";
783 840 } else if (message.status) {
784 841 reasoning_storage[message_id].status = message.status;
785 842 } else if (message.token) {
@@ -952,6 +1009,7 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
952 1009 }
953 1010 await safe_remove_cancel_button();
954 1011 await register_message_buttons();
1012 await register_message_images();
955 1013 await load_conversations();
956 1014 regenerate_button.classList.remove("regenerate-hidden");
957 1015 }
@@ -1201,8 +1259,8 @@ const load_conversation = async (conversation_id, scroll=true) => {
1201 1259 } else {
1202 1260 buffer = "";
1203 1261 }
1204 buffer = buffer.replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "");
1205 new_content = item.content.replace(/ \[aborted\]$/g, "").replace(/ \[error\]$/g, "");
1262 buffer = filter_message_content(buffer);
1263 new_content = filter_message_content(item.content);
1206 1264 buffer = merge_messages(buffer, new_content);
1207 1265 last_model = item.provider?.model;
1208 1266 providers.push(item.provider?.name);
@@ -1658,12 +1716,9 @@ const register_settings_storage = async () => {
1658 1716 const load_settings_storage = async () => {
1659 1717 const optionElements = document.querySelectorAll(optionElementsSelector);
1660 1718 optionElements.forEach((element) => {
1661 if (element.name && element.name != element.id && (value = appStorage.getItem(element.name))) {
1662 appStorage.setItem(element.id, value);
1663 appStorage.removeItem(element.name);
1664 }
1665 if (!(value = appStorage.getItem(element.id))) {
1666 return;
1719 value = appStorage.getItem(element.id);
1720 if (value == null && element.dataset.value) {
1721 value = element.dataset.value;
1667 1722 }
1668 1723 if (value) {
1669 1724 switch (element.type) {
@@ -1677,10 +1732,10 @@ const load_settings_storage = async () => {
1677 1732 case "number":
1678 1733 case "textarea":
1679 1734 if (element.id.endsWith("-api_key")) {
1680 element.placeholder = value && value.length >= 22 ? (value.substring(0, 12)+"*".repeat(12)+value.substring(value.length-12)) : "*".repeat(value.length);
1735 element.placeholder = value && value.length >= 22 ? (value.substring(0, 12)+"*".repeat(12)+value.substring(value.length-12)) : "*".repeat(value ? value.length : 0);
1681 1736 element.dataset.value = value;
1682 1737 } else {
1683 element.value = value;
1738 element.value = value == null ? element.dataset.value : value;
1684 1739 }
1685 1740 break;
1686 1741 default:
@@ -1834,7 +1889,7 @@ async function on_load() {
1834 1889 let chat_url = new URL(window.location.href)
1835 1890 let chat_params = new URLSearchParams(chat_url.search);
1836 1891 if (chat_params.get("prompt")) {
1837 messageInput.value = `${chat_params.title}\n${chat_params.prompt}\n${chat_params.url}`.trim();
1892 messageInput.value = `${window.location.href}\n`;
1838 1893 messageInput.style.height = messageInput.scrollHeight + "px";
1839 1894 messageInput.focus();
1840 1895 //await handle_ask();
@@ -2255,7 +2310,9 @@ chatPrompt?.addEventListener("input", async () => {
2255 2310 });
2256 2311
2257 2312 function get_selected_model() {
2258 if (modelProvider.selectedIndex >= 0) {
2313 if (custom_model.value) {
2314 return custom_model;
2315 } else if (modelProvider.selectedIndex >= 0) {
2259 2316 return modelProvider.options[modelProvider.selectedIndex];
2260 2317 } else if (modelSelect.selectedIndex >= 0) {
2261 2318 model = modelSelect.options[modelSelect.selectedIndex];
@@ -2401,17 +2458,31 @@ async function load_provider_models(provider=null) {
2401 2458 if (!provider) {
2402 2459 provider = providerSelect.value;
2403 2460 }
2461 if (!custom_model.value) {
2462 custom_model.classList.add("hidden");
2463 }
2464 if (provider == "Custom Model" || custom_model.value) {
2465 modelProvider.classList.add("hidden");
2466 modelSelect.classList.add("hidden");
2467 document.getElementById("model3").classList.remove("hidden");
2468 return;
2469 }
2404 2470 modelProvider.innerHTML = '';
2405 2471 modelProvider.name = `model[${provider}]`;
2406 2472 if (!provider) {
2407 2473 modelProvider.classList.add("hidden");
2408 2474 modelSelect.classList.remove("hidden");
2475 document.getElementById("model3").value = "";
2476 document.getElementById("model3").classList.remove("hidden");
2409 2477 return;
2410 2478 }
2411 2479 const models = await api('models', provider);
2412 2480 if (models && models.length > 0) {
2413 2481 modelSelect.classList.add("hidden");
2414 modelProvider.classList.remove("hidden");
2482 if (!custom_model.value) {
2483 custom_model.classList.add("hidden");
2484 modelProvider.classList.remove("hidden");
2485 }
2415 2486 let defaultIndex = 0;
2416 2487 models.forEach((model, i) => {
2417 2488 let option = document.createElement('option');
@@ -2423,11 +2494,13 @@ async function load_provider_models(provider=null) {
2423 2494 defaultIndex = i;
2424 2495 }
2425 2496 });
2426 modelProvider.selectedIndex = defaultIndex;
2427 2497 let value = appStorage.getItem(modelProvider.name);
2428 2498 if (value) {
2429 2499 modelProvider.value = value;
2430 2500 }
2501 modelProvider.selectedIndex = defaultIndex;
2502 } else if (custom_model.value) {
2503 modelSelect.classList.add("hidden");
2431 2504 } else {
2432 2505 modelProvider.classList.add("hidden");
2433 2506 modelSelect.classList.remove("hidden");
@@ -2439,6 +2512,12 @@ providerSelect.addEventListener("change", () => {
2439 2512 });
2440 2513 modelSelect.addEventListener("change", () => messageInput.focus());
2441 2514 modelProvider.addEventListener("change", () => messageInput.focus());
2515 custom_model.addEventListener("change", () => {
2516 if (!custom_model.value) {
2517 load_provider_models();
2518 }
2519 messageInput.focus();
2520 });
2442 2521
2443 2522 document.getElementById("pin").addEventListener("click", async () => {
2444 2523 const pin_container = document.getElementById("pin_container");
Modified g4f/gui/server/backend_api.py +1 -1
@@ -276,7 +276,7 @@ class Backend_Api(Api):
276 276 response = iter_run_tools(ChatCompletion.create, **parameters)
277 277
278 278 if do_filter_markdown:
279 return Response(filter_markdown(response, do_filter_markdown), mimetype='text/plain')
279 return Response(filter_markdown("".join([str(chunk) for chunk in response]), do_filter_markdown), mimetype='text/plain')
280 280 def cast_str():
281 281 for chunk in response:
282 282 if not isinstance(chunk, Exception):
Modified g4f/tools/run_tools.py +20 -8