gpt4free/g4f/Provider/needs_auth/Gemini.py
Heiner Lohaus 6a624acf55 Use custom user data dir for each provider
Reuse cookies and access token in Copilot
Send in the gui messages to multiple providers at once
Add GUI documenation
2024-12-07 19:38:04 +01:00

332 lines
No EOL
13 KiB
Python

from __future__ import annotations
import os
import json
import random
import re
import base64
from aiohttp import ClientSession, BaseConnector
try:
import nodriver
has_nodriver = True
except ImportError:
has_nodriver = False
from ... import debug
from ...typing import Messages, Cookies, ImageType, AsyncResult, AsyncIterator
from ..base_provider import AsyncGeneratorProvider, BaseConversation, SynthesizeData
from ..helper import format_prompt, get_cookies
from ...requests.raise_for_status import raise_for_status
from ...requests.aiohttp import get_connector
from ...requests import get_nodriver
from ...errors import MissingAuthError
from ...image import ImageResponse, to_bytes
from ... import debug
REQUEST_HEADERS = {
"authority": "gemini.google.com",
"origin": "https://gemini.google.com",
"referer": "https://gemini.google.com/",
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36',
'x-same-domain': '1',
}
REQUEST_BL_PARAM = "boq_assistant-bard-web-server_20240519.16_p0"
REQUEST_URL = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
UPLOAD_IMAGE_URL = "https://content-push.googleapis.com/upload/"
UPLOAD_IMAGE_HEADERS = {
"authority": "content-push.googleapis.com",
"accept": "*/*",
"accept-language": "en-US,en;q=0.7",
"authorization": "Basic c2F2ZXM6cyNMdGhlNmxzd2F2b0RsN3J1d1U=",
"content-type": "application/x-www-form-urlencoded;charset=UTF-8",
"origin": "https://gemini.google.com",
"push-id": "feeds/mcudyrk2a4khkz",
"referer": "https://gemini.google.com/",
"x-goog-upload-command": "start",
"x-goog-upload-header-content-length": "",
"x-goog-upload-protocol": "resumable",
"x-tenant-id": "bard-storage",
}
class Gemini(AsyncGeneratorProvider):
url = "https://gemini.google.com"
needs_auth = True
working = True
default_model = 'gemini'
image_models = ["gemini"]
default_vision_model = "gemini"
models = ["gemini", "gemini-1.5-flash", "gemini-1.5-pro"]
synthesize_content_type = "audio/vnd.wav"
_cookies: Cookies = None
_snlm0e: str = None
_sid: str = None
@classmethod
async def nodriver_login(cls, proxy: str = None) -> AsyncIterator[str]:
if not has_nodriver:
if debug.logging:
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 f"Please login: [Google Gemini]({login_url})\n\n"
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
@classmethod
async def create_async_generator(
cls,
model: str,
messages: Messages,
proxy: str = None,
cookies: Cookies = None,
connector: BaseConnector = None,
image: ImageType = None,
image_name: str = None,
return_conversation: bool = False,
conversation: Conversation = None,
language: str = "en",
**kwargs
) -> AsyncResult:
prompt = format_prompt(messages) if conversation is None else messages[-1]["content"]
cls._cookies = cookies or cls._cookies or get_cookies(".google.com", False, True)
base_connector = get_connector(connector, proxy)
async with ClientSession(
headers=REQUEST_HEADERS,
connector=base_connector
) as session:
if not cls._snlm0e:
await cls.fetch_snlm0e(session, cls._cookies) if cls._cookies else None
if not cls._snlm0e:
try:
async for chunk in cls.nodriver_login(proxy):
yield chunk
except Exception as e:
raise MissingAuthError('Missing or invalid "__Secure-1PSID" cookie', e)
if not cls._snlm0e:
if cls._cookies is None or "__Secure-1PSID" not in cls._cookies:
raise MissingAuthError('Missing "__Secure-1PSID" cookie')
await cls.fetch_snlm0e(session, cls._cookies)
if not cls._snlm0e:
raise RuntimeError("Invalid cookies. SNlM0e not found")
yield SynthesizeData(cls.__name__, {"text": messages[-1]["content"]})
image_url = await cls.upload_image(base_connector, to_bytes(image), image_name) if image else None
async with ClientSession(
cookies=cls._cookies,
headers=REQUEST_HEADERS,
connector=base_connector,
) as client:
params = {
'bl': REQUEST_BL_PARAM,
'hl': language,
'_reqid': random.randint(1111, 9999),
'rt': 'c',
"f.sid": cls._sid,
}
data = {
'at': cls._snlm0e,
'f.req': json.dumps([None, json.dumps(cls.build_request(
prompt,
language=language,
conversation=conversation,
image_url=image_url,
image_name=image_name
))])
}
async with client.post(
REQUEST_URL,
data=data,
params=params,
) as response:
await raise_for_status(response)
image_prompt = response_part = None
last_content = ""
async for line in response.content:
try:
try:
line = json.loads(line)
except ValueError:
continue
if not isinstance(line, list):
continue
if len(line[0]) < 3 or not line[0][2]:
continue
response_part = json.loads(line[0][2])
if not response_part[4]:
continue
if return_conversation:
yield Conversation(response_part[1][0], response_part[1][1], response_part[4][0][0])
content = response_part[4][0][1][0]
except (ValueError, KeyError, TypeError, IndexError) as e:
debug.log(f"{cls.__name__}:{e.__class__.__name__}:{e}")
continue
match = re.search(r'\[Imagen of (.*?)\]', content)
if match:
image_prompt = match.group(1)
content = content.replace(match.group(0), '')
pattern = r"http://googleusercontent.com/image_generation_content/\d+"
content = re.sub(pattern, "", content)
if last_content and content.startswith(last_content):
yield content[len(last_content):]
else:
yield content
last_content = content
if image_prompt:
try:
images = [image[0][3][3] for image in response_part[4][0][12][7][0]]
image_prompt = image_prompt.replace("a fake image", "")
yield ImageResponse(images, image_prompt, {"cookies": cls._cookies})
except TypeError:
pass
@classmethod
async def synthesize(cls, params: dict, proxy: str = None) -> AsyncIterator[bytes]:
if "text" not in params:
raise ValueError("Missing parameter text")
async with ClientSession(
cookies=cls._cookies,
headers=REQUEST_HEADERS,
connector=get_connector(proxy=proxy),
) as session:
if not cls._snlm0e:
await cls.fetch_snlm0e(session, cls._cookies) if cls._cookies else None
inner_data = json.dumps([None, params["text"], "de-DE", None, 2])
async with session.post(
"https://gemini.google.com/_/BardChatUi/data/batchexecute",
data={
"f.req": json.dumps([[["XqA3Ic", inner_data, None, "generic"]]]),
"at": cls._snlm0e,
},
params={
"rpcids": "XqA3Ic",
"source-path": "/app/2704fb4aafcca926",
"bl": "boq_assistant-bard-web-server_20241119.00_p1",
"f.sid": "" if cls._sid is None else cls._sid,
"hl": "de",
"_reqid": random.randint(1111, 9999),
"rt": "c"
},
) as response:
await raise_for_status(response)
iter_base64_response = iter_filter_base64(response.content.iter_chunked(1024))
async for chunk in iter_base64_decode(iter_base64_response):
yield chunk
def build_request(
prompt: str,
language: str,
conversation: Conversation = None,
image_url: str = None,
image_name: str = None,
tools: list[list[str]] = []
) -> list:
image_list = [[[image_url, 1], image_name]] if image_url else []
return [
[prompt, 0, None, image_list, None, None, 0],
[language],
[
None if conversation is None else conversation.conversation_id,
None if conversation is None else conversation.response_id,
None if conversation is None else conversation.choice_id,
None,
None,
[]
],
None,
None,
None,
[1],
0,
[],
tools,
1,
0,
]
async def upload_image(connector: BaseConnector, image: bytes, image_name: str = None):
async with ClientSession(
headers=UPLOAD_IMAGE_HEADERS,
connector=connector
) as session:
async with session.options(UPLOAD_IMAGE_URL) as response:
await raise_for_status(response)
headers = {
"size": str(len(image)),
"x-goog-upload-command": "start"
}
data = f"File name: {image_name}" if image_name else None
async with session.post(
UPLOAD_IMAGE_URL, headers=headers, data=data
) as response:
await raise_for_status(response)
upload_url = response.headers["X-Goog-Upload-Url"]
async with session.options(upload_url, headers=headers) as response:
await raise_for_status(response)
headers["x-goog-upload-command"] = "upload, finalize"
headers["X-Goog-Upload-Offset"] = "0"
async with session.post(
upload_url, headers=headers, data=image
) as response:
await raise_for_status(response)
return await response.text()
@classmethod
async def fetch_snlm0e(cls, session: ClientSession, cookies: Cookies):
async with session.get(cls.url, cookies=cookies) as response:
await raise_for_status(response)
response_text = await response.text()
match = re.search(r'SNlM0e\":\"(.*?)\"', response_text)
if match:
cls._snlm0e = match.group(1)
sid_match = re.search(r'"FdrFJe":"([\d-]+)"', response_text)
if sid_match:
cls._sid = sid_match.group(1)
class Conversation(BaseConversation):
def __init__(self,
conversation_id: str = "",
response_id: str = "",
choice_id: str = ""
) -> None:
self.conversation_id = conversation_id
self.response_id = response_id
self.choice_id = choice_id
async def iter_filter_base64(response_iter: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
search_for = b'[["wrb.fr","XqA3Ic","[\\"'
end_with = b'\\'
is_started = False
async for chunk in response_iter:
if is_started:
if end_with in chunk:
yield chunk.split(end_with, 1).pop(0)
break
else:
yield chunk
elif search_for in chunk:
is_started = True
yield chunk.split(search_for, 1).pop()
else:
raise RuntimeError(f"Response: {chunk}")
async def iter_base64_decode(response_iter: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
buffer = b""
async for chunk in response_iter:
chunk = buffer + chunk
rest = len(chunk) % 4
buffer = chunk[-rest:]
yield base64.b64decode(chunk[:-rest])