Merge pull request #2362 from hlohaus/data-uri

Add nodriver to Gemini provider,
This commit is contained in:
H Lohaus 2024-11-17 18:32:51 +01:00 committed by GitHub
commit 275574d71e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 240 additions and 145 deletions

View File

@ -48,3 +48,15 @@ jobs:
labels: ${{ steps.metadata.outputs.labels }}
build-args: |
G4F_VERSION=${{ github.ref_name }}
- name: Build and push slim image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile-slim
push: true
tags: |
hlohaus789/g4f=slim
hlohaus789/g4f=${{ github.ref_name }}-slim
labels: ${{ steps.metadata.outputs.labels }}
build-args: |
G4F_VERSION=${{ github.ref_name }}

25
docker-compose-slim.yml Normal file
View File

@ -0,0 +1,25 @@
version: '3'
services:
g4f-gui:
container_name: g4f-gui
image: hlohaus789/g4f:slim
build:
context: .
dockerfile: docker/Dockerfile-slim
command: python -m g4f.cli gui -debug
volumes:
- .:/app
ports:
- '8080:8080'
g4f-api:
container_name: g4f-api
image: hlohaus789/g4f:slim
build:
context: .
dockerfile: docker/Dockerfile-slim
command: python -m g4f.cli api
volumes:
- .:/app
ports:
- '1337:1337'

View File

@ -40,6 +40,7 @@ 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

68
docker/Dockerfile-slim Normal file
View File

