Gemini.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. from __future__ import annotations
  2. import os
  3. import json
  4. import random
  5. import re
  6. from aiohttp import ClientSession, BaseConnector
  7. try:
  8. import nodriver
  9. has_nodriver = True
  10. except ImportError:
  11. has_nodriver = False
  12. from ... import debug
  13. from ...typing import Messages, Cookies, ImageType, AsyncResult, AsyncIterator
  14. from ..base_provider import AsyncGeneratorProvider, BaseConversation
  15. from ..helper import format_prompt, get_cookies
  16. from ...requests.raise_for_status import raise_for_status
  17. from ...requests.aiohttp import get_connector
  18. from ...errors import MissingAuthError
  19. from ...image import ImageResponse, to_bytes
  20. REQUEST_HEADERS = {
  21. "authority": "gemini.google.com",
  22. "origin": "https://gemini.google.com",
  23. "referer": "https://gemini.google.com/",
  24. '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',
  25. 'x-same-domain': '1',
  26. }
  27. REQUEST_BL_PARAM = "boq_assistant-bard-web-server_20240519.16_p0"
  28. REQUEST_URL = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
  29. UPLOAD_IMAGE_URL = "https://content-push.googleapis.com/upload/"
  30. UPLOAD_IMAGE_HEADERS = {
  31. "authority": "content-push.googleapis.com",
  32. "accept": "*/*",
  33. "accept-language": "en-US,en;q=0.7",
  34. "authorization": "Basic c2F2ZXM6cyNMdGhlNmxzd2F2b0RsN3J1d1U=",
  35. "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
  36. "origin": "https://gemini.google.com",
  37. "push-id": "feeds/mcudyrk2a4khkz",
  38. "referer": "https://gemini.google.com/",
  39. "x-goog-upload-command": "start",
  40. "x-goog-upload-header-content-length": "",
  41. "x-goog-upload-protocol": "resumable",
  42. "x-tenant-id": "bard-storage",
  43. }
  44. class Gemini(AsyncGeneratorProvider):
  45. url = "https://gemini.google.com"
  46. needs_auth = True
  47. working = True
  48. default_model = 'gemini'
  49. image_models = ["gemini"]
  50. default_vision_model = "gemini"
  51. models = ["gemini", "gemini-1.5-flash", "gemini-1.5-pro"]
  52. _cookies: Cookies = None
  53. _snlm0e: str = None
  54. _sid: str = None
  55. @classmethod
  56. async def nodriver_login(cls, proxy: str = None) -> AsyncIterator[str]:
  57. if not has_nodriver:
  58. if debug.logging:
  59. print("Skip nodriver login in Gemini provider")
  60. return
  61. try:
  62. from platformdirs import user_config_dir
  63. user_data_dir = user_config_dir("g4f-nodriver")
  64. except:
  65. user_data_dir = None
  66. if debug.logging:
  67. print(f"Open nodriver with user_dir: {user_data_dir}")
  68. browser = await nodriver.start(
  69. user_data_dir=user_data_dir,
  70. browser_args=None if proxy is None else [f"--proxy-server={proxy}"],
  71. )
  72. login_url = os.environ.get("G4F_LOGIN_URL")
  73. if login_url:
  74. yield f"Please login: [Google Gemini]({login_url})\n\n"
  75. page = await browser.get(f"{cls.url}/app")
  76. await page.select("div.ql-editor.textarea", 240)
  77. cookies = {}
  78. for c in await page.browser.cookies.get_all():
  79. if c.domain.endswith(".google.com"):
  80. cookies[c.name] = c.value
  81. await page.close()
  82. cls._cookies = cookies
  83. @classmethod
  84. async def create_async_generator(
  85. cls,
  86. model: str,
  87. messages: Messages,
  88. proxy: str = None,
  89. cookies: Cookies = None,
  90. connector: BaseConnector = None,
  91. image: ImageType = None,
  92. image_name: str = None,
  93. response_format: str = None,
  94. return_conversation: bool = False,
  95. conversation: Conversation = None,
  96. language: str = "en",
  97. **kwargs
  98. ) -> AsyncResult:
  99. prompt = format_prompt(messages) if conversation is None else messages[-1]["content"]
  100. cls._cookies = cookies or cls._cookies or get_cookies(".google.com", False, True)
  101. base_connector = get_connector(connector, proxy)
  102. async with ClientSession(
  103. headers=REQUEST_HEADERS,
  104. connector=base_connector
  105. ) as session:
  106. if not cls._snlm0e:
  107. await cls.fetch_snlm0e(session, cls._cookies) if cls._cookies else None
  108. if not cls._snlm0e:
  109. async for chunk in cls.nodriver_login(proxy):
  110. yield chunk
  111. if not cls._snlm0e:
  112. if cls._cookies is None or "__Secure-1PSID" not in cls._cookies:
  113. raise MissingAuthError('Missing "__Secure-1PSID" cookie')
  114. await cls.fetch_snlm0e(session, cls._cookies)
  115. if not cls._snlm0e:
  116. raise RuntimeError("Invalid cookies. SNlM0e not found")
  117. image_url = await cls.upload_image(base_connector, to_bytes(image), image_name) if image else None
  118. async with ClientSession(
  119. cookies=cls._cookies,
  120. headers=REQUEST_HEADERS,
  121. connector=base_connector,
  122. ) as client:
  123. params = {
  124. 'bl': REQUEST_BL_PARAM,
  125. 'hl': language,
  126. '_reqid': random.randint(1111, 9999),
  127. 'rt': 'c',
  128. "f.sid": cls._sid,
  129. }
  130. data = {
  131. 'at': cls._snlm0e,
  132. 'f.req': json.dumps([None, json.dumps(cls.build_request(
  133. prompt,
  134. language=language,
  135. conversation=conversation,
  136. image_url=image_url,
  137. image_name=image_name
  138. ))])
  139. }
  140. async with client.post(
  141. REQUEST_URL,
  142. data=data,
  143. params=params,
  144. ) as response:
  145. await raise_for_status(response)
  146. image_prompt = response_part = None
  147. last_content_len = 0
  148. async for line in response.content:
  149. try:
  150. try:
  151. line = json.loads(line)
  152. except ValueError:
  153. continue
  154. if not isinstance(line, list):
  155. continue
  156. if len(line[0]) < 3 or not line[0][2]:
  157. continue
  158. response_part = json.loads(line[0][2])
  159. if not response_part[4]:
  160. continue
  161. if return_conversation:
  162. yield Conversation(response_part[1][0], response_part[1][1], response_part[4][0][0])
  163. content = response_part[4][0][1][0]
  164. except (ValueError, KeyError, TypeError, IndexError) as e:
  165. print(f"{cls.__name__}:{e.__class__.__name__}:{e}")
  166. continue
  167. match = re.search(r'\[Imagen of (.*?)\]', content)
  168. if match:
  169. image_prompt = match.group(1)
  170. content = content.replace(match.group(0), '')
  171. yield content[last_content_len:]
  172. last_content_len = len(content)
  173. if image_prompt:
  174. try:
  175. images = [image[0][3][3] for image in response_part[4][0][12][7][0]]
  176. if response_format == "b64_json":
  177. yield ImageResponse(images, image_prompt, {"cookies": cls._cookies})
  178. else:
  179. resolved_images = []
  180. preview = []
  181. for image in images:
  182. async with client.get(image, allow_redirects=False) as fetch:
  183. image = fetch.headers["location"]
  184. async with client.get(image, allow_redirects=False) as fetch:
  185. image = fetch.headers["location"]
  186. resolved_images.append(image)
  187. preview.append(image.replace('=s512', '=s200'))
  188. yield ImageResponse(resolved_images, image_prompt, {"orginal_links": images, "preview": preview})
  189. except TypeError:
  190. pass
  191. def build_request(
  192. prompt: str,
  193. language: str,
  194. conversation: Conversation = None,
  195. image_url: str = None,
  196. image_name: str = None,
  197. tools: list[list[str]] = []
  198. ) -> list:
  199. image_list = [[[image_url, 1], image_name]] if image_url else []
  200. return [
  201. [prompt, 0, None, image_list, None, None, 0],
  202. [language],
  203. [
  204. None if conversation is None else conversation.conversation_id,
  205. None if conversation is None else conversation.response_id,
  206. None if conversation is None else conversation.choice_id,
  207. None,
  208. None,
  209. []
  210. ],
  211. None,
  212. None,
  213. None,
  214. [1],
  215. 0,
  216. [],
  217. tools,
  218. 1,
  219. 0,
  220. ]
  221. async def upload_image(connector: BaseConnector, image: bytes, image_name: str = None):
  222. async with ClientSession(
  223. headers=UPLOAD_IMAGE_HEADERS,
  224. connector=connector
  225. ) as session:
  226. async with session.options(UPLOAD_IMAGE_URL) as response:
  227. await raise_for_status(response)
  228. headers = {
  229. "size": str(len(image)),
  230. "x-goog-upload-command": "start"
  231. }
  232. data = f"File name: {image_name}" if image_name else None
  233. async with session.post(
  234. UPLOAD_IMAGE_URL, headers=headers, data=data
  235. ) as response:
  236. await raise_for_status(response)
  237. upload_url = response.headers["X-Goog-Upload-Url"]
  238. async with session.options(upload_url, headers=headers) as response:
  239. await raise_for_status(response)
  240. headers["x-goog-upload-command"] = "upload, finalize"
  241. headers["X-Goog-Upload-Offset"] = "0"
  242. async with session.post(
  243. upload_url, headers=headers, data=image
  244. ) as response:
  245. await raise_for_status(response)
  246. return await response.text()
  247. @classmethod
  248. async def fetch_snlm0e(cls, session: ClientSession, cookies: Cookies):
  249. async with session.get(cls.url, cookies=cookies) as response:
  250. await raise_for_status(response)
  251. response_text = await response.text()
  252. match = re.search(r'SNlM0e\":\"(.*?)\"', response_text)
  253. if match:
  254. cls._snlm0e = match.group(1)
  255. sid_match = re.search(r'"FdrFJe":"([\d-]+)"', response_text)
  256. if sid_match:
  257. cls._sid = sid_match.group(1)
  258. class Conversation(BaseConversation):
  259. def __init__(self,
  260. conversation_id: str = "",
  261. response_id: str = "",
  262. choice_id: str = ""
  263. ) -> None:
  264. self.conversation_id = conversation_id
  265. self.response_id = response_id
  266. self.choice_id = choice_id