From 09f8258fa7dd92f8b270d39ceeaf62ea25868220 Mon Sep 17 00:00:00 2001 From: hlohaus <983577+hlohaus@users.noreply.github.com> Date: Sat, 25 Jan 2025 03:56:37 +0100 Subject: Add MiniMax providers, add HailuoAI provider Add OpenaiTemplate provider Remove some webdriver providers --- g4f/Provider/DeepInfraChat.py | 11 +-- g4f/Provider/Jmuz.py | 9 +- g4f/Provider/Mhystical.py | 21 +--- g4f/Provider/__init__.py | 2 +- g4f/Provider/mini_max/HailuoAI.py | 103 +++++++++++++++++++ g4f/Provider/mini_max/MiniMax.py | 16 +++ g4f/Provider/mini_max/__init__.py | 2 + g4f/Provider/mini_max/crypt.py | 103 +++++++++++++++++++ g4f/Provider/needs_auth/Custom.py | 7 +- g4f/Provider/needs_auth/DeepInfra.py | 43 ++++---- g4f/Provider/needs_auth/GlhfChat.py | 10 +- g4f/Provider/needs_auth/Groq.py | 6 +- g4f/Provider/needs_auth/HuggingFaceAPI.py | 5 +- g4f/Provider/needs_auth/OpenaiAPI.py | 158 +----------------------------- g4f/Provider/needs_auth/OpenaiChat.py | 2 + g4f/Provider/needs_auth/OpenaiTemplate.py | 158 ++++++++++++++++++++++++++++++ g4f/Provider/needs_auth/PerplexityApi.py | 5 +- g4f/Provider/needs_auth/ThebApi.py | 7 +- g4f/Provider/needs_auth/xAI.py | 8 +- g4f/Provider/selenium/PerplexityAi.py | 107 -------------------- g4f/Provider/selenium/Phind.py | 102 ------------------- g4f/Provider/selenium/TalkAi.py | 85 ---------------- g4f/Provider/selenium/__init__.py | 3 - g4f/Provider/you/__init__.py | 0 g4f/Provider/you/har_file.py | 112 --------------------- g4f/providers/base_provider.py | 6 +- g4f/requests/__init__.py | 8 +- 27 files changed, 448 insertions(+), 651 deletions(-) create mode 100644 g4f/Provider/mini_max/HailuoAI.py create mode 100644 g4f/Provider/mini_max/MiniMax.py create mode 100644 g4f/Provider/mini_max/__init__.py create mode 100644 g4f/Provider/mini_max/crypt.py create mode 100644 g4f/Provider/needs_auth/OpenaiTemplate.py delete mode 100644 g4f/Provider/selenium/PerplexityAi.py delete mode 100644 g4f/Provider/selenium/Phind.py delete mode 100644 g4f/Provider/selenium/TalkAi.py delete mode 100644 g4f/Provider/selenium/__init__.py delete mode 100644 g4f/Provider/you/__init__.py delete mode 100644 g4f/Provider/you/har_file.py diff --git a/g4f/Provider/DeepInfraChat.py b/g4f/Provider/DeepInfraChat.py index 535097de..a2432d37 100644 --- a/g4f/Provider/DeepInfraChat.py +++ b/g4f/Provider/DeepInfraChat.py @@ -1,19 +1,12 @@ from __future__ import annotations from ..typing import AsyncResult, Messages -from .needs_auth import OpenaiAPI +from .needs_auth.OpenaiTemplate import OpenaiTemplate -class DeepInfraChat(OpenaiAPI): - label = __name__ +class DeepInfraChat(OpenaiTemplate): url = "https://deepinfra.com/chat" - login_url = None - needs_auth = False api_base = "https://api.deepinfra.com/v1/openai" - working = True - supports_stream = True - supports_system_message = True - supports_message_history = True default_model = 'meta-llama/Llama-3.3-70B-Instruct-Turbo' models = [ diff --git a/g4f/Provider/Jmuz.py b/g4f/Provider/Jmuz.py index a8f8bb2c..edc7c0c5 100644 --- a/g4f/Provider/Jmuz.py +++ b/g4f/Provider/Jmuz.py @@ -1,18 +1,13 @@ from __future__ import annotations from ..typing import AsyncResult, Messages -from .needs_auth.OpenaiAPI import OpenaiAPI +from .needs_auth.OpenaiTemplate import OpenaiTemplate -class Jmuz(OpenaiAPI): - label = "Jmuz" +class Jmuz(OpenaiTemplate): url = "https://discord.gg/Ew6JzjA2NR" - login_url = None api_base = "https://jmuz.me/gpt/api/v2" api_key = "prod" - working = True - needs_auth = False - supports_stream = True supports_system_message = False default_model = "gpt-4o" diff --git a/g4f/Provider/Mhystical.py b/g4f/Provider/Mhystical.py index 89f4be9b..1257a80f 100644 --- a/g4f/Provider/Mhystical.py +++ b/g4f/Provider/Mhystical.py @@ -1,31 +1,18 @@ from __future__ import annotations from ..typing import AsyncResult, Messages -from .needs_auth.OpenaiAPI import OpenaiAPI +from .needs_auth.OpenaiTemplate import OpenaiTemplate -""" - Mhystical.cc - ~~~~~~~~~~~~ - Author: NoelP.dev - Last Updated: 2024-05-11 - - Author Site: https://noelp.dev - Provider Site: https://mhystical.cc - -""" - -class Mhystical(OpenaiAPI): - label = "Mhystical" +class Mhystical(OpenaiTemplate): url = "https://mhystical.cc" api_endpoint = "https://api.mhystical.cc/v1/completions" login_url = "https://mhystical.cc/dashboard" api_key = "mhystical" - + working = True - needs_auth = False supports_stream = False # Set to False, as streaming is not specified in ChatifyAI supports_system_message = False - + default_model = 'gpt-4' models = [default_model] diff --git a/g4f/Provider/__init__.py b/g4f/Provider/__init__.py index e8df1d51..5db86513 100644 --- a/g4f/Provider/__init__.py +++ b/g4f/Provider/__init__.py @@ -6,11 +6,11 @@ from ..providers.base_provider import AsyncProvider, AsyncGeneratorProvider from ..providers.create_images import CreateImagesProvider from .deprecated import * -from .selenium import * from .needs_auth import * from .not_working import * from .local import * from .hf_space import HuggingSpace +from .mini_max import HailuoAI, MiniMax from .AIChatFree import AIChatFree from .AIUncensored import AIUncensored diff --git a/g4f/Provider/mini_max/HailuoAI.py b/g4f/Provider/mini_max/HailuoAI.py new file mode 100644 index 00000000..810120f7 --- /dev/null +++ b/g4f/Provider/mini_max/HailuoAI.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import os +import json +from typing import AsyncIterator +from aiohttp import ClientSession, FormData + +from ...typing import AsyncResult, Messages +from ..base_provider import AsyncAuthedProvider, ProviderModelMixin, format_prompt +from ..mini_max.crypt import CallbackResults, get_browser_callback, generate_yy_header +from ...requests import get_args_from_nodriver, raise_for_status +from ...providers.response import AuthResult, JsonConversation, RequestLogin, TitleGeneration +from ... import debug + +class Conversation(JsonConversation): + def __init__(self, token: str, chatID: str, characterID: str = 1): + self.token = token + self.chatID = chatID + self.characterID = characterID + +class HailuoAI(AsyncAuthedProvider, ProviderModelMixin): + label = "Hailuo AI" + url = "https://www.hailuo.ai" + working = True + supports_stream = True + default_model = "MiniMax" + + @classmethod + async def on_auth_async(cls, proxy: str = None, **kwargs) -> AsyncIterator: + login_url = os.environ.get("G4F_LOGIN_URL") + if login_url: + yield RequestLogin(cls.label, login_url) + callback_results = CallbackResults() + yield AuthResult( + **await get_args_from_nodriver( + cls.url, + proxy=proxy, + callback=await get_browser_callback(callback_results) + ), + **callback_results.get_dict() + ) + + @classmethod + async def create_authed( + cls, + model: str, + messages: Messages, + auth_result: AuthResult, + return_conversation: bool = False, + conversation: Conversation = None, + **kwargs + ) -> AsyncResult: + args = auth_result.get_dict().copy() + args.pop("impersonate") + token = args.pop("token") + path_and_query = args.pop("path_and_query") + timestamp = args.pop("timestamp") + + async with ClientSession(**args) as session: + if conversation is not None and conversation.token != token: + conversation = None + form_data = { + "characterID": 1 if conversation is None else getattr(conversation, "characterID", 1), + "msgContent": format_prompt(messages) if conversation is None else messages[-1]["content"], + "chatID": 0 if conversation is None else getattr(conversation, "chatID", 0), + "searchMode": 0 + } + data = FormData(default_to_multipart=True) + for name, value in form_data.items(): + form_data[name] = str(value) + data.add_field(name, str(value)) + headers = { + "token": token, + "yy": generate_yy_header(auth_result.path_and_query, form_data, "POST", timestamp) + } + async with session.post(f"{cls.url}{path_and_query}", data=data, headers=headers) as response: + await raise_for_status(response) + event = None + yield_content_len = 0 + async for line in response.content: + if not line: + continue + if line.startswith(b"event:"): + event = line[6:].decode(errors="replace").strip() + if event == "close_chunk": + break + if line.startswith(b"data:"): + try: + data = json.loads(line[5:]) + except json.JSONDecodeError as e: + debug.log(f"Failed to decode JSON: {line}, error: {e}") + continue + if event == "send_result": + send_result = data["data"]["sendResult"] + if "chatTitle" in send_result: + yield TitleGeneration(send_result["chatTitle"]) + if "chatID" in send_result and return_conversation: + yield Conversation(token, send_result["chatID"]) + elif event == "message_result": + message_result = data["data"]["messageResult"] + if "content" in message_result: + yield message_result["content"][yield_content_len:] + yield_content_len = len(message_result["content"]) \ No newline at end of file diff --git a/g4f/Provider/mini_max/MiniMax.py b/g4f/Provider/mini_max/MiniMax.py new file mode 100644 index 00000000..c954fb62 --- /dev/null +++ b/g4f/Provider/mini_max/MiniMax.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from ..needs_auth.OpenaiTemplate import OpenaiTemplate + +class MiniMax(OpenaiTemplate): + label = "MiniMax API" + url = "https://www.hailuo.ai/chat" + login_url = "https://intl.minimaxi.com/user-center/basic-information/interface-key" + api_base = "https://api.minimaxi.chat/v1" + working = True + needs_auth = True + + default_model = "MiniMax-Text-01" + default_vision_model = default_model + models = [default_model, "abab6.5s-chat"] + model_aliases = {"MiniMax": default_model} \ No newline at end of file diff --git a/g4f/Provider/mini_max/__init__.py b/g4f/Provider/mini_max/__init__.py new file mode 100644 index 00000000..2c60d8dc --- /dev/null +++ b/g4f/Provider/mini_max/__init__.py @@ -0,0 +1,2 @@ +from .HailuoAI import HailuoAI +from .MiniMax import MiniMax \ No newline at end of file diff --git a/g4f/Provider/mini_max/crypt.py b/g4f/Provider/mini_max/crypt.py new file mode 100644 index 00000000..c96491f4 --- /dev/null +++ b/g4f/Provider/mini_max/crypt.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import asyncio +import hashlib +import json +from urllib.parse import quote + +from ...providers.response import JsonMixin +from ...requests import Tab + +API_PATH = "/v4/api/chat/msg" + +class CallbackResults(JsonMixin): + def __init__(self): + self.token: str = None + self.path_and_query: str = None + self.timestamp: int = None + +def hash_function(base_string: str) -> str: + """ + Mimics the hashFunction using MD5. + """ + return hashlib.md5(base_string.encode()).hexdigest() + +def generate_yy_header(has_search_params_path: str, body: dict, method: str, time: int) -> str: + """ + Python equivalent of the generateYYHeader function. + """ + body_to_yy=get_body_to_yy(body) + + if method and method.lower() == 'post': + s = body or {} + else: + s = {} + + s = json.dumps(s, ensure_ascii=True, sort_keys=True) + if body_to_yy: + s = body_to_yy + # print("Encoded Path:", quote(has_search_params_path, "")) + # print("Stringified Body:", s) + # print("Hashed Time:", hash_function(str(time))) + + encoded_path = quote(has_search_params_path, "") + time_hash = hash_function(str(time)) + combined_string = f"{encoded_path}_{s}{time_hash}ooui" + + # print("Combined String:", combined_string) + # print("Hashed Combined String:", hash_function(combined_string)) + return hash_function(combined_string) + +def get_body_to_yy(l): + L = l["msgContent"].replace("\r\n", "").replace("\n", "").replace("\r", "") + M = hash_function(l["characterID"]) + hash_function(L) + hash_function(l["chatID"]) + M += hash_function("") # Mimics hashFunction(undefined) in JS + + # print("bodyToYY:", M) + return M + +async def get_browser_callback(auth_result: CallbackResults): + async def callback(page: Tab): + while not auth_result.token: + auth_result.token = await page.evaluate("localStorage.getItem('_token')") + if not auth_result.token: + await asyncio.sleep(1) + (auth_result.path_and_query, auth_result.timestamp) = await page.evaluate(""" + const device_id = localStorage.getItem("USER_HARD_WARE_INFO"); + const uuid = localStorage.getItem("UNIQUE_USER_ID"); + const os_name = navigator.userAgentData?.platform || navigator.platform || "Unknown"; + const browser_name = (() => { + const userAgent = navigator.userAgent.toLowerCase(); + if (userAgent.includes("chrome") && !userAgent.includes("edg")) return "chrome"; + if (userAgent.includes("edg")) return "edge"; + if (userAgent.includes("firefox")) return "firefox"; + if (userAgent.includes("safari") && !userAgent.includes("chrome")) return "safari"; + return "unknown"; + })(); + const cpu_core_num = navigator.hardwareConcurrency || 8; + const browser_language = navigator.language || "unknown"; + const browser_platform = `${navigator.platform || "unknown"}`; + const screen_width = window.screen.width || "unknown"; + const screen_height = window.screen.height || "unknown"; + const unix = Date.now(); // Current Unix timestamp in milliseconds + const params = { + device_platform: "web", + biz_id: 2, + app_id: 3001, + version_code: 22201, + lang: "en", + uuid, + device_id, + os_name, + browser_name, + cpu_core_num, + browser_language, + browser_platform, + screen_width, + screen_height, + unix + }; + [new URLSearchParams(params).toString(), unix] + """) + auth_result.path_and_query = f"{API_PATH}?{auth_result.path_and_query}" + return callback \ No newline at end of file diff --git a/g4f/Provider/needs_auth/Custom.py b/g4f/Provider/needs_auth/Custom.py index 8332394b..17a61a7b 100644 --- a/g4f/Provider/needs_auth/Custom.py +++ b/g4f/Provider/needs_auth/Custom.py @@ -1,12 +1,9 @@ from __future__ import annotations -from .OpenaiAPI import OpenaiAPI +from .OpenaiTemplate import OpenaiTemplate -class Custom(OpenaiAPI): +class Custom(OpenaiTemplate): label = "Custom Provider" - url = None - login_url = None working = True api_base = "http://localhost:8080/v1" - needs_auth = False sort_models = False \ No newline at end of file diff --git a/g4f/Provider/needs_auth/DeepInfra.py b/g4f/Provider/needs_auth/DeepInfra.py index ea537b3b..6c077a5e 100644 --- a/g4f/Provider/needs_auth/DeepInfra.py +++ b/g4f/Provider/needs_auth/DeepInfra.py @@ -4,22 +4,17 @@ import requests from ...typing import AsyncResult, Messages from ...requests import StreamSession, raise_for_status from ...image import ImageResponse -from .OpenaiAPI import OpenaiAPI -from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin +from .OpenaiTemplate import OpenaiTemplate -class DeepInfra(OpenaiAPI, AsyncGeneratorProvider, ProviderModelMixin): - label = "DeepInfra" +class DeepInfra(OpenaiTemplate): url = "https://deepinfra.com" login_url = "https://deepinfra.com/dash/api_keys" - working = True api_base = "https://api.deepinfra.com/v1/openai" + working = True needs_auth = True - supports_stream = True - supports_message_history = True + default_model = "meta-llama/Meta-Llama-3.1-70B-Instruct" default_image_model = "stabilityai/sd3.5" - models = [] - image_models = [] @classmethod def get_models(cls, **kwargs): @@ -38,7 +33,7 @@ class DeepInfra(OpenaiAPI, AsyncGeneratorProvider, ProviderModelMixin): cls.image_models.append(model['model_name']) cls.models.extend(cls.image_models) - + return cls.models @classmethod @@ -48,15 +43,24 @@ class DeepInfra(OpenaiAPI, AsyncGeneratorProvider, ProviderModelMixin): return cls.image_models @classmethod - def create_async_generator( + async def create_async_generator( cls, model: str, messages: Messages, stream: bool, + prompt: str = None, temperature: float = 0.7, max_tokens: int = 1028, **kwargs ) -> AsyncResult: + if model in cls.get_image_models(): + yield cls.create_async_image( + messages[-1]["content"] if prompt is None else prompt, + model, + **kwargs + ) + return + headers = { 'Accept-Encoding': 'gzip, deflate, br', 'Accept-Language': 'en-US', @@ -65,14 +69,15 @@ class DeepInfra(OpenaiAPI, AsyncGeneratorProvider, ProviderModelMixin): 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', 'X-Deepinfra-Source': 'web-embed', } - return super().create_async_generator( + async for chunk in super().create_async_generator( model, messages, stream=stream, temperature=temperature, max_tokens=max_tokens, headers=headers, **kwargs - ) + ): + yield chunk @classmethod async def create_async_image( @@ -118,14 +123,4 @@ class DeepInfra(OpenaiAPI, AsyncGeneratorProvider, ProviderModelMixin): if not images: raise RuntimeError(f"Response: {data}") images = images[0] if len(images) == 1 else images - return ImageResponse(images, prompt) - - @classmethod - async def create_async_image_generator( - cls, - model: str, - messages: Messages, - prompt: str = None, - **kwargs - ) -> AsyncResult: - yield await cls.create_async_image(messages[-1]["content"] if prompt is None else prompt, model, **kwargs) + return ImageResponse(images, prompt) \ No newline at end of file diff --git a/g4f/Provider/needs_auth/GlhfChat.py b/g4f/Provider/needs_auth/GlhfChat.py index be56ebb6..fbd7ebcd 100644 --- a/g4f/Provider/needs_auth/GlhfChat.py +++ b/g4f/Provider/needs_auth/GlhfChat.py @@ -1,14 +1,14 @@ from __future__ import annotations -from .OpenaiAPI import OpenaiAPI +from .OpenaiTemplate import OpenaiTemplate -class GlhfChat(OpenaiAPI): - label = "GlhfChat" +class GlhfChat(OpenaiTemplate): url = "https://glhf.chat" login_url = "https://glhf.chat/user-settings/api" api_base = "https://glhf.chat/api/openai/v1" - + working = True - + needs_auth = True + default_model = "hf:meta-llama/Llama-3.3-70B-Instruct" models = ["hf:meta-llama/Llama-3.1-405B-Instruct", default_model, "hf:deepseek-ai/DeepSeek-V3", "hf:Qwen/QwQ-32B-Preview", "hf:huihui-ai/Llama-3.3-70B-Instruct-abliterated", "hf:anthracite-org/magnum-v4-12b", "hf:meta-llama/Llama-3.1-70B-Instruct", "hf:meta-llama/Llama-3.1-8B-Instruct", "hf:meta-llama/Llama-3.2-3B-Instruct", "hf:meta-llama/Llama-3.2-11B-Vision-Instruct", "hf:meta-llama/Llama-3.2-90B-Vision-Instruct", "hf:Qwen/Qwen2.5-72B-Instruct", "hf:Qwen/Qwen2.5-Coder-32B-Instruct", "hf:google/gemma-2-9b-it", "hf:google/gemma-2-27b-it", "hf:mistralai/Mistral-7B-Instruct-v0.3", "hf:mistralai/Mixtral-8x7B-Instruct-v0.1", "hf:mistralai/Mixtral-8x22B-Instruct-v0.1", "hf:NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO", "hf:Qwen/Qwen2.5-7B-Instruct", "hf:upstage/SOLAR-10.7B-Instruct-v1.0", "hf:nvidia/Llama-3.1-Nemotron-70B-Instruct-HF"] diff --git a/g4f/Provider/needs_auth/Groq.py b/g4f/Provider/needs_auth/Groq.py index da3b2b18..4299d211 100644 --- a/g4f/Provider/needs_auth/Groq.py +++ b/g4f/Provider/needs_auth/Groq.py @@ -1,13 +1,13 @@ from __future__ import annotations -from .OpenaiAPI import OpenaiAPI +from .OpenaiTemplate import OpenaiTemplate -class Groq(OpenaiAPI): - label = "Groq" +class Groq(OpenaiTemplate): url = "https://console.groq.com/playground" login_url = "https://console.groq.com/keys" api_base = "https://api.groq.com/openai/v1" working = True + needs_auth = True default_model = "mixtral-8x7b-32768" fallback_models = [ "distil-whisper-large-v3-en", diff --git a/g4f/Provider/needs_auth/HuggingFaceAPI.py b/g4f/Provider/needs_auth/HuggingFaceAPI.py index 5c329965..b36f0e0a 100644 --- a/g4f/Provider/needs_auth/HuggingFaceAPI.py +++ b/g4f/Provider/needs_auth/HuggingFaceAPI.py @@ -1,15 +1,16 @@ from __future__ import annotations -from .OpenaiAPI import OpenaiAPI +from .OpenaiTemplate import OpenaiTemplate from .HuggingChat import HuggingChat from ...providers.types import Messages -class HuggingFaceAPI(OpenaiAPI): +class HuggingFaceAPI(OpenaiTemplate): label = "HuggingFace (Inference API)" parent = "HuggingFace" url = "https://api-inference.huggingface.com" api_base = "https://api-inference.huggingface.co/v1" working = True + needs_auth = True default_model = "meta-llama/Llama-3.2-11B-Vision-Instruct" default_vision_model = default_model diff --git a/g4f/Provider/needs_auth/OpenaiAPI.py b/g4f/Provider/needs_auth/OpenaiAPI.py index 1b2e7ae4..1720564e 100644 --- a/g4f/Provider/needs_auth/OpenaiAPI.py +++ b/g4f/Provider/needs_auth/OpenaiAPI.py @@ -1,163 +1,11 @@ from __future__ import annotations -import json -import requests +from . OpenaiTemplate import OpenaiTemplate -from ..helper import filter_none -from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin -from ...typing import Union, Optional, AsyncResult, Messages, ImagesType -from ...requests import StreamSession, raise_for_status -from ...providers.response import FinishReason, ToolCalls, Usage -from ...errors import MissingAuthError, ResponseError -from ...image import to_data_uri -from ... import debug - -class OpenaiAPI(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin): +class OpenaiAPI(OpenaiTemplate): label = "OpenAI API" url = "https://platform.openai.com" login_url = "https://platform.openai.com/settings/organization/api-keys" api_base = "https://api.openai.com/v1" working = True - needs_auth = True - supports_message_history = True - supports_system_message = True - default_model = "" - fallback_models = [] - sort_models = True - - @classmethod - def get_models(cls, api_key: str = None, api_base: str = None) -> list[str]: - if not cls.models: - try: - headers = {} - if api_base is None: - api_base = cls.api_base - if api_key is not None: - headers["authorization"] = f"Bearer {api_key}" - response = requests.get(f"{api_base}/models", headers=headers) - raise_for_status(response) - data = response.json() - data = data.get("data") if isinstance(data, dict) else data - cls.image_models = [model.get("id") for model in data if model.get("image")] - cls.models = [model.get("id") for model in data] - if cls.sort_models: - cls.models.sort() - except Exception as e: - debug.log(e) - cls.models = cls.fallback_models - return cls.models - - @classmethod - async def create_async_generator( - cls, - model: str, - messages: Messages, - proxy: str = None, - timeout: int = 120, - images: ImagesType = None, - api_key: str = None, - api_endpoint: str = None, - api_base: str = None, - temperature: float = None, - max_tokens: int = None, - top_p: float = None, - stop: Union[str, list[str]] = None, - stream: bool = False, - headers: dict = None, - impersonate: str = None, - tools: Optional[list] = None, - extra_data: dict = {}, - **kwargs - ) -> AsyncResult: - if cls.needs_auth and api_key is None: - raise MissingAuthError('Add a "api_key"') - if api_base is None: - api_base = cls.api_base - if images is not None and messages: - if not model and hasattr(cls, "default_vision_model"): - model = cls.default_vision_model - last_message = messages[-1].copy() - last_message["content"] = [ - *[{ - "type": "image_url", - "image_url": {"url": to_data_uri(image)} - } for image, _ in images], - { - "type": "text", - "text": messages[-1]["content"] - } - ] - messages[-1] = last_message - async with StreamSession( - proxy=proxy, - headers=cls.get_headers(stream, api_key, headers), - timeout=timeout, - impersonate=impersonate, - ) as session: - data = filter_none( - messages=messages, - model=cls.get_model(model, api_key=api_key, api_base=api_base), - temperature=temperature, - max_tokens=max_tokens, - top_p=top_p, - stop=stop, - stream=stream, - tools=tools, - **extra_data - ) - if api_endpoint is None: - api_endpoint = f"{api_base.rstrip('/')}/chat/completions" - async with session.post(api_endpoint, json=data) as response: - content_type = response.headers.get("content-type", "text/event-stream" if stream else "application/json") - if content_type.startswith("application/json"): - data = await response.json() - cls.raise_error(data) - await raise_for_status(response) - choice = data["choices"][0] - if "content" in choice["message"] and choice["message"]["content"]: - yield choice["message"]["content"].strip() - elif "tool_calls" in choice["message"]: - yield ToolCalls(choice["message"]["tool_calls"]) - if "usage" in data: - yield Usage(**data["usage"]) - if "finish_reason" in choice and choice["finish_reason"] is not None: - yield FinishReason(choice["finish_reason"]) - return - elif content_type.startswith("text/event-stream"): - await raise_for_status(response) - first = True - async for line in response.iter_lines(): - if line.startswith(b"data: "): - chunk = line[6:] - if chunk == b"[DONE]": - break - data = json.loads(chunk) - cls.raise_error(data) - choice = data["choices"][0] - if "content" in choice["delta"] and choice["delta"]["content"]: - delta = choice["delta"]["content"] - if first: - delta = delta.lstrip() - if delta: - first = False - yield delta - if "usage" in data and data["usage"]: - yield Usage(**data["usage"]) - if "finish_reason" in choice and choice["finish_reason"] is not None: - yield FinishReason(choice["finish_reason"]) - break - else: - await raise_for_status(response) - raise ResponseError(f"Not supported content-type: {content_type}") - - @classmethod - def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict: - return { - "Accept": "text/event-stream" if stream else "application/json", - "Content-Type": "application/json", - **( - {"Authorization": f"Bearer {api_key}"} - if api_key is not None else {} - ), - **({} if headers is None else headers) - } \ No newline at end of file + needs_auth = True \ No newline at end of file diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py index 25227f0f..69b55fd5 100644 --- a/g4f/Provider/needs_auth/OpenaiChat.py +++ b/g4f/Provider/needs_auth/OpenaiChat.py @@ -473,6 +473,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin): buffer = "" else: yield chunk + if conversation.finish_reason is not None: + break if sources.list: yield sources if return_conversation: diff --git a/g4f/Provider/needs_auth/OpenaiTemplate.py b/g4f/Provider/needs_auth/OpenaiTemplate.py new file mode 100644 index 00000000..bbf5b7cf --- /dev/null +++ b/g4f/Provider/needs_auth/OpenaiTemplate.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import json +import requests + +from ..helper import filter_none +from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin +from ...typing import Union, Optional, AsyncResult, Messages, ImagesType +from ...requests import StreamSession, raise_for_status +from ...providers.response import FinishReason, ToolCalls, Usage +from ...errors import MissingAuthError, ResponseError +from ...image import to_data_uri +from ... import debug + +class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin): + api_base = "" + supports_message_history = True + supports_system_message = True + default_model = "" + fallback_models = [] + sort_models = True + + @classmethod + def get_models(cls, api_key: str = None, api_base: str = None) -> list[str]: + if not cls.models: + try: + headers = {} + if api_base is None: + api_base = cls.api_base + if api_key is not None: + headers["authorization"] = f"Bearer {api_key}" + response = requests.get(f"{api_base}/models", headers=headers) + raise_for_status(response) + data = response.json() + data = data.get("data") if isinstance(data, dict) else data + cls.image_models = [model.get("id") for model in data if model.get("image")] + cls.models = [model.get("id") for model in data] + if cls.sort_models: + cls.models.sort() + except Exception as e: + debug.log(e) + return cls.fallback_models + return cls.models + + @classmethod + async def create_async_generator( + cls, + model: str, + messages: Messages, + proxy: str = None, + timeout: int = 120, + images: ImagesType = None, + api_key: str = None, + api_endpoint: str = None, + api_base: str = None, + temperature: float = None, + max_tokens: int = None, + top_p: float = None, + stop: Union[str, list[str]] = None, + stream: bool = False, + headers: dict = None, + impersonate: str = None, + tools: Optional[list] = None, + extra_data: dict = {}, + **kwargs + ) -> AsyncResult: + if cls.needs_auth and api_key is None: + raise MissingAuthError('Add a "api_key"') + if api_base is None: + api_base = cls.api_base + if images is not None and messages: + if not model and hasattr(cls, "default_vision_model"): + model = cls.default_vision_model + last_message = messages[-1].copy() + last_message["content"] = [ + *[{ + "type": "image_url", + "image_url": {"url": to_data_uri(image)} + } for image, _ in images], + { + "type": "text", + "text": messages[-1]["content"] + } + ] + messages[-1] = last_message + async with StreamSession( + proxy=proxy, + headers=cls.get_headers(stream, api_key, headers), + timeout=timeout, + impersonate=impersonate, + ) as session: + data = filter_none( + messages=messages, + model=cls.get_model(model, api_key=api_key, api_base=api_base), + temperature=temperature, + max_tokens=max_tokens, + top_p=top_p, + stop=stop, + stream=stream, + tools=tools, + **extra_data + ) + if api_endpoint is None: + api_endpoint = f"{api_base.rstrip('/')}/chat/completions" + async with session.post(api_endpoint, json=data) as response: + content_type = response.headers.get("content-type", "text/event-stream" if stream else "application/json") + if content_type.startswith("application/json"): + data = await response.json() + cls.raise_error(data) + await raise_for_status(response) + choice = data["choices"][0] + if "content" in choice["message"] and choice["message"]["content"]: + yield choice["message"]["content"].strip() + elif "tool_calls" in choice["message"]: + yield ToolCalls(choice["message"]["tool_calls"]) + if "usage" in data: + yield Usage(**data["usage"]) + if "finish_reason" in choice and choice["finish_reason"] is not None: + yield FinishReason(choice["finish_reason"]) + return + elif content_type.startswith("text/event-stream"): + await raise_for_status(response) + first = True + async for line in response.iter_lines(): + if line.startswith(b"data: "): + chunk = line[6:] + if chunk == b"[DONE]": + break + data = json.loads(chunk) + cls.raise_error(data) + choice = data["choices"][0] + if "content" in choice["delta"] and choice["delta"]["content"]: + delta = choice["delta"]["content"] + if first: + delta = delta.lstrip() + if delta: + first = False + yield delta + if "usage" in data and data["usage"]: + yield Usage(**data["usage"]) + if "finish_reason" in choice and choice["finish_reason"] is not None: + yield FinishReason(choice["finish_reason"]) + break + else: + await raise_for_status(response) + raise ResponseError(f"Not supported content-type: {content_type}") + + @classmethod + def get_headers(cls, stream: bool, api_key: str = None, headers: dict = None) -> dict: + return { + "Accept": "text/event-stream" if stream else "application/json", + "Content-Type": "application/json", + **( + {"Authorization": f"Bearer {api_key}"} + if api_key is not None else {} + ), + **({} if headers is None else headers) + } \ No newline at end of file diff --git a/g4f/Provider/needs_auth/PerplexityApi.py b/g4f/Provider/needs_auth/PerplexityApi.py index 3d8aa9bc..57b67cb4 100644 --- a/g4f/Provider/needs_auth/PerplexityApi.py +++ b/g4f/Provider/needs_auth/PerplexityApi.py @@ -1,12 +1,13 @@ from __future__ import annotations -from .OpenaiAPI import OpenaiAPI +from .OpenaiTemplate import OpenaiTemplate -class PerplexityApi(OpenaiAPI): +class PerplexityApi(OpenaiTemplate): label = "Perplexity API" url = "https://www.perplexity.ai" login_url = "https://www.perplexity.ai/settings/api" working = True + needs_auth = True api_base = "https://api.perplexity.ai" default_model = "llama-3-sonar-large-32k-online" models = [ diff --git a/g4f/Provider/needs_auth/ThebApi.py b/g4f/Provider/needs_auth/ThebApi.py index ccf33bc4..50b95a1c 100644 --- a/g4f/Provider/needs_auth/ThebApi.py +++ b/g4f/Provider/needs_auth/ThebApi.py @@ -2,7 +2,7 @@ from __future__ import annotations from ...typing import CreateResult, Messages from ..helper import filter_none -from .OpenaiAPI import OpenaiAPI +from .OpenaiTemplate import OpenaiTemplate models = { "theb-ai": "TheB.AI", @@ -19,13 +19,14 @@ models = { "qwen-2-72b": "Qwen" } -class ThebApi(OpenaiAPI): +class ThebApi(OpenaiTemplate): label = "TheB.AI API" url = "https://theb.ai" login_url = "https://beta.theb.ai/home" - working = True api_base = "https://api.theb.ai/v1" + working = True needs_auth = True + default_model = "theb-ai" fallback_models = list(models) diff --git a/g4f/Provider/needs_auth/xAI.py b/g4f/Provider/needs_auth/xAI.py index 7d96d259..154d18d0 100644 --- a/g4f/Provider/needs_auth/xAI.py +++ b/g4f/Provider/needs_auth/xAI.py @@ -1,10 +1,10 @@ from __future__ import annotations -from .OpenaiAPI import OpenaiAPI +from .OpenaiTemplate import OpenaiTemplate -class xAI(OpenaiAPI): - label = "xAI" +class xAI(OpenaiTemplate): url = "https://console.x.ai" login_url = "https://console.x.ai" api_base = "https://api.x.ai/v1" - working = True \ No newline at end of file + working = True + needs_auth = True \ No newline at end of file diff --git a/g4f/Provider/selenium/PerplexityAi.py b/g4f/Provider/selenium/PerplexityAi.py deleted file mode 100644 index 0f6c3f68..00000000 --- a/g4f/Provider/selenium/PerplexityAi.py +++ /dev/null @@ -1,107 +0,0 @@ -from __future__ import annotations - -import time - -try: - from selenium.webdriver.common.by import By - from selenium.webdriver.support.ui import WebDriverWait - from selenium.webdriver.support import expected_conditions as EC -except ImportError: - pass - -from ...typing import CreateResult, Messages -from ..base_provider import AbstractProvider -from ..helper import format_prompt - -class PerplexityAi(AbstractProvider): - url = "https://www.perplexity.ai" - working = False - supports_gpt_35_turbo = True - supports_stream = True - - @classmethod - def create_completion( - cls, - model: str, - messages: Messages, - stream: bool, - proxy: str = None, - timeout: int = 120, - webdriver = None, - virtual_display: bool = True, - copilot: bool = False, - **kwargs - ) -> CreateResult: - with WebDriverSession(webdriver, "", virtual_display=virtual_display, proxy=proxy) as driver: - prompt = format_prompt(messages) - - driver.get(f"{cls.url}/") - wait = WebDriverWait(driver, timeout) - - # Is page loaded? - wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "textarea[placeholder='Ask anything...']"))) - - # Register WebSocket hook - script = """ -window._message = window._last_message = ""; -window._message_finished = false; -const _socket_send = WebSocket.prototype.send; -WebSocket.prototype.send = function(...args) { - if (!window.socket_onmessage) { - window._socket_onmessage = this; - this.addEventListener("message", (event) => { - if (event.data.startsWith("42")) { - let data = JSON.parse(event.data.substring(2)); - if (data[0] =="query_progress" || data[0] == "query_answered") { - let content = JSON.parse(data[1]["text"]); - if (data[1]["mode"] == "copilot") { - content = content[content.length-1]["content"]["answer"]; - content = JSON.parse(content); - } - window._message = content["answer"]; - if (!window._message_finished) { - window._message_finished = data[0] == "query_answered"; - } - } - } - }); - } - return _socket_send.call(this, ...args); -}; -""" - driver.execute_script(script) - - if copilot: - try: - # Check for account - driver.find_element(By.CSS_SELECTOR, "img[alt='User avatar']") - # Enable copilot - driver.find_element(By.CSS_SELECTOR, "button[data-testid='copilot-toggle']").click() - except: - raise RuntimeError("You need a account for copilot") - - # Submit prompt - element_send_text(driver.find_element(By.CSS_SELECTOR, "textarea[placeholder='Ask anything...']"), prompt) - - # Stream response - script = """ -if(window._message && window._message != window._last_message) { - try { - return window._message.substring(window._last_message.length); - } finally { - window._last_message = window._message; - } -} else if(window._message_finished) { - return null; -} else { - return ''; -} -""" - while True: - chunk = driver.execute_script(script) - if chunk: - yield chunk - elif chunk != "": - break - else: - time.sleep(0.1) diff --git a/g4f/Provider/selenium/Phind.py b/g4f/Provider/selenium/Phind.py deleted file mode 100644 index d17eb27e..00000000 --- a/g4f/Provider/selenium/Phind.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import annotations - -import time -from urllib.parse import quote - -from ...typing import CreateResult, Messages -from ..base_provider import AbstractProvider -from ..helper import format_prompt - -class Phind(AbstractProvider): - url = "https://www.phind.com" - working = False - supports_gpt_4 = True - supports_stream = True - - @classmethod - def create_completion( - cls, - model: str, - messages: Messages, - stream: bool, - proxy: str = None, - timeout: int = 120, - webdriver = None, - creative_mode: bool = None, - **kwargs - ) -> CreateResult: - with WebDriverSession(webdriver, "", proxy=proxy) as driver: - from selenium.webdriver.common.by import By - from selenium.webdriver.support.ui import WebDriverWait - from selenium.webdriver.support import expected_conditions as EC - - # Register fetch hook - source = """ -window._fetch = window.fetch; -window.fetch = async (url, options) => { - const response = await window._fetch(url, options); - if (url != "/api/infer/answer") { - return response; - } - copy = response.clone(); - window._reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - return copy; -} -""" - driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { - "source": source - }) - - prompt = quote(format_prompt(messages)) - driver.get(f"{cls.url}/search?q={prompt}&source=searchbox") - - # Need to change settings - wait = WebDriverWait(driver, timeout) - def open_dropdown(): - # Open settings dropdown - wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, "button.text-dark.dropdown-toggle"))) - driver.find_element(By.CSS_SELECTOR, "button.text-dark.dropdown-toggle").click() - # Wait for dropdown toggle - wait.until(EC.visibility_of_element_located((By.XPATH, "//button[text()='GPT-4']"))) - if model.startswith("gpt-4") or creative_mode: - # Enable GPT-4 - if model.startswith("gpt-4"): - open_dropdown() - driver.find_element(By.XPATH, "//button[text()='GPT-4']").click() - # Enable creative mode - if creative_mode or creative_mode == None: - open_dropdown() - driver.find_element(By.ID, "Creative Mode").click() - # Submit changes - driver.find_element(By.CSS_SELECTOR, ".search-bar-input-group button[type='submit']").click() - # Wait for page reload - wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".search-container"))) - - while True: - chunk = driver.execute_script(""" -if(window._reader) { - chunk = await window._reader.read(); - if (chunk['done']) { - return null; - } - content = ''; - chunk['value'].split('\\r\\n').forEach((line, index) => { - if (line.startsWith('data: ')) { - line = line.substring('data: '.length); - if (!line.startsWith('')) { - if (line) content += line; - else content += '\\n'; - } - } - }); - return content.replace('\\n\\n', '\\n'); -} else { - return '' -} -""") - if chunk: - yield chunk - elif chunk != "": - break - else: - time.sleep(0.1) \ No newline at end of file diff --git a/g4f/Provider/selenium/TalkAi.py b/g4f/Provider/selenium/TalkAi.py deleted file mode 100644 index d722022d..00000000 --- a/g4f/Provider/selenium/TalkAi.py +++ /dev/null @@ -1,85 +0,0 @@ -from __future__ import annotations - -import time, json, time - -from ...typing import CreateResult, Messages -from ..base_provider import AbstractProvider - -class TalkAi(AbstractProvider): - url = "https://talkai.info" - working = False - supports_gpt_35_turbo = True - supports_stream = True - - @classmethod - def create_completion( - cls, - model: str, - messages: Messages, - stream: bool, - proxy: str = None, - webdriver = None, - **kwargs - ) -> CreateResult: - with WebDriverSession(webdriver, "", virtual_display=True, proxy=proxy) as driver: - from selenium.webdriver.common.by import By - from selenium.webdriver.support.ui import WebDriverWait - from selenium.webdriver.support import expected_conditions as EC - - driver.get(f"{cls.url}/chat/") - - # Wait for page load - WebDriverWait(driver, 240).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "body.chat-page")) - ) - - data = { - "type": "chat", - "message": messages[-1]["content"], - "messagesHistory": [{ - "from": "you" if message["role"] == "user" else "chatGPT", - "content": message["content"] - } for message in messages], - "model": model if model else "gpt-3.5-turbo", - "max_tokens": 2048, - "temperature": 1, - "top_p": 1, - "presence_penalty": 0, - "frequency_penalty": 0, - **kwargs - } - script = """ -const response = await fetch("/chat/send2/", { - "headers": { - "Accept": "application/json", - "Content-Type": "application/json", - }, - "body": {body}, - "method": "POST" -}); -window._reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); -""" - driver.execute_script( - script.replace("{body}", json.dumps(json.dumps(data))) - ) - # Read response - while True: - chunk = driver.execute_script(""" -chunk = await window._reader.read(); -if (chunk.done) { - return null; -} -content = ""; -for (line of chunk.value.split("\\n")) { - if (line.startsWith('data: ')) { - content += line.substring('data: '.length); - } -} -return content; -""") - if chunk: - yield chunk.replace("\\n", "\n") - elif chunk != "": - break - else: - time.sleep(0.1) diff --git a/g4f/Provider/selenium/__init__.py b/g4f/Provider/selenium/__init__.py deleted file mode 100644 index 44adf5fb..00000000 --- a/g4f/Provider/selenium/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .PerplexityAi import PerplexityAi -from .Phind import Phind -from .TalkAi import TalkAi diff --git a/g4f/Provider/you/__init__.py b/g4f/Provider/you/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/g4f/Provider/you/har_file.py b/g4f/Provider/you/har_file.py deleted file mode 100644 index 5ed0abd6..00000000 --- a/g4f/Provider/you/har_file.py +++ /dev/null @@ -1,112 +0,0 @@ -from __future__ import annotations - -import json -import os -import os.path -import random -import logging - -from ...requests import StreamSession, raise_for_status -from ...cookies import get_cookies_dir -from ...errors import MissingRequirementsError, NoValidHarFileError -from ... import debug - -logger = logging.getLogger(__name__) - -class arkReq: - def __init__(self, arkURL, arkHeaders, arkBody, arkCookies, userAgent): - self.arkURL = arkURL - self.arkHeaders = arkHeaders - self.arkBody = arkBody - self.arkCookies = arkCookies - self.userAgent = userAgent - -telemetry_url = "https://telemetry.stytch.com/submit" -public_token = "public-token-live-507a52ad-7e69-496b-aee0-1c9863c7c819" -chatArks: list = None - -def readHAR(): - harPath = [] - chatArks = [] - for root, dirs, files in os.walk(get_cookies_dir()): - for file in files: - if file.endswith(".har"): - harPath.append(os.path.join(root, file)) - if harPath: - break - if not harPath: - raise NoValidHarFileError("No .har file found") - for path in harPath: - with open(path, 'rb') as file: - try: - harFile = json.load(file) - except json.JSONDecodeError: - # Error: not a HAR file! - continue - for v in harFile['log']['entries']: - if v['request']['url'] == telemetry_url: - chatArks.append(parseHAREntry(v)) - if not chatArks: - raise NoValidHarFileError("No telemetry in .har files found") - return chatArks - -def parseHAREntry(entry) -> arkReq: - tmpArk = arkReq( - arkURL=entry['request']['url'], - arkHeaders={h['name'].lower(): h['value'] for h in entry['request']['headers'] if h['name'].lower() not in ['content-length', 'cookie'] and not h['name'].startswith(':')}, - arkBody=entry['request']['postData']['text'], - arkCookies={c['name']: c['value'] for c in entry['request']['cookies']}, - userAgent="" - ) - tmpArk.userAgent = tmpArk.arkHeaders.get('user-agent', '') - return tmpArk - -async def sendRequest(tmpArk: arkReq, proxy: str = None): - async with StreamSession(headers=tmpArk.arkHeaders, cookies=tmpArk.arkCookies, proxy=proxy) as session: - async with session.post(tmpArk.arkURL, data=tmpArk.arkBody) as response: - await raise_for_status(response) - return await response.text() - -async def create_telemetry_id(proxy: str = None): - global chatArks - if chatArks is None: - chatArks = readHAR() - return await sendRequest(random.choice(chatArks), proxy) - -async def get_telemetry_ids(proxy: str = None) -> list: - try: - return [await create_telemetry_id(proxy)] - except NoValidHarFileError as e: - if debug.logging: - logger.error(e) - - try: - from nodriver import start - except ImportError: - raise MissingRequirementsError('Add .har file from you.com or install "nodriver" package | pip install -U nodriver') - if debug.logging: - logger.error('Getting telemetry_id for you.com with nodriver') - - browser = page = None - try: - browser = await start( - browser_args=None if proxy is None else [f"--proxy-server={proxy}"], - ) - page = await browser.get("https://you.com") - while not await page.evaluate('"GetTelemetryID" in this'): - await page.sleep(1) - async def get_telemetry_id(): - return await page.evaluate( - f'this.GetTelemetryID("{public_token}", "{telemetry_url}");', - await_promise=True - ) - return [await get_telemetry_id()] - finally: - try: - if page is not None: - await page.close() - if browser is not None: - await browser.stop() - except Exception as e: - if debug.logging: - logger.error(e) diff --git a/g4f/providers/base_provider.py b/g4f/providers/base_provider.py index 4d9fd85e..4096d95a 100644 --- a/g4f/providers/base_provider.py +++ b/g4f/providers/base_provider.py @@ -379,6 +379,8 @@ class RaiseErrorMixin(): raise ResponseError(data["error"]["message"]) else: raise ResponseError(data["error"]) + elif "choices" not in data or not data["choices"]: + raise ResponseError(f"Invalid response: {json.dumps(data)}") class AsyncAuthedProvider(AsyncGeneratorProvider): @@ -422,14 +424,12 @@ class AsyncAuthedProvider(AsyncGeneratorProvider): auth_result = AuthResult(**json.load(f)) else: auth_result = cls.on_auth(**kwargs) - try: for chunk in auth_result: + print(hasattr(chunk, "get_dict")) if hasattr(chunk, "get_dict"): auth_result = chunk else: yield chunk - except TypeError: - pass yield from to_sync_generator(cls.create_authed(model, messages, auth_result, **kwargs)) except (MissingAuthError, NoValidHarFileError): auth_result = cls.on_auth(**kwargs) diff --git a/g4f/requests/__init__.py b/g4f/requests/__init__.py index 8e00ad14..27bc89e8 100644 --- a/g4f/requests/__init__.py +++ b/g4f/requests/__init__.py @@ -22,9 +22,10 @@ try: import nodriver from nodriver.cdp.network import CookieParam from nodriver.core.config import find_chrome_executable - from nodriver import Browser + from nodriver import Browser, Tab has_nodriver = True except ImportError: + from typing import Type as Tab has_nodriver = False try: from platformdirs import user_config_dir @@ -79,6 +80,7 @@ async def get_args_from_nodriver( proxy: str = None, timeout: int = 120, wait_for: str = None, + callback: callable = None, cookies: Cookies = None ) -> dict: browser = await get_nodriver(proxy=proxy) @@ -94,6 +96,8 @@ async def get_args_from_nodriver( await page.wait_for("body:not(.no-js)", timeout=timeout) if wait_for is not None: await page.wait_for(wait_for, timeout=timeout) + if callback is not None: + await callback(page) for c in await page.send(nodriver.cdp.network.get_cookies([url])): cookies[c.name] = c.value await page.close() @@ -106,7 +110,7 @@ async def get_args_from_nodriver( "user-agent": user_agent, "referer": url, }, - "proxy": proxy + "proxy": proxy, } def merge_cookies(cookies: Iterator[Morsel], response: Response) -> Cookies: -- cgit v1.2.3 From 036d41b9f469e2ee5a111f4c77370d508e0c1df6 Mon Sep 17 00:00:00 2001 From: hlohaus <983577+hlohaus@users.noreply.github.com> Date: Sat, 25 Jan 2025 09:10:18 +0100 Subject: Update base image of main docker image --- README.md | 22 +++++++++++---------- docker-compose-slim.yml | 19 ++++-------------- docker-compose.yml | 1 - docker/Dockerfile | 48 +++++++--------------------------------------- docker/supervisor-api.conf | 2 +- docker/supervisor-gui.conf | 12 ------------ g4f/version.py | 2 +- 7 files changed, 25 insertions(+), 81 deletions(-) delete mode 100755 docker/supervisor-gui.conf diff --git a/README.md b/README.md index 70889214..debd5940 100644 --- a/README.md +++ b/README.md @@ -99,22 +99,24 @@ Is your site on this repository and you want to take it down? Send an email to t ### 🐳 Using Docker 1. **Install Docker:** [Download and install Docker](https://docs.docker.com/get-docker/). 2. **Set Up Directories:** Before running the container, make sure the necessary data directories exist or can be created. For example, you can create and set ownership on these directories by running: - ```bash +```bash mkdir -p ${PWD}/har_and_cookies ${PWD}/generated_images - chown -R 1000:1000 ${PWD}/har_and_cookies ${PWD}/generated_images - ``` + sudo chown -R 1200:1201 ${PWD}/har_and_cookies ${PWD}/generated_images +``` 3. **Run the Docker Container:** Use the following commands to pull the latest image and start the container: - ```bash +```bash docker pull hlohaus789/g4f - docker run -p 8080:8080 -p 1337:1337 -p 7900:7900 \ + docker run -p 8080:8080 -p 7900:7900 \ --shm-size="2g" \ -v ${PWD}/har_and_cookies:/app/har_and_cookies \ -v ${PWD}/generated_images:/app/generated_images \ hlohaus789/g4f:latest - ``` +``` -4. **Running the Slim Docker Image:** Use the following command to run the Slim Docker image. This command also updates the `g4f` package at startup and installs any additional dependencies: - ```bash +4. **Running the Slim Docker Image:** And use the following commands to run the Slim Docker image. This command also updates the `g4f` package at startup and installs any additional dependencies: +```bash + mkdir -p ${PWD}/har_and_cookies ${PWD}/generated_images + chown -R 1000:1000 ${PWD}/har_and_cookies ${PWD}/generated_images docker run \ -p 1337:1337 \ -v ${PWD}/har_and_cookies:/app/har_and_cookies \ @@ -126,8 +128,8 @@ Is your site on this repository and you want to take it down? Send an email to t ``` 5. **Access the Client Interface:** - - **To use the included client, navigate to:** [http://localhost:8080/chat/](http://localhost:8080/chat/) or [http://localhost:1337/chat/](http://localhost:1337/chat/) - - **Or set the API base for your client to:** [http://localhost:1337/v1](http://localhost:1337/v1) + - **To use the included client, navigate to:** [http://localhost:8080/chat/](http://localhost:8080/chat/) + - **Or set the API base for your client to:** [http://localhost:8080/v1](http://localhost:8080/v1) 6. **(Optional) Provider Login:** If required, you can access the container's desktop here: http://localhost:7900/?autoconnect=1&resize=scale&password=secret for provider login purposes. diff --git a/docker-compose-slim.yml b/docker-compose-slim.yml index f56948de..7a02810f 100644 --- a/docker-compose-slim.yml +++ b/docker-compose-slim.yml @@ -1,25 +1,14 @@ version: '3' services: - g4f-gui: - container_name: g4f-gui + g4f-slim: + container_name: g4f-slim image: hlohaus789/g4f:latest-slim build: context: . dockerfile: docker/Dockerfile-slim - command: python -m g4f.cli gui -debug + command: python -m g4f --debug --port 8080 volumes: - .:/app ports: - - '8080:8080' - g4f-api: - container_name: g4f-api - image: hlohaus789/g4f:latest-slim - build: - context: . - dockerfile: docker/Dockerfile-slim - command: python -m g4f.cli api - volumes: - - .:/app - ports: - - '1337:1337' \ No newline at end of file + - '8080:8080' \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3f8bc4ea..1984bc0b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,6 @@ services: - .:/app ports: - '8080:8080' - - '1337:1337' - '7900:7900' environment: - OLLAMA_HOST=host.docker.internal diff --git a/docker/Dockerfile b/docker/Dockerfile index c73418f9..cafea013 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,26 +1,11 @@ -FROM seleniarm/node-chromium +FROM selenium/node-chrome ARG G4F_VERSION -ARG G4F_USER=g4f -ARG G4F_USER_ID=1000 -ARG G4F_NO_GUI -ARG G4F_PASS=secret - ENV G4F_VERSION $G4F_VERSION -ENV G4F_USER $G4F_USER -ENV G4F_USER_ID $G4F_USER_ID -ENV G4F_NO_GUI $G4F_NO_GUI ENV SE_SCREEN_WIDTH 1850 -ENV PYTHONUNBUFFERED 1 ENV G4F_DIR /app -ENV G4F_LOGIN_URL http://localhost:7900/?autoconnect=1&resize=scale&password=$G4F_PASS -ENV HOME /home/$G4F_USER -ENV PATH $PATH:$HOME/.local/bin -ENV SE_DOWNLOAD_DIR $HOME/Downloads -ENV SEL_USER $G4F_USER -ENV SEL_UID $G4F_USER_ID -ENV SEL_GID $G4F_USER_ID +ENV G4F_LOGIN_URL http://localhost:7900/?autoconnect=1&resize=scale&password=secret USER root @@ -43,30 +28,15 @@ RUN apt-get -qqy update \ # Update entrypoint COPY docker/supervisor.conf /etc/supervisor/conf.d/selenium.conf COPY docker/supervisor-api.conf /etc/supervisor/conf.d/api.conf -COPY docker/supervisor-gui.conf /etc/supervisor/conf.d/gui.conf - -# If no gui -RUN if [ "$G4F_NO_GUI" ] ; then \ - rm /etc/supervisor/conf.d/gui.conf \ - ; fi # Change background image COPY docker/background.png /usr/share/images/fluxbox/ubuntu-light.png # Add user, fix permissions -RUN groupadd -g $G4F_USER_ID $G4F_USER \ - && useradd -rm -G sudo -u $G4F_USER_ID -g $G4F_USER_ID $G4F_USER \ - && echo "${G4F_USER}:${G4F_PASS}" | chpasswd \ - && mkdir "${SE_DOWNLOAD_DIR}" \ - && chown "${G4F_USER_ID}:${G4F_USER_ID}" $SE_DOWNLOAD_DIR /var/run/supervisor /var/log/supervisor \ - && chown "${G4F_USER_ID}:${G4F_USER_ID}" -R /opt/bin/ /usr/bin/chromedriver /opt/selenium/ +RUN chown "${SEL_UID}:${SEL_GID}" $HOME/.local # Switch user -USER $G4F_USER_ID - -# Set VNC password -RUN mkdir -p ${HOME}/.vnc \ - && x11vnc -storepasswd ${G4F_PASS} ${HOME}/.vnc/passwd +USER $SEL_UID # Set the working directory in the container. WORKDIR $G4F_DIR @@ -76,14 +46,10 @@ COPY requirements.txt $G4F_DIR # Upgrade pip for the latest features and install the project's Python dependencies. RUN pip install --break-system-packages --upgrade pip \ - && pip install --break-system-packages -r requirements.txt \ - && pip install --break-system-packages \ - undetected-chromedriver selenium-wire \ - && pip uninstall -y --break-system-packages \ - pywebview + && pip install --break-system-packages -r requirements.txt # Copy the entire package into the container. -ADD --chown=$G4F_USER:$G4F_USER g4f $G4F_DIR/g4f +ADD --chown=$SEL_UID:$SEL_GID g4f $G4F_DIR/g4f # Expose ports -EXPOSE 8080 1337 7900 +EXPOSE 8080 7900 diff --git a/docker/supervisor-api.conf b/docker/supervisor-api.conf index 74572634..6679bcbc 100755 --- a/docker/supervisor-api.conf +++ b/docker/supervisor-api.conf @@ -1,6 +1,6 @@ [program:g4f-api] priority=15 -command=python -m g4f.cli api +command=python -m g4f --port 8080 --debug directory=/app stopasgroup=true autostart=true diff --git a/docker/supervisor-gui.conf b/docker/supervisor-gui.conf deleted file mode 100755 index 0c77ffc5..00000000 --- a/docker/supervisor-gui.conf +++ /dev/null @@ -1,12 +0,0 @@ -[program:g4f-gui] -priority=15 -command=python -m g4f.cli gui -debug -directory=/app -stopasgroup=true -autostart=true -autorestart=true - -;Logs (all Hub activity redirected to stdout so it can be seen through "docker logs" -redirect_stderr=true -stdout_logfile=/dev/stdout -stdout_logfile_maxbytes=0 \ No newline at end of file diff --git a/g4f/version.py b/g4f/version.py index 3cde30c7..2d23cb48 100644 --- a/g4f/version.py +++ b/g4f/version.py @@ -86,7 +86,7 @@ class VersionUtils: except CalledProcessError: pass - raise VersionNotFoundError("Version not found") + return None @property def latest_version(self) -> str: -- cgit v1.2.3 From cc6f9d1a18b2beb1ac41803612fe81251817359a Mon Sep 17 00:00:00 2001 From: hlohaus <983577+hlohaus@users.noreply.github.com> Date: Sat, 25 Jan 2025 10:53:30 +0100 Subject: Add default vision model, Add MiniMax to models.py --- g4f/__init__.py | 9 ++++---- g4f/client/__init__.py | 12 ++++++----- g4f/client/service.py | 13 ++++++++---- g4f/gui/server/api.py | 3 ++- g4f/models.py | 56 ++++++++++++++++++++++++++++++-------------------- 5 files changed, 57 insertions(+), 36 deletions(-) diff --git a/g4f/__init__.py b/g4f/__init__.py index 17d10b8b..b03dd0a8 100644 --- a/g4f/__init__.py +++ b/g4f/__init__.py @@ -33,13 +33,14 @@ class ChatCompletion: ignore_working: bool = False, ignore_stream: bool = False, **kwargs) -> Union[CreateResult, str]: + if image is not None: + kwargs["images"] = [(image, image_name)] model, provider = get_model_and_provider( model, provider, stream, ignore_working, - ignore_stream + ignore_stream, + has_images="images" in kwargs, ) - if image is not None: - kwargs["images"] = [(image, image_name)] if "proxy" not in kwargs: proxy = os.environ.get("G4F_PROXY") if proxy: @@ -61,9 +62,9 @@ class ChatCompletion: ignore_stream: bool = False, ignore_working: bool = False, **kwargs) -> Union[AsyncResult, Coroutine[str]]: - model, provider = get_model_and_provider(model, provider, False, ignore_working) if image is not None: kwargs["images"] = [(image, image_name)] + model, provider = get_model_and_provider(model, provider, False, ignore_working, has_images="images" in kwargs) if "proxy" not in kwargs: proxy = os.environ.get("G4F_PROXY") if proxy: diff --git a/g4f/client/__init__.py b/g4f/client/__init__.py index 02cddaa8..7fd5dc9a 100644 --- a/g4f/client/__init__.py +++ b/g4f/client/__init__.py @@ -241,16 +241,17 @@ class Completions: ignore_stream: Optional[bool] = False, **kwargs ) -> ChatCompletion: + if image is not None: + kwargs["images"] = [(image, image_name)] model, provider = get_model_and_provider( model, self.provider if provider is None else provider, stream, ignore_working, ignore_stream, + has_images="images" in kwargs ) stop = [stop] if isinstance(stop, str) else stop - if image is not None: - kwargs["images"] = [(image, image_name)] if ignore_stream: kwargs["ignore_stream"] = True @@ -526,16 +527,17 @@ class AsyncCompletions: ignore_stream: Optional[bool] = False, **kwargs ) -> Awaitable[ChatCompletion]: + if image is not None: + kwargs["images"] = [(image, image_name)] model, provider = get_model_and_provider( model, self.provider if provider is None else provider, stream, ignore_working, - ignore_stream, + ignore_stream,, + has_images="images" in kwargs ) stop = [stop] if isinstance(stop, str) else stop - if image is not None: - kwargs["images"] = [(image, image_name)] if ignore_stream: kwargs["ignore_stream"] = True diff --git a/g4f/client/service.py b/g4f/client/service.py index 7f3972f9..6a5ad097 100644 --- a/g4f/client/service.py +++ b/g4f/client/service.py @@ -4,7 +4,7 @@ from typing import Union from .. import debug, version from ..errors import ProviderNotFoundError, ModelNotFoundError, ProviderNotWorkingError, StreamNotSupportedError -from ..models import Model, ModelUtils, default +from ..models import Model, ModelUtils, default, default_vision from ..Provider import ProviderUtils from ..providers.types import BaseRetryProvider, ProviderType from ..providers.retry_provider import IterListProvider @@ -26,7 +26,8 @@ def get_model_and_provider(model : Union[Model, str], stream : bool, ignore_working: bool = False, ignore_stream: bool = False, - logging: bool = True) -> tuple[str, ProviderType]: + logging: bool = True, + has_images: bool = False) -> tuple[str, ProviderType]: """ Retrieves the model and provider based on input parameters. @@ -60,8 +61,12 @@ def get_model_and_provider(model : Union[Model, str], if not provider: if not model: - model = default - provider = model.best_provider + if has_images: + model = default_vision + provider = default_vision.best_provider + else: + model = default + provider = model.best_provider elif isinstance(model, str): if model in ProviderUtils.convert: provider = ProviderUtils.convert[model] diff --git a/g4f/gui/server/api.py b/g4f/gui/server/api.py index b584f7b6..42f9d9fc 100644 --- a/g4f/gui/server/api.py +++ b/g4f/gui/server/api.py @@ -147,7 +147,8 @@ class Api: kwargs.get("model"), provider, stream=True, ignore_stream=True, - logging=False + logging=False, + has_images="images" in kwargs, ) except Exception as e: logger.exception(e) diff --git a/g4f/models.py b/g4f/models.py index 0d905ffb..4570d4a8 100644 --- a/g4f/models.py +++ b/g4f/models.py @@ -37,8 +37,10 @@ from .Provider import ( Gemini, GeminiPro, GigaChat, + HailuoAI, HuggingChat, HuggingFace, + HuggingFaceAPI, MetaAI, MicrosoftDesigner, OpenaiAccount, @@ -70,7 +72,7 @@ class ImageModel(Model): ### Default ### default = Model( - name = "", + name = "", base_provider = "", best_provider = IterListProvider([ DDG, @@ -90,6 +92,21 @@ default = Model( ]) ) +default_vision = Model( + name = "", + base_provider = "", + best_provider = IterListProvider([ + Blackbox, + PollinationsAI, + HuggingSpace, + GeminiPro, + HuggingFaceAPI, + CopilotAccount, + OpenaiAccount, + Gemini, + ], shuffle=False) +) + ############ ### Text ### ############ @@ -523,6 +540,13 @@ glm_4 = Model( best_provider = ChatGLM ) +### MiniMax +mini_max = Model( + name = "MiniMax", + base_provider = "MiniMax", + best_provider = HailuoAI +) + ### Uncensored AI ### evil = Model( name = 'evil', @@ -570,7 +594,7 @@ sd_3_5 = ImageModel( flux = ImageModel( name = 'flux', base_provider = 'Flux AI', - best_provider = IterListProvider([Blackbox, PollinationsAI]) + best_provider = IterListProvider([Blackbox, PollinationsAI, HuggingSpace]) ) flux_dev = ImageModel( @@ -742,26 +766,14 @@ class ModelUtils: deepseek_chat.name: deepseek_chat, deepseek_r1.name: deepseek_r1, - ### Nvidia ### - nemotron_70b.name: nemotron_70b, - - ### Liquid ### - lfm_40b.name: lfm_40b, - - ### Databricks ### - dbrx_instruct.name: dbrx_instruct, - - ### PollinationsAI ### - p1.name: p1, - - ### CablyAI ### - cably_80b.name: cably_80b, - - ### THUDM ### - glm_4.name: glm_4, - - ### Uncensored AI ### - evil.name: evil, + nemotron_70b.name: nemotron_70b, ### Nvidia ### + lfm_40b.name: lfm_40b, ### Liquid ### + dbrx_instruct.name: dbrx_instruct, ### Databricks ### + p1.name: p1, ### PollinationsAI ### + cably_80b.name: cably_80b, ### CablyAI ### + glm_4.name: glm_4, ### THUDM ### + mini_max.name: mini_max, ## MiniMax + evil.name: evil, ### Uncensored AI ### ### Other ### midijourney.name: midijourney, -- cgit v1.2.3 From 3e1e50561920d617fb5612990adbce309c4e3910 Mon Sep 17 00:00:00 2001 From: hlohaus <983577+hlohaus@users.noreply.github.com> Date: Sat, 25 Jan 2025 12:02:37 +0100 Subject: Add lock file for nodriver, add finally stop browser to all calls --- g4f/Provider/Copilot.py | 51 ++++++++------- g4f/Provider/You.py | 15 +++-- g4f/Provider/needs_auth/Gemini.py | 23 ++++--- g4f/Provider/needs_auth/MicrosoftDesigner.py | 47 ++++++------- g4f/Provider/needs_auth/OpenaiChat.py | 95 ++++++++++++++------------- g4f/client/__init__.py | 4 +- g4f/providers/base_provider.py | 1 - g4f/requests/__init__.py | 98 +++++++++++++++++++--------- 8 files changed, 191 insertions(+), 143 deletions(-) diff --git a/g4f/Provider/Copilot.py b/g4f/Provider/Copilot.py index a259bda8..bdbbcc4b 100644 --- a/g4f/Provider/Copilot.py +++ b/g4f/Provider/Copilot.py @@ -207,30 +207,33 @@ class Copilot(AbstractProvider, ProviderModelMixin): async def get_access_token_and_cookies(url: str, proxy: str = None, target: str = "ChatAI",): browser = await get_nodriver(proxy=proxy, user_data_dir="copilot") - page = await browser.get(url) - access_token = None - while access_token is None: - access_token = await page.evaluate(""" - (() => { - for (var i = 0; i < localStorage.length; i++) { - try { - item = JSON.parse(localStorage.getItem(localStorage.key(i))); - if (item.credentialType == "AccessToken" - && item.expiresOn > Math.floor(Date.now() / 1000) - && item.target.includes("target")) { - return item.secret; - } - } catch(e) {} - } - })() - """.replace('"target"', json.dumps(target))) - if access_token is None: - await asyncio.sleep(1) - cookies = {} - for c in await page.send(nodriver.cdp.network.get_cookies([url])): - cookies[c.name] = c.value - await page.close() - return access_token, cookies + try: + page = await browser.get(url) + access_token = None + while access_token is None: + access_token = await page.evaluate(""" + (() => { + for (var i = 0; i < localStorage.length; i++) { + try { + item = JSON.parse(localStorage.getItem(localStorage.key(i))); + if (item.credentialType == "AccessToken" + && item.expiresOn > Math.floor(Date.now() / 1000) + && item.target.includes("target")) { + return item.secret; + } + } catch(e) {} + } + })() + """.replace('"target"', json.dumps(target))) + if access_token is None: + await asyncio.sleep(1) + cookies = {} + for c in await page.send(nodriver.cdp.network.get_cookies([url])): + cookies[c.name] = c.value + await page.close() + return access_token, cookies + finally: + browser.stop() def readHAR(url: str): api_key = None diff --git a/g4f/Provider/You.py b/g4f/Provider/You.py index b91fb0d8..71b92837 100644 --- a/g4f/Provider/You.py +++ b/g4f/Provider/You.py @@ -76,12 +76,15 @@ class You(AsyncGeneratorProvider, ProviderModelMixin): cookies = get_cookies(".you.com") except MissingRequirementsError: browser = await get_nodriver(proxy=proxy) - page = await browser.get(cls.url) - await page.wait_for('[data-testid="user-profile-button"]', timeout=900) - cookies = {} - for c in await page.send(nodriver.cdp.network.get_cookies([cls.url])): - cookies[c.name] = c.value - await page.close() + try: + page = await browser.get(cls.url) + await page.wait_for('[data-testid="user-profile-button"]', timeout=900) + cookies = {} + for c in await page.send(nodriver.cdp.network.get_cookies([cls.url])): + cookies[c.name] = c.value + await page.close() + finally: + browser.stop() async with StreamSession( proxy=proxy, impersonate="chrome", diff --git a/g4f/Provider/needs_auth/Gemini.py b/g4f/Provider/needs_auth/Gemini.py index 0e1a733f..0f563b32 100644 --- a/g4f/Provider/needs_auth/Gemini.py +++ b/g4f/Provider/needs_auth/Gemini.py @@ -78,16 +78,19 @@ class Gemini(AsyncGeneratorProvider, ProviderModelMixin): print("Skip nodriver login in Gemini provider") return browser = await get_nodriver(proxy=proxy, user_data_dir="gemini") - login_url = os.environ.get("G4F_LOGIN_URL") - if login_url: - yield RequestLogin(cls.label, login_url) - page = await browser.get(f"{cls.url}/app") - await page.select("div.ql-editor.textarea", 240) - cookies = {} - for c in await page.send(nodriver.cdp.network.get_cookies([cls.url])): - cookies[c.name] = c.value - await page.close() - cls._cookies = cookies + try: + login_url = os.environ.get("G4F_LOGIN_URL") + if login_url: + yield RequestLogin(cls.label, login_url) + page = await browser.get(f"{cls.url}/app") + await page.select("div.ql-editor.textarea", 240) + cookies = {} + for c in await page.send(nodriver.cdp.network.get_cookies([cls.url])): + cookies[c.name] = c.value + await page.close() + cls._cookies = cookies + finally: + browser.stop() @classmethod async def create_async_generator( diff --git a/g4f/Provider/needs_auth/MicrosoftDesigner.py b/g4f/Provider/needs_auth/MicrosoftDesigner.py index 57b96e2d..2a89e8be 100644 --- a/g4f/Provider/needs_auth/MicrosoftDesigner.py +++ b/g4f/Provider/needs_auth/MicrosoftDesigner.py @@ -143,25 +143,28 @@ def readHAR(url: str) -> tuple[str, str]: async def get_access_token_and_user_agent(url: str, proxy: str = None): browser = await get_nodriver(proxy=proxy, user_data_dir="designer") - page = await browser.get(url) - user_agent = await page.evaluate("navigator.userAgent") - access_token = None - while access_token is None: - access_token = await page.evaluate(""" - (() => { - for (var i = 0; i < localStorage.length; i++) { - try { - item = JSON.parse(localStorage.getItem(localStorage.key(i))); - if (item.credentialType == "AccessToken" - && item.expiresOn > Math.floor(Date.now() / 1000) - && item.target.includes("designerappservice")) { - return item.secret; - } - } catch(e) {} - } - })() - """) - if access_token is None: - await asyncio.sleep(1) - await page.close() - return access_token, user_agent \ No newline at end of file + try: + page = await browser.get(url) + user_agent = await page.evaluate("navigator.userAgent") + access_token = None + while access_token is None: + access_token = await page.evaluate(""" + (() => { + for (var i = 0; i < localStorage.length; i++) { + try { + item = JSON.parse(localStorage.getItem(localStorage.key(i))); + if (item.credentialType == "AccessToken" + && item.expiresOn > Math.floor(Date.now() / 1000) + && item.target.includes("designerappservice")) { + return item.secret; + } + } catch(e) {} + } + })() + """) + if access_token is None: + await asyncio.sleep(1) + await page.close() + return access_token, user_agent + finally: + browser.stop() \ No newline at end of file diff --git a/g4f/Provider/needs_auth/OpenaiChat.py b/g4f/Provider/needs_auth/OpenaiChat.py index 69b55fd5..a8a4018e 100644 --- a/g4f/Provider/needs_auth/OpenaiChat.py +++ b/g4f/Provider/needs_auth/OpenaiChat.py @@ -624,53 +624,56 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin): @classmethod async def nodriver_auth(cls, proxy: str = None): browser = await get_nodriver(proxy=proxy) - page = browser.main_tab - def on_request(event: nodriver.cdp.network.RequestWillBeSent): - if event.request.url == start_url or event.request.url.startswith(conversation_url): - RequestConfig.headers = event.request.headers - elif event.request.url in (backend_url, backend_anon_url): - if "OpenAI-Sentinel-Proof-Token" in event.request.headers: - RequestConfig.proof_token = json.loads(base64.b64decode( - event.request.headers["OpenAI-Sentinel-Proof-Token"].split("gAAAAAB", 1)[-1].encode() - ).decode()) - if "OpenAI-Sentinel-Turnstile-Token" in event.request.headers: - RequestConfig.turnstile_token = event.request.headers["OpenAI-Sentinel-Turnstile-Token"] - if "Authorization" in event.request.headers: - cls._api_key = event.request.headers["Authorization"].split()[-1] - elif event.request.url == arkose_url: - RequestConfig.arkose_request = arkReq( - arkURL=event.request.url, - arkBx=None, - arkHeader=event.request.headers, - arkBody=event.request.post_data, - userAgent=event.request.headers.get("User-Agent") - ) - await page.send(nodriver.cdp.network.enable()) - page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request) - page = await browser.get(cls.url) - user_agent = await page.evaluate("window.navigator.userAgent") - await page.select("#prompt-textarea", 240) - await page.evaluate("document.getElementById('prompt-textarea').innerText = 'Hello'") - await page.evaluate("document.querySelector('[data-testid=\"send-button\"]').click()") - while True: - if cls._api_key is not None or not cls.needs_auth: - break - body = await page.evaluate("JSON.stringify(window.__remixContext)") - if body: - match = re.search(r'"accessToken":"(.*?)"', body) - if match: - cls._api_key = match.group(1) + try: + page = browser.main_tab + def on_request(event: nodriver.cdp.network.RequestWillBeSent): + if event.request.url == start_url or event.request.url.startswith(conversation_url): + RequestConfig.headers = event.request.headers + elif event.request.url in (backend_url, backend_anon_url): + if "OpenAI-Sentinel-Proof-Token" in event.request.headers: + RequestConfig.proof_token = json.loads(base64.b64decode( + event.request.headers["OpenAI-Sentinel-Proof-Token"].split("gAAAAAB", 1)[-1].encode() + ).decode()) + if "OpenAI-Sentinel-Turnstile-Token" in event.request.headers: + RequestConfig.turnstile_token = event.request.headers["OpenAI-Sentinel-Turnstile-Token"] + if "Authorization" in event.request.headers: + cls._api_key = event.request.headers["Authorization"].split()[-1] + elif event.request.url == arkose_url: + RequestConfig.arkose_request = arkReq( + arkURL=event.request.url, + arkBx=None, + arkHeader=event.request.headers, + arkBody=event.request.post_data, + userAgent=event.request.headers.get("User-Agent") + ) + await page.send(nodriver.cdp.network.enable()) + page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request) + page = await browser.get(cls.url) + user_agent = await page.evaluate("window.navigator.userAgent") + await page.select("#prompt-textarea", 240) + await page.evaluate("document.getElementById('prompt-textarea').innerText = 'Hello'") + await page.evaluate("document.querySelector('[data-testid=\"send-button\"]').click()") + while True: + if cls._api_key is not None or not cls.needs_auth: break - await asyncio.sleep(1) - while True: - if RequestConfig.proof_token: - break - await asyncio.sleep(1) - RequestConfig.data_build = await page.evaluate("document.documentElement.getAttribute('data-build')") - RequestConfig.cookies = await page.send(get_cookies([cls.url])) - await page.close() - cls._create_request_args(RequestConfig.cookies, RequestConfig.headers, user_agent=user_agent) - cls._set_api_key(cls._api_key) + body = await page.evaluate("JSON.stringify(window.__remixContext)") + if body: + match = re.search(r'"accessToken":"(.*?)"', body) + if match: + cls._api_key = match.group(1) + break + await asyncio.sleep(1) + while True: + if RequestConfig.proof_token: + break + await asyncio.sleep(1) + RequestConfig.data_build = await page.evaluate("document.documentElement.getAttribute('data-build')") + RequestConfig.cookies = await page.send(get_cookies([cls.url])) + await page.close() + cls._create_request_args(RequestConfig.cookies, RequestConfig.headers, user_agent=user_agent) + cls._set_api_key(cls._api_key) + finally: + browser.stop() @staticmethod def get_default_headers() -> Dict[str, str]: diff --git a/g4f/client/__init__.py b/g4f/client/__init__.py index 7fd5dc9a..c5afb4f1 100644 --- a/g4f/client/__init__.py +++ b/g4f/client/__init__.py @@ -534,8 +534,8 @@ class AsyncCompletions: self.provider if provider is None else provider, stream, ignore_working, - ignore_stream,, - has_images="images" in kwargs + ignore_stream, + has_images="images" in kwargs, ) stop = [stop] if isinstance(stop, str) else stop if ignore_stream: diff --git a/g4f/providers/base_provider.py b/g4f/providers/base_provider.py index 4096d95a..5d11f3ec 100644 --- a/g4f/providers/base_provider.py +++ b/g4f/providers/base_provider.py @@ -425,7 +425,6 @@ class AsyncAuthedProvider(AsyncGeneratorProvider): else: auth_result = cls.on_auth(**kwargs) for chunk in auth_result: - print(hasattr(chunk, "get_dict")) if hasattr(chunk, "get_dict"): auth_result = chunk else: diff --git a/g4f/requests/__init__.py b/g4f/requests/__init__.py index 27bc89e8..2811f046 100644 --- a/g4f/requests/__init__.py +++ b/g4f/requests/__init__.py @@ -1,9 +1,12 @@ from __future__ import annotations import os +import time +import random from urllib.parse import urlparse from typing import Iterator from http.cookies import Morsel +from pathlib import Path try: from curl_cffi.requests import Session, Response from .curl_cffi import StreamResponse, StreamSession, FormData @@ -37,6 +40,7 @@ from .. import debug from .raise_for_status import raise_for_status from ..errors import MissingRequirementsError from ..typing import Cookies +from ..cookies import get_cookies_dir from .defaults import DEFAULT_HEADERS, WEBVIEW_HAEDERS if not has_curl_cffi: @@ -83,35 +87,37 @@ async def get_args_from_nodriver( callback: callable = None, cookies: Cookies = None ) -> dict: - browser = await get_nodriver(proxy=proxy) - if debug.logging: - print(f"Open nodriver with url: {url}") - 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) - user_agent = await page.evaluate("window.navigator.userAgent") - await page.wait_for("body:not(.no-js)", timeout=timeout) - if wait_for is not None: - await page.wait_for(wait_for, timeout=timeout) - if callback is not None: - await callback(page) - for c in await page.send(nodriver.cdp.network.get_cookies([url])): - cookies[c.name] = c.value - await page.close() - browser.stop() - return { - "impersonate": "chrome", - "cookies": cookies, - "headers": { - **DEFAULT_HEADERS, - "user-agent": user_agent, - "referer": url, - }, - "proxy": proxy, - } + browser = await get_nodriver(proxy=proxy, timeout=timeout) + try: + if debug.logging: + print(f"Open nodriver with url: {url}") + 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) + user_agent = await page.evaluate("window.navigator.userAgent") + await page.wait_for("body:not(.no-js)", timeout=timeout) + if wait_for is not None: + await page.wait_for(wait_for, timeout=timeout) + if callback is not None: + await callback(page) + for c in await page.send(nodriver.cdp.network.get_cookies([url])): + cookies[c.name] = c.value + await page.close() + return { + "impersonate": "chrome", + "cookies": cookies, + "headers": { + **DEFAULT_HEADERS, + "user-agent": user_agent, + "referer": url, + }, + "proxy": proxy, + } + finally: + browser.stop() def merge_cookies(cookies: Iterator[Morsel], response: Response) -> Cookies: if cookies is None: @@ -119,7 +125,13 @@ def merge_cookies(cookies: Iterator[Morsel], response: Response) -> Cookies: for cookie in response.cookies.jar: cookies[cookie.name] = cookie.value -async def get_nodriver(proxy: str = None, user_data_dir = "nodriver", browser_executable_path=None, **kwargs)-> Browser: +async def get_nodriver( + proxy: str = None, + user_data_dir = "nodriver", + timeout: int = 120, + browser_executable_path=None, + **kwargs +) -> Browser: if not has_nodriver: raise MissingRequirementsError('Install "nodriver" and "platformdirs" package | pip install -U nodriver platformdirs') user_data_dir = user_config_dir(f"g4f-{user_data_dir}") if has_platformdirs else None @@ -131,10 +143,32 @@ async def get_nodriver(proxy: str = None, user_data_dir = "nodriver", browser_ex browser_executable_path = "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe" if not os.path.exists(browser_executable_path): browser_executable_path = None + lock_file = Path(get_cookies_dir()) / ".nodriver_is_open" + # Implement a short delay (milliseconds) to prevent race conditions. + await asyncio.sleep(0.1 * random.randint(0, 50)) + if lock_file.exists(): + opend_at = float(lock_file.read_text()) + time_open = time.time() - opend_at + if timeout * 2 > time_open: + debug.log(f"Nodriver: Browser is already in use since {time_open} secs.") + for _ in range(timeout): + if lock_file.exists(): + await asyncio.sleep(1) + else: + break + lock_file.write_text(str(time.time())) debug.log(f"Open nodriver with user_dir: {user_data_dir}") - return await nodriver.start( + browser = await nodriver.start( user_data_dir=user_data_dir, browser_args=None if proxy is None else [f"--proxy-server={proxy}"], browser_executable_path=browser_executable_path, **kwargs - ) \ No newline at end of file + ) + stop = browser.stop + def on_stop(): + try: + stop() + finally: + lock_file.unlink(missing_ok=True) + browser.stop = on_stop + return browser \ No newline at end of file -- cgit v1.2.3 From 8bae5dd8d39c21b31eb90b4d9c4a20a5747e9aab Mon Sep 17 00:00:00 2001 From: hlohaus <983577+hlohaus@users.noreply.github.com> Date: Sat, 25 Jan 2025 12:07:42 +0100 Subject: Fix unittests --- etc/unittest/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/etc/unittest/main.py b/etc/unittest/main.py index a50bd8f1..3c0b9be6 100644 --- a/etc/unittest/main.py +++ b/etc/unittest/main.py @@ -7,8 +7,7 @@ DEFAULT_MESSAGES = [{'role': 'user', 'content': 'Hello'}] class TestGetLastProvider(unittest.TestCase): def test_get_latest_version(self): - try: + current_version = g4f.version.utils.current_version + if current_version is not None: self.assertIsInstance(g4f.version.utils.current_version, str) - except VersionNotFoundError: - pass self.assertIsInstance(g4f.version.utils.latest_version, str) \ No newline at end of file -- cgit v1.2.3