@ -0,0 +1,68 @@
FROM python:bookworm
ARG G4F_VERSION
ARG G4F_USER=g4f
ARG G4F_USER_ID=1000
ARG PYDANTIC_VERSION=1.8.1
ENV G4F_VERSION $G4F_VERSION
ENV G4F_USER $G4F_USER
ENV G4F_USER_ID $G4F_USER_ID
ENV G4F_DIR /app
RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y git \
&& apt-get install --quiet --yes --no-install-recommends \
build-essential \
# Add user and user group
&& groupadd -g $G4F_USER_ID $G4F_USER \
&& useradd -rm -G sudo -u $G4F_USER_ID -g $G4F_USER_ID $G4F_USER \
&& mkdir -p /var/log/supervisor \
&& chown "${G4F_USER_ID}:${G4F_USER_ID}" /var/log/supervisor \
&& echo "${G4F_USER}:${G4F_USER}" | chpasswd
USER $G4F_USER_ID
WORKDIR $G4F_DIR
ENV HOME /home/$G4F_USER
ENV PATH "${HOME}/.local/bin:${HOME}/.cargo/bin:${PATH}"
# Create app dir and copy the project's requirements file into it
RUN mkdir -p $G4F_DIR
COPY requirements-slim.txt $G4F_DIR
# Install rust toolchain
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
# Upgrade pip for the latest features and install the project's Python dependencies.
RUN python -m pip install --upgrade pip \
&& pip install --no-cache-dir \
Cython==0.29.22 \
setuptools \
# Install PyDantic
&& pip install \
-vvv \
--no-cache-dir \
--no-binary pydantic \
--global-option=build_ext \
--global-option=-j8 \
pydantic==${PYDANTIC_VERSION} \
&& pip install --no-cache-dir -r requirements-slim.txt \
# Remove build packages
&& pip uninstall --yes \
Cython \
setuptools
USER root
# Clean up build deps
RUN rustup self uninstall -y \
&& apt-get purge --auto-remove --yes \
build-essential \
&& apt-get clean \
&& rm --recursive --force /var/lib/apt/lists/* /tmp/* /var/tmp/*
USER $G4F_USER_ID
# Copy the entire package into the container.
ADD --chown=$G4F_USER:$G4F_USER g4f $G4F_DIR/g4f

12
docker/supervisor-api.conf Executable file
View File

@ -0,0 +1,12 @@
[program:g4f-api]
priority=15
command=python -m g4f.cli api
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

View File

@ -1,6 +1,6 @@
[program:g4f-gui]
priority=15
command=python -m g4f.cli gui
command=python -m g4f.cli gui -debug
directory=/app
stopasgroup=true
autostart=true

View File

@ -47,17 +47,4 @@ stderr_logfile_maxbytes=50MB
stdout_logfile_backups=5
stderr_logfile_backups=5
stdout_capture_maxbytes=50MB
stderr_capture_maxbytes=50MB
[program:g4f-api]
priority=15
command=python -m g4f.cli api
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
stderr_capture_maxbytes=50MB

View File

@ -28,12 +28,22 @@
```
2. **Build and Run with Docker Compose**
Pull the latest image and run a container with Google Chrome support:
```bash
docker-compose up --build
docker pull hlohaus789/g4f
docker-compose up -d
```
Or run the small docker images without Google Chrome:
```bash
docker-compose -f docker-compose-slim.yml up -d
```
3. **Access the API**
The server will be accessible at `http://localhost:1337`
3. **Access the API or the GUI**
The api server will be accessible at `http://localhost:1337`
And the gui at this url: `http://localhost:8080`
### Non-Docker Method
If you encounter issues with Docker, you can run the project directly using Python:
@ -54,8 +64,12 @@ If you encounter issues with Docker, you can run the project directly using Pyth
python -m g4f.api.run
```
4. **Access the API**
The server will be accessible at `http://localhost:1337`
4. **Access the API or the GUI**
The api server will be accessible at `http://localhost:1337`
And the gui at this url: `http://localhost:8080`
## Testing the API
**You can test the API using curl or by creating a simple Python script:**

View File

@ -7,6 +7,7 @@ import uuid
from ..typing import AsyncResult, Messages, Cookies
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin, get_running_loop
from ..requests import Session, StreamSession, get_args_from_nodriver, raise_for_status, merge_cookies
from ..errors import ResponseStatusError
class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin):
label = "Cloudflare AI"
@ -42,10 +43,14 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin):
cls._args = asyncio.run(args)
with Session(**cls._args) as session:
response = session.get(cls.models_url)
raise_for_status(response)
cls._args["cookies"] = merge_cookies(cls._args["cookies"] , response)
try:
raise_for_status(response)
except ResponseStatusError as e:
cls._args = None
raise e
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
@ -74,8 +79,12 @@ class Cloudflare(AsyncGeneratorProvider, ProviderModelMixin):
cls.api_endpoint,
json=data,
) as response:
await raise_for_status(response)
cls._args["cookies"] = merge_cookies(cls._args["cookies"] , response)
try:
await raise_for_status(response)
except ResponseStatusError as e:
cls._args = None
raise e
async for line in response.iter_lines():
if line.startswith(b'data: '):
if line == b'data: [DONE]':

View File

@ -4,12 +4,13 @@ import json
import requests
try:
from curl_cffi import requests as cf_reqs
from curl_cffi import Session
has_curl_cffi = True
except ImportError:
has_curl_cffi = False
from ..typing import CreateResult, Messages
from ..errors import MissingRequirementsError
from ..requests.raise_for_status import raise_for_status
from .base_provider import ProviderModelMixin, AbstractProvider
from .helper import format_prompt
@ -18,7 +19,7 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
working = True
supports_stream = True
default_model = "meta-llama/Meta-Llama-3.1-70B-Instruct"
models = [
'meta-llama/Meta-Llama-3.1-70B-Instruct',
'CohereForAI/c4ai-command-r-plus-08-2024',
@ -30,7 +31,7 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
'mistralai/Mistral-Nemo-Instruct-2407',
'microsoft/Phi-3.5-mini-instruct',
]
model_aliases = {
"llama-3.1-70b": "meta-llama/Meta-Llama-3.1-70B-Instruct",
"command-r-plus": "CohereForAI/c4ai-command-r-plus-08-2024",
@ -43,15 +44,6 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
"phi-3.5-mini": "microsoft/Phi-3.5-mini-instruct",
}
@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 create_completion(
cls,
@ -65,7 +57,7 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
model = cls.get_model(model)
if model in cls.models:
session = cf_reqs.Session()
session = Session()
session.headers = {
'accept': '*/*',
'accept-language': 'en',
@ -82,20 +74,18 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
'sec-fetch-site': 'same-origin',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36',
}
json_data = {
'model': model,
}
response = session.post('https://huggingface.co/chat/conversation', json=json_data)
if response.status_code != 200:
raise RuntimeError(f"Request failed with status code: {response.status_code}, response: {response.text}")
raise_for_status(response)
conversationId = response.json().get('conversationId')
# Get the data response and parse it properly
response = session.get(f'https://huggingface.co/chat/conversation/{conversationId}/__data.json?x-sveltekit-invalidated=11')
raise_for_status(response)
# Split the response content by newlines and parse each line as JSON
try:
json_data = None
@ -156,6 +146,7 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
headers=headers,
files=files,
)
raise_for_status(response)
full_response = ""
for line in response.iter_lines():
@ -182,9 +173,4 @@ class HuggingChat(AbstractProvider, ProviderModelMixin):
full_response = full_response.replace('<|im_end|', '').replace('\u0000', '').strip()
if not stream:
yield full_response
@classmethod
def supports_model(cls, model: str) -> bool:
"""Check if the model is supported by the provider."""
return model in cls.models or model in cls.model_aliases
yield full_response

