From 12c413fd2e89b1d3a4d0f9e48fb6d12d0556c7ba Mon Sep 17 00:00:00 2001 From: Heiner Lohaus Date: Sun, 5 Jan 2025 17:02:15 +0100 Subject: [PATCH 1/3] 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 --- g4f/Provider/Cloudflare.py | 19 +++- g4f/Provider/DDG.py | 48 +++++++--- g4f/Provider/needs_auth/HuggingFace.py | 2 +- g4f/Provider/needs_auth/OpenaiChat.py | 15 +-- g4f/api/__init__.py | 5 +- g4f/gui/client/home.html | 95 ++++++++++++++++--- g4f/gui/client/index.html | 3 +- g4f/gui/client/static/css/style.css | 21 ++++- g4f/gui/client/static/js/chat.v1.js | 60 ++++++------ g4f/gui/server/backend_api.py | 35 +++++-- g4f/providers/base_provider.py | 24 ++++- g4f/providers/response.py | 4 +- g4f/providers/retry_provider.py | 8 +- g4f/requests/__init__.py | 14 ++- g4f/tools/files.py | 123 ++++++++++++++++--------- g4f/tools/web_search.py | 27 ++++-- 16 files changed, 364 insertions(+), 139 deletions(-) diff --git a/g4f/Provider/Cloudflare.py b/g4f/Provider/Cloudflare.py index f69b81287bd..e6f0dab3bd9 100644 --- a/g4f/Provider/Cloudflare.py +++ b/g4f/Provider/Cloudflare.py @@ -2,12 +2,14 @@ import asyncio import json +from pathlib import Path from ..typing import AsyncResult, Messages, Cookies from .base_provider import AsyncGeneratorProvider, ProviderModelMixin, get_running_loop from ..requests import Session, StreamSession, get_args_from_nodriver, raise_for_status, merge_cookies from ..requests import DEFAULT_HEADERS, has_nodriver, has_curl_cffi from ..providers.response import FinishReason +from ..cookies import get_cookies_dir from ..errors import ResponseStatusError, ModelNotFoundError class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin): @@ -19,7 +21,7 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin): supports_stream = True supports_system_message = True supports_message_history = True - default_model = "@cf/meta/llama-3.1-8b-instruct" + default_model = "@cf/meta/llama-3.3-70b-instruct-fp8-fast" model_aliases = { "llama-2-7b": "@cf/meta/llama-2-7b-chat-fp16", "llama-2-7b": "@cf/meta/llama-2-7b-chat-int8", @@ -33,6 +35,10 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin): } _args: dict = None + @classmethod + def get_cache_file(cls) -> Path: + return Path(get_cookies_dir()) / f"auth_{cls.parent if hasattr(cls, 'parent') else cls.__name__}.json" + @classmethod def get_models(cls) -> str: if not cls.models: @@ -67,7 +73,11 @@ async def create_async_generator( timeout: int = 300, **kwargs ) -> AsyncResult: + cache_file = cls.get_cache_file() if cls._args is None: + if cache_file.exists(): + with cache_file.open("r") as f: + cls._args = json.load(f) if has_nodriver: cls._args = await get_args_from_nodriver(cls.url, proxy, timeout, cookies) else: @@ -93,6 +103,8 @@ async def create_async_generator( await raise_for_status(response) except ResponseStatusError: cls._args = None + if cache_file.exists(): + cache_file.unlink() raise reason = None async for line in response.iter_lines(): @@ -109,4 +121,7 @@ async def create_async_generator( except Exception: continue if reason is not None: - yield FinishReason(reason) \ No newline at end of file + yield FinishReason(reason) + + with cache_file.open("w") as f: + json.dump(cls._args, f) \ No newline at end of file diff --git a/g4f/Provider/DDG.py b/g4f/Provider/DDG.py index fb29203dcc8..78e13568cdc 100644 --- a/g4f/Provider/DDG.py +++ b/g4f/Provider/DDG.py @@ -1,14 +1,18 @@ from __future__ import annotations -from aiohttp import ClientSession, ClientTimeout, ClientError +import asyncio +from aiohttp import ClientSession, ClientTimeout, ClientError, ClientResponseError import json + from ..typing import AsyncResult, Messages from .base_provider import AsyncGeneratorProvider, ProviderModelMixin, BaseConversation -from .helper import format_prompt +from ..providers.response import FinishReason +from .. import debug class Conversation(BaseConversation): vqd: str = None message_history: Messages = [] + cookies: dict = {} def __init__(self, model: str): self.model = model @@ -65,20 +69,24 @@ async def create_async_generator( conversation: Conversation = None, return_conversation: bool = False, proxy: str = None, + headers: dict = { + "Content-Type": "application/json", + }, + cookies: dict = None, + max_retries: int = 3, **kwargs ) -> AsyncResult: - headers = { - "Content-Type": "application/json", - } - async with ClientSession(headers=headers, timeout=ClientTimeout(total=30)) as session: + if cookies is None and conversation is not None: + cookies = conversation.cookies + async with ClientSession(headers=headers, cookies=cookies, timeout=ClientTimeout(total=30)) as session: # Fetch VQD token if conversation is None: conversation = Conversation(model) - - if conversation.vqd is None: + conversation.cookies = session.cookie_jar conversation.vqd = await cls.fetch_vqd(session) - headers["x-vqd-4"] = conversation.vqd + if conversation.vqd is not None: + headers["x-vqd-4"] = conversation.vqd if return_conversation: yield conversation @@ -97,15 +105,33 @@ async def create_async_generator( async with session.post(cls.api_endpoint, headers=headers, json=payload, proxy=proxy) as response: conversation.vqd = response.headers.get("x-vqd-4") response.raise_for_status() + reason = None async for line in response.content: line = line.decode("utf-8").strip() if line.startswith("data:"): try: message = json.loads(line[5:].strip()) - if "message" in message: - yield message["message"] + if "message" in message and message["message"]: + yield message["message"] + reason = "max_tokens" + elif message.get("message") == '': + reason = "stop" except json.JSONDecodeError: continue + if reason is not None: + yield FinishReason(reason) + except ClientResponseError as e: + if e.code in (400, 429) and max_retries > 0: + debug.log(f"Retry: max_retries={max_retries}, wait={512 - max_retries * 48}: {e}") + await asyncio.sleep(512 - max_retries * 48) + is_started = False + async for chunk in cls.create_async_generator(model, messages, conversation, return_conversation, max_retries=max_retries-1, **kwargs): + if chunk: + yield chunk + is_started = True + if is_started: + return + raise e except ClientError as e: raise Exception(f"HTTP ClientError occurred: {e}") except asyncio.TimeoutError: diff --git a/g4f/Provider/needs_auth/HuggingFace.py b/g4f/Provider/needs_auth/HuggingFace.py index e9c861e369b..80c0d97b847 100644 --- a/g4f/Provider/needs_auth/HuggingFace.py +++ b/g4f/Provider/needs_auth/HuggingFace.py @@ -137,7 +137,7 @@ async def create_async_generator( else: is_special = True debug.log(f"Special token: {is_special}") - yield FinishReason("stop" if is_special else "max_tokens", actions=["variant"] if is_special else ["continue", "variant"]) + yield FinishReason("stop" if is_special else "length", actions=["variant"] if is_special else ["continue", "variant"]) else: if response.headers["content-type"].startswith("image/"): base64_data = base64.b64encode(b"".join([chunk async for chunk in response.iter_content()])) diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py index 7718ef39ae7..0fe7cafe7eb 100644 --- a/g4f/Provider/needs_auth/OpenaiChat.py +++ b/g4f/Provider/needs_auth/OpenaiChat.py @@ -105,11 +105,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin): _expires: int = None @classmethod - async def on_auth_async(cls, **kwargs) -> AuthResult: + async def on_auth_async(cls, **kwargs) -> AsyncIterator: if cls.needs_auth: - async for _ in cls.login(): - pass - return AuthResult( + async for chunk in cls.login(): + yield chunk + yield AuthResult( api_key=cls._api_key, cookies=cls._cookies or RequestConfig.cookies or {}, headers=cls._headers or RequestConfig.headers or cls.get_default_headers(), @@ -174,7 +174,8 @@ async def upload_image(image, image_name): "use_case": "multimodal" } # Post the image data to the service and get the image data - async with session.post(f"{cls.url}/backend-api/files", json=data, headers=auth_result.headers) as response: + headers = auth_result.headers if hasattr(auth_result, "headers") else None + async with session.post(f"{cls.url}/backend-api/files", json=data, headers=headers) as response: cls._update_request_args(auth_result, session) await raise_for_status(response, "Create file failed") image_data = { @@ -360,7 +361,7 @@ async def create_authed( f"{cls.url}/backend-anon/sentinel/chat-requirements" if cls._api_key is None else f"{cls.url}/backend-api/sentinel/chat-requirements", - json={"p": None if auth_result.proof_token is None else get_requirements_token(auth_result.proof_token)}, + json={"p": None if not getattr(auth_result, "proof_token") else get_requirements_token(auth_result.proof_token)}, headers=cls._headers ) as response: if response.status == 401: @@ -386,7 +387,7 @@ async def create_authed( proofofwork = generate_proof_token( **chat_requirements["proofofwork"], user_agent=auth_result.headers.get("user-agent"), - proof_token=auth_result.proof_token + proof_token=getattr(auth_result, "proof_token") ) [debug.log(text) for text in ( #f"Arkose: {'False' if not need_arkose else auth_result.arkose_token[:12]+'...'}", diff --git a/g4f/api/__init__.py b/g4f/api/__init__.py index 934f9049428..73a9f64e64e 100644 --- a/g4f/api/__init__.py +++ b/g4f/api/__init__.py @@ -41,7 +41,7 @@ from g4f.cookies import read_cookie_files, get_cookies_dir from g4f.Provider import ProviderType, ProviderUtils, __providers__ from g4f.gui import get_gui_app -from g4f.tools.files import supports_filename, get_streaming +from g4f.tools.files import supports_filename, get_async_streaming from .stubs import ( ChatCompletionsConfig, ImageGenerationConfig, ProviderResponseModel, ModelResponseModel, @@ -436,7 +436,8 @@ def read_files(request: Request, bucket_id: str, delete_files: bool = True, refi event_stream = "text/event-stream" in request.headers.get("accept", "") if not os.path.isdir(bucket_dir): return ErrorResponse.from_message("Bucket dir not found", 404) - return StreamingResponse(get_streaming(bucket_dir, delete_files, refine_chunks_with_spacy, event_stream), media_type="text/plain") + return StreamingResponse(get_async_streaming(bucket_dir, delete_files, refine_chunks_with_spacy, event_stream), + media_type="text/event-stream" if event_stream else "text/plain") @self.app.post("/v1/files/{bucket_id}", responses={ HTTP_200_OK: {"model": UploadResponseModel} diff --git a/g4f/gui/client/home.html b/g4f/gui/client/home.html index 2ca678be876..fa21306105d 100644 --- a/g4f/gui/client/home.html +++ b/g4f/gui/client/home.html @@ -103,17 +103,29 @@ z-index: -1; } - iframe.stream { + .stream-widget { max-height: 0; transition: max-height 0.15s ease-out; + color: var(--colour-5); + overflow: scroll; + text-align: left; } - iframe.stream.show { + .stream-widget.show { max-height: 1000px; height: 1000px; transition: max-height 0.25s ease-in; background: rgba(255,255,255,0.7); border-top: 2px solid rgba(255,255,255,0.5); + padding: 20px; + } + + .stream-widget img { + max-width: 320px; + } + + #stream-container { + width: 100%; } .description { @@ -207,32 +219,87 @@

Powered by the G4F framework

- +