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

XFEstudio/gpt4free

Add Edge as Browser for nodriver Fix for RetryProviders doesn't retry Add retry and continue for DuckDuckGo provider Add cache for Cloudflare provider Add cache for prompts on gui home Add scroll to bottom checkbox in gui Improve prompts on home gui Fix response content type in api for files

12c413fd
Heiner Lohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

16 个文件 +364 -139
Modified g4f/Provider/Cloudflare.py +17 -2
@@ -2,12 +2,14 @@ from __future__ import annotations
2 2
3 3 import asyncio
4 4 import json
5 from pathlib import Path
5 6
6 7 from ..typing import AsyncResult, Messages, Cookies
7 8 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin, get_running_loop
8 9 from ..requests import Session, StreamSession, get_args_from_nodriver, raise_for_status, merge_cookies
9 10 from ..requests import DEFAULT_HEADERS, has_nodriver, has_curl_cffi
10 11 from ..providers.response import FinishReason
12 from ..cookies import get_cookies_dir
11 13 from ..errors import ResponseStatusError, ModelNotFoundError
12 14
13 15 class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin):
@@ -19,7 +21,7 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin):
19 21 supports_stream = True
20 22 supports_system_message = True
21 23 supports_message_history = True
22 default_model = "@cf/meta/llama-3.1-8b-instruct"
24 default_model = "@cf/meta/llama-3.3-70b-instruct-fp8-fast"
23 25 model_aliases = {
24 26 "llama-2-7b": "@cf/meta/llama-2-7b-chat-fp16",
25 27 "llama-2-7b": "@cf/meta/llama-2-7b-chat-int8",
@@ -33,6 +35,10 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin):
33 35 }
34 36 _args: dict = None
35 37
38 @classmethod
39 def get_cache_file(cls) -> Path:
40 return Path(get_cookies_dir()) / f"auth_{cls.parent if hasattr(cls, 'parent') else cls.__name__}.json"
41
36 42 @classmethod
37 43 def get_models(cls) -> str:
38 44 if not cls.models:
@@ -67,7 +73,11 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin):
67 73 timeout: int = 300,
68 74 **kwargs
69 75 ) -> AsyncResult:
76 cache_file = cls.get_cache_file()
70 77 if cls._args is None:
78 if cache_file.exists():
79 with cache_file.open("r") as f:
80 cls._args = json.load(f)
71 81 if has_nodriver:
72 82 cls._args = await get_args_from_nodriver(cls.url, proxy, timeout, cookies)
73 83 else:
@@ -93,6 +103,8 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin):
93 103 await raise_for_status(response)
94 104 except ResponseStatusError:
95 105 cls._args = None
106 if cache_file.exists():
107 cache_file.unlink()
96 108 raise
97 109 reason = None
98 110 async for line in response.iter_lines():
@@ -109,4 +121,7 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin):
109 121 except Exception:
110 122 continue
111 123 if reason is not None:
112 yield FinishReason(reason)
124 yield FinishReason(reason)
125
126 with cache_file.open("w") as f:
127 json.dump(cls._args, f)
Modified g4f/Provider/DDG.py +37 -11
@@ -1,14 +1,18 @@
1 1 from __future__ import annotations
2 2
3 from aiohttp import ClientSession, ClientTimeout, ClientError
3 import asyncio
4 from aiohttp import ClientSession, ClientTimeout, ClientError, ClientResponseError
4 5 import json
6
5 7 from ..typing import AsyncResult, Messages
6 8 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin, BaseConversation
7 from .helper import format_prompt
9 from ..providers.response import FinishReason
10 from .. import debug
8 11
9 12 class Conversation(BaseConversation):
10 13 vqd: str = None
11 14 message_history: Messages = []
15 cookies: dict = {}
12 16
13 17 def __init__(self, model: str):
14 18 self.model = model
@@ -65,20 +69,24 @@ class DDG(AsyncGeneratorProvider, ProviderModelMixin):
65 69 conversation: Conversation = None,
66 70 return_conversation: bool = False,
67 71 proxy: str = None,
72 headers: dict = {
73 "Content-Type": "application/json",
74 },
75 cookies: dict = None,
76 max_retries: int = 3,
68 77 **kwargs
69 78 ) -> AsyncResult:
70 headers = {
71 "Content-Type": "application/json",
72 }
73 async with ClientSession(headers=headers, timeout=ClientTimeout(total=30)) as session:
79 if cookies is None and conversation is not None:
80 cookies = conversation.cookies
81 async with ClientSession(headers=headers, cookies=cookies, timeout=ClientTimeout(total=30)) as session:
74 82 # Fetch VQD token
75 83 if conversation is None:
76 84 conversation = Conversation(model)
77
78 if conversation.vqd is None:
85 conversation.cookies = session.cookie_jar
79 86 conversation.vqd = await cls.fetch_vqd(session)
80 87
81 headers["x-vqd-4"] = conversation.vqd
88 if conversation.vqd is not None:
89 headers["x-vqd-4"] = conversation.vqd
82 90
83 91 if return_conversation:
84 92 yield conversation
@@ -97,15 +105,33 @@ class DDG(AsyncGeneratorProvider, ProviderModelMixin):
97 105 async with session.post(cls.api_endpoint, headers=headers, json=payload, proxy=proxy) as response:
98 106 conversation.vqd = response.headers.get("x-vqd-4")
99 107 response.raise_for_status()
108 reason = None
100 109 async for line in response.content:
101 110 line = line.decode("utf-8").strip()
102 111 if line.startswith("data:"):
103 112 try:
104 113 message = json.loads(line[5:].strip())
105 if "message" in message:
106 yield message["message"]
114 if "message" in message and message["message"]:
115 yield message["message"]
116 reason = "max_tokens"
117 elif message.get("message") == '':
118 reason = "stop"
107 119 except json.JSONDecodeError:
108 120 continue
121 if reason is not None:
122 yield FinishReason(reason)
123 except ClientResponseError as e:
124 if e.code in (400, 429) and max_retries > 0:
125 debug.log(f"Retry: max_retries={max_retries}, wait={512 - max_retries * 48}: {e}")
126 await asyncio.sleep(512 - max_retries * 48)
127 is_started = False
128 async for chunk in cls.create_async_generator(model, messages, conversation, return_conversation, max_retries=max_retries-1, **kwargs):
129 if chunk:
130 yield chunk
131 is_started = True
132 if is_started:
133 return
134 raise e
109 135 except ClientError as e:
110 136 raise Exception(f"HTTP ClientError occurred: {e}")
111 137 except asyncio.TimeoutError:
Modified g4f/Provider/needs_auth/HuggingFace.py +1 -1
@@ -137,7 +137,7 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
137 137 else:
138 138 is_special = True
139 139 debug.log(f"Special token: {is_special}")
140 yield FinishReason("stop" if is_special else "max_tokens", actions=["variant"] if is_special else ["continue", "variant"])
140 yield FinishReason("stop" if is_special else "length", actions=["variant"] if is_special else ["continue", "variant"])
141 141 else:
142 142 if response.headers["content-type"].startswith("image/"):
143 143 base64_data = base64.b64encode(b"".join([chunk async for chunk in response.iter_content()]))
Modified g4f/Provider/needs_auth/OpenaiChat.py +8 -7
@@ -105,11 +105,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
105 105 _expires: int = None
106 106
107 107 @classmethod
108 async def on_auth_async(cls, **kwargs) -> AuthResult:
108 async def on_auth_async(cls, **kwargs) -> AsyncIterator:
109 109 if cls.needs_auth:
110 async for _ in cls.login():
111 pass
112 return AuthResult(
110 async for chunk in cls.login():
111 yield chunk
112 yield AuthResult(
113 113 api_key=cls._api_key,
114 114 cookies=cls._cookies or RequestConfig.cookies or {},
115 115 headers=cls._headers or RequestConfig.headers or cls.get_default_headers(),
@@ -174,7 +174,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
174 174 "use_case": "multimodal"
175 175 }
176 176 # Post the image data to the service and get the image data
177 async with session.post(f"{cls.url}/backend-api/files", json=data, headers=auth_result.headers) as response:
177 headers = auth_result.headers if hasattr(auth_result, "headers") else None
178 async with session.post(f"{cls.url}/backend-api/files", json=data, headers=headers) as response:
178 179 cls._update_request_args(auth_result, session)
179 180 await raise_for_status(response, "Create file failed")
180 181 image_data = {
@@ -360,7 +361,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
360 361 f"{cls.url}/backend-anon/sentinel/chat-requirements"
361 362 if cls._api_key is None else
362 363 f"{cls.url}/backend-api/sentinel/chat-requirements",
363 json={"p": None if auth_result.proof_token is None else get_requirements_token(auth_result.proof_token)},
364 json={"p": None if not getattr(auth_result, "proof_token") else get_requirements_token(auth_result.proof_token)},
364 365 headers=cls._headers
365 366 ) as response:
366 367 if response.status == 401:
@@ -386,7 +387,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
386 387 proofofwork = generate_proof_token(
387 388 **chat_requirements["proofofwork"],
388 389 user_agent=auth_result.headers.get("user-agent"),
389 proof_token=auth_result.proof_token
390 proof_token=getattr(auth_result, "proof_token")
390 391 )
391 392 [debug.log(text) for text in (
392 393 #f"Arkose: {'False' if not need_arkose else auth_result.arkose_token[:12]+'...'}",
Modified g4f/api/__init__.py +3 -2
@@ -41,7 +41,7 @@ from g4f.errors import ProviderNotFoundError, ModelNotFoundError, MissingAuthErr
41 41 from g4f.cookies import read_cookie_files, get_cookies_dir
42 42 from g4f.Provider import ProviderType, ProviderUtils, __providers__
43 43 from g4f.gui import get_gui_app
44 from g4f.tools.files import supports_filename, get_streaming
44 from g4f.tools.files import supports_filename, get_async_streaming
45 45 from .stubs import (
46 46 ChatCompletionsConfig, ImageGenerationConfig,
47 47 ProviderResponseModel, ModelResponseModel,
@@ -436,7 +436,8 @@ class Api:
436 436 event_stream = "text/event-stream" in request.headers.get("accept", "")
437 437 if not os.path.isdir(bucket_dir):
438 438 return ErrorResponse.from_message("Bucket dir not found", 404)
439 return StreamingResponse(get_streaming(bucket_dir, delete_files, refine_chunks_with_spacy, event_stream), media_type="text/plain")
439 return StreamingResponse(get_async_streaming(bucket_dir, delete_files, refine_chunks_with_spacy, event_stream),
440 media_type="text/event-stream" if event_stream else "text/plain")
440 441
441 442 @self.app.post("/v1/files/{bucket_id}", responses={
442 443 HTTP_200_OK: {"model": UploadResponseModel}
Modified g4f/gui/client/home.html +81 -14
@@ -103,17 +103,29 @@
103 103 z-index: -1;
104 104 }
105 105
106 iframe.stream {
106 .stream-widget {
107 107 max-height: 0;
108 108 transition: max-height 0.15s ease-out;
109 color: var(--colour-5);
110 overflow: scroll;
111 text-align: left;
109 112 }
110 113
111 iframe.stream.show {
114 .stream-widget.show {
112 115 max-height: 1000px;
113 116 height: 1000px;
114 117 transition: max-height 0.25s ease-in;
115 118 background: rgba(255,255,255,0.7);
116 119 border-top: 2px solid rgba(255,255,255,0.5);
120 padding: 20px;
121 }
122
123 .stream-widget img {
124 max-width: 320px;
125 }
126
127 #stream-container {
128 width: 100%;
117 129 }
118 130
119 131 .description {
@@ -207,32 +219,87 @@
207 219 <p>Powered by the G4F framework</p>
208 220 </div>
209 221
210 <iframe id="stream-widget" class="stream" frameborder="0"></iframe>
222 <iframe class="stream-widget" frameborder="0"></iframe>
211 223 </div>
212 224 <script>
213 const iframe = document.getElementById('stream-widget');""
214 let search = (navigator.language == "de" ? "news in deutschland" : navigator.language == "en" ? "world news" : navigator.language);
215 if (Math.floor(Math.random() * 6) % 2 == 0) {
225 const iframe = document.querySelector('.stream-widget');
226 const rand_idx = Math.floor(Math.random() * 9)
227 if (rand_idx < 3) {
216 228 search = "xtekky/gpt4free releases";
229 } else if (rand_idx < 5) {
230 search = "developer news";
231 } else {
232 search = (navigator.language == "de" ? "news in deutsch" : navigator.language == "en" ? "world news" : `news in ${navigator.language}`);
217 233 }
218 const url = "/backend-api/v2/create?prompt=Create of overview of the news in plain text&stream=1&web_search=" + search;
234 const summary_prompt = "Give a summary of the provided text in ```markdown``` format. Add maybe one or more images.";
235 const url = `/backend-api/v2/create?prompt=${summary_prompt}&stream=1&web_search=${search}`;
219 236 iframe.src = url;
220 setTimeout(()=>iframe.classList.add('show'), 3000);
237 const message = "Loading...";
238 setTimeout(()=>{
239 iframe.classList.add('show');
240 const iframeDocument = iframe.contentDocument || iframe.contentWindow?.document;
241 if (iframeDocument) {
242 const iframeBody = iframeDocument.querySelector("body");
243 if (iframeBody) {
244 iframeBody.innerHTML = message + iframeBody.innerHTML;
245 }
246 } else {
247 iframe.parentElement.removeChild(iframe);
248 }
249 }, 1000);
250
251 function filterMarkdown(text, allowedTypes = null, defaultValue = null) {
252 const match = text.match(/```(.+)\n(?<code>[\s\S]+?)(\n```|$)/);
253 if (match) {
254 const [, type, code] = match;
255 if (!allowedTypes || allowedTypes.includes(type)) {
256 return code;
257 }
258 }
259 return defaultValue;
260 }
261
262 let scroll_to_bottom_callback = () => {
263 const i = document.querySelector(".stream-widget");
264 if (!i.contentWindow || !i.contentDocument) {
265 return;
266 }
267 clientHeight = i.contentDocument.body.scrollHeight;
268 i.contentWindow.scrollTo(0, clientHeight);
269 if (clientHeight - i.contentWindow.scrollY < 2 * clientHeight) {
270 setTimeout(scroll_to_bottom_callback, 1000);
271 }
272 };
273 setTimeout(scroll_to_bottom_callback, 1000);
274
221 275 iframe.onload = () => {
222 276 const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
223 const iframeBody = iframeDocument.querySelector("body");
224 277 const iframeContent = iframeDocument.querySelector("pre");
278 let iframeText = iframeContent.innerHTML;
225 279 const markdown = window.markdownit();
226 iframeBody.innerHTML = markdown.render(iframeContent.innerHTML);
280 const iframeContainer = document.querySelector(".container");
281 iframe.remove()
282 if (iframeText.indexOf('"error"') < 0) {
283 iframeContainer.innerHTML += `<div class="stream-widget show">${markdown.render(filterMarkdown(iframeText, "markdown", iframeText))}</div>`;
284 }
285 scroll_to_bottom_callback = () => null;
227 286 }
228 287
229 288 (async () => {
230 const prompt = `
289 const today = new Date().toJSON().slice(0, 10);
290 const max = 100;
291 const cache_id = Math.floor(Math.random() * max);
292 let prompt;
293 if (cache_id % 2 == 0) {
294 prompt = `
231 295 Today is ${new Date().toJSON().slice(0, 10)}.
232 296 Create a single-page HTML screensaver reflecting the current season (based on the date).
233 For example, if it's Spring, it might use floral patterns or pastel colors.
234 Avoid using any text. Consider a subtle animation or transition effect.`;
235 const response = await fetch(`/backend-api/v2/create?prompt=${prompt}&filter_markdown=html`)
297 Avoid using any text.`;
298 } else {
299 prompt = `Create a single-page HTML screensaver. Avoid using any text.`;
300 const response = await fetch(`/backend-api/v2/create?prompt=${prompt}&filter_markdown=html&cache=${cache_id}`);
301 }
302 const response = await fetch(`/backend-api/v2/create?prompt=${prompt}&filter_markdown=html&cache=${cache_id}`);
236 303 const text = await response.text()
237 304 background.src = `data:text/html;charset=utf-8,${encodeURIComponent(text)}`;
238 305 const gradient = document.querySelector('.gradient');
Modified g4f/gui/client/index.html +2 -1
@@ -239,7 +239,8 @@
239 239 <button class="hide-input">
240 240 <i class="fa-solid fa-angles-down"></i>
241 241 </button>
242 <span class="text"></span>
242 <input type="checkbox" id="agree" name="agree" value="yes" checked>
243 <label for="agree" class="text" onclick="this.innerText='';">Scroll to bottom</label>
243 244 </div>
244 245 <div class="stop_generating stop_generating-hidden">
245 246 <button id="cancelButton">
Modified g4f/gui/client/static/css/style.css +17 -4
@@ -516,7 +516,11 @@ body:not(.white) a:visited{
516 516 padding: 6px 6px;
517 517 }
518 518
519 #input-count .text {
519 input-count .text {
520 min-width: 12px
521 }
522
523 #input-count .text, #input-count input {
520 524 padding: 0 4px;
521 525 }
522 526
@@ -793,7 +797,7 @@ select {
793 797 appearance: none;
794 798 width: 100%;
795 799 height: 20px;
796 background: var(--accent);
800 background: var(--colour-2);
797 801 outline: none;
798 802 transition: opacity .2s;
799 803 border-radius: 10px;
@@ -859,11 +863,18 @@ select:hover,
859 863 font-size: 15px;
860 864 width: 100%;
861 865 color: var(--colour-3);
862 min-height: 49px;
863 866 height: 59px;
864 867 outline: none;
865 868 padding: var(--inner-gap) var(--section-gap);
866 869 resize: vertical;
870 min-height: 59px;
871 transition: max-height 0.15s ease-out;
872 }
873
874 #systemPrompt:focus {
875 min-height: 200px;
876 max-height: 1000px;
877 transition: max-height 0.25s ease-in;
867 878 }
868 879
869 880 .pswp {
@@ -929,6 +940,9 @@ select:hover,
929 940 body:not(.white) .gradient{
930 941 display: block;
931 942 }
943 .settings .label, form .label, .settings label, form label {
944 min-width: 200px;
945 }
932 946 }
933 947
934 948 .input-box {
@@ -1354,7 +1368,6 @@ form .field.saved .fa-xmark {
1354 1368 .settings .label, form .label, .settings label, form label {
1355 1369 font-size: 15px;
1356 1370 margin-left: var(--inner-gap);
1357 min-width: 200px;
1358 1371 }
1359 1372
1360 1373 .settings .label, form .label {
Modified g4f/gui/client/static/js/chat.v1.js +33 -27
@@ -511,7 +511,9 @@ const prepare_messages = (messages, message_index = -1, do_continue = false) =>
511 511 // Include only not regenerated messages
512 512 if (new_message && !new_message.regenerate) {
513 513 // Remove generated images from history
514 new_message.content = filter_message(new_message.content);
514 if (new_message.content) {
515 new_message.content = filter_message(new_message.content);
516 }
515 517 // Remove internal fields
516 518 delete new_message.provider;
517 519 delete new_message.synthesize;
@@ -658,7 +660,7 @@ async function load_provider_parameters(provider) {
658 660 }
659 661 }
660 662
661 async function add_message_chunk(message, message_id, provider) {
663 async function add_message_chunk(message, message_id, provider, scroll) {
662 664 content_map = content_storage[message_id];
663 665 if (message.type == "conversation") {
664 666 const conversation = await get_conversation(window.conversation_id);
@@ -698,7 +700,7 @@ async function add_message_chunk(message, message_id, provider) {
698 700 content_map.inner.innerHTML = markdown_render(message.preview);
699 701 } else if (message.type == "content") {
700 702 message_storage[message_id] += message.content;
701 update_message(content_map, message_id);
703 update_message(content_map, message_id, null, scroll);
702 704 content_map.inner.style.height = "";
703 705 } else if (message.type == "log") {
704 706 let p = document.createElement("p");
@@ -709,9 +711,7 @@ async function add_message_chunk(message, message_id, provider) {
709 711 } else if (message.type == "title") {
710 712 title_storage[message_id] = message.title;
711 713 } else if (message.type == "login") {
712 update_message(content_map, message_id, message.login);
713 } else if (message.type == "login") {
714 update_message(content_map, message_id, message.login);
714 update_message(content_map, message_id, message.login, scroll);
715 715 } else if (message.type == "finish") {
716 716 finish_storage[message_id] = message.finish;
717 717 } else if (message.type == "parameters") {
@@ -734,8 +734,11 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
734 734 messages = prepare_messages(conversation.items, message_index, action=="continue");
735 735 message_storage[message_id] = "";
736 736 stop_generating.classList.remove("stop_generating-hidden");
737
738 if (message_index == -1) {
737 const scroll = true;
738 if (message_index > 0 && message_index + 1 < messages.length) {
739 scroll = false;
740 }
741 if (scroll) {
739 742 await lazy_scroll_to_bottom();
740 743 }
741 744
@@ -780,7 +783,7 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
780 783 update_timeouts: [],
781 784 message_index: message_index,
782 785 }
783 if (message_index == -1) {
786 if (scroll) {
784 787 await lazy_scroll_to_bottom();
785 788 }
786 789 try {
@@ -801,7 +804,7 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
801 804 download_images: download_images,
802 805 api_key: api_key,
803 806 ignored: ignored,
804 }, files, message_id);
807 }, files, message_id, scroll);
805 808 content_map.update_timeouts.forEach((timeoutId)=>clearTimeout(timeoutId));
806 809 content_map.update_timeouts = [];
807 810 if (!error_storage[message_id]) {
@@ -836,12 +839,12 @@ const ask_gpt = async (message_id, message_index = -1, regenerate = false, provi
836 839 );
837 840 delete message_storage[message_id];
838 841 if (!error_storage[message_id]) {
839 await safe_load_conversation(window.conversation_id, message_index == -1);
842 await safe_load_conversation(window.conversation_id, scroll);
840 843 }
841 844 }
842 845 let cursorDiv = message_el.querySelector(".cursor");
843 846 if (cursorDiv) cursorDiv.parentNode.removeChild(cursorDiv);
844 if (message_index == -1) {
847 if (scroll) {
845 848 await lazy_scroll_to_bottom();
846 849 }
847 850 await safe_remove_cancel_button();
@@ -856,7 +859,7 @@ async function scroll_to_bottom() {
856 859 }
857 860
858 861 async function lazy_scroll_to_bottom() {
859 if (message_box.scrollHeight - message_box.scrollTop < 2 * message_box.clientHeight) {
862 if (document.querySelector("#input-count input").checked) {
860 863 await scroll_to_bottom();
861 864 }
862 865 }
@@ -1013,6 +1016,8 @@ const load_conversation = async (conversation_id, scroll=true) => {
1013 1016 if (newContent.startsWith("```")) {
1014 1017 const index = str.indexOf("\n");
1015 1018 newContent = newContent.substring(index);
1019 } else if (newContent.startsWith("...")) {
1020 newContent = " " + newContent.substring(3);
1016 1021 }
1017 1022 if (newContent.startsWith(lastLine)) {
1018 1023 newContent = newContent.substring(lastLine.length);
@@ -1054,7 +1059,7 @@ const load_conversation = async (conversation_id, scroll=true) => {
1054 1059 if (item.finish && item.finish.actions) {
1055 1060 actions = item.finish.actions
1056 1061 }
1057 if (!("continue" in actions)) {
1062 if (item.role == "assistant" && !actions.includes("continue")) {
1058 1063 let reason = "stop";
1059 1064 // Read finish reason from conversation
1060 1065 if (item.finish && item.finish.reason) {
@@ -1067,7 +1072,7 @@ const load_conversation = async (conversation_id, scroll=true) => {
1067 1072 reason = "error";
1068 1073 // Has an even number of start or end code tags
1069 1074 } else if (buffer.split("```").length - 1 % 2 === 1) {
1070 reason = "error";
1075 reason = "length";
1071 1076 // Has a end token at the end
1072 1077 } else if (lastLine.endsWith("```") || lastLine.endsWith(".") || lastLine.endsWith("?") || lastLine.endsWith("!")
1073 1078 || lastLine.endsWith('"') || lastLine.endsWith("'") || lastLine.endsWith(")")
@@ -1152,7 +1157,7 @@ const load_conversation = async (conversation_id, scroll=true) => {
1152 1157 highlight(message_box);
1153 1158 regenerate_button.classList.remove("regenerate-hidden");
1154 1159
1155 if (scroll) {
1160 if (document.querySelector("#input-count input").checked) {
1156 1161 message_box.scrollTo({ top: message_box.scrollHeight, behavior: "smooth" });
1157 1162
1158 1163 setTimeout(() => {
@@ -1517,7 +1522,7 @@ function count_words_and_tokens(text, model) {
1517 1522 return `(${count_words(text)} words, ${count_chars(text)} chars, ${count_tokens(model, text)} tokens)`;
1518 1523 }
1519 1524
1520 function update_message(content_map, message_id, content = null) {
1525 function update_message(content_map, message_id, content = null, scroll = true) {
1521 1526 content_map.update_timeouts.push(setTimeout(() => {
1522 1527 if (!content) content = message_storage[message_id];
1523 1528 html = markdown_render(content);
@@ -1538,7 +1543,7 @@ function update_message(content_map, message_id, content = null) {
1538 1543 content_map.inner.innerHTML = html;
1539 1544 content_map.count.innerText = count_words_and_tokens(message_storage[message_id], provider_storage[message_id]?.model);
1540 1545 highlight(content_map.inner);
1541 if (content_map.message_index == -1) {
1546 if (scroll) {
1542 1547 lazy_scroll_to_bottom();
1543 1548 }
1544 1549 content_map.update_timeouts.forEach((timeoutId)=>clearTimeout(timeoutId));
@@ -1890,7 +1895,7 @@ fileInput.addEventListener('change', async (event) => {
1890 1895 fileInput.value = "";
1891 1896 inputCount.innerText = `${count} Conversations were imported successfully`;
1892 1897 } else {
1893 is_cookie_file = false;
1898 is_cookie_file = data.api_key;
1894 1899 if (Array.isArray(data)) {
1895 1900 data.forEach((item) => {
1896 1901 if (item.domain && item.name && item.value) {
@@ -1927,7 +1932,7 @@ function get_selected_model() {
1927 1932 }
1928 1933 }
1929 1934
1930 async function api(ressource, args=null, files=null, message_id=null) {
1935 async function api(ressource, args=null, files=null, message_id=null, scroll=true) {
1931 1936 let api_key;
1932 1937 if (ressource == "models" && args) {
1933 1938 api_key = get_api_key_by_provider(args);
@@ -1957,7 +1962,7 @@ async function api(ressource, args=null, files=null, message_id=null) {
1957 1962 headers: headers,
1958 1963 body: body,
1959 1964 });
1960 return read_response(response, message_id, args.provider || null);
1965 return read_response(response, message_id, args.provider || null, scroll);
1961 1966 }
1962 1967 response = await fetch(url, {headers: headers});
1963 1968 if (response.status == 200) {
@@ -1966,7 +1971,7 @@ async function api(ressource, args=null, files=null, message_id=null) {
1966 1971 console.error(response);
1967 1972 }
1968 1973
1969 async function read_response(response, message_id, provider) {
1974 async function read_response(response, message_id, provider, scroll) {
1970 1975 const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
1971 1976 let buffer = ""
1972 1977 while (true) {
@@ -1979,7 +1984,7 @@ async function read_response(response, message_id, provider) {
1979 1984 continue;
1980 1985 }
1981 1986 try {
1982 add_message_chunk(JSON.parse(buffer + line), message_id, provider);
1987 add_message_chunk(JSON.parse(buffer + line), message_id, provider, scroll);
1983 1988 buffer = "";
1984 1989 } catch {
1985 1990 buffer += line
@@ -2106,6 +2111,7 @@ if (SpeechRecognition) {
2106 2111 recognition.maxAlternatives = 1;
2107 2112
2108 2113 let startValue;
2114 let buffer;
2109 2115 let lastDebounceTranscript;
2110 2116 recognition.onstart = function() {
2111 2117 microLabel.classList.add("recognition");
@@ -2114,6 +2120,7 @@ if (SpeechRecognition) {
2114 2120 messageInput.readOnly = true;
2115 2121 };
2116 2122 recognition.onend = function() {
2123 messageInput.value = `${startValue ? startValue + "\n" : ""}${buffer}`;
2117 2124 messageInput.readOnly = false;
2118 2125 messageInput.focus();
2119 2126 };
@@ -2131,18 +2138,17 @@ if (SpeechRecognition) {
2131 2138 lastDebounceTranscript = transcript;
2132 2139 }
2133 2140 if (transcript) {
2134 messageInput.value = `${startValue ? startValue+"\n" : ""}${transcript.trim()}`;
2141 inputCount.innerText = transcript;
2135 2142 if (isFinal) {
2136 startValue = messageInput.value;
2143 buffer = `${buffer ? buffer + "\n" : ""}${transcript.trim()}`;
2137 2144 }
2138 messageInput.style.height = messageInput.scrollHeight + "px";
2139 messageInput.scrollTop = messageInput.scrollHeight;
2140 2145 }
2141 2146 };
2142 2147
2143 2148 microLabel.addEventListener("click", (e) => {
2144 2149 if (microLabel.classList.contains("recognition")) {
2145 2150 recognition.stop();
2151 messageInput.value = `${startValue ? startValue + "\n" : ""}${buffer}`;
2146 2152 microLabel.classList.remove("recognition");
2147 2153 } else {
2148 2154 const lang = document.getElementById("recognition-language")?.value;
Modified g4f/gui/server/backend_api.py +26 -9
@@ -9,6 +9,8 @@ import shutil
9 9 from flask import Flask, Response, request, jsonify
10 10 from typing import Generator
11 11 from pathlib import Path
12 from urllib.parse import quote_plus
13 from hashlib import sha256
12 14 from werkzeug.utils import secure_filename
13 15
14 16 from ...image import is_allowed_extension, to_image
@@ -123,15 +125,30 @@ class Backend_Api(Api):
123 125 "type": "function"
124 126 })
125 127 do_filter_markdown = request.args.get("filter_markdown")
126 response = iter_run_tools(
127 ChatCompletion.create,
128 model=request.args.get("model"),
129 messages=[{"role": "user", "content": request.args.get("prompt")}],
130 provider=request.args.get("provider", None),
131 stream=not do_filter_markdown,
132 ignore_stream=not request.args.get("stream"),
133 tool_calls=tool_calls,
134 )
128 cache_id = request.args.get('cache')
129 parameters = {
130 "model": request.args.get("model"),
131 "messages": [{"role": "user", "content": request.args.get("prompt")}],
132 "provider": request.args.get("provider", None),
133 "stream": not do_filter_markdown and not cache_id,
134 "ignore_stream": not request.args.get("stream"),
135 "tool_calls": tool_calls,
136 }
137 if cache_id:
138 cache_id = sha256(cache_id.encode() + json.dumps(parameters, sort_keys=True).encode()).hexdigest()
139 cache_dir = Path(get_cookies_dir()) / ".scrape_cache" / "create"
140 cache_file = cache_dir / f"{quote_plus(request.args.get('prompt').strip()[:20])}.{cache_id}.txt"
141 if cache_file.exists():
142 with cache_file.open("r") as f:
143 response = f.read()
144 else:
145 response = iter_run_tools(ChatCompletion.create, **parameters)
146 cache_dir.mkdir(parents=True, exist_ok=True)
147 with cache_file.open("w") as f:
148 f.write(response)
149 else:
150 response = iter_run_tools(ChatCompletion.create, **parameters)
151
135 152 if do_filter_markdown:
136 153 return Response(filter_markdown(response, do_filter_markdown), mimetype='text/plain')
137 154 def cast_str():
Modified g4f/providers/base_provider.py +20 -4
@@ -269,7 +269,7 @@ class AsyncProvider(AbstractProvider):
269 269 def get_async_create_function(cls) -> callable:
270 270 return cls.create_async
271 271
272 class AsyncGeneratorProvider(AsyncProvider):
272 class AsyncGeneratorProvider(AbstractProvider):
273 273 """
274 274 Provides asynchronous generator functionality for streaming results.
275 275 """
@@ -395,6 +395,10 @@ class AsyncAuthedProvider(AsyncGeneratorProvider):
395 395 def get_async_create_function(cls) -> callable:
396 396 return cls.create_async_generator
397 397
398 @classmethod
399 def get_cache_file(cls) -> Path:
400 return Path(get_cookies_dir()) / f"auth_{cls.parent if hasattr(cls, 'parent') else cls.__name__}.json"
401
398 402 @classmethod
399 403 def create_completion(
400 404 cls,
@@ -404,18 +408,24 @@ class AsyncAuthedProvider(AsyncGeneratorProvider):
404 408 ) -> CreateResult:
405 409 try:
406 410 auth_result = AuthResult()
407 cache_file = Path(get_cookies_dir()) / f"auth_{cls.parent if hasattr(cls, 'parent') else cls.__name__}.json"
411 cache_file = cls.get_cache_file()
408 412 if cache_file.exists():
409 413 with cache_file.open("r") as f:
410 414 auth_result = AuthResult(**json.load(f))
411 415 else:
412 416 auth_result = cls.on_auth(**kwargs)
413 return to_sync_generator(cls.create_authed(model, messages, auth_result, **kwargs))
417 if hasattr(auth_result, "_iter__"):
418 for chunk in auth_result:
419 if isinstance(chunk, AsyncResult):
420 auth_result = chunk
421 else:
422 yield chunk
423 yield from to_sync_generator(cls.create_authed(model, messages, auth_result, **kwargs))
414 424 except (MissingAuthError, NoValidHarFileError):
415 425 if cache_file.exists():
416 426 cache_file.unlink()
417 427 auth_result = cls.on_auth(**kwargs)
418 return to_sync_generator(cls.create_authed(model, messages, auth_result, **kwargs))
428 yield from to_sync_generator(cls.create_authed(model, messages, auth_result, **kwargs))
419 429 finally:
420 430 cache_file.parent.mkdir(parents=True, exist_ok=True)
421 431 cache_file.write_text(json.dumps(auth_result.get_dict()))
@@ -434,6 +444,12 @@ class AsyncAuthedProvider(AsyncGeneratorProvider):
434 444 auth_result = AuthResult(**json.load(f))
435 445 else:
436 446 auth_result = await cls.on_auth_async(**kwargs)
447 if hasattr(auth_result, "_aiter__"):
448 async for chunk in auth_result:
449 if isinstance(chunk, AsyncResult):
450 auth_result = chunk
451 else:
452 yield chunk
437 453 response = to_async_iterator(cls.create_authed(model, messages, **kwargs, auth_result=auth_result))
438 454 async for chunk in response:
439 455 yield chunk
Modified g4f/providers/response.py +3 -1
@@ -19,7 +19,9 @@ def quote_url(url: str) -> str:
19 19
20 20 def quote_title(title: str) -> str:
21 21 if title:
22 return title.replace("\n", "").replace('"', '')
22 title = title.strip()
23 title = " ".join(title.split())
24 return title.replace('[', '').replace(']', '')
23 25 return ""
24 26
25 27 def format_link(url: str, title: str = None) -> str:
Modified g4f/providers/retry_provider.py +5 -3
Modified g4f/requests/__init__.py +13 -1
Modified g4f/tools/files.py +77 -46
Modified g4f/tools/web_search.py +21 -6