summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md9
-rw-r--r--g4f/Provider/needs_auth/OpenaiChat.py224
-rw-r--r--g4f/Provider/openai/har_file.py1
-rw-r--r--g4f/client/__init__.py12
-rw-r--r--g4f/client/helper.py30
-rw-r--r--g4f/gui/server/backend.py19
-rw-r--r--g4f/providers/asyncio.py65
-rw-r--r--g4f/providers/base_provider.py63
-rw-r--r--setup.py17
9 files changed, 104 insertions, 336 deletions
diff --git a/README.md b/README.md
index cfaaadc0..4f61d791 100644
--- a/README.md
+++ b/README.md
@@ -105,15 +105,18 @@ docker run \
hlohaus789/g4f:latest
```
-Or run this command to start the gui without a browser and in the debug mode:
+Start the GUI without a browser requirement and in debug mode.
+There's no need to update the Docker image every time.
+Simply remove the g4f package from the image and install the Python package:
```bash
-docker pull hlohaus789/g4f:latest-slim
docker run \
-p 8080:8080 \
-v ${PWD}/har_and_cookies:/app/har_and_cookies \
-v ${PWD}/generated_images:/app/generated_images \
hlohaus789/g4f:latest-slim \
- python -m g4f.cli gui -debug
+ rm -r -f /app/g4f/ \
+ && pip install -U g4f[slim] \
+ && python -m g4f.cli gui -d
```
3. **Access the Client:**
diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py
index 38b97022..f50b9f9d 100644
--- a/g4f/Provider/needs_auth/OpenaiChat.py
+++ b/g4f/Provider/needs_auth/OpenaiChat.py
@@ -15,18 +15,14 @@ try:
has_nodriver = True
except ImportError:
has_nodriver = False
-try:
- from platformdirs import user_config_dir
- has_platformdirs = True
-except ImportError:
- has_platformdirs = False
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
from ...typing import AsyncResult, Messages, Cookies, ImageType, AsyncIterator
from ...requests.raise_for_status import raise_for_status
-from ...requests.aiohttp import StreamSession
+from ...requests import StreamSession
+from ...requests import get_nodriver
from ...image import ImageResponse, ImageRequest, to_image, to_bytes, is_accepted_format
-from ...errors import MissingAuthError, ResponseError
+from ...errors import MissingAuthError
from ...providers.response import BaseConversation, FinishReason, SynthesizeData
from ..helper import format_cookies
from ..openai.har_file import get_request_config, NoValidHarFileError
@@ -62,7 +58,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
supports_system_message = True
default_model = "auto"
default_vision_model = "gpt-4o"
- fallback_models = ["auto", "gpt-4", "gpt-4o", "gpt-4o-mini", "gpt-4o-canmore", "o1-preview", "o1-mini"]
+ fallback_models = [default_model, "gpt-4", "gpt-4o", "gpt-4o-mini", "gpt-4o-canmore", "o1-preview", "o1-mini"]
vision_models = fallback_models
image_models = fallback_models
@@ -84,51 +80,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
return cls.models
@classmethod
- async def create(
- cls,
- prompt: str = None,
- model: str = "",
- messages: Messages = [],
- action: str = "next",
- **kwargs
- ) -> Response:
- """
- Create a new conversation or continue an existing one
-
- Args:
- prompt: The user input to start or continue the conversation
- model: The name of the model to use for generating responses
- messages: The list of previous messages in the conversation
- history_disabled: A flag indicating if the history and training should be disabled
- action: The type of action to perform, either "next", "continue", or "variant"
- conversation_id: The ID of the existing conversation, if any
- parent_id: The ID of the parent message, if any
- image: The image to include in the user input, if any
- **kwargs: Additional keyword arguments to pass to the generator
-
- Returns:
- A Response object that contains the generator, action, messages, and options
- """
- # Add the user input to the messages list
- if prompt is not None:
- messages.append({
- "role": "user",
- "content": prompt
- })
- generator = cls.create_async_generator(
- model,
- messages,
- return_conversation=True,
- **kwargs
- )
- return Response(
- generator,
- action,
- messages,
- kwargs
- )
-
- @classmethod
async def upload_image(
cls,
session: StreamSession,
@@ -190,32 +141,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
return ImageRequest(image_data)
@classmethod
- async def get_default_model(cls, session: StreamSession, headers: dict):
- """
- Get the default model name from the service
-
- Args:
- session: The StreamSession object to use for requests
- headers: The headers to include in the requests
-
- Returns:
- The default model name as a string
- """
- if not cls.default_model:
- url = f"{cls.url}/backend-anon/models" if cls._api_key is None else f"{cls.url}/backend-api/models"
- async with session.get(url, headers=headers) as response:
- cls._update_request_args(session)
- if response.status == 401:
- raise MissingAuthError('Add a .har file for OpenaiChat' if cls._api_key is None else "Invalid api key")
- await raise_for_status(response)
- data = await response.json()
- if "categories" in data:
- cls.default_model = data["categories"][-1]["default_model"]
- return cls.default_model
- raise ResponseError(data)
- return cls.default_model
-
- @classmethod
def create_messages(cls, messages: Messages, image_request: ImageRequest = None):
"""
Create a list of messages for the user input
@@ -297,37 +222,12 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
raise RuntimeError(f"Error in downloading image: {e}")
@classmethod
- async def delete_conversation(cls, session: StreamSession, headers: dict, conversation_id: str):
- """
- Deletes a conversation by setting its visibility to False.
-
- This method sends an HTTP PATCH request to update the visibility of a conversation.
- It's used to effectively delete a conversation from being accessed or displayed in the future.
-
- Args:
- session (StreamSession): The StreamSession object used for making HTTP requests.
- headers (dict): HTTP headers to be used for the request.
- conversation_id (str): The unique identifier of the conversation to be deleted.
-
- Raises:
- HTTPError: If the HTTP request fails or returns an unsuccessful status code.
- """
- async with session.patch(
- f"{cls.url}/backend-api/conversation/{conversation_id}",
- json={"is_visible": False},
- headers=headers
- ) as response:
- cls._update_request_args(session)
- ...
-
- @classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
timeout: int = 180,
- api_key: str = None,
cookies: Cookies = None,
auto_continue: bool = False,
history_disabled: bool = False,
@@ -465,7 +365,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
continue
await raise_for_status(response)
if return_conversation:
- history_disabled = False
yield conversation
async for line in response.iter_lines():
async for chunk in cls.iter_messages_line(session, line, conversation):
@@ -483,19 +382,6 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
else:
break
yield FinishReason(conversation.finish_reason)
- if history_disabled and auto_continue:
- await cls.delete_conversation(session, cls._headers, conversation.conversation_id)
-
- @classmethod
- async def iter_messages_chunk(
- cls,
- messages: AsyncIterator,
- session: StreamSession,
- fields: Conversation,
- ) -> AsyncIterator:
- async for message in messages:
- async for chunk in cls.iter_messages_line(session, message, fields):
- yield chunk
@classmethod
async def iter_messages_line(cls, session: StreamSession, line: bytes, fields: Conversation) -> AsyncIterator:
@@ -575,15 +461,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
@classmethod
async def nodriver_auth(cls, proxy: str = None):
- if has_platformdirs:
- user_data_dir = user_config_dir("g4f-nodriver")
- else:
- user_data_dir = None
- debug.log(f"Open nodriver with user_dir: {user_data_dir}")
- browser = await nodriver.start(
- user_data_dir=user_data_dir,
- browser_args=None if proxy is None else [f"--proxy-server={proxy}"],
- )
+ browser = await get_nodriver(proxy=proxy)
page = browser.main_tab
def on_request(event: nodriver.cdp.network.RequestWillBeSent):
if event.request.url == start_url or event.request.url.startswith(conversation_url):
@@ -622,14 +500,14 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
pass
for c in await page.send(nodriver.cdp.network.get_cookies([cls.url])):
RequestConfig.cookies[c.name] = c.value
- RequestConfig.user_agent = await page.evaluate("window.navigator.userAgent")
+ user_agent = await page.evaluate("window.navigator.userAgent")
await page.select("#prompt-textarea", 240)
while True:
if RequestConfig.proof_token:
break
await asyncio.sleep(1)
await page.close()
- cls._create_request_args(RequestConfig.cookies, RequestConfig.headers, user_agent=RequestConfig.user_agent)
+ cls._create_request_args(RequestConfig.cookies, RequestConfig.headers, user_agent=user_agent)
cls._set_api_key(RequestConfig.access_token)
@staticmethod
@@ -672,90 +550,4 @@ class Conversation(BaseConversation):
self.conversation_id = conversation_id
self.message_id = message_id
self.finish_reason = finish_reason
- self.is_recipient = False
-
-class Response():
- """
- Class to encapsulate a response from the chat service.
- """
- def __init__(
- self,
- generator: AsyncResult,
- action: str,
- messages: Messages,
- options: dict
- ):
- self._generator = generator
- self.action = action
- self.is_end = False
- self._message = None
- self._messages = messages
- self._options = options
- self._fields = None
-
- async def generator(self) -> AsyncIterator:
- if self._generator is not None:
- self._generator = None
- chunks = []
- async for chunk in self._generator:
- if isinstance(chunk, Conversation):
- self._fields = chunk
- else:
- yield chunk
- chunks.append(str(chunk))
- self._message = "".join(chunks)
- if self._fields is None:
- raise RuntimeError("Missing response fields")
- self.is_end = self._fields.finish_reason == "stop"
-
- def __aiter__(self):
- return self.generator()
-
- async def get_message(self) -> str:
- await self.generator()
- return self._message
-
- async def get_fields(self) -> dict:
- await self.generator()
- return {
- "conversation_id": self._fields.conversation_id,
- "parent_id": self._fields.message_id
- }
-
- async def create_next(self, prompt: str, **kwargs) -> Response:
- return await OpenaiChat.create(
- **self._options,
- prompt=prompt,
- messages=await self.get_messages(),
- action="next",
- **await self.get_fields(),
- **kwargs
- )
-
- async def do_continue(self, **kwargs) -> Response:
- fields = await self.get_fields()
- if self.is_end:
- raise RuntimeError("Can't continue message. Message already finished.")
- return await OpenaiChat.create(
- **self._options,
- messages=await self.get_messages(),
- action="continue",
- **fields,
- **kwargs
- )
-
- async def create_variant(self, **kwargs) -> Response:
- if self.action != "next":
- raise RuntimeError("Can't create variant from continue or variant request.")
- return await OpenaiChat.create(
- **self._options,
- messages=self._messages,
- action="variant",
- **await self.get_fields(),
- **kwargs
- )
-
- async def get_messages(self) -> list:
- messages = self._messages
- messages.append({"role": "assistant", "content": await self.message()})
- return messages
+ self.is_recipient = False \ No newline at end of file
diff --git a/g4f/Provider/openai/har_file.py b/g4f/Provider/openai/har_file.py
index 2c7c1604..e863b6ac 100644
--- a/g4f/Provider/openai/har_file.py
+++ b/g4f/Provider/openai/har_file.py
@@ -25,7 +25,6 @@ class NoValidHarFileError(Exception):
pass
class RequestConfig:
- user_agent: str = None
cookies: dict = None
headers: dict = None
access_request_id: str = None
diff --git a/g4f/client/__init__.py b/g4f/client/__init__.py
index 38269cf6..f6a0f5e8 100644
--- a/g4f/client/__init__.py
+++ b/g4f/client/__init__.py
@@ -6,7 +6,6 @@ import random
import string
import asyncio
import base64
-import logging
from typing import Union, AsyncIterator, Iterator, Coroutine, Optional
from ..providers.base_provider import AsyncGeneratorProvider
@@ -16,13 +15,13 @@ from ..providers.types import ProviderType
from ..providers.response import ResponseType, FinishReason, BaseConversation, SynthesizeData
from ..errors import NoImageResponseError, ModelNotFoundError
from ..providers.retry_provider import IterListProvider
-from ..providers.base_provider import get_running_loop
+from ..providers.asyncio import get_running_loop, to_sync_generator, async_generator_to_list
from ..Provider.needs_auth.BingCreateImages import BingCreateImages
from .stubs import ChatCompletion, ChatCompletionChunk, Image, ImagesResponse
from .image_models import ImageModels
from .types import IterResponse, ImageProvider, Client as BaseClient
from .service import get_model_and_provider, get_last_provider, convert_to_provider
-from .helper import find_stop, filter_json, filter_none, safe_aclose, to_sync_iter, to_async_iterator
+from .helper import find_stop, filter_json, filter_none, safe_aclose, to_async_iterator
ChatCompletionResponseType = Iterator[Union[ChatCompletion, ChatCompletionChunk, BaseConversation]]
AsyncChatCompletionResponseType = AsyncIterator[Union[ChatCompletion, ChatCompletionChunk, BaseConversation]]
@@ -50,8 +49,7 @@ def iter_response(
idx = 0
if hasattr(response, '__aiter__'):
- # It's an async iterator, wrap it into a sync iterator
- response = to_sync_iter(response)
+ response = to_sync_generator(response)
for chunk in response:
if isinstance(chunk, FinishReason):
@@ -231,10 +229,10 @@ class Completions:
response = asyncio.run(response)
if stream and hasattr(response, '__aiter__'):
# It's an async generator, wrap it into a sync iterator
- response = to_sync_iter(response)
+ response = to_sync_generator(response)
elif hasattr(response, '__aiter__'):
# If response is an async generator, collect it into a list
- response = list(to_sync_iter(response))
+ response = asyncio.run(async_generator_to_list(response))
response = iter_response(response, stream, response_format, max_tokens, stop)
response = iter_append_model_and_provider(response)
if stream:
diff --git a/g4f/client/helper.py b/g4f/client/helper.py
index 93588c07..909cc132 100644
--- a/g4f/client/helper.py
+++ b/g4f/client/helper.py
@@ -1,10 +1,7 @@
from __future__ import annotations
import re
-import queue
-import threading
import logging
-import asyncio
from typing import AsyncIterator, Iterator, AsyncGenerator, Optional
@@ -53,33 +50,6 @@ async def safe_aclose(generator: AsyncGenerator) -> None:
except Exception as e:
logging.warning(f"Error while closing generator: {e}")
-# Helper function to convert an async generator to a synchronous iterator
-def to_sync_iter(async_gen: AsyncIterator) -> Iterator:
- q = queue.Queue()
- loop = asyncio.new_event_loop()
- done = object()
-
- def _run():
- asyncio.set_event_loop(loop)
-
- async def iterate():
- try:
- async for item in async_gen:
- q.put(item)
- finally:
- q.put(done)
-
- loop.run_until_complete(iterate())
- loop.close()
-
- threading.Thread(target=_run).start()
-
- while True:
- item = q.get()
- if item is done:
- break
- yield item
-
# Helper function to convert a synchronous iterator to an async iterator
async def to_async_iterator(iterator: Iterator) -> AsyncIterator:
for item in iterator:
diff --git a/g4f/gui/server/backend.py b/g4f/gui/server/backend.py
index fe020bb8..102c5685 100644
--- a/g4f/gui/server/backend.py
+++ b/g4f/gui/server/backend.py
@@ -1,13 +1,13 @@
import json
-import asyncio
import flask
import os
from flask import request, Flask
-from typing import AsyncGenerator, Generator
+from typing import Generator
from werkzeug.utils import secure_filename
from g4f.image import is_allowed_extension, to_image
from g4f.client.service import convert_to_provider
+from g4f.providers.asyncio import to_sync_generator
from g4f.errors import ProviderNotFoundError
from g4f.cookies import get_cookies_dir
from .api import Api
@@ -19,21 +19,6 @@ def safe_iter_generator(generator: Generator) -> Generator:
yield from generator
return iter_generator()
-def to_sync_generator(gen: AsyncGenerator) -> Generator:
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
- gen = gen.__aiter__()
- async def get_next():
- try:
- obj = await gen.__anext__()
- return False, obj
- except StopAsyncIteration: return True, None
- while True:
- done, obj = loop.run_until_complete(get_next())
- if done:
- break
- yield obj
-
class Backend_Api(Api):
"""
Handles various endpoints in a Flask application for backend operations.
diff --git a/g4f/providers/asyncio.py b/g4f/providers/asyncio.py
new file mode 100644
index 00000000..cf0ce1a0
--- /dev/null
+++ b/g4f/providers/asyncio.py
@@ -0,0 +1,65 @@
+from __future__ import annotations
+
+import asyncio
+from asyncio import AbstractEventLoop, runners
+from typing import Union, Callable, AsyncGenerator, Generator
+
+from ..errors import NestAsyncioError
+
+try:
+ import nest_asyncio
+ has_nest_asyncio = True
+except ImportError:
+ has_nest_asyncio = False
+try:
+ import uvloop
+ has_uvloop = True
+except ImportError:
+ has_uvloop = False
+
+def get_running_loop(check_nested: bool) -> Union[AbstractEventLoop, None]:
+ try:
+ loop = asyncio.get_running_loop()
+ # Do not patch uvloop loop because its incompatible.
+ if has_uvloop:
+ if isinstance(loop, uvloop.Loop):
+ return loop
+ if not hasattr(loop.__class__, "_nest_patched"):
+ if has_nest_asyncio:
+ nest_asyncio.apply(loop)
+ elif check_nested:
+ raise NestAsyncioError('Install "nest_asyncio" package | pip install -U nest_asyncio')
+ return loop
+ except RuntimeError:
+ pass
+
+# Fix for RuntimeError: async generator ignored GeneratorExit
+async def await_callback(callback: Callable):
+ return await callback()
+
+async def async_generator_to_list(generator: AsyncGenerator) -> list:
+ return [item async for item in generator]
+
+def to_sync_generator(generator: AsyncGenerator) -> Generator:
+ loop = get_running_loop(check_nested=False)
+ new_loop = False
+ if loop is None:
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ new_loop = True
+ gen = generator.__aiter__()
+ try:
+ while True:
+ yield loop.run_until_complete(await_callback(gen.__anext__))
+ except StopAsyncIteration:
+ pass
+ finally:
+ if new_loop:
+ try:
+ runners._cancel_all_tasks(loop)
+ loop.run_until_complete(loop.shutdown_asyncgens())
+ if hasattr(loop, "shutdown_default_executor"):
+ loop.run_until_complete(loop.shutdown_default_executor())
+ finally:
+ asyncio.set_event_loop(None)
+ loop.close() \ No newline at end of file
diff --git a/g4f/providers/base_provider.py b/g4f/providers/base_provider.py
index c6d0d950..e2c2f46a 100644
--- a/g4f/providers/base_provider.py
+++ b/g4f/providers/base_provider.py
@@ -7,30 +7,14 @@ from asyncio import AbstractEventLoop
from concurrent.futures import ThreadPoolExecutor
from abc import abstractmethod
from inspect import signature, Parameter
-from typing import Callable, Union
from ..typing import CreateResult, AsyncResult, Messages
from .types import BaseProvider
+from .asyncio import get_running_loop, to_sync_generator
from .response import FinishReason, BaseConversation, SynthesizeData
-from ..errors import NestAsyncioError, ModelNotSupportedError
+from ..errors import ModelNotSupportedError
from .. import debug
-if sys.version_info < (3, 10):
- NoneType = type(None)
-else:
- from types import NoneType
-
-try:
- import nest_asyncio
- has_nest_asyncio = True
-except ImportError:
- has_nest_asyncio = False
-try:
- import uvloop
- has_uvloop = True
-except ImportError:
- has_uvloop = False
-
# Set Windows event loop policy for better compatibility with asyncio and curl_cffi
if sys.platform == 'win32':
try:
@@ -41,26 +25,6 @@ if sys.platform == 'win32':
except ImportError:
pass
-def get_running_loop(check_nested: bool) -> Union[AbstractEventLoop, None]:
- try:
- loop = asyncio.get_running_loop()
- # Do not patch uvloop loop because its incompatible.
- if has_uvloop:
- if isinstance(loop, uvloop.Loop):
- return loop
- if not hasattr(loop.__class__, "_nest_patched"):
- if has_nest_asyncio:
- nest_asyncio.apply(loop)
- elif check_nested:
- raise NestAsyncioError('Install "nest_asyncio" package | pip install -U nest_asyncio')
- return loop
- except RuntimeError:
- pass
-
-# Fix for RuntimeError: async generator ignored GeneratorExit
-async def await_callback(callback: Callable):
- return await callback()
-
class AbstractProvider(BaseProvider):
"""
Abstract class for providing asynchronous functionality to derived classes.
@@ -136,7 +100,6 @@ class AbstractProvider(BaseProvider):
return f"g4f.Provider.{cls.__name__} supports: ({args}\n)"
-
class AsyncProvider(AbstractProvider):
"""
Provides asynchronous functionality for creating completions.
@@ -218,25 +181,9 @@ class AsyncGeneratorProvider(AsyncProvider):
Returns:
CreateResult: The result of the streaming completion creation.
"""
- loop = get_running_loop(check_nested=False)
- new_loop = False
- if loop is None:
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
- new_loop = True
-
- generator = cls.create_async_generator(model, messages, stream=stream, **kwargs)
- gen = generator.__aiter__()
-
- try:
- while True:
- yield loop.run_until_complete(await_callback(gen.__anext__))
- except StopAsyncIteration:
- pass
- finally:
- if new_loop:
- loop.close()
- asyncio.set_event_loop(None)
+ return to_sync_generator(
+ cls.create_async_generator(model, messages, stream=stream, **kwargs)
+ )
@classmethod
async def create_async(
diff --git a/setup.py b/setup.py
index 0cafb642..12581be9 100644
--- a/setup.py
+++ b/setup.py
@@ -23,18 +23,27 @@ EXTRA_REQUIRE = {
"browser_cookie3", # get_cookies
"duckduckgo-search>=5.0" ,# internet.search
"beautifulsoup4", # internet.search and bing.create_images
- "brotli", # openai, bing
"platformdirs",
- "cryptography",
"aiohttp_socks", # proxy
"pillow", # image
"cairosvg", # svg image
"werkzeug", "flask", # gui
"fastapi", # api
- "uvicorn", "nest_asyncio", # api
- "pycryptodome", # openai
+ "uvicorn", # api
"nodriver",
],
+ 'slim': [
+ "curl_cffi>=0.6.2",
+ "certifi",
+ "duckduckgo-search>=5.0" ,# internet.search
+ "beautifulsoup4", # internet.search and bing.create_images
+ "aiohttp_socks", # proxy
+ "pillow", # image
+ "cairosvg", # svg image
+ "werkzeug", "flask", # gui
+ "fastapi", # api
+ "uvicorn", # api
+ ],
"image": [
"pillow",
"cairosvg",