From 6ce493d4dfc2884832ff5b5be4479a55818b2fe7 Mon Sep 17 00:00:00 2001 From: H Lohaus Date: Sat, 16 Nov 2024 13:19:51 +0100 Subject: Fix api streaming, fix AsyncClient (#2357) * Fix api streaming, fix AsyncClient, Improve Client class, Some providers fixes, Update models list, Fix some tests, Update model list in Airforce provid er, Add OpenAi image generation url to api, Fix reload and debug in api arguments, Fix websearch in gui * Fix Cloadflare and Pi and AmigoChat provider * Fix conversation support in DDG provider, Add cloudflare bypass with nodriver * Fix unittests without curl_cffi --- g4f/Provider/Airforce.py | 184 +++++++++--- g4f/Provider/AmigoChat.py | 176 +++++++++++ g4f/Provider/Cloudflare.py | 136 +++------ g4f/Provider/DDG.py | 108 ++++--- g4f/Provider/HuggingChat.py | 9 +- g4f/Provider/Liaobots.py | 2 +- g4f/Provider/Pi.py | 80 ++--- g4f/Provider/__init__.py | 1 + g4f/Provider/bing/create_images.py | 2 +- g4f/Provider/needs_auth/OpenaiChat.py | 2 +- g4f/Provider/not_working/AmigoChat.py | 189 ------------ g4f/Provider/not_working/__init__.py | 1 - g4f/Provider/openai/proofofwork.py | 1 - g4f/api/__init__.py | 92 +++--- g4f/cli.py | 27 +- g4f/client/__init__.py | 470 ++++++++++++++++++++++++++++- g4f/client/client.py | 541 ---------------------------------- g4f/client/helper.py | 47 ++- g4f/client/service.py | 30 +- g4f/client/stubs.py | 13 +- g4f/client/types.py | 24 +- g4f/gui/server/internet.py | 2 +- g4f/models.py | 187 ++---------- g4f/providers/base_provider.py | 35 ++- g4f/providers/types.py | 1 + g4f/requests/__init__.py | 62 +++- g4f/requests/raise_for_status.py | 2 + 27 files changed, 1210 insertions(+), 1214 deletions(-) create mode 100644 g4f/Provider/AmigoChat.py delete mode 100644 g4f/Provider/not_working/AmigoChat.py delete mode 100644 g4f/client/client.py (limited to 'g4f') diff --git a/g4f/Provider/Airforce.py b/g4f/Provider/Airforce.py index c7ae44c0..6254e160 100644 --- a/g4f/Provider/Airforce.py +++ b/g4f/Provider/Airforce.py @@ -1,59 +1,171 @@ from __future__ import annotations -from typing import Any, Dict -import inspect -from aiohttp import ClientSession +import random +import json +import re from ..typing import AsyncResult, Messages from .base_provider import AsyncGeneratorProvider, ProviderModelMixin -from .helper import format_prompt +from ..image import ImageResponse +from ..requests import StreamSession, raise_for_status from .airforce.AirforceChat import AirforceChat from .airforce.AirforceImage import AirforceImage class Airforce(AsyncGeneratorProvider, ProviderModelMixin): url = "https://api.airforce" api_endpoint_completions = AirforceChat.api_endpoint - api_endpoint_imagine2 = AirforceImage.api_endpoint + api_endpoint_imagine = AirforceImage.api_endpoint working = True - supports_stream = AirforceChat.supports_stream - supports_system_message = AirforceChat.supports_system_message - supports_message_history = AirforceChat.supports_message_history - - default_model = AirforceChat.default_model - models = [*AirforceChat.models, *AirforceImage.models] - + default_model = "gpt-4o-mini" + supports_system_message = True + supports_message_history = True + text_models = [ + 'gpt-4-turbo', + default_model, + 'llama-3.1-70b-turbo', + 'llama-3.1-8b-turbo', + ] + image_models = [ + 'flux', + 'flux-realism', + 'flux-anime', + 'flux-3d', + 'flux-disney', + 'flux-pixel', + 'flux-4o', + 'any-dark', + ] + models = [ + *text_models, + *image_models, + ] model_aliases = { - **AirforceChat.model_aliases, - **AirforceImage.model_aliases + "gpt-4o": "chatgpt-4o-latest", + "llama-3.1-70b": "llama-3.1-70b-turbo", + "llama-3.1-8b": "llama-3.1-8b-turbo", + "gpt-4": "gpt-4-turbo", } @classmethod - def get_model(cls, model: str) -> str: - if model in cls.models: - return model - elif model in cls.model_aliases: - return cls.model_aliases[model] + def create_async_generator( + cls, + model: str, + messages: Messages, + proxy: str = None, + seed: int = None, + size: str = "1:1", + stream: bool = False, + **kwargs + ) -> AsyncResult: + model = cls.get_model(model) + + if model in cls.image_models: + return cls._generate_image(model, messages, proxy, seed, size) else: - return cls.default_model + return cls._generate_text(model, messages, proxy, stream, **kwargs) @classmethod - async def create_async_generator(cls, model: str, messages: Messages, **kwargs) -> AsyncResult: - model = cls.get_model(model) - - provider = AirforceChat if model in AirforceChat.text_models else AirforceImage + async def _generate_image( + cls, + model: str, + messages: Messages, + proxy: str = None, + seed: int = None, + size: str = "1:1", + **kwargs + ) -> AsyncResult: + headers = { + "accept": "*/*", + "accept-language": "en-US,en;q=0.9", + "cache-control": "no-cache", + "origin": "https://llmplayground.net", + "user-agent": "Mozilla/5.0" + } + if seed is None: + seed = random.randint(0, 100000) + prompt = messages[-1]['content'] - if model not in provider.models: - raise ValueError(f"Unsupported model: {model}") + async with StreamSession(headers=headers, proxy=proxy) as session: + params = { + "model": model, + "prompt": prompt, + "size": size, + "seed": seed + } + async with session.get(f"{cls.api_endpoint_imagine}", params=params) as response: + await raise_for_status(response) + content_type = response.headers.get('Content-Type', '').lower() - # Get the signature of the provider's create_async_generator method - sig = inspect.signature(provider.create_async_generator) - - # Filter kwargs to only include parameters that the provider's method accepts - filtered_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} + if 'application/json' in content_type: + raise RuntimeError(await response.json().get("error", {}).get("message")) + elif 'image' in content_type: + image_data = b"" + async for chunk in response.iter_content(): + if chunk: + image_data += chunk + image_url = f"{cls.api_endpoint_imagine}?model={model}&prompt={prompt}&size={size}&seed={seed}" + yield ImageResponse(images=image_url, alt=prompt) - # Add model and messages to filtered_kwargs - filtered_kwargs['model'] = model - filtered_kwargs['messages'] = messages + @classmethod + async def _generate_text( + cls, + model: str, + messages: Messages, + proxy: str = None, + stream: bool = False, + max_tokens: int = 4096, + temperature: float = 1, + top_p: float = 1, + **kwargs + ) -> AsyncResult: + headers = { + "accept": "*/*", + "accept-language": "en-US,en;q=0.9", + "authorization": "Bearer missing api key", + "content-type": "application/json", + "user-agent": "Mozilla/5.0" + } + async with StreamSession(headers=headers, proxy=proxy) as session: + data = { + "messages": messages, + "model": model, + "max_tokens": max_tokens, + "temperature": temperature, + "top_p": top_p, + "stream": stream + } + async with session.post(cls.api_endpoint_completions, json=data) as response: + await raise_for_status(response) + content_type = response.headers.get('Content-Type', '').lower() + if 'application/json' in content_type: + json_data = await response.json() + if json_data.get("model") == "error": + raise RuntimeError(json_data['choices'][0]['message'].get('content', '')) + if stream: + async for line in response.iter_lines(): + if line: + line = line.decode('utf-8').strip() + if line.startswith("data: ") and line != "data: [DONE]": + json_data = json.loads(line[6:]) + content = json_data['choices'][0]['delta'].get('content', '') + if content: + yield cls._filter_content(content) + else: + json_data = await response.json() + content = json_data['choices'][0]['message']['content'] + yield cls._filter_content(content) - async for result in provider.create_async_generator(**filtered_kwargs): - yield result + @classmethod + def _filter_content(cls, part_response: str) -> str: + part_response = re.sub( + r"One message exceeds the \d+chars per message limit\..+https:\/\/discord\.com\/invite\/\S+", + '', + part_response + ) + + part_response = re.sub( + r"Rate limit \(\d+\/minute\) exceeded\. Join our discord for more: .+https:\/\/discord\.com\/invite\/\S+", + '', + part_response + ) + return part_response \ No newline at end of file diff --git a/g4f/Provider/AmigoChat.py b/g4f/Provider/AmigoChat.py new file mode 100644 index 00000000..2e66dccf --- /dev/null +++ b/g4f/Provider/AmigoChat.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +import json +import uuid + +from ..typing import AsyncResult, Messages +from .base_provider import AsyncGeneratorProvider, ProviderModelMixin +from ..image import ImageResponse +from ..requests import StreamSession, raise_for_status +from ..errors import ResponseStatusError + +class AmigoChat(AsyncGeneratorProvider, ProviderModelMixin): + url = "https://amigochat.io/chat/" + chat_api_endpoint = "https://api.amigochat.io/v1/chat/completions" + image_api_endpoint = "https://api.amigochat.io/v1/images/generations" + working = True + supports_stream = True + supports_system_message = True + supports_message_history = True + + default_model = 'gpt-4o-mini' + + chat_models = [ + 'gpt-4o', + default_model, + 'o1-preview', + 'o1-mini', + 'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo', + 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo', + 'claude-3-sonnet-20240229', + 'gemini-1.5-pro', + ] + + image_models = [ + 'flux-pro/v1.1', + 'flux-realism', + 'flux-pro', + 'dalle-e-3', + ] + + models = [*chat_models, *image_models] + + model_aliases = { + "o1": "o1-preview", + "llama-3.1-405b": "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", + "llama-3.2-90b": "meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo", + "claude-3.5-sonnet": "claude-3-sonnet-20240229", + "gemini-pro": "gemini-1.5-pro", + + "flux-pro": "flux-pro/v1.1", + "dalle-3": "dalle-e-3", + } + + persona_ids = { + 'gpt-4o': "gpt", + 'gpt-4o-mini': "amigo", + 'o1-preview': "openai-o-one", + 'o1-mini': "openai-o-one-mini", + 'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo': "llama-three-point-one", + 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo': "llama-3-2", + 'claude-3-sonnet-20240229': "claude", + 'gemini-1.5-pro': "gemini-1-5-pro", + 'flux-pro/v1.1': "flux-1-1-pro", + 'flux-realism': "flux-realism", + 'flux-pro': "flux-pro", + 'dalle-e-3': "dalle-three", + } + + @classmethod + def get_personaId(cls, model: str) -> str: + return cls.persona_ids[model] + + @classmethod + async def create_async_generator( + cls, + model: str, + messages: Messages, + proxy: str = None, + stream: bool = False, + timeout: int = 300, + frequency_penalty: float = 0, + max_tokens: int = 4000, + presence_penalty: float = 0, + temperature: float = 0.5, + top_p: float = 0.95, + **kwargs + ) -> AsyncResult: + model = cls.get_model(model) + + device_uuid = str(uuid.uuid4()) + max_retries = 3 + retry_count = 0 + + while retry_count < max_retries: + try: + headers = { + "accept": "*/*", + "accept-language": "en-US,en;q=0.9", + "authorization": "Bearer", + "cache-control": "no-cache", + "content-type": "application/json", + "origin": cls.url, + "pragma": "no-cache", + "priority": "u=1, i", + "referer": f"{cls.url}/", + "sec-ch-ua": '"Chromium";v="129", "Not=A?Brand";v="8"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Linux"', + "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", + "x-device-language": "en-US", + "x-device-platform": "web", + "x-device-uuid": device_uuid, + "x-device-version": "1.0.41" + } + + async with StreamSession(headers=headers, proxy=proxy) as session: + if model not in cls.image_models: + data = { + "messages": messages, + "model": model, + "personaId": cls.get_personaId(model), + "frequency_penalty": frequency_penalty, + "max_tokens": max_tokens, + "presence_penalty": presence_penalty, + "stream": stream, + "temperature": temperature, + "top_p": top_p + } + async with session.post(cls.chat_api_endpoint, json=data, timeout=timeout) as response: + await raise_for_status(response) + async for line in response.iter_lines(): + line = line.decode('utf-8').strip() + if line.startswith('data: '): + if line == 'data: [DONE]': + break + try: + chunk = json.loads(line[6:]) # Remove 'data: ' prefix + if 'choices' in chunk and len(chunk['choices']) > 0: + choice = chunk['choices'][0] + if 'delta' in choice: + content = choice['delta'].get('content') + elif 'text' in choice: + content = choice['text'] + else: + content = None + if content: + yield content + except json.JSONDecodeError: + pass + else: + # Image generation + prompt = messages[-1]['content'] + data = { + "prompt": prompt, + "model": model, + "personaId": cls.get_personaId(model) + } + async with session.post(cls.image_api_endpoint, json=data) as response: + await raise_for_status(response) + response_data = await response.json() + if "data" in response_data: + image_urls = [] + for item in response_data["data"]: + if "url" in item: + image_url = item["url"] + image_urls.append(image_url) + if image_urls: + yield ImageResponse(image_urls, prompt) + else: + yield None + break + except (ResponseStatusError, Exception) as e: + retry_count += 1 + if retry_count >= max_retries: + raise e + device_uuid = str(uuid.uuid4()) diff --git a/g4f/Provider/Cloudflare.py b/g4f/Provider/Cloudflare.py index 8fb37bef..825c5027 100644 --- a/g4f/Provider/Cloudflare.py +++ b/g4f/Provider/Cloudflare.py @@ -1,72 +1,52 @@ from __future__ import annotations -from aiohttp import ClientSession import asyncio import json import uuid -import cloudscraper -from typing import AsyncGenerator -from ..typing import AsyncResult, Messages -from .base_provider import AsyncGeneratorProvider, ProviderModelMixin -from .helper import format_prompt +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 class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin): label = "Cloudflare AI" url = "https://playground.ai.cloudflare.com" api_endpoint = "https://playground.ai.cloudflare.com/api/inference" + models_url = "https://playground.ai.cloudflare.com/api/models" working = True supports_stream = True supports_system_message = True supports_message_history = True - - default_model = '@cf/meta/llama-3.1-8b-instruct-awq' - models = [ - '@cf/meta/llama-2-7b-chat-fp16', - '@cf/meta/llama-2-7b-chat-int8', - - '@cf/meta/llama-3-8b-instruct', - '@cf/meta/llama-3-8b-instruct-awq', - '@hf/meta-llama/meta-llama-3-8b-instruct', - - default_model, - '@cf/meta/llama-3.1-8b-instruct-fp8', - - '@cf/meta/llama-3.2-1b-instruct', - - '@hf/mistral/mistral-7b-instruct-v0.2', - - '@cf/qwen/qwen1.5-7b-chat-awq', - - '@cf/defog/sqlcoder-7b-2', - ] - + default_model = "@cf/meta/llama-3.1-8b-instruct" model_aliases = { "llama-2-7b": "@cf/meta/llama-2-7b-chat-fp16", "llama-2-7b": "@cf/meta/llama-2-7b-chat-int8", - "llama-3-8b": "@cf/meta/llama-3-8b-instruct", "llama-3-8b": "@cf/meta/llama-3-8b-instruct-awq", "llama-3-8b": "@hf/meta-llama/meta-llama-3-8b-instruct", - "llama-3.1-8b": "@cf/meta/llama-3.1-8b-instruct-awq", "llama-3.1-8b": "@cf/meta/llama-3.1-8b-instruct-fp8", - "llama-3.2-1b": "@cf/meta/llama-3.2-1b-instruct", - "qwen-1.5-7b": "@cf/qwen/qwen1.5-7b-chat-awq", - - #"sqlcoder-7b": "@cf/defog/sqlcoder-7b-2", } + _args: dict = None @classmethod - def get_model(cls, model: str) -> str: - if model in cls.models: - return model - elif model in cls.model_aliases: - return cls.model_aliases[model] - else: - return cls.default_model + def get_models(cls) -> str: + if not cls.models: + if cls._args is None: + get_running_loop(check_nested=True) + args = get_args_from_nodriver(cls.url, cookies={ + '__cf_bm': uuid.uuid4().hex, + }) + cls._args = asyncio.run(args) + with Session(**cls._args) as session: + response = session.get(cls.models_url) + raise_for_status(response) + json_data = response.json() + cls.models = [model.get("name") for model in json_data.get("models")] + cls._args["cookies"] = merge_cookies(cls._args["cookies"] , response) + return cls.models @classmethod async def create_async_generator( @@ -75,76 +55,34 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin): messages: Messages, proxy: str = None, max_tokens: int = 2048, + cookies: Cookies = None, + timeout: int = 300, **kwargs ) -> AsyncResult: model = cls.get_model(model) - - headers = { - 'Accept': 'text/event-stream', - 'Accept-Language': 'en-US,en;q=0.9', - 'Cache-Control': 'no-cache', - 'Content-Type': 'application/json', - 'Origin': cls.url, - 'Pragma': 'no-cache', - 'Referer': f'{cls.url}/', - 'Sec-Ch-Ua': '"Chromium";v="129", "Not=A?Brand";v="8"', - 'Sec-Ch-Ua-Mobile': '?0', - 'Sec-Ch-Ua-Platform': '"Linux"', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36', - } - - cookies = { - '__cf_bm': uuid.uuid4().hex, - } - - scraper = cloudscraper.create_scraper() - + if cls._args is None: + cls._args = await get_args_from_nodriver(cls.url, proxy, timeout, cookies) data = { - "messages": [ - {"role": "user", "content": format_prompt(messages)} - ], + "messages": messages, "lora": None, "model": model, "max_tokens": max_tokens, "stream": True } - - max_retries = 3 - full_response = "" - - for attempt in range(max_retries): - try: - response = scraper.post( - cls.api_endpoint, - headers=headers, - cookies=cookies, - json=data, - stream=True, - proxies={'http': proxy, 'https': proxy} if proxy else None - ) - - if response.status_code == 403: - await asyncio.sleep(2 ** attempt) - continue - - response.raise_for_status() - - for line in response.iter_lines(): + async with StreamSession(**cls._args) as session: + async with session.post( + cls.api_endpoint, + json=data, + ) as response: + await raise_for_status(response) + cls._args["cookies"] = merge_cookies(cls._args["cookies"] , response) + async for line in response.iter_lines(): if line.startswith(b'data: '): if line == b'data: [DONE]': - if full_response: - yield full_response break try: - content = json.loads(line[6:].decode('utf-8')) - if 'response' in content and content['response'] != '': + content = json.loads(line[6:].decode()) + if content.get("response") and content.get("response") != '': yield content['response'] except Exception: - continue - break - except Exception as e: - if attempt == max_retries - 1: - raise + continue \ No newline at end of file diff --git a/g4f/Provider/DDG.py b/g4f/Provider/DDG.py index 43cc39c0..c4be0ea8 100644 --- a/g4f/Provider/DDG.py +++ b/g4f/Provider/DDG.py @@ -2,12 +2,31 @@ from __future__ import annotations import json import aiohttp -from aiohttp import ClientSession +from aiohttp import ClientSession, BaseConnector from ..typing import AsyncResult, Messages -from .base_provider import AsyncGeneratorProvider, ProviderModelMixin +from .base_provider import AsyncGeneratorProvider, ProviderModelMixin, BaseConversation from .helper import format_prompt +from ..requests.aiohttp import get_connector +from ..requests.raise_for_status import raise_for_status +from .. import debug +MODELS = [ + {"model":"gpt-4o","modelName":"GPT-4o","modelVariant":None,"modelStyleId":"gpt-4o-mini","createdBy":"OpenAI","moderationLevel":"HIGH","isAvailable":1,"inputCharLimit":16e3,"settingId":"4"}, + {"model":"gpt-4o-mini","modelName":"GPT-4o","modelVariant":"mini","modelStyleId":"gpt-4o-mini","createdBy":"OpenAI","moderationLevel":"HIGH","isAvailable":0,"inputCharLimit":16e3,"settingId":"3"}, + {"model":"claude-3-5-sonnet-20240620","modelName":"Claude 3.5","modelVariant":"Sonnet","modelStyleId":"claude-3-haiku","createdBy":"Anthropic","moderationLevel":"HIGH","isAvailable":1,"inputCharLimit":16e3,"settingId":"7"}, + {"model":"claude-3-opus-20240229","modelName":"Claude 3","modelVariant":"Opus","modelStyleId":"claude-3-haiku","createdBy":"Anthropic","moderationLevel":"HIGH","isAvailable":1,"inputCharLimit":16e3,"settingId":"2"}, + {"model":"claude-3-haiku-20240307","modelName":"Claude 3","modelVariant":"Haiku","modelStyleId":"claude-3-haiku","createdBy":"Anthropic","moderationLevel":"HIGH","isAvailable":0,"inputCharLimit":16e3,"settingId":"1"}, + {"model":"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo","modelName":"Llama 3.1","modelVariant":"70B","modelStyleId":"llama-3","createdBy":"Meta","moderationLevel":"MEDIUM","isAvailable":0,"isOpenSource":0,"inputCharLimit":16e3,"settingId":"5"}, + {"model":"mistralai/Mixtral-8x7B-Instruct-v0.1","modelName":"Mixtral","modelVariant":"8x7B","modelStyleId":"mixtral","createdBy":"Mistral AI","moderationLevel":"LOW","isAvailable":0,"isOpenSource":0,"inputCharLimit":16e3,"settingId":"6"} +] + +class Conversation(BaseConversation): + vqd: str = None + message_history: Messages = [] + + def __init__(self, model: str): + self.model = model class DDG(AsyncGeneratorProvider, ProviderModelMixin): url = "https://duckduckgo.com" @@ -18,81 +37,74 @@ class DDG(AsyncGeneratorProvider, ProviderModelMixin): supports_message_history = True default_model = "gpt-4o-mini" - models = [ - "gpt-4o-mini", - "claude-3-haiku-20240307", - "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", - "mistralai/Mixtral-8x7B-Instruct-v0.1" - ] + models = [model.get("model") for model in MODELS] model_aliases = { "claude-3-haiku": "claude-3-haiku-20240307", "llama-3.1-70b": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", - "mixtral-8x7b": "mistralai/Mixtral-8x7B-Instruct-v0.1" + "mixtral-8x7b": "mistralai/Mixtral-8x7B-Instruct-v0.1", + "gpt-4": "gpt-4o-mini" } @classmethod - def get_model(cls, model: str) -> str: - return cls.model_aliases.get(model, model) if model in cls.model_aliases else cls.default_model - - @classmethod - async def get_vqd(cls): + async def get_vqd(cls, proxy: str, connector: BaseConnector = None): status_url = "https://duckduckgo.com/duckchat/v1/status" - headers = { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', 'Accept': 'text/event-stream', 'x-vqd-accept': '1' } - - async with aiohttp.ClientSession() as session: - try: - async with session.get(status_url, headers=headers) as response: - if response.status == 200: - return response.headers.get("x-vqd-4") - else: - print(f"Error: Status code {response.status}") - return None - except Exception as e: - print(f"Error getting VQD: {e}") - return None + async with aiohttp.ClientSession(connector=get_connector(connector, proxy)) as session: + async with session.get(status_url, headers=headers) as response: + await raise_for_status(response) + return response.headers.get("x-vqd-4") @classmethod async def create_async_generator( cls, model: str, messages: Messages, - conversation: dict = None, + conversation: Conversation = None, + return_conversation: bool = False, proxy: str = None, + connector: BaseConnector = None, **kwargs ) -> AsyncResult: model = cls.get_model(model) + is_new_conversation = False + if conversation is None: + conversation = Conversation(model) + is_new_conversation = True + debug.last_model = model + if conversation.vqd is None: + conversation.vqd = await cls.get_vqd(proxy, connector) + if not conversation.vqd: + raise Exception("Failed to obtain VQD token") + headers = { 'accept': 'text/event-stream', 'content-type': 'application/json', 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + 'x-vqd-4': conversation.vqd, } - - vqd = conversation.get('vqd') if conversation else await cls.get_vqd() - if not vqd: - raise Exception("Failed to obtain VQD token") - - headers['x-vqd-4'] = vqd - - if conversation: - message_history = conversation.get('messages', []) - message_history.append({"role": "user", "content": format_prompt(messages)}) - else: - message_history = [{"role": "user", "content": format_prompt(messages)}] - - async with ClientSession(headers=headers) as session: + async with ClientSession(headers=headers, connector=get_connector(connector, proxy)) as session: + if is_new_conversation: + conversation.message_history = [{"role": "user", "content": format_prompt(messages)}] + else: + conversation.message_history = [ + *conversation.message_history, + messages[-2], + messages[-1] + ] + if return_conversation: + yield conversation data = { - "model": model, - "messages": message_history + "model": conversation.model, + "messages": conversation.message_history } - - async with session.post(cls.api_endpoint, json=data, proxy=proxy) as response: - response.raise_for_status() + async with session.post(cls.api_endpoint, json=data) as response: + conversation.vqd = response.headers.get("x-vqd-4") + await raise_for_status(response) async for line in response.content: if line: decoded_line = line.decode('utf-8') @@ -105,4 +117,4 @@ class DDG(AsyncGeneratorProvider, ProviderModelMixin): if 'message' in json_data: yield json_data['message'] except json.JSONDecodeError: - pass + pass \ No newline at end of file diff --git a/g4f/Provider/HuggingChat.py b/g4f/Provider/HuggingChat.py index d4a4b497..509a7f16 100644 --- a/g4f/Provider/HuggingChat.py +++ b/g4f/Provider/HuggingChat.py @@ -3,8 +3,13 @@ from __future__ import annotations import json import requests -from curl_cffi import requests as cf_reqs +try: + from curl_cffi import requests as cf_reqs + has_curl_cffi = True +except ImportError: + has_curl_cffi = False from ..typing import CreateResult, Messages +from ..errors import MissingRequirementsError from .base_provider import ProviderModelMixin, AbstractProvider from .helper import format_prompt @@ -55,6 +60,8 @@ class HuggingChat(AbstractProvider, ProviderModelMixin): stream: bool, **kwargs ) -> CreateResult: + if not has_curl_cffi: + raise MissingRequirementsError('Install "curl_cffi" package | pip install -U curl_cffi') model = cls.get_model(model) if model in cls.models: diff --git a/g4f/Provider/Liaobots.py b/g4f/Provider/Liaobots.py index 7ccfa877..fc50bdee 100644 --- a/g4f/Provider/Liaobots.py +++ b/g4f/Provider/Liaobots.py @@ -179,7 +179,7 @@ class Liaobots(AsyncGeneratorProvider, ProviderModelMixin): "gpt-4o": "gpt-4o-2024-08-06", "gpt-4-turbo": "gpt-4-turbo-2024-04-09", - "gpt-4": "gpt-4-0613", + "gpt-4": "gpt-4o-mini-free", "claude-3-opus": "claude-3-opus-20240229", "claude-3-opus": "claude-3-opus-20240229-aws", diff --git a/g4f/Provider/Pi.py b/g4f/Provider/Pi.py index 68a7357f..6aabe7b1 100644 --- a/g4f/Provider/Pi.py +++ b/g4f/Provider/Pi.py @@ -2,20 +2,21 @@ from __future__ import annotations import json -from ..typing import CreateResult, Messages -from .base_provider import AbstractProvider, format_prompt -from ..requests import Session, get_session_from_browser, raise_for_status +from ..typing import AsyncResult, Messages, Cookies +from .base_provider import AsyncGeneratorProvider, format_prompt +from ..requests import StreamSession, get_args_from_nodriver, raise_for_status, merge_cookies -class Pi(AbstractProvider): +class Pi(AsyncGeneratorProvider): url = "https://pi.ai/talk" working = True supports_stream = True - _session = None default_model = "pi" models = [default_model] + _headers: dict = None + _cookies: Cookies = {} @classmethod - def create_completion( + async def create_async_generator( cls, model: str, messages: Messages, @@ -23,49 +24,52 @@ class Pi(AbstractProvider): proxy: str = None, timeout: int = 180, conversation_id: str = None, - webdriver: WebDriver = None, **kwargs - ) -> CreateResult: - if cls._session is None: - cls._session = get_session_from_browser(url=cls.url, proxy=proxy, timeout=timeout) - if not conversation_id: - conversation_id = cls.start_conversation(cls._session) - prompt = format_prompt(messages) - else: - prompt = messages[-1]["content"] - answer = cls.ask(cls._session, prompt, conversation_id) - for line in answer: - if "text" in line: - yield line["text"] - + ) -> AsyncResult: + if cls._headers is None: + args = await get_args_from_nodriver(cls.url, proxy=proxy, timeout=timeout) + cls._cookies = args.get("cookies", {}) + cls._headers = args.get("headers") + async with StreamSession(headers=cls._headers, cookies=cls._cookies, proxy=proxy) as session: + if not conversation_id: + conversation_id = await cls.start_conversation(session) + prompt = format_prompt(messages) + else: + prompt = messages[-1]["content"] + answer = cls.ask(session, prompt, conversation_id) + async for line in answer: + if "text" in line: + yield line["text"] + @classmethod - def start_conversation(cls, session: Session) -> str: - response = session.post('https://pi.ai/api/chat/start', data="{}", headers={ + async def start_conversation(cls, session: StreamSession) -> str: + async with session.post('https://pi.ai/api/chat/start', data="{}", headers={ 'accept': 'application/json', 'x-api-version': '3' - }) - raise_for_status(response) - return response.json()['conversations'][0]['sid'] + }) as response: + await raise_for_status(response) + return (await response.json())['conversations'][0]['sid'] - def get_chat_history(session: Session, conversation_id: str): + async def get_chat_history(session: StreamSession, conversation_id: str): params = { 'conversation': conversation_id, } - response = session.get('https://pi.ai/api/chat/history', params=params) - raise_for_status(response) - return response.json() + async with session.get('https://pi.ai/api/chat/history', params=params) as response: + await raise_for_status(response) + return await response.json() - def ask(session: Session, prompt: str, conversation_id: str): + @classmethod + async def ask(cls, session: StreamSession, prompt: str, conversation_id: str): json_data = { 'text': prompt, 'conversation': conversation_id, 'mode': 'BASE', } - response = session.post('https://pi.ai/api/chat', json=json_data, stream=True) - raise_for_status(response) - for line in response.iter_lines(): - if line.startswith(b'data: {"text":'): - yield json.loads(line.split(b'data: ')[1]) - elif line.startswith(b'data: {"title":'): - yield json.loads(line.split(b'data: ')[1]) - + async with session.post('https://pi.ai/api/chat', json=json_data) as response: + await raise_for_status(response) + cls._cookies = merge_cookies(cls._cookies, response) + async for line in response.iter_lines(): + if line.startswith(b'data: {"text":'): + yield json.loads(line.split(b'data: ')[1]) + elif line.startswith(b'data: {"title":'): + yield json.loads(line.split(b'data: ')[1]) diff --git a/g4f/Provider/__init__.py b/g4f/Provider/__init__.py index da0eacfe..8a162baf 100644 --- a/g4f/Provider/__init__.py +++ b/g4f/Provider/__init__.py @@ -13,6 +13,7 @@ from .local import * from .AIUncensored import AIUncensored from .Airforce import Airforce +from .AmigoChat import AmigoChat from .Bing import Bing from .Blackbox import Blackbox from .ChatGpt import ChatGpt diff --git a/g4f/Provider/bing/create_images.py b/g4f/Provider/bing/create_images.py index 7a08ddfe..45ba30b6 100644 --- a/g4f/Provider/bing/create_images.py +++ b/g4f/Provider/bing/create_images.py @@ -132,7 +132,7 @@ async def create_images(session: ClientSession, prompt: str, timeout: int = TIME redirect_url = response.headers["Location"].replace("&nfy=1", "") redirect_url = f"{BING_URL}{redirect_url}" - request_id = redirect_url.split("id=")[1] + request_id = redirect_url.split("id=")[-1] async with session.get(redirect_url) as response: response.raise_for_status() diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py index 3a0d6b29..85e11181 100644 --- a/g4f/Provider/needs_auth/OpenaiChat.py +++ b/g4f/Provider/needs_auth/OpenaiChat.py @@ -196,7 +196,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin): async with session.get(url, headers=headers) as response: cls._update_request_args(session) if response.status == 401: - raise MissingAuthError('Add a "api_key" or a .har file' if cls._api_key is None else "Invalid api key") + 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: diff --git a/g4f/Provider/not_working/AmigoChat.py b/g4f/Provider/not_working/AmigoChat.py deleted file mode 100644 index 274a5e14..00000000 --- a/g4f/Provider/not_working/AmigoChat.py +++ /dev/null @@ -1,189 +0,0 @@ -from __future__ import annotations - -import json -import uuid -from aiohttp import ClientSession, ClientTimeout, ClientResponseError - -from ...typing import AsyncResult, Messages -from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin -from ..helper import format_prompt -from ...image import ImageResponse - -class AmigoChat(AsyncGeneratorProvider, ProviderModelMixin): - url = "https://amigochat.io/chat/" - chat_api_endpoint = "https://api.amigochat.io/v1/chat/completions" - image_api_endpoint = "https://api.amigochat.io/v1/images/generations" - working = False - supports_stream = True - supports_system_message = True - supports_message_history = True - - default_model = 'gpt-4o-mini' - - chat_models = [ - 'gpt-4o', - default_model, - 'o1-preview', - 'o1-mini', - 'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo', - 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo', - 'claude-3-sonnet-20240229', - 'gemini-1.5-pro', - ] - - image_models = [ - 'flux-pro/v1.1', - 'flux-realism', - 'flux-pro', - 'dalle-e-3', - ] - - models = [*chat_models, *image_models] - - model_aliases = { - "o1": "o1-preview", - "llama-3.1-405b": "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", - "llama-3.2-90b": "meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo", - "claude-3.5-sonnet": "claude-3-sonnet-20240229", - "gemini-pro": "gemini-1.5-pro", - - "flux-pro": "flux-pro/v1.1", - "dalle-3": "dalle-e-3", - } - - persona_ids = { - 'gpt-4o': "gpt", - 'gpt-4o-mini': "amigo", - 'o1-preview': "openai-o-one", - 'o1-mini': "openai-o-one-mini", - 'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo': "llama-three-point-one", - 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo': "llama-3-2", - 'claude-3-sonnet-20240229': "claude", - 'gemini-1.5-pro': "gemini-1-5-pro", - 'flux-pro/v1.1': "flux-1-1-pro", - 'flux-realism': "flux-realism", - 'flux-pro': "flux-pro", - 'dalle-e-3': "dalle-three", - } - - @classmethod - def get_model(cls, model: str) -> str: - if model in cls.models: - return model - elif model in cls.model_aliases: - return cls.model_aliases[model] - else: - return cls.default_model - - @classmethod - def get_personaId(cls, model: str) -> str: - return cls.persona_ids[model] - - @classmethod - async def create_async_generator( - cls, - model: str, - messages: Messages, - proxy: str = None, - stream: bool = False, - **kwargs - ) -> AsyncResult: - model = cls.get_model(model) - - device_uuid = str(uuid.uuid4()) - max_retries = 3 - retry_count = 0 - - while retry_count < max_retries: - try: - headers = { - "accept": "*/*", - "accept-language": "en-US,en;q=0.9", - "authorization": "Bearer", - "cache-control": "no-cache", - "content-type": "application/json", - "origin": cls.url, - "pragma": "no-cache", - "priority": "u=1, i", - "referer": f"{cls.url}/", - "sec-ch-ua": '"Chromium";v="129", "Not=A?Brand";v="8"', - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": '"Linux"', - "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36", - "x-device-language": "en-US", - "x-device-platform": "web", - "x-device-uuid": device_uuid, - "x-device-version": "1.0.32" - } - - async with ClientSession(headers=headers) as session: - if model in cls.chat_models: - # Chat completion - data = { - "messages": [{"role": m["role"], "content": m["content"]} for m in messages], - "model": model, - "personaId": cls.get_personaId(model), - "frequency_penalty": 0, - "max_tokens": 4000, - "presence_penalty": 0, - "stream": stream, - "temperature": 0.5, - "top_p": 0.95 - } - - timeout = ClientTimeout(total=300) # 5 minutes timeout - async with session.post(cls.chat_api_endpoint, json=data, proxy=proxy, timeout=timeout) as response: - if response.status not in (200, 201): - error_text = await response.text() - raise Exception(f"Error {response.status}: {error_text}") - - async for line in response.content: - line = line.decode('utf-8').strip() - if line.startswith('data: '): - if line == 'data: [DONE]': - break - try: - chunk = json.loads(line[6:]) # Remove 'data: ' prefix - if 'choices' in chunk and len(chunk['choices']) > 0: - choice = chunk['choices'][0] - if 'delta' in choice: - content = choice['delta'].get('content') - elif 'text' in choice: - content = choice['text'] - else: - content = None - if content: - yield content - except json.JSONDecodeError: - pass - else: - # Image generation - prompt = messages[-1]['content'] - data = { - "prompt": prompt, - "model": model, - "personaId": cls.get_personaId(model) - } - async with session.post(cls.image_api_endpoint, json=data, proxy=proxy) as response: - response.raise_for_status() - - response_data = await response.json() - - if "data" in response_data: - image_urls = [] - for item in response_data["data"]: - if "url" in item: - image_url = item["url"] - image_urls.append(image_url) - if image_urls: - yield ImageResponse(image_urls, prompt) - else: - yield None - - break - - except (ClientResponseError, Exception) as e: - retry_count += 1 - if retry_count >= max_retries: - raise e - device_uuid = str(uuid.uuid4()) diff --git a/g4f/Provider/not_working/__init__.py b/g4f/Provider/not_working/__init__.py index a6edf5f8..1bfe7ed9 100644 --- a/g4f/Provider/not_working/__init__.py +++ b/g4f/Provider/not_working/__init__.py @@ -2,7 +2,6 @@ from .AI365VIP import AI365VIP from .AIChatFree import AIChatFree from .AiChatOnline import AiChatOnline from .AiChats import AiChats -from .AmigoChat import AmigoChat from .Aura import Aura from .Chatgpt4o import Chatgpt4o from .ChatgptFree import ChatgptFree diff --git a/g4f/Provider/openai/proofofwork.py b/g4f/Provider/openai/proofofwork.py index baf8a0ea..55603892 100644 --- a/g4f/Provider/openai/proofofwork.py +++ b/g4f/Provider/openai/proofofwork.py @@ -4,7 +4,6 @@ import json import base64 from datetime import datetime, timezone - def generate_proof_token(required: bool, seed: str = "", difficulty: str = "", user_agent: str = None, proofTokens: list = None): if not required: return diff --git a/g4f/api/__init__.py b/g4f/api/__init__.py index e7f87260..02ba5260 100644 --- a/g4f/api/__init__.py +++ b/g4f/api/__init__.py @@ -14,11 +14,12 @@ from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY, HTTP_401_UNAUTHORIZE from fastapi.encoders import jsonable_encoder from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel -from typing import Union, Optional, Iterator +from typing import Union, Optional import g4f import g4f.debug -from g4f.client import Client, ChatCompletion, ChatCompletionChunk, ImagesResponse +from g4f.client import AsyncClient, ChatCompletion +from g4f.client.helper import filter_none from g4f.typing import Messages from g4f.cookies import read_cookie_files @@ -47,6 +48,10 @@ def create_app(g4f_api_key: str = None): return app +def create_app_debug(g4f_api_key: str = None): + g4f.debug.logging = True + return create_app(g4f_api_key) + class ChatCompletionsConfig(BaseModel): messages: Messages model: str @@ -62,13 +67,19 @@ class ChatCompletionsConfig(BaseModel): class ImageGenerationConfig(BaseModel): prompt: str model: Optional[str] = None + provider: Optional[str] = None response_format: str = "url" + api_key: Optional[str] = None + proxy: Optional[str] = None class AppConfig: ignored_providers: Optional[list[str]] = None g4f_api_key: Optional[str] = None ignore_cookie_files: bool = False - defaults: dict = {} + model: str = None, + provider: str = None + image_provider: str = None + proxy: str = None @classmethod def set_config(cls, **data): @@ -84,7 +95,7 @@ def set_list_ignored_providers(ignored: list[str]): class Api: def __init__(self, app: FastAPI, g4f_api_key=None) -> None: self.app = app - self.client = Client() + self.client = AsyncClient() self.g4f_api_key = g4f_api_key self.get_g4f_api_key = APIKeyHeader(name="g4f-api-key") @@ -133,8 +144,8 @@ class Api: @self.app.get("/v1") async def read_root_v1(): return HTMLResponse('g4f API: Go to ' - 'chat/completions, ' - 'models, or ' + 'models, ' + 'chat/completions, or ' 'images/generate.') @self.app.get("/v1/models") @@ -177,31 +188,24 @@ class Api: # Create the completion response response = self.client.chat.completions.create( - **{ - **AppConfig.defaults, - **config.dict(exclude_none=True), - }, - ignored=AppConfig.ignored_providers + **filter_none( + **{ + "model": AppConfig.model, + "provider": AppConfig.provider, + "proxy": AppConfig.proxy, + **config.dict(exclude_none=True), + }, + ignored=AppConfig.ignored_providers + ), ) - # Check if the response is synchronous or asynchronous - if isinstance(response, ChatCompletion): - # Synchronous response - return JSONResponse(response.to_json()) - if not config.stream: - # If the response is an iterator but not streaming, collect the result - response_list = list(response) if isinstance(response, Iterator) else [response] - return JSONResponse(response_list[0].to_json()) - - # Streaming response - async def async_generator(sync_gen): - for item in sync_gen: - yield item + response: ChatCompletion = await response + return JSONResponse(response.to_json()) async def streaming(): try: - async for chunk in async_generator(response): + async for chunk in response: yield f"data: {json.dumps(chunk.to_json())}\n\n" except GeneratorExit: pass @@ -217,30 +221,38 @@ class Api: return Response(content=format_exception(e, config), status_code=500, media_type="application/json") @self.app.post("/v1/images/generate") + @self.app.post("/v1/images/generations") async def generate_image(config: ImageGenerationConfig): try: - response: ImagesResponse = await self.client.images.async_generate( + response = await self.client.images.generate( prompt=config.prompt, model=config.model, - response_format=config.response_format + provider=AppConfig.image_provider if config.provider is None else config.provider, + **filter_none( + response_format = config.response_format, + api_key = config.api_key, + proxy = config.proxy + ) ) - # Convert Image objects to dictionaries - response_data = [{"url": image.url, "b64_json": image.b64_json} for image in response.data] - return JSONResponse({"data": response_data}) + return JSONResponse(response.to_json()) except Exception as e: logger.exception(e) - return Response(content=format_exception(e, config), status_code=500, media_type="application/json") + return Response(content=format_exception(e, config, True), status_code=500, media_type="application/json") @self.app.post("/v1/completions") async def completions(): return Response(content=json.dumps({'info': 'Not working yet.'}, indent=4), media_type="application/json") -def format_exception(e: Exception, config: Union[ChatCompletionsConfig, ImageGenerationConfig]) -> str: - last_provider = g4f.get_last_provider(True) +def format_exception(e: Exception, config: Union[ChatCompletionsConfig, ImageGenerationConfig], image: bool = False) -> str: + last_provider = {} if not image else g4f.get_last_provider(True) + provider = (AppConfig.image_provider if image else AppConfig.provider) if config.provider is None else config.provider + model = AppConfig.model if config.model is None else config.model return json.dumps({ "error": {"message": f"{e.__class__.__name__}: {e}"}, - "model": last_provider.get("model") if last_provider else getattr(config, 'model', None), - "provider": last_provider.get("name") if last_provider else getattr(config, 'provider', None) + "model": last_provider.get("model") if model is None else model, + **filter_none( + provider=last_provider.get("name") if provider is None else provider + ) }) def run_api( @@ -250,21 +262,19 @@ def run_api( debug: bool = False, workers: int = None, use_colors: bool = None, - g4f_api_key: str = None + reload: bool = False ) -> None: print(f'Starting server... [g4f v-{g4f.version.utils.current_version}]' + (" (debug)" if debug else "")) if use_colors is None: use_colors = debug if bind is not None: host, port = bind.split(":") - if debug: - g4f.debug.logging = True uvicorn.run( - "g4f.api:create_app", + f"g4f.api:create_app{'_debug' if debug else ''}", host=host, port=int(port), workers=workers, use_colors=use_colors, factory=True, - reload=debug - ) + reload=reload + ) \ No newline at end of file diff --git a/g4f/cli.py b/g4f/cli.py index 408dc914..e3017e31 100644 --- a/g4f/cli.py +++ b/g4f/cli.py @@ -11,16 +11,19 @@ def main(): api_parser = subparsers.add_parser("api") api_parser.add_argument("--bind", default="0.0.0.0:1337", help="The bind string.") api_parser.add_argument("--debug", action="store_true", help="Enable verbose logging.") - api_parser.add_argument("--model", default=None, help="Default model for chat completion. (incompatible with --debug and --workers)") + api_parser.add_argument("--model", default=None, help="Default model for chat completion. (incompatible with --reload and --workers)") api_parser.add_argument("--provider", choices=[provider.__name__ for provider in Provider.__providers__ if provider.working], - default=None, help="Default provider for chat completion. (incompatible with --debug and --workers)") - api_parser.add_argument("--proxy", default=None, help="Default used proxy.") + default=None, help="Default provider for chat completion. (incompatible with --reload and --workers)") + api_parser.add_argument("--image-provider", choices=[provider.__name__ for provider in Provider.__providers__ if provider.working and hasattr(provider, "image_models")], + default=None, help="Default provider for image generation. (incompatible with --reload and --workers)"), + api_parser.add_argument("--proxy", default=None, help="Default used proxy. (incompatible with --reload and --workers)") api_parser.add_argument("--workers", type=int, default=None, help="Number of workers.") api_parser.add_argument("--disable-colors", action="store_true", help="Don't use colors.") - api_parser.add_argument("--ignore-cookie-files", action="store_true", help="Don't read .har and cookie files.") - api_parser.add_argument("--g4f-api-key", type=str, default=None, help="Sets an authentication key for your API. (incompatible with --debug and --workers)") + api_parser.add_argument("--ignore-cookie-files", action="store_true", help="Don't read .har and cookie files. (incompatible with --reload and --workers)") + api_parser.add_argument("--g4f-api-key", type=str, default=None, help="Sets an authentication key for your API. (incompatible with --reload and --workers)") api_parser.add_argument("--ignored-providers", nargs="+", choices=[provider.__name__ for provider in Provider.__providers__ if provider.working], - default=[], help="List of providers to ignore when processing request. (incompatible with --debug and --workers)") + default=[], help="List of providers to ignore when processing request. (incompatible with --reload and --workers)") + api_parser.add_argument("--reload", action="store_true", help="Enable reloading.") subparsers.add_parser("gui", parents=[gui_parser()], add_help=False) args = parser.parse_args() @@ -39,17 +42,17 @@ def run_api_args(args): ignore_cookie_files=args.ignore_cookie_files, ignored_providers=args.ignored_providers, g4f_api_key=args.g4f_api_key, - defaults={ - "model": args.model, - "provider": args.provider, - "proxy": args.proxy - } + provider=args.provider, + image_provider=args.image_provider, + proxy=args.proxy, + model=args.model ) run_api( bind=args.bind, debug=args.debug, workers=args.workers, - use_colors=not args.disable_colors + use_colors=not args.disable_colors, + reload=args.reload ) if __name__ == "__main__": diff --git a/g4f/client/__init__.py b/g4f/client/__init__.py index d1e7e298..5ffe9288 100644 --- a/g4f/client/__init__.py +++ b/g4f/client/__init__.py @@ -1,2 +1,468 @@ -from .stubs import ChatCompletion, ChatCompletionChunk, ImagesResponse -from .client import Client, AsyncClient +from __future__ import annotations + +import os +import time +import random +import string +import asyncio +import base64 +import aiohttp +import logging +from typing import Union, AsyncIterator, Iterator, Coroutine + +from ..providers.base_provider import AsyncGeneratorProvider +from ..image import ImageResponse, to_image, to_data_uri, is_accepted_format, EXTENSIONS_MAP +from ..typing import Messages, Cookies, Image +from ..providers.types import ProviderType, FinishReason, BaseConversation +from ..errors import NoImageResponseError +from ..providers.retry_provider import IterListProvider +from ..Provider.needs_auth.BingCreateImages import BingCreateImages +from ..requests.aiohttp import get_connector +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 + +try: + anext # Python 3.8+ +except NameError: + async def anext(aiter): + try: + return await aiter.__anext__() + except StopAsyncIteration: + raise StopIteration + +# Synchronous iter_response function +def iter_response( + response: Union[Iterator[str], AsyncIterator[str]], + stream: bool, + response_format: dict = None, + max_tokens: int = None, + stop: list = None +) -> Iterator[Union[ChatCompletion, ChatCompletionChunk]]: + content = "" + finish_reason = None + completion_id = ''.join(random.choices(string.ascii_letters + string.digits, k=28)) + idx = 0 + + if hasattr(response, '__aiter__'): + # It's an async iterator, wrap it into a sync iterator + response = to_sync_iter(response) + + for chunk in response: + if isinstance(chunk, FinishReason): + finish_reason = chunk.reason + break + elif isinstance(chunk, BaseConversation): + yield chunk + continue + + chunk = str(chunk) + content += chunk + + if max_tokens is not None and idx + 1 >= max_tokens: + finish_reason = "length" + + first, content, chunk = find_stop(stop, content, chunk if stream else None) + + if first != -1: + finish_reason = "stop" + + if stream: + yield ChatCompletionChunk(chunk, None, completion_id, int(time.time())) + + if finish_reason is not None: + break + + idx += 1 + + finish_reason = "stop" if finish_reason is None else finish_reason + + if stream: + yield ChatCompletionChunk(None, finish_reason, completion_id, int(time.time())) + else: + if response_format is not None and "type" in response_format: + if response_format["type"] == "json_object": + content = filter_json(content) + yield ChatCompletion(content, finish_reason, completion_id, int(time.time())) + +# Synchronous iter_append_model_and_provider function +def iter_append_model_and_provider(response: Iterator[ChatCompletionChunk]) -> Iterator[ChatCompletionChunk]: + last_provider = None + + for chunk in response: + last_provider = get_last_provider(True) if last_provider is None else last_provider + chunk.model = last_provider.get("model") + chunk.provider = last_provider.get("name") + yield chunk + +async def async_iter_response( + response: AsyncIterator[str], + stream: bool, + response_format: dict = None, + max_tokens: int = None, + stop: list = None +) -> AsyncIterator[Union[ChatCompletion, ChatCompletionChunk]]: + content = "" + finish_reason = None + completion_id = ''.join(random.choices(string.ascii_letters + string.digits, k=28)) + idx = 0 + + try: + async for chunk in response: + if isinstance(chunk, FinishReason): + finish_reason = chunk.reason + break + elif isinstance(chunk, BaseConversation): + yield chunk + continue + + chunk = str(chunk) + content += chunk + idx += 1 + + if max_tokens is not None and idx >= max_tokens: + finish_reason = "length" + + first, content, chunk = find_stop(stop, content, chunk if stream else None) + + if first != -1: + finish_reason = "stop" + + if stream: + yield ChatCompletionChunk(chunk, None, completion_id, int(time.time())) + + if finish_reason is not None: + break + + finish_reason = "stop" if finish_reason is None else finish_reason + + if stream: + yield ChatCompletionChunk(None, finish_reason, completion_id, int(time.time())) + else: + if response_format is not None and "type" in response_format: + if response_format["type"] == "json_object": + content = filter_json(content) + yield ChatCompletion(content, finish_reason, completion_id, int(time.time())) + finally: + if hasattr(response, 'aclose'): + await safe_aclose(response) + +async def async_iter_append_model_and_provider(response: AsyncIterator[ChatCompletionChunk]) -> AsyncIterator: + last_provider = None + try: + async for chunk in response: + last_provider = get_last_provider(True) if last_provider is None else last_provider + chunk.model = last_provider.get("model") + chunk.provider = last_provider.get("name") + yield chunk + finally: + if hasattr(response, 'aclose'): + await safe_aclose(response) + +class Client(BaseClient): + def __init__( + self, + provider: ProviderType = None, + image_provider: ImageProvider = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + self.chat: Chat = Chat(self, provider) + self.images: Images = Images(self, image_provider) + +class Completions: + def __init__(self, client: Client, provider: ProviderType = None): + self.client: Client = client + self.provider: ProviderType = provider + + def create( + self, + messages: Messages, + model: str, + provider: ProviderType = None, + stream: bool = False, + proxy: str = None, + response_format: dict = None, + max_tokens: int = None, + stop: Union[list[str], str] = None, + api_key: str = None, + ignored: list[str] = None, + ignore_working: bool = False, + ignore_stream: bool = False, + **kwargs + ) -> IterResponse: + model, provider = get_model_and_provider( + model, + self.provider if provider is None else provider, + stream, + ignored, + ignore_working, + ignore_stream, + ) + stop = [stop] if isinstance(stop, str) else stop + + response = provider.create_completion( + model, + messages, + stream=stream, + **filter_none( + proxy=self.client.proxy if proxy is None else proxy, + max_tokens=max_tokens, + stop=stop, + api_key=self.client.api_key if api_key is None else api_key + ), + **kwargs + ) + if asyncio.iscoroutinefunction(provider.create_completion): + # Run the asynchronous function in an event loop + 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) + elif hasattr(response, '__aiter__'): + # If response is an async generator, collect it into a list + response = list(to_sync_iter(response)) + response = iter_response(response, stream, response_format, max_tokens, stop) + response = iter_append_model_and_provider(response) + if stream: + return response + else: + return next(response) + +class Chat: + completions: Completions + + def __init__(self, client: Client, provider: ProviderType = None): + self.completions = Completions(client, provider) + +class Images: + def __init__(self, client: Client, provider: ProviderType = None): + self.client: Client = client + self.provider: ProviderType = provider + self.models: ImageModels = ImageModels(client) + + def generate(self, prompt: str, model: str = None, provider: ProviderType = None, response_format: str = "url", proxy: str = None, **kwargs) -> ImagesResponse: + """ + Synchronous generate method that runs the async_generate method in an event loop. + """ + return asyncio.run(self.async_generate(prompt, model, provider, response_format=response_format, proxy=proxy **kwargs)) + + async def async_generate(self, prompt: str, model: str = None, provider: ProviderType = None, response_format: str = "url", proxy: str = None, **kwargs) -> ImagesResponse: + if provider is None: + provider_handler = self.models.get(model, provider or self.provider or BingCreateImages) + elif isinstance(provider, str): + provider_handler = convert_to_provider(provider) + if provider_handler is None: + raise ValueError(f"Unknown model: {model}") + if proxy is None: + proxy = self.client.proxy + + if isinstance(provider_handler, IterListProvider): + if provider_handler.providers: + provider_handler = provider.providers[0] + else: + raise ValueError(f"IterListProvider for model {model} has no providers") + + response = None + if hasattr(provider_handler, "create_async_generator"): + messages = [{"role": "user", "content": prompt}] + async for item in provider_handler.create_async_generator(model, messages, **kwargs): + if isinstance(item, ImageResponse): + response = item + break + elif hasattr(provider, 'create'): + if asyncio.iscoroutinefunction(provider_handler.create): + response = await provider_handler.create(prompt) + else: + response = provider_handler.create(prompt) + if isinstance(response, str): + response = ImageResponse([response], prompt) + else: + raise ValueError(f"Provider {provider} does not support image generation") + if isinstance(response, ImageResponse): + return await self._process_image_response(response, response_format, proxy, model=model, provider=provider) + + raise NoImageResponseError(f"Unexpected response type: {type(response)}") + + async def _process_image_response(self, response: ImageResponse, response_format: str, proxy: str = None, model: str = None, provider: str = None) -> ImagesResponse: + async def process_image_item(session: aiohttp.ClientSession, image_data: str): + if image_data.startswith('http://') or image_data.startswith('https://'): + if response_format == "url": + return Image(url=image_data, revised_prompt=response.alt) + elif response_format == "b64_json": + # Fetch the image data and convert it to base64 + image_content = await self._fetch_image(session, image_data) + file_name = self._save_image(image_data_bytes) + b64_json = base64.b64encode(image_content).decode('utf-8') + return Image(b64_json=b64_json, url=file_name, revised_prompt=response.alt) + else: + # Assume image_data is base64 data or binary + if response_format == "url": + if image_data.startswith('data:image'): + # Remove the data URL scheme and get the base64 data + base64_data = image_data.split(',', 1)[-1] + else: + base64_data = image_data + # Decode the base64 data + image_data_bytes = base64.b64decode(base64_data) + # Convert bytes to an image + file_name = self._save_image(image_data_bytes) + return Image(url=file_name, revised_prompt=response.alt) + elif response_format == "b64_json": + if isinstance(image_data, bytes): + file_name = self._save_image(image_data_bytes) + b64_json = base64.b64encode(image_data).decode('utf-8') + else: + b64_json = image_data # If already base64-encoded string + return Image(b64_json=b64_json, url=file_name, revised_prompt=response.alt) + + last_provider = get_last_provider(True) + async with aiohttp.ClientSession(cookies=response.get("cookies"), connector=get_connector(proxy=proxy)) as session: + return ImagesResponse( + await asyncio.gather(*[process_image_item(session, image_data) for image_data in response.get_list()]), + model=last_provider.get("model") if model is None else model, + provider=last_provider.get("name") if provider is None else provider + ) + + async def _fetch_image(self, session: aiohttp.ClientSession, url: str) -> bytes: + # Asynchronously fetch image data from the URL + async with session.get(url) as resp: + if resp.status == 200: + return await resp.read() + else: + raise RuntimeError(f"Failed to fetch image from {url}, status code {resp.status}") + + def _save_image(self, image_data_bytes: bytes) -> str: + os.makedirs('generated_images', exist_ok=True) + image = to_image(image_data_bytes) + file_name = f"generated_images/image_{int(time.time())}_{random.randint(0, 10000)}.{EXTENSIONS_MAP[is_accepted_format(image_data_bytes)]}" + image.save(file_name) + return file_name + + def create_variation(self, image: Union[str, bytes], model: str = None, provider: ProviderType = None, response_format: str = "url", **kwargs) -> ImagesResponse: + return asyncio.run(self.async_create_variation( + image, model, provider, response_format + **kwargs + )) + + async def async_create_variation(self, image: Union[str, bytes], model: str = None, provider: ProviderType = None, response_format: str = "url", proxy: str = None, **kwargs) -> ImagesResponse: + if provider is None: + provider = self.models.get(model, provider or self.provider or BingCreateImages) + if provider is None: + raise ValueError(f"Unknown model: {model}") + if isinstance(provider, str): + provider = convert_to_provider(provider) + if proxy is None: + proxy = self.client.proxy + + if isinstance(provider, type) and issubclass(provider, AsyncGeneratorProvider): + messages = [{"role": "user", "content": "create a variation of this image"}] + image_data = to_data_uri(image) + generator = None + try: + generator = provider.create_async_generator(model, messages, image=image_data, response_format=response_format, proxy=proxy, **kwargs) + async for response in generator: + if isinstance(response, ImageResponse): + return self._process_image_response(response) + except RuntimeError as e: + if "async generator ignored GeneratorExit" in str(e): + logging.warning("Generator ignored GeneratorExit in create_variation, handling gracefully") + else: + raise + finally: + if generator and hasattr(generator, 'aclose'): + await safe_aclose(generator) + logging.info("AsyncGeneratorProvider processing completed in create_variation") + elif hasattr(provider, 'create_variation'): + if asyncio.iscoroutinefunction(provider.create_variation): + response = await provider.create_variation(image, model=model, response_format=response_format, proxy=proxy, **kwargs) + else: + response = provider.create_variation(image, model=model, response_format=response_format, proxy=proxy, **kwargs) + if isinstance(response, str): + response = ImageResponse([response]) + return self._process_image_response(response) + else: + raise ValueError(f"Provider {provider} does not support image variation") + +class AsyncClient(BaseClient): + def __init__( + self, + provider: ProviderType = None, + image_provider: ImageProvider = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + self.chat: AsyncChat = AsyncChat(self, provider) + self.images: AsyncImages = AsyncImages(self, image_provider) + +class AsyncChat: + completions: AsyncCompletions + + def __init__(self, client: AsyncClient, provider: ProviderType = None): + self.completions = AsyncCompletions(client, provider) + +class AsyncCompletions: + def __init__(self, client: AsyncClient, provider: ProviderType = None): + self.client: AsyncClient = client + self.provider: ProviderType = provider + + def create( + self, + messages: Messages, + model: str, + provider: ProviderType = None, + stream: bool = False, + proxy: str = None, + response_format: dict = None, + max_tokens: int = None, + stop: Union[list[str], str] = None, + api_key: str = None, + ignored: list[str] = None, + ignore_working: bool = False, + ignore_stream: bool = False, + **kwargs + ) -> Union[Coroutine[ChatCompletion], AsyncIterator[ChatCompletionChunk]]: + model, provider = get_model_and_provider( + model, + self.provider if provider is None else provider, + stream, + ignored, + ignore_working, + ignore_stream, + ) + stop = [stop] if isinstance(stop, str) else stop + + response = provider.create_completion( + model, + messages, + stream=stream, + **filter_none( + proxy=self.client.proxy if proxy is None else proxy, + max_tokens=max_tokens, + stop=stop, + api_key=self.client.api_key if api_key is None else api_key + ), + **kwargs + ) + + if not isinstance(response, AsyncIterator): + response = to_async_iterator(response) + response = async_iter_response(response, stream, response_format, max_tokens, stop) + response = async_iter_append_model_and_provider(response) + return response if stream else anext(response) + +class AsyncImages(Images): + def __init__(self, client: AsyncClient, provider: ImageProvider = None): + self.client: AsyncClient = client + self.provider: ImageProvider = provider + self.models: ImageModels = ImageModels(client) + + async def generate(self, prompt: str, model: str = None, provider: ProviderType = None, response_format: str = "url", **kwargs) -> ImagesResponse: + return await self.async_generate(prompt, model, provider, response_format, **kwargs) + + async def create_variation(self, image: Union[str, bytes], model: str = None, provider: ProviderType = None, response_format: str = "url", **kwargs) -> ImagesResponse: + return await self.async_create_variation( + image, model, provider, response_format, **kwargs + ) \ No newline at end of file diff --git a/g4f/client/client.py b/g4f/client/client.py deleted file mode 100644 index 73d8fea3..00000000 --- a/g4f/client/client.py +++ /dev/null @@ -1,541 +0,0 @@ -from __future__ import annotations - -import os -import time -import random -import string -import threading -import asyncio -import base64 -import aiohttp -import queue -from typing import Union, AsyncIterator, Iterator - -from ..providers.base_provider import AsyncGeneratorProvider -from ..image import ImageResponse, to_image, to_data_uri -from ..typing import Messages, ImageType -from ..providers.types import BaseProvider, ProviderType, FinishReason -from ..providers.conversation import BaseConversation -from ..image import ImageResponse as ImageProviderResponse -from ..errors import NoImageResponseError -from .stubs import ChatCompletion, ChatCompletionChunk, Image, ImagesResponse -from .image_models import ImageModels -from .types import IterResponse, ImageProvider -from .types import Client as BaseClient -from .service import get_model_and_provider, get_last_provider -from .helper import find_stop, filter_json, filter_none -from ..models import ModelUtils -from ..Provider import IterListProvider - -# 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): - for item in iterator: - yield item - -# Synchronous iter_response function -def iter_response( - response: Union[Iterator[str], AsyncIterator[str]], - stream: bool, - response_format: dict = None, - max_tokens: int = None, - stop: list = None -) -> Iterator[Union[ChatCompletion, ChatCompletionChunk]]: - content = "" - finish_reason = None - completion_id = ''.join(random.choices(string.ascii_letters + string.digits, k=28)) - idx = 0 - - if hasattr(response, '__aiter__'): - # It's an async iterator, wrap it into a sync iterator - response = to_sync_iter(response) - - for chunk in response: - if isinstance(chunk, FinishReason): - finish_reason = chunk.reason - break - elif isinstance(chunk, BaseConversation): - yield chunk - continue - - content += str(chunk) - - if max_tokens is not None and idx + 1 >= max_tokens: - finish_reason = "length" - - first, content, chunk = find_stop(stop, content, chunk if stream else None) - - if first != -1: - finish_reason = "stop" - - if stream: - yield ChatCompletionChunk(chunk, None, completion_id, int(time.time())) - - if finish_reason is not None: - break - - idx += 1 - - finish_reason = "stop" if finish_reason is None else finish_reason - - if stream: - yield ChatCompletionChunk(None, finish_reason, completion_id, int(time.time())) - else: - if response_format is not None and "type" in response_format: - if response_format["type"] == "json_object": - content = filter_json(content) - yield ChatCompletion(content, finish_reason, completion_id, int(time.time())) - -# Synchronous iter_append_model_and_provider function -def iter_append_model_and_provider(response: Iterator) -> Iterator: - last_provider = None - - for chunk in response: - last_provider = get_last_provider(True) if last_provider is None else last_provider - chunk.model = last_provider.get("model") - chunk.provider = last_provider.get("name") - yield chunk - -class Client(BaseClient): - def __init__( - self, - provider: ProviderType = None, - image_provider: ImageProvider = None, - **kwargs - ) -> None: - super().__init__(**kwargs) - self.chat: Chat = Chat(self, provider) - self._images: Images = Images(self, image_provider) - - @property - def images(self) -> Images: - return self._images - - async def async_images(self) -> Images: - return self._images - -# For backwards compatibility and legacy purposes, use Client instead -class AsyncClient(Client): - """Legacy AsyncClient that redirects to the main Client class. - This class exists for backwards compatibility.""" - - def __init__(self, *args, **kwargs): - import warnings - warnings.warn( - "AsyncClient is deprecated and will be removed in future versions." - "Use Client instead, which now supports both sync and async operations.", - DeprecationWarning, - stacklevel=2 - ) - super().__init__(*args, **kwargs) - - async def async_create(self, *args, **kwargs): - """Asynchronous create method that calls the synchronous method.""" - return await super().async_create(*args, **kwargs) - - async def async_generate(self, *args, **kwargs): - """Asynchronous image generation method.""" - return await super().async_generate(*args, **kwargs) - - async def async_images(self) -> Images: - """Asynchronous access to images.""" - return await super().async_images() - - async def async_fetch_image(self, url: str) -> bytes: - """Asynchronous fetching of an image by URL.""" - return await self._fetch_image(url) - -class Completions: - def __init__(self, client: Client, provider: ProviderType = None): - self.client: Client = client - self.provider: ProviderType = provider - - def create( - self, - messages: Messages, - model: str, - provider: ProviderType = None, - stream: bool = False, - proxy: str = None, - response_format: dict = None, - max_tokens: int = None, - stop: Union[list[str], str] = None, - api_key: str = None, - ignored: list[str] = None, - ignore_working: bool = False, - ignore_stream: bool = False, - **kwargs - ) -> Union[ChatCompletion, Iterator[ChatCompletionChunk]]: - model, provider = get_model_and_provider( - model, - self.provider if provider is None else provider, - stream, - ignored, - ignore_working, - ignore_stream, - ) - - stop = [stop] if isinstance(stop, str) else stop - - if asyncio.iscoroutinefunction(provider.create_completion): - # Run the asynchronous function in an event loop - response = asyncio.run(provider.create_completion( - model, - messages, - stream=stream, - **filter_none( - proxy=self.client.get_proxy() if proxy is None else proxy, - max_tokens=max_tokens, - stop=stop, - api_key=self.client.api_key if api_key is None else api_key - ), - **kwargs - )) - else: - response = provider.create_completion( - model, - messages, - stream=stream, - **filter_none( - proxy=self.client.get_proxy() if proxy is None else proxy, - max_tokens=max_tokens, - stop=stop, - api_key=self.client.api_key if api_key is None else api_key - ), - **kwargs - ) - - if stream: - if hasattr(response, '__aiter__'): - # It's an async generator, wrap it into a sync iterator - response = to_sync_iter(response) - - # Now 'response' is an iterator - response = iter_response(response, stream, response_format, max_tokens, stop) - response = iter_append_model_and_provider(response) - return response - else: - if hasattr(response, '__aiter__'): - # If response is an async generator, collect it into a list - response = list(to_sync_iter(response)) - response = iter_response(response, stream, response_format, max_tokens, stop) - response = iter_append_model_and_provider(response) - return next(response) - - async def async_create( - self, - messages: Messages, - model: str, - provider: ProviderType = None, - stream: bool = False, - proxy: str = None, - response_format: dict = None, - max_tokens: int = None, - stop: Union[list[str], str] = None, - api_key: str = None, - ignored: list[str] = None, - ignore_working: bool = False, - ignore_stream: bool = False, - **kwargs - ) -> Union[ChatCompletion, AsyncIterator[ChatCompletionChunk]]: - model, provider = get_model_and_provider( - model, - self.provider if provider is None else provider, - stream, - ignored, - ignore_working, - ignore_stream, - ) - - stop = [stop] if isinstance(stop, str) else stop - - if asyncio.iscoroutinefunction(provider.create_completion): - response = await provider.create_completion( - model, - messages, - stream=stream, - **filter_none( - proxy=self.client.get_proxy() if proxy is None else proxy, - max_tokens=max_tokens, - stop=stop, - api_key=self.client.api_key if api_key is None else api_key - ), - **kwargs - ) - else: - response = provider.create_completion( - model, - messages, - stream=stream, - **filter_none( - proxy=self.client.get_proxy() if proxy is None else proxy, - max_tokens=max_tokens, - stop=stop, - api_key=self.client.api_key if api_key is None else api_key - ), - **kwargs - ) - - # Removed 'await' here since 'async_iter_response' returns an async generator - response = async_iter_response(response, stream, response_format, max_tokens, stop) - response = async_iter_append_model_and_provider(response) - - if stream: - return response - else: - async for result in response: - return result - -class Chat: - completions: Completions - - def __init__(self, client: Client, provider: ProviderType = None): - self.completions = Completions(client, provider) - -# Asynchronous versions of the helper functions -async def async_iter_response( - response: Union[AsyncIterator[str], Iterator[str]], - stream: bool, - response_format: dict = None, - max_tokens: int = None, - stop: list = None -) -> AsyncIterator[Union[ChatCompletion, ChatCompletionChunk]]: - content = "" - finish_reason = None - completion_id = ''.join(random.choices(string.ascii_letters + string.digits, k=28)) - idx = 0 - - if not hasattr(response, '__aiter__'): - response = to_async_iterator(response) - - async for chunk in response: - if isinstance(chunk, FinishReason): - finish_reason = chunk.reason - break - elif isinstance(chunk, BaseConversation): - yield chunk - continue - - content += str(chunk) - - if max_tokens is not None and idx + 1 >= max_tokens: - finish_reason = "length" - - first, content, chunk = find_stop(stop, content, chunk if stream else None) - - if first != -1: - finish_reason = "stop" - - if stream: - yield ChatCompletionChunk(chunk, None, completion_id, int(time.time())) - - if finish_reason is not None: - break - - idx += 1 - - finish_reason = "stop" if finish_reason is None else finish_reason - - if stream: - yield ChatCompletionChunk(None, finish_reason, completion_id, int(time.time())) - else: - if response_format is not None and "type" in response_format: - if response_format["type"] == "json_object": - content = filter_json(content) - yield ChatCompletion(content, finish_reason, completion_id, int(time.time())) - -async def async_iter_append_model_and_provider(response: AsyncIterator) -> AsyncIterator: - last_provider = None - - if not hasattr(response, '__aiter__'): - response = to_async_iterator(response) - - async for chunk in response: - last_provider = get_last_provider(True) if last_provider is None else last_provider - chunk.model = last_provider.get("model") - chunk.provider = last_provider.get("name") - yield chunk - -async def iter_image_response(response: AsyncIterator) -> Union[ImagesResponse, None]: - response_list = [] - async for chunk in response: - if isinstance(chunk, ImageProviderResponse): - response_list.extend(chunk.get_list()) - elif isinstance(chunk, str): - response_list.append(chunk) - - if response_list: - return ImagesResponse([Image(image) for image in response_list]) - - return None - -async def create_image(client: Client, provider: ProviderType, prompt: str, model: str = "", **kwargs) -> AsyncIterator: - if isinstance(provider, type) and provider.__name__ == "You": - kwargs["chat_mode"] = "create" - else: - prompt = f"create an image with: {prompt}" - - if asyncio.iscoroutinefunction(provider.create_completion): - response = await provider.create_completion( - model, - [{"role": "user", "content": prompt}], - stream=True, - proxy=client.get_proxy(), - **kwargs - ) - else: - response = provider.create_completion( - model, - [{"role": "user", "content": prompt}], - stream=True, - proxy=client.get_proxy(), - **kwargs - ) - - # Wrap synchronous iterator into async iterator if necessary - if not hasattr(response, '__aiter__'): - response = to_async_iterator(response) - - return response - -class Image: - def __init__(self, url: str = None, b64_json: str = None): - self.url = url - self.b64_json = b64_json - - def __repr__(self): - return f"Image(url={self.url}, b64_json={'' if self.b64_json else None})" - -class ImagesResponse: - def __init__(self, data: list[Image]): - self.data = data - - def __repr__(self): - return f"ImagesResponse(data={self.data})" - -class Images: - def __init__(self, client: 'Client', provider: 'ImageProvider' = None): - self.client: 'Client' = client - self.provider: 'ImageProvider' = provider - self.models: ImageModels = ImageModels(client) - - def generate(self, prompt: str, model: str = None, response_format: str = "url", **kwargs) -> ImagesResponse: - """ - Synchronous generate method that runs the async_generate method in an event loop. - """ - return asyncio.run(self.async_generate(prompt, model, response_format=response_format, **kwargs)) - - async def async_generate(self, prompt: str, model: str = None, response_format: str = "url", **kwargs) -> ImagesResponse: - provider = self.models.get(model, self.provider) - if provider is None: - raise ValueError(f"Unknown model: {model}") - - if isinstance(provider, IterListProvider): - if provider.providers: - provider = provider.providers[0] - else: - raise ValueError(f"IterListProvider for model {model} has no providers") - - if isinstance(provider, type) and issubclass(provider, AsyncGeneratorProvider): - messages = [{"role": "user", "content": prompt}] - async for response in provider.create_async_generator(model, messages, **kwargs): - if isinstance(response, ImageResponse): - return await self._process_image_response(response, response_format) - elif isinstance(response, str): - image_response = ImageResponse([response], prompt) - return await self._process_image_response(image_response, response_format) - elif hasattr(provider, 'create'): - if asyncio.iscoroutinefunction(provider.create): - response = await provider.create(prompt) - else: - response = provider.create(prompt) - - if isinstance(response, ImageResponse): - return await self._process_image_response(response, response_format) - elif isinstance(response, str): - image_response = ImageResponse([response], prompt) - return await self._process_image_response(image_response, response_format) - else: - raise ValueError(f"Provider {provider} does not support image generation") - - raise NoImageResponseError(f"Unexpected response type: {type(response)}") - - async def _process_image_response(self, response: ImageResponse, response_format: str) -> ImagesResponse: - processed_images = [] - - for image_data in response.get_list(): - if image_data.startswith('http://') or image_data.startswith('https://'): - if response_format == "url": - processed_images.append(Image(url=image_data)) - elif response_format == "b64_json": - # Fetch the image data and convert it to base64 - image_content = await self._fetch_image(image_data) - b64_json = base64.b64encode(image_content).decode('utf-8') - processed_images.append(Image(b64_json=b64_json)) - else: - # Assume image_data is base64 data or binary - if response_format == "url": - if image_data.startswith('data:image'): - # Remove the data URL scheme and get the base64 data - header, base64_data = image_data.split(',', 1) - else: - base64_data = image_data - # Decode the base64 data - image_data_bytes = base64.b64decode(base64_data) - # Convert bytes to an image - image = to_image(image_data_bytes) - file_name = self._save_image(image) - processed_images.append(Image(url=file_name)) - elif response_format == "b64_json": - if isinstance(image_data, bytes): - b64_json = base64.b64encode(image_data).decode('utf-8') - else: - b64_json = image_data # If already base64-encoded string - processed_images.append(Image(b64_json=b64_json)) - - return ImagesResponse(processed_images) - - async def _fetch_image(self, url: str) -> bytes: - # Asynchronously fetch image data from the URL - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - if resp.status == 200: - return await resp.read() - else: - raise Exception(f"Failed to fetch image from {url}, status code {resp.status}") - - def _save_image(self, image: 'PILImage') -> str: - os.makedirs('generated_images', exist_ok=True) - file_name = f"generated_images/image_{int(time.time())}_{random.randint(0, 10000)}.png" - image.save(file_name) - return file_name - - async def create_variation(self, image: Union[str, bytes], model: str = None, response_format: str = "url", **kwargs): - # Existing implementation, adjust if you want to support b64_json here as well - pass diff --git a/g4f/client/helper.py b/g4f/client/helper.py index c502d478..71bfd38a 100644 --- a/g4f/client/helper.py +++ b/g4f/client/helper.py @@ -1,7 +1,12 @@ from __future__ import annotations import re -from typing import Iterable, AsyncIterator +import queue +import threading +import logging +import asyncio + +from typing import AsyncIterator, Iterator, AsyncGenerator def filter_json(text: str) -> str: """ @@ -42,6 +47,40 @@ def filter_none(**kwargs) -> dict: if value is not None } -async def cast_iter_async(iter: Iterable) -> AsyncIterator: - for chunk in iter: - yield chunk \ No newline at end of file +async def safe_aclose(generator: AsyncGenerator) -> None: + try: + await generator.aclose() + 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: + yield item \ No newline at end of file diff --git a/g4f/client/service.py b/g4f/client/service.py index 5fdb150c..aa209b22 100644 --- a/g4f/client/service.py +++ b/g4f/client/service.py @@ -55,7 +55,6 @@ def get_model_and_provider(model : Union[Model, str], provider = convert_to_provider(provider) if isinstance(model, str): - if model in ModelUtils.convert: model = ModelUtils.convert[model] @@ -75,11 +74,11 @@ def get_model_and_provider(model : Union[Model, str], if not ignore_working and not provider.working: raise ProviderNotWorkingError(f'{provider.__name__} is not working') - if not ignore_working and isinstance(provider, BaseRetryProvider): - provider.providers = [p for p in provider.providers if p.working] - - if ignored and isinstance(provider, BaseRetryProvider): - provider.providers = [p for p in provider.providers if p.__name__ not in ignored] + if isinstance(provider, BaseRetryProvider): + if not ignore_working: + provider.providers = [p for p in provider.providers if p.working] + if ignored: + provider.providers = [p for p in provider.providers if p.__name__ not in ignored] if not ignore_stream and not provider.supports_stream and stream: raise StreamNotSupportedError(f'{provider.__name__} does not support "stream" argument') @@ -95,7 +94,7 @@ def get_model_and_provider(model : Union[Model, str], return model, provider -def get_last_provider(as_dict: bool = False) -> Union[ProviderType, dict[str, str]]: +def get_last_provider(as_dict: bool = False) -> Union[ProviderType, dict[str, str], None]: """ Retrieves the last used provider. @@ -108,11 +107,14 @@ def get_last_provider(as_dict: bool = False) -> Union[ProviderType, dict[str, st last = debug.last_provider if isinstance(last, BaseRetryProvider): last = last.last_provider - if last and as_dict: - return { - "name": last.__name__, - "url": last.url, - "model": debug.last_model, - "label": last.label if hasattr(last, "label") else None - } + if as_dict: + if last: + return { + "name": last.__name__, + "url": last.url, + "model": debug.last_model, + "label": getattr(last, "label", None) if hasattr(last, "label") else None + } + else: + return {} return last \ No newline at end of file diff --git a/g4f/client/stubs.py b/g4f/client/stubs.py index 8cf2bcba..b38c9f6c 100644 --- a/g4f/client/stubs.py +++ b/g4f/client/stubs.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Union +from time import time class Model(): ... @@ -108,8 +109,18 @@ class Image(Model): return self.__dict__ class ImagesResponse(Model): - def __init__(self, data: list[Image], created: int = 0) -> None: + data: list[Image] + model: str + provider: str + created: int + + def __init__(self, data: list[Image], created: int = None, model: str = None, provider: str = None) -> None: self.data = data + if created is None: + created = int(time()) + self.model = model + if provider is not None: + self.provider = provider self.created = created def to_json(self): diff --git a/g4f/client/types.py b/g4f/client/types.py index 100be432..4f252ba9 100644 --- a/g4f/client/types.py +++ b/g4f/client/types.py @@ -11,7 +11,17 @@ Proxies = Union[dict, str] IterResponse = Iterator[Union[ChatCompletion, ChatCompletionChunk]] AsyncIterResponse = AsyncIterator[Union[ChatCompletion, ChatCompletionChunk]] -class ClientProxyMixin(): +class Client(): + def __init__( + self, + api_key: str = None, + proxies: Proxies = None, + **kwargs + ) -> None: + self.api_key: str = api_key + self.proxies= proxies + self.proxy: str = self.get_proxy() + def get_proxy(self) -> Union[str, None]: if isinstance(self.proxies, str): return self.proxies @@ -20,14 +30,4 @@ class ClientProxyMixin(): elif "all" in self.proxies: return self.proxies["all"] elif "https" in self.proxies: - return self.proxies["https"] - -class Client(ClientProxyMixin): - def __init__( - self, - api_key: str = None, - proxies: Proxies = None, - **kwargs - ) -> None: - self.api_key: str = api_key - self.proxies: Proxies = proxies \ No newline at end of file + return self.proxies["https"] \ No newline at end of file diff --git a/g4f/gui/server/internet.py b/g4f/gui/server/internet.py index 1e366e46..b41b5eae 100644 --- a/g4f/gui/server/internet.py +++ b/g4f/gui/server/internet.py @@ -96,7 +96,7 @@ async def fetch_and_scrape(session: ClientSession, url: str, max_words: int = No async def search(query: str, n_results: int = 5, max_words: int = 2500, add_text: bool = True) -> SearchResults: if not has_requirements: - raise MissingRequirementsError('Install "duckduckgo-search" and "beautifulsoup4" package') + raise MissingRequirementsError('Install "duckduckgo-search" and "beautifulsoup4" package | pip install -U g4f[search]') with DDGS() as ddgs: results = [] for result in ddgs.text( diff --git a/g4f/models.py b/g4f/models.py index 0a7eed35..8825242f 100644 --- a/g4f/models.py +++ b/g4f/models.py @@ -2,8 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from .Provider.not_working import Ai4Chat - from .Provider import IterListProvider, ProviderType from .Provider import ( AIChatFree, @@ -19,12 +17,10 @@ from .Provider import ( DDG, DeepInfraChat, Free2GPT, - FreeGpt, FreeNetfly, + GigaChat, Gemini, GeminiPro, - GizAI, - GigaChat, HuggingChat, HuggingFace, Liaobots, @@ -42,7 +38,6 @@ from .Provider import ( Upstage, ) - @dataclass(unsafe_hash=True) class Model: """ @@ -62,7 +57,6 @@ class Model: """Returns a list of all model names.""" return _all_models - ### Default ### default = Model( name = "", @@ -85,8 +79,6 @@ default = Model( ]) ) - - ############ ### Text ### ############ @@ -115,29 +107,15 @@ gpt_4o_mini = Model( gpt_4_turbo = Model( name = 'gpt-4-turbo', base_provider = 'OpenAI', - best_provider = IterListProvider([ChatGpt, Airforce, Liaobots, Bing]) + best_provider = IterListProvider([Liaobots, Bing]) ) gpt_4 = Model( name = 'gpt-4', base_provider = 'OpenAI', - best_provider = IterListProvider([Mhystical, Chatgpt4Online, ChatGpt, Bing, OpenaiChat, gpt_4_turbo.best_provider, gpt_4o.best_provider, gpt_4o_mini.best_provider]) + best_provider = IterListProvider([Chatgpt4Online, Bing, OpenaiChat, DDG, Liaobots, Airforce]) ) -# o1 -o1 = Model( - name = 'o1', - base_provider = 'OpenAI', - best_provider = None -) - -o1_mini = Model( - name = 'o1-mini', - base_provider = 'OpenAI', - best_provider = None -) - - ### GigaChat ### gigachat = Model( name = 'GigaChat:latest', @@ -145,7 +123,6 @@ gigachat = Model( best_provider = GigaChat ) - ### Meta ### meta = Model( name = "meta-ai", @@ -157,13 +134,13 @@ meta = Model( llama_2_7b = Model( name = "llama-2-7b", base_provider = "Meta Llama", - best_provider = IterListProvider([Cloudflare, Airforce]) + best_provider = Cloudflare ) # llama 3 llama_3_8b = Model( name = "llama-3-8b", base_provider = "Meta Llama", - best_provider = IterListProvider([Cloudflare]) + best_provider = Cloudflare ) # llama 3.1 @@ -198,13 +175,6 @@ llama_3_2_11b = Model( best_provider = IterListProvider([HuggingChat, HuggingFace]) ) -### Mistral ### -mistral_7b = Model( - name = "mistral-7b", - base_provider = "Mistral", - best_provider = IterListProvider([Free2GPT]) -) - mixtral_8x7b = Model( name = "mixtral-8x7b", base_provider = "Mistral", @@ -217,27 +187,12 @@ mistral_nemo = Model( best_provider = IterListProvider([HuggingChat, HuggingFace]) ) - -### NousResearch ### -hermes_2_pro = Model( - name = "hermes-2-pro", - base_provider = "NousResearch", - best_provider = Airforce -) - -hermes_2_dpo = Model( - name = "hermes-2-dpo", - base_provider = "NousResearch", - best_provider = Airforce -) - hermes_3 = Model( name = "hermes-3", base_provider = "NousResearch", best_provider = IterListProvider([HuggingChat, HuggingFace]) ) - ### Microsoft ### phi_2 = Model( name = "phi-2", @@ -256,13 +211,13 @@ phi_3_5_mini = Model( gemini_pro = Model( name = 'gemini-pro', base_provider = 'Google DeepMind', - best_provider = IterListProvider([GeminiPro, Blackbox, AIChatFree, FreeGpt, Liaobots]) + best_provider = IterListProvider([GeminiPro, Blackbox, AIChatFree, Liaobots]) ) gemini_flash = Model( name = 'gemini-flash', base_provider = 'Google DeepMind', - best_provider = IterListProvider([Blackbox, GizAI, Liaobots]) + best_provider = IterListProvider([Blackbox, Liaobots]) ) gemini = Model( @@ -278,7 +233,6 @@ gemma_2b = Model( best_provider = ReplicateHome ) - ### Anthropic ### claude_2_1 = Model( name = 'claude-2.1', @@ -290,13 +244,13 @@ claude_2_1 = Model( claude_3_opus = Model( name = 'claude-3-opus', base_provider = 'Anthropic', - best_provider = IterListProvider([Liaobots]) + best_provider = Liaobots ) claude_3_sonnet = Model( name = 'claude-3-sonnet', base_provider = 'Anthropic', - best_provider = IterListProvider([Liaobots]) + best_provider = Liaobots ) claude_3_haiku = Model( @@ -312,7 +266,6 @@ claude_3_5_sonnet = Model( best_provider = IterListProvider([Blackbox, Liaobots]) ) - ### Reka AI ### reka_core = Model( name = 'reka-core', @@ -320,7 +273,6 @@ reka_core = Model( best_provider = Reka ) - ### Blackbox AI ### blackboxai = Model( name = 'blackboxai', @@ -341,7 +293,6 @@ command_r_plus = Model( best_provider = HuggingChat ) - ### Qwen ### # qwen 1_5 qwen_1_5_7b = Model( @@ -477,7 +428,6 @@ german_7b = Model( best_provider = Airforce ) - ### HuggingFaceH4 ### zephyr_7b = Model( name = 'zephyr-7b', @@ -492,8 +442,6 @@ neural_7b = Model( best_provider = Airforce ) - - ############# ### Image ### ############# @@ -527,66 +475,55 @@ flux = Model( name = 'flux', base_provider = 'Flux AI', best_provider = IterListProvider([Blackbox, AIUncensored, Airforce]) - ) flux_pro = Model( name = 'flux-pro', base_provider = 'Flux AI', - best_provider = IterListProvider([Airforce]) - + best_provider = Airforce ) flux_realism = Model( name = 'flux-realism', base_provider = 'Flux AI', - best_provider = IterListProvider([Airforce]) - + best_provider = Airforce ) flux_anime = Model( name = 'flux-anime', base_provider = 'Flux AI', best_provider = Airforce - ) flux_3d = Model( name = 'flux-3d', base_provider = 'Flux AI', best_provider = Airforce - ) flux_disney = Model( name = 'flux-disney', base_provider = 'Flux AI', best_provider = Airforce - ) flux_pixel = Model( name = 'flux-pixel', base_provider = 'Flux AI', best_provider = Airforce - ) flux_4o = Model( name = 'flux-4o', base_provider = 'Flux AI', best_provider = Airforce - ) - - ### Other ### any_dark = Model( name = 'any-dark', base_provider = '', best_provider = Airforce - ) class ModelUtils: @@ -596,13 +533,15 @@ class ModelUtils: Attributes: convert (dict[str, Model]): Dictionary mapping model string identifiers to Model instances. """ - convert: dict[str, Model] = { - + convert: dict[str, Model] = { ############ ### Text ### ############ - + ### OpenAI ### + # gpt-3 + 'gpt-3': gpt_35_turbo, + # gpt-3.5 'gpt-3.5-turbo': gpt_35_turbo, @@ -612,11 +551,6 @@ class ModelUtils: 'gpt-4': gpt_4, 'gpt-4-turbo': gpt_4_turbo, - # o1 - 'o1': o1, - 'o1-mini': o1_mini, - - ### Meta ### "meta-ai": meta, @@ -636,32 +570,25 @@ class ModelUtils: 'llama-3.2-11b': llama_3_2_11b, ### Mistral ### - 'mistral-7b': mistral_7b, 'mixtral-8x7b': mixtral_8x7b, 'mistral-nemo': mistral_nemo, - - + ### NousResearch ### - 'hermes-2-pro': hermes_2_pro, - 'hermes-2-dpo': hermes_2_dpo, 'hermes-3': hermes_3, - - + ### Microsoft ### 'phi-2': phi_2, 'phi-3.5-mini': phi_3_5_mini, - ### Google ### # gemini 'gemini': gemini, 'gemini-pro': gemini_pro, 'gemini-flash': gemini_flash, - + # gemma 'gemma-2b': gemma_2b, - ### Anthropic ### 'claude-2.1': claude_2_1, @@ -672,115 +599,64 @@ class ModelUtils: # claude 3.5 'claude-3.5-sonnet': claude_3_5_sonnet, - - + ### Reka AI ### 'reka-core': reka_core, - - + ### Blackbox AI ### 'blackboxai': blackboxai, 'blackboxai-pro': blackboxai_pro, - - + ### CohereForAI ### 'command-r+': command_r_plus, - ### GigaChat ### 'gigachat': gigachat, - - - - ### Qwen ### - # qwen 1.5 - 'qwen-1.5-7b': qwen_1_5_7b, - # qwen 2 + 'qwen-1.5-7b': qwen_1_5_7b, 'qwen-2-72b': qwen_2_72b, - - # qwen 2.5 - 'qwen-2.5-coder-32b': qwen_2_5_coder_32b, - ### Upstage ### - 'solar-mini': solar_mini, 'solar-pro': solar_pro, - ### Inflection ### 'pi': pi, - - ### DeepSeek ### - 'deepseek-coder': deepseek_coder, - - ### Yorickvp ### 'llava-13b': llava_13b, - ### WizardLM ### 'wizardlm-2-8x22b': wizardlm_2_8x22b, - - + ### OpenChat ### 'openchat-3.5': openchat_3_5, - - + ### x.ai ### 'grok-2': grok_2, 'grok-2-mini': grok_2_mini, 'grok-beta': grok_beta, - - + ### Perplexity AI ### 'sonar-online': sonar_online, 'sonar-chat': sonar_chat, - - + ### TheBloke ### 'german-7b': german_7b, - - + ### Nvidia ### 'nemotron-70b': nemotron_70b, - - ### Teknium ### - 'openhermes-2.5': openhermes_2_5, - - - ### Liquid ### - 'lfm-40b': lfm_40b, - - - ### DiscoResearch ### - 'german-7b': german_7b, - - - ### HuggingFaceH4 ### - 'zephyr-7b': zephyr_7b, - - - ### Inferless ### - 'neural-7b': neural_7b, - - - ############# ### Image ### ############# - + ### Stability AI ### 'sdxl': sdxl, 'sd-3': sd_3, - - + ### Playground ### 'playground-v2.5': playground_v2_5, - ### Flux AI ### 'flux': flux, 'flux-pro': flux_pro, @@ -791,9 +667,8 @@ class ModelUtils: 'flux-pixel': flux_pixel, 'flux-4o': flux_4o, - ### Other ### 'any-dark': any_dark, } -_all_models = list(ModelUtils.convert.keys()) +_all_models = list(ModelUtils.convert.keys()) \ No newline at end of file diff --git a/g4f/providers/base_provider.py b/g4f/providers/base_provider.py index 5d48f2e0..128fb5a0 100644 --- a/g4f/providers/base_provider.py +++ b/g4f/providers/base_provider.py @@ -2,11 +2,13 @@ from __future__ import annotations import sys import asyncio + 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, FinishReason from ..errors import NestAsyncioError, ModelNotSupportedError @@ -17,6 +19,17 @@ if sys.version_info < (3, 10): 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: @@ -31,18 +44,14 @@ def get_running_loop(check_nested: bool) -> Union[AbstractEventLoop, None]: try: loop = asyncio.get_running_loop() # Do not patch uvloop loop because its incompatible. - try: - import uvloop + if has_uvloop: if isinstance(loop, uvloop.Loop): - return loop - except (ImportError, ModuleNotFoundError): - pass - if check_nested and not hasattr(loop.__class__, "_nest_patched"): - try: - import nest_asyncio + return loop + if not hasattr(loop.__class__, "_nest_patched"): + if has_nest_asyncio: nest_asyncio.apply(loop) - except ImportError: - raise NestAsyncioError('Install "nest_asyncio" package') + elif check_nested: + raise NestAsyncioError('Install "nest_asyncio" package | pip install -U nest_asyncio') return loop except RuntimeError: pass @@ -154,7 +163,7 @@ class AsyncProvider(AbstractProvider): Returns: CreateResult: The result of the completion creation. """ - get_running_loop(check_nested=True) + get_running_loop(check_nested=False) yield asyncio.run(cls.create_async(model, messages, **kwargs)) @staticmethod @@ -208,7 +217,7 @@ class AsyncGeneratorProvider(AsyncProvider): Returns: CreateResult: The result of the streaming completion creation. """ - loop = get_running_loop(check_nested=True) + loop = get_running_loop(check_nested=False) new_loop = False if loop is None: loop = asyncio.new_event_loop() @@ -222,7 +231,7 @@ class AsyncGeneratorProvider(AsyncProvider): while True: yield loop.run_until_complete(await_callback(gen.__anext__)) except StopAsyncIteration: - ... + pass finally: if new_loop: loop.close() diff --git a/g4f/providers/types.py b/g4f/providers/types.py index 69941a26..e7ca32ee 100644 --- a/g4f/providers/types.py +++ b/g4f/providers/types.py @@ -3,6 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from typing import Union, Dict, Type from ..typing import Messages, CreateResult +from .conversation import BaseConversation class BaseProvider(ABC): """ diff --git a/g4f/requests/__init__.py b/g4f/requests/__init__.py index 80fc44b3..a8c0e286 100644 --- a/g4f/requests/__init__.py +++ b/g4f/requests/__init__.py @@ -1,5 +1,8 @@ from __future__ import annotations +from urllib.parse import urlparse +from typing import Iterator +from http.cookies import Morsel try: from curl_cffi.requests import Session, Response from .curl_cffi import StreamResponse, StreamSession, FormData @@ -14,11 +17,19 @@ try: has_webview = True except ImportError: has_webview = False +try: + import nodriver + from nodriver.cdp.network import CookieParam + has_nodriver = True +except ImportError: + has_nodriver = False +from .. import debug from .raise_for_status import raise_for_status from ..webdriver import WebDriver, WebDriverSession from ..webdriver import bypass_cloudflare, get_driver_cookies from ..errors import MissingRequirementsError +from ..typing import Cookies from .defaults import DEFAULT_HEADERS, WEBVIEW_HAEDERS async def get_args_from_webview(url: str) -> dict: @@ -105,4 +116,53 @@ def get_session_from_browser(url: str, webdriver: WebDriver = None, proxy: str = proxies={"https": proxy, "http": proxy}, timeout=timeout, impersonate="chrome" - ) \ No newline at end of file + ) +def get_cookie_params_from_dict(cookies: Cookies, url: str = None, domain: str = None) -> list[CookieParam]: + [CookieParam.from_json({ + "name": key, + "value": value, + "url": url, + "domain": domain + }) for key, value in cookies.items()] + +async def get_args_from_nodriver( + url: str, + proxy: str = None, + timeout: int = 120, + cookies: Cookies = None +) -> dict: + if not has_nodriver: + raise MissingRequirementsError('Install "nodriver" package | pip install -U nodriver') + if debug.logging: + print(f"Open nodriver with url: {url}") + browser = await nodriver.start( + browser_args=None if proxy is None else [f"--proxy-server={proxy}"], + ) + domain = urlparse(url).netloc + if cookies is None: + cookies = {} + else: + await browser.cookies.set_all(get_cookie_params_from_dict(cookies, url=url, domain=domain)) + page = await browser.get(url) + for c in await browser.cookies.get_all(): + if c.domain.endswith(domain): + cookies[c.name] = c.value + user_agent = await page.evaluate("window.navigator.userAgent") + await page.wait_for("body:not(.no-js)", timeout=timeout) + await page.close() + browser.stop() + return { + "cookies": cookies, + "headers": { + **DEFAULT_HEADERS, + "user-agent": user_agent, + "referer": url, + }, + "proxy": proxy + } + +def merge_cookies(cookies: Iterator[Morsel], response: Response) -> Cookies: + if cookies is None: + cookies = {} + for cookie in response.cookies.jar: + cookies[cookie.name] = cookie.value \ No newline at end of file diff --git a/g4f/requests/raise_for_status.py b/g4f/requests/raise_for_status.py index 0e91505e..1699d9a4 100644 --- a/g4f/requests/raise_for_status.py +++ b/g4f/requests/raise_for_status.py @@ -11,6 +11,8 @@ class CloudflareError(ResponseStatusError): ... def is_cloudflare(text: str) -> bool: + if "Attention Required! | Cloudflare" in text: + return True return '
' in text or "Just a moment..." in text def is_openai(text: str) -> bool: -- cgit v1.2.3