View File

@ -6,24 +6,20 @@ import random
import re
from aiohttp import ClientSession, BaseConnector
from ..helper import get_connector
try:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import nodriver
has_nodriver = True
except ImportError:
pass
has_nodriver = False
from ... import debug
from ...typing import Messages, Cookies, ImageType, AsyncResult, AsyncIterator
from ..base_provider import AsyncGeneratorProvider, BaseConversation
from ..helper import format_prompt, get_cookies
from ...requests.raise_for_status import raise_for_status
from ...errors import MissingAuthError, MissingRequirementsError
from ...requests.aiohttp import get_connector
from ...errors import MissingAuthError
from ...image import ImageResponse, to_bytes
from ...webdriver import get_browser, get_driver_cookies
REQUEST_HEADERS = {
"authority": "gemini.google.com",
@ -64,9 +60,9 @@ class Gemini(AsyncGeneratorProvider):
@classmethod
async def nodriver_login(cls, proxy: str = None) -> AsyncIterator[str]:
try:
import nodriver as uc
except ImportError:
if not has_nodriver:
if debug.logging:
print("Skip nodriver login in Gemini provider")
return
try:
from platformdirs import user_config_dir
@ -75,7 +71,7 @@ class Gemini(AsyncGeneratorProvider):
user_data_dir = None
if debug.logging:
print(f"Open nodriver with user_dir: {user_data_dir}")
browser = await uc.start(
browser = await nodriver.start(
user_data_dir=user_data_dir,
browser_args=None if proxy is None else [f"--proxy-server={proxy}"],
)
@ -91,30 +87,6 @@ class Gemini(AsyncGeneratorProvider):
await page.close()
cls._cookies = cookies
@classmethod
async def webdriver_login(cls, proxy: str) -> AsyncIterator[str]:
driver = None
try:
driver = get_browser(proxy=proxy)
try:
driver.get(f"{cls.url}/app")
WebDriverWait(driver, 5).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "div.ql-editor.textarea"))
)
except:
login_url = os.environ.get("G4F_LOGIN_URL")
if login_url:
yield f"Please login: [Google Gemini]({login_url})\n\n"
WebDriverWait(driver, 240).until(
EC.visibility_of_element_located((By.CSS_SELECTOR, "div.ql-editor.textarea"))
)
cls._cookies = get_driver_cookies(driver)
except MissingRequirementsError:
pass
finally:
if driver:
driver.close()
@classmethod
async def create_async_generator(
cls,
@ -143,9 +115,6 @@ class Gemini(AsyncGeneratorProvider):
if not cls._snlm0e:
async for chunk in cls.nodriver_login(proxy):
yield chunk
if cls._cookies is None:
async for chunk in cls.webdriver_login(proxy):
yield chunk
if not cls._snlm0e:
if cls._cookies is None or "__Secure-1PSID" not in cls._cookies:
raise MissingAuthError('Missing "__Secure-1PSID" cookie')
@ -211,20 +180,23 @@ class Gemini(AsyncGeneratorProvider):
yield content[last_content_len:]
last_content_len = len(content)
if image_prompt:
images = [image[0][3][3] for image in response_part[4][0][12][7][0]]
if response_format == "b64_json":
yield ImageResponse(images, image_prompt, {"cookies": cls._cookies})
else:
resolved_images = []
preview = []
for image in images:
async with client.get(image, allow_redirects=False) as fetch:
image = fetch.headers["location"]
async with client.get(image, allow_redirects=False) as fetch:
image = fetch.headers["location"]
resolved_images.append(image)
preview.append(image.replace('=s512', '=s200'))
yield ImageResponse(resolved_images, image_prompt, {"orginal_links": images, "preview": preview})
try:
images = [image[0][3][3] for image in response_part[4][0][12][7][0]]
if response_format == "b64_json":
yield ImageResponse(images, image_prompt, {"cookies": cls._cookies})
else:
resolved_images = []
preview = []
for image in images:
async with client.get(image, allow_redirects=False) as fetch:
image = fetch.headers["location"]
async with client.get(image, allow_redirects=False) as fetch:
image = fetch.headers["location"]
resolved_images.append(image)
preview.append(image.replace('=s512', '=s200'))
yield ImageResponse(resolved_images, image_prompt, {"orginal_links": images, "preview": preview})
except TypeError:
pass
def build_request(
prompt: str,

View File

@ -16,9 +16,9 @@ class GeminiPro(AsyncGeneratorProvider, ProviderModelMixin):
working = True
supports_message_history = True
needs_auth = True
default_model = "gemini-1.5-pro-latest"
default_model = "gemini-1.5-pro"
default_vision_model = default_model
models = [default_model, "gemini-pro", "gemini-pro-vision", "gemini-1.5-flash"]
models = [default_model, "gemini-pro", "gemini-1.5-flash", "gemini-1.5-flash-8b"]
@classmethod
async def create_async_generator(

View File

@ -1,13 +1,11 @@
from __future__ import annotations
import json
from aiohttp import ClientSession, BaseConnector
from ...typing import AsyncResult, Messages
from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
from ..helper import get_connector
from ...errors import RateLimitError, ModelNotFoundError
from ...requests.raise_for_status import raise_for_status
from ...errors import ModelNotFoundError
from ...requests import StreamSession, raise_for_status
from ..HuggingChat import HuggingChat
@ -20,15 +18,6 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
models = HuggingChat.models
model_aliases = HuggingChat.model_aliases
@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
async def create_async_generator(
cls,
@ -36,7 +25,6 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
messages: Messages,
stream: bool = True,
proxy: str = None,
connector: BaseConnector = None,
api_base: str = "https://api-inference.huggingface.co",
api_key: str = None,
max_new_tokens: int = 1024,
@ -62,7 +50,6 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
}
if api_key is not None:
headers["Authorization"] = f"Bearer {api_key}"
params = {
"return_full_text": False,
"max_new_tokens": max_new_tokens,
@ -70,10 +57,9 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
**kwargs
}
payload = {"inputs": format_prompt(messages), "parameters": params, "stream": stream}
async with ClientSession(
async with StreamSession(
headers=headers,
connector=get_connector(connector, proxy)
proxy=proxy
) as session:
async with session.post(f"{api_base.rstrip('/')}/models/{model}", json=payload) as response:
if response.status == 404:
@ -81,7 +67,7 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
await raise_for_status(response)
if stream:
first = True
async for line in response.content:
async for line in response.iter_lines():
if line.startswith(b"data:"):
data = json.loads(line[5:])
if not data["token"]["special"]:
@ -89,7 +75,8 @@ class HuggingFace(AsyncGeneratorProvider, ProviderModelMixin):
if first:
first = False
chunk = chunk.lstrip()
yield chunk
if chunk:
yield chunk
else:
yield (await response.json())[0]["generated_text"].strip()
@ -101,4 +88,4 @@ def format_prompt(messages: Messages) -> str:
for idx, message in enumerate(messages)
if message["role"] == "assistant"
])
return f"{history}<s>[INST] {question} [/INST]"
return f"{history}<s>[INST] {question} [/INST]"

View File

@ -79,7 +79,6 @@ class MetaAI(AsyncGeneratorProvider, ProviderModelMixin):
self.access_token = None
if self.access_token is None and cookies is None:
await self.update_access_token()
if self.access_token is None:
url = "https://www.meta.ai/api/graphql/"
payload = {"lsd": self.lsd, 'fb_dtsg': self.dtsg}
@ -128,6 +127,8 @@ class MetaAI(AsyncGeneratorProvider, ProviderModelMixin):
json_line = json.loads(line)
except json.JSONDecodeError:
continue
if json_line.get("errors"):
raise RuntimeError("\n".join([error.get("message") for error in json_line.get("errors")]))
bot_response_message = json_line.get("data", {}).get("node", {}).get("bot_response_message", {})
streaming_state = bot_response_message.get("streaming_state")
fetch_id = bot_response_message.get("fetch_id") or fetch_id

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from ...typing import AsyncResult, Messages, Cookies
from ..helper import format_prompt, get_cookies
from ..MetaAI import MetaAI
from .MetaAI import MetaAI
class MetaAIAccount(MetaAI):
needs_auth = True

View File

@ -11,6 +11,7 @@ from .GeminiPro import GeminiPro
from .Groq import Groq
from .HuggingFace import HuggingFace
from .MetaAI import MetaAI
from .MetaAIAccount import MetaAIAccount
from .OpenaiAPI import OpenaiAPI
from .OpenaiChat import OpenaiChat
from .PerplexityApi import PerplexityApi

View File

@ -6,14 +6,14 @@ import uuid
import asyncio
import time
from aiohttp import ClientSession
from typing import Iterator, Optional, AsyncIterator, Union
from typing import Iterator, Optional
from flask import send_from_directory
from g4f import version, models
from g4f import get_last_provider, ChatCompletion
from g4f.errors import VersionNotFoundError
from g4f.typing import Cookies
from g4f.image import ImagePreview, ImageResponse, is_accepted_format
from g4f.image import ImagePreview, ImageResponse, is_accepted_format, extract_data_uri
from g4f.requests.aiohttp import get_connector
from g4f.Provider import ProviderType, __providers__, __map__
from g4f.providers.base_provider import ProviderModelMixin, FinishReason
@ -31,7 +31,6 @@ def ensure_images_dir():
conversations: dict[dict[str, BaseConversation]] = {}
class Api:
@staticmethod
def get_models() -> list[str]:
@ -176,18 +175,22 @@ class Api:
connector=get_connector(None, os.environ.get("G4F_PROXY")),
cookies=cookies
) as session:
async def copy_image(image):
async with session.get(image) as response:
target = os.path.join(images_dir, f"{int(time.time())}_{str(uuid.uuid4())}")
async def copy_image(image: str) -> str:
target = os.path.join(images_dir, f"{int(time.time())}_{str(uuid.uuid4())}")
if image.startswith("data:"):
with open(target, "wb") as f:
async for chunk in response.content.iter_any():
f.write(chunk)
with open(target, "rb") as f:
extension = is_accepted_format(f.read(12)).split("/")[-1]
extension = "jpg" if extension == "jpeg" else extension
new_target = f"{target}.{extension}"
os.rename(target, new_target)
return f"/images/{os.path.basename(new_target)}"
f.write(extract_data_uri(image))
else:
async with session.get(image) as response:
with open(target, "wb") as f:
async for chunk in response.content.iter_any():
f.write(chunk)
with open(target, "rb") as f:
extension = is_accepted_format(f.read(12)).split("/")[-1]
extension = "jpg" if extension == "jpeg" else extension
new_target = f"{target}.{extension}"
os.rename(target, new_target)
return f"/images/{os.path.basename(new_target)}"
return await asyncio.gather(*[copy_image(image) for image in images])
@ -197,7 +200,6 @@ class Api:
response_type: content
}
def get_error_message(exception: Exception) -> str:
message = f"{type(exception).__name__}: {exception}"
provider = get_last_provider()

View File

@ -133,7 +133,7 @@ def extract_data_uri(data_uri: str) -> bytes:
Returns:
bytes: The extracted binary data.
"""
data = data_uri.split(",")[1]
data = data_uri.split(",")[-1]
data = base64.b64decode(data)
return data

16
requirements-slim.txt Normal file
View File

@ -0,0 +1,16 @@
requests
pycryptodome
curl_cffi>=0.6.2
aiohttp
certifi
duckduckgo-search>=5.0
nest_asyncio
werkzeug
pillow
fastapi
uvicorn
flask
brotli
beautifulsoup4
aiohttp_socks
cryptography

View File

@ -4,7 +4,6 @@ curl_cffi>=0.6.2
aiohttp
certifi
browser_cookie3
PyExecJS
duckduckgo-search>=5.0
nest_asyncio
werkzeug
@ -19,5 +18,4 @@ aiohttp_socks
pywebview
plyer
cryptography
nodriver
cloudscraper
nodriver

View File

@ -62,6 +62,10 @@ EXTRA_REQUIRE = {
"duckduckgo-search>=5.0",
"browser_cookie3"
],
"search": [
"beautifulsoup4", "pillow",
"duckduckgo-search>=5.0",
],
"local": [
"gpt4all"
]