diff --git a/docker/Dockerfile b/docker/Dockerfile index 625312e217e..a84703aab66 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -78,7 +78,7 @@ RUN pip install --break-system-packages --upgrade pip \ && pip install --break-system-packages \ undetected-chromedriver selenium-wire \ && pip uninstall -y --break-system-packages \ - pywebview plyer + pywebview # Copy the entire package into the container. ADD --chown=$G4F_USER:$G4F_USER g4f $G4F_DIR/g4f 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

- +