OpenaiChat.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. from __future__ import annotations
  2. import os
  3. import re
  4. import asyncio
  5. import uuid
  6. import json
  7. import base64
  8. import time
  9. import random
  10. from typing import AsyncIterator, Iterator, Optional, Generator, Dict, List
  11. from copy import copy
  12. try:
  13. import nodriver
  14. has_nodriver = True
  15. except ImportError:
  16. has_nodriver = False
  17. from ..base_provider import AsyncAuthedProvider, ProviderModelMixin
  18. from ...typing import AsyncResult, Messages, Cookies, ImagesType
  19. from ...requests.raise_for_status import raise_for_status
  20. from ...requests import StreamSession
  21. from ...requests import get_nodriver
  22. from ...image import ImageRequest, to_image, to_bytes, is_accepted_format
  23. from ...errors import MissingAuthError, NoValidHarFileError
  24. from ...providers.response import JsonConversation, FinishReason, SynthesizeData, AuthResult, ImageResponse
  25. from ...providers.response import Sources, TitleGeneration, RequestLogin, Parameters, Reasoning
  26. from ..helper import format_cookies
  27. from ..openai.models import default_model, default_image_model, models, image_models, text_models
  28. from ..openai.har_file import get_request_config
  29. from ..openai.har_file import RequestConfig, arkReq, arkose_url, start_url, conversation_url, backend_url, backend_anon_url
  30. from ..openai.proofofwork import generate_proof_token
  31. from ..openai.new import get_requirements_token, get_config
  32. from ... import debug
  33. DEFAULT_HEADERS = {
  34. "accept": "*/*",
  35. "accept-encoding": "gzip, deflate, br, zstd",
  36. 'accept-language': 'en-US,en;q=0.8',
  37. "referer": "https://chatgpt.com/",
  38. "sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
  39. "sec-ch-ua-mobile": "?0",
  40. "sec-ch-ua-platform": "\"Windows\"",
  41. "sec-fetch-dest": "empty",
  42. "sec-fetch-mode": "cors",
  43. "sec-fetch-site": "same-origin",
  44. "sec-gpc": "1",
  45. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
  46. }
  47. INIT_HEADERS = {
  48. 'accept': '*/*',
  49. 'accept-language': 'en-US,en;q=0.8',
  50. 'cache-control': 'no-cache',
  51. 'pragma': 'no-cache',
  52. 'priority': 'u=0, i',
  53. "sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
  54. 'sec-ch-ua-arch': '"arm"',
  55. 'sec-ch-ua-bitness': '"64"',
  56. 'sec-ch-ua-mobile': '?0',
  57. 'sec-ch-ua-model': '""',
  58. "sec-ch-ua-platform": "\"Windows\"",
  59. 'sec-ch-ua-platform-version': '"14.4.0"',
  60. 'sec-fetch-dest': 'document',
  61. 'sec-fetch-mode': 'navigate',
  62. 'sec-fetch-site': 'none',
  63. 'sec-fetch-user': '?1',
  64. 'upgrade-insecure-requests': '1',
  65. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
  66. }
  67. UPLOAD_HEADERS = {
  68. "accept": "application/json, text/plain, */*",
  69. 'accept-language': 'en-US,en;q=0.8',
  70. "referer": "https://chatgpt.com/",
  71. "priority": "u=1, i",
  72. "sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
  73. "sec-ch-ua-mobile": "?0",
  74. 'sec-ch-ua-platform': '"macOS"',
  75. "sec-fetch-dest": "empty",
  76. "sec-fetch-mode": "cors",
  77. "sec-fetch-site": "cross-site",
  78. "x-ms-blob-type": "BlockBlob",
  79. "x-ms-version": "2020-04-08",
  80. "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
  81. }
  82. class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
  83. """A class for creating and managing conversations with OpenAI chat service"""
  84. label = "OpenAI ChatGPT"
  85. url = "https://chatgpt.com"
  86. working = True
  87. use_nodriver = True
  88. supports_gpt_4 = True
  89. supports_message_history = True
  90. supports_system_message = True
  91. default_model = default_model
  92. default_image_model = default_image_model
  93. image_models = image_models
  94. vision_models = text_models
  95. models = models
  96. synthesize_content_type = "audio/mpeg"
  97. request_config = RequestConfig()
  98. _api_key: str = None
  99. _headers: dict = None
  100. _cookies: Cookies = None
  101. _expires: int = None
  102. @classmethod
  103. async def on_auth_async(cls, **kwargs) -> AsyncIterator:
  104. async for chunk in cls.login():
  105. yield chunk
  106. yield AuthResult(
  107. api_key=cls._api_key,
  108. cookies=cls._cookies or cls.request_config.cookies or {},
  109. headers=cls._headers or cls.request_config.headers or cls.get_default_headers(),
  110. expires=cls._expires,
  111. proof_token=cls.request_config.proof_token,
  112. turnstile_token=cls.request_config.turnstile_token
  113. )
  114. @classmethod
  115. async def upload_images(
  116. cls,
  117. session: StreamSession,
  118. auth_result: AuthResult,
  119. images: ImagesType,
  120. ) -> ImageRequest:
  121. """
  122. Upload an image to the service and get the download URL
  123. Args:
  124. session: The StreamSession object to use for requests
  125. headers: The headers to include in the requests
  126. images: The images to upload, either a PIL Image object or a bytes object
  127. Returns:
  128. An ImageRequest object that contains the download URL, file name, and other data
  129. """
  130. async def upload_image(image, image_name):
  131. # Convert the image to a PIL Image object and get the extension
  132. data_bytes = to_bytes(image)
  133. image = to_image(data_bytes)
  134. extension = image.format.lower()
  135. data = {
  136. "file_name": "" if image_name is None else image_name,
  137. "file_size": len(data_bytes),
  138. "use_case": "multimodal"
  139. }
  140. # Post the image data to the service and get the image data
  141. headers = auth_result.headers if hasattr(auth_result, "headers") else None
  142. async with session.post(f"{cls.url}/backend-api/files", json=data, headers=headers) as response:
  143. cls._update_request_args(auth_result, session)
  144. await raise_for_status(response, "Create file failed")
  145. image_data = {
  146. **data,
  147. **await response.json(),
  148. "mime_type": is_accepted_format(data_bytes),
  149. "extension": extension,
  150. "height": image.height,
  151. "width": image.width
  152. }
  153. # Put the image bytes to the upload URL and check the status
  154. await asyncio.sleep(1)
  155. async with session.put(
  156. image_data["upload_url"],
  157. data=data_bytes,
  158. headers={
  159. **UPLOAD_HEADERS,
  160. "Content-Type": image_data["mime_type"],
  161. "x-ms-blob-type": "BlockBlob",
  162. "x-ms-version": "2020-04-08",
  163. "Origin": "https://chatgpt.com",
  164. }
  165. ) as response:
  166. await raise_for_status(response)
  167. # Post the file ID to the service and get the download URL
  168. async with session.post(
  169. f"{cls.url}/backend-api/files/{image_data['file_id']}/uploaded",
  170. json={},
  171. headers=auth_result.headers
  172. ) as response:
  173. cls._update_request_args(auth_result, session)
  174. await raise_for_status(response, "Get download url failed")
  175. image_data["download_url"] = (await response.json())["download_url"]
  176. return ImageRequest(image_data)
  177. if not images:
  178. return
  179. return [await upload_image(image, image_name) for image, image_name in images]
  180. @classmethod
  181. def create_messages(cls, messages: Messages, image_requests: ImageRequest = None, system_hints: list = None):
  182. """
  183. Create a list of messages for the user input
  184. Args:
  185. prompt: The user input as a string
  186. image_response: The image response object, if any
  187. Returns:
  188. A list of messages with the user input and the image, if any
  189. """
  190. # Create a message object with the user role and the content
  191. messages = [{
  192. "id": str(uuid.uuid4()),
  193. "author": {"role": message["role"]},
  194. "content": {"content_type": "text", "parts": [message["content"]]},
  195. "metadata": {"serialization_metadata": {"custom_symbol_offsets": []}, **({"system_hints": system_hints} if system_hints else {})},
  196. "create_time": time.time(),
  197. } for message in messages]
  198. # Check if there is an image response
  199. if image_requests:
  200. # Change content in last user message
  201. messages[-1]["content"] = {
  202. "content_type": "multimodal_text",
  203. "parts": [*[{
  204. "asset_pointer": f"file-service://{image_request.get('file_id')}",
  205. "height": image_request.get("height"),
  206. "size_bytes": image_request.get("file_size"),
  207. "width": image_request.get("width"),
  208. }
  209. for image_request in image_requests],
  210. messages[-1]["content"]["parts"][0]]
  211. }
  212. # Add the metadata object with the attachments
  213. messages[-1]["metadata"] = {
  214. "attachments": [{
  215. "height": image_request.get("height"),
  216. "id": image_request.get("file_id"),
  217. "mimeType": image_request.get("mime_type"),
  218. "name": image_request.get("file_name"),
  219. "size": image_request.get("file_size"),
  220. "width": image_request.get("width"),
  221. }
  222. for image_request in image_requests]
  223. }
  224. return messages
  225. @classmethod
  226. async def get_generated_image(cls, session: StreamSession, auth_result: AuthResult, element: dict, prompt: str = None) -> ImageResponse:
  227. try:
  228. prompt = element["metadata"]["dalle"]["prompt"]
  229. file_id = element["asset_pointer"].split("file-service://", 1)[1]
  230. except TypeError:
  231. return
  232. except Exception as e:
  233. raise RuntimeError(f"No Image: {e.__class__.__name__}: {e}")
  234. try:
  235. async with session.get(f"{cls.url}/backend-api/files/{file_id}/download", headers=auth_result.headers) as response:
  236. cls._update_request_args(auth_result, session)
  237. await raise_for_status(response)
  238. download_url = (await response.json())["download_url"]
  239. return ImageResponse(download_url, prompt)
  240. except Exception as e:
  241. raise RuntimeError(f"Error in downloading image: {e}")
  242. @classmethod
  243. async def create_authed(
  244. cls,
  245. model: str,
  246. messages: Messages,
  247. auth_result: AuthResult,
  248. proxy: str = None,
  249. timeout: int = 180,
  250. auto_continue: bool = False,
  251. history_disabled: bool = False,
  252. action: str = "next",
  253. conversation_id: str = None,
  254. conversation: Conversation = None,
  255. images: ImagesType = None,
  256. return_conversation: bool = False,
  257. max_retries: int = 0,
  258. web_search: bool = False,
  259. **kwargs
  260. ) -> AsyncResult:
  261. """
  262. Create an asynchronous generator for the conversation.
  263. Args:
  264. model (str): The model name.
  265. messages (Messages): The list of previous messages.
  266. proxy (str): Proxy to use for requests.
  267. timeout (int): Timeout for requests.
  268. api_key (str): Access token for authentication.
  269. auto_continue (bool): Flag to automatically continue the conversation.
  270. history_disabled (bool): Flag to disable history and training.
  271. action (str): Type of action ('next', 'continue', 'variant').
  272. conversation_id (str): ID of the conversation.
  273. images (ImagesType): Images to include in the conversation.
  274. return_conversation (bool): Flag to include response fields in the output.
  275. **kwargs: Additional keyword arguments.
  276. Yields:
  277. AsyncResult: Asynchronous results from the generator.
  278. Raises:
  279. RuntimeError: If an error occurs during processing.
  280. """
  281. async with StreamSession(
  282. proxy=proxy,
  283. impersonate="chrome",
  284. timeout=timeout
  285. ) as session:
  286. image_requests = None
  287. if not cls.needs_auth:
  288. if cls._headers is None:
  289. cls._create_request_args(cls._cookies)
  290. async with session.get(cls.url, headers=INIT_HEADERS) as response:
  291. cls._update_request_args(auth_result, session)
  292. await raise_for_status(response)
  293. else:
  294. if cls._headers is None and getattr(auth_result, "cookies", None):
  295. cls._create_request_args(auth_result.cookies, auth_result.headers)
  296. if not cls._set_api_key(getattr(auth_result, "api_key", None)):
  297. raise MissingAuthError("Access token is not valid")
  298. async with session.get(cls.url, headers=cls._headers) as response:
  299. cls._update_request_args(auth_result, session)
  300. await raise_for_status(response)
  301. try:
  302. image_requests = await cls.upload_images(session, auth_result, images) if images else None
  303. except Exception as e:
  304. debug.log("OpenaiChat: Upload image failed")
  305. debug.log(f"{e.__class__.__name__}: {e}")
  306. model = cls.get_model(model)
  307. if conversation is None:
  308. conversation = Conversation(conversation_id, str(uuid.uuid4()), getattr(auth_result, "cookies", {}).get("oai-did"))
  309. else:
  310. conversation = copy(conversation)
  311. if getattr(auth_result, "cookies", {}).get("oai-did") != getattr(conversation, "user_id", None):
  312. conversation = Conversation(None, str(uuid.uuid4()))
  313. if cls._api_key is None:
  314. auto_continue = False
  315. conversation.finish_reason = None
  316. sources = Sources([])
  317. while conversation.finish_reason is None:
  318. async with session.post(
  319. f"{cls.url}/backend-anon/sentinel/chat-requirements"
  320. if cls._api_key is None else
  321. f"{cls.url}/backend-api/sentinel/chat-requirements",
  322. json={"p": None if not getattr(auth_result, "proof_token", None) else get_requirements_token(getattr(auth_result, "proof_token", None))},
  323. headers=cls._headers
  324. ) as response:
  325. if response.status in (401, 403):
  326. auth_result.reset()
  327. else:
  328. cls._update_request_args(auth_result, session)
  329. await raise_for_status(response)
  330. chat_requirements = await response.json()
  331. need_turnstile = chat_requirements.get("turnstile", {}).get("required", False)
  332. need_arkose = chat_requirements.get("arkose", {}).get("required", False)
  333. chat_token = chat_requirements.get("token")
  334. # if need_arkose and cls.request_config.arkose_token is None:
  335. # await get_request_config(proxy)
  336. # cls._create_request_args(auth_result.cookies, auth_result.headers)
  337. # cls._set_api_key(auth_result.access_token)
  338. # if auth_result.arkose_token is None:
  339. # raise MissingAuthError("No arkose token found in .har file")
  340. if "proofofwork" in chat_requirements:
  341. if getattr(auth_result, "proof_token", None) is None:
  342. auth_result.proof_token = get_config(auth_result.headers.get("user-agent"))
  343. proofofwork = generate_proof_token(
  344. **chat_requirements["proofofwork"],
  345. user_agent=getattr(auth_result, "headers", {}).get("user-agent"),
  346. proof_token=getattr(auth_result, "proof_token", None)
  347. )
  348. [debug.log(text) for text in (
  349. #f"Arkose: {'False' if not need_arkose else auth_result.arkose_token[:12]+'...'}",
  350. #f"Proofofwork: {'False' if proofofwork is None else proofofwork[:12]+'...'}",
  351. #f"AccessToken: {'False' if cls._api_key is None else cls._api_key[:12]+'...'}",
  352. )]
  353. if action == "continue" and conversation.message_id is None:
  354. action = "next"
  355. data = {
  356. "action": action,
  357. "parent_message_id": conversation.message_id,
  358. "model": model,
  359. "timezone_offset_min":-60,
  360. "timezone":"Europe/Berlin",
  361. "suggestions":[],
  362. "history_and_training_disabled": history_disabled and not auto_continue and not return_conversation or not cls.needs_auth,
  363. "conversation_mode":{"kind":"primary_assistant","plugin_ids":None},
  364. "force_paragen":False,
  365. "force_paragen_model_slug":"",
  366. "force_rate_limit":False,
  367. "reset_rate_limits":False,
  368. "websocket_request_id": str(uuid.uuid4()),
  369. "system_hints": ["search"] if web_search else None,
  370. "supported_encodings":["v1"],
  371. "conversation_origin":None,
  372. "client_contextual_info":{"is_dark_mode":False,"time_since_loaded":random.randint(20, 500),"page_height":578,"page_width":1850,"pixel_ratio":1,"screen_height":1080,"screen_width":1920},
  373. "paragen_stream_type_override":None,
  374. "paragen_cot_summary_display_override":"allow",
  375. "supports_buffering":True
  376. }
  377. if conversation.conversation_id is not None:
  378. data["conversation_id"] = conversation.conversation_id
  379. debug.log(f"OpenaiChat: Use conversation: {conversation.conversation_id}")
  380. if action != "continue":
  381. data["parent_message_id"] = getattr(conversation, "parent_message_id", conversation.message_id)
  382. conversation.parent_message_id = None
  383. messages = messages if conversation_id is None else [messages[-1]]
  384. data["messages"] = cls.create_messages(messages, image_requests, ["search"] if web_search else None)
  385. headers = {
  386. **cls._headers,
  387. "accept": "text/event-stream",
  388. "content-type": "application/json",
  389. "openai-sentinel-chat-requirements-token": chat_token,
  390. }
  391. #if cls.request_config.arkose_token:
  392. # headers["openai-sentinel-arkose-token"] = cls.request_config.arkose_token
  393. if proofofwork is not None:
  394. headers["openai-sentinel-proof-token"] = proofofwork
  395. if need_turnstile and getattr(auth_result, "turnstile_token", None) is not None:
  396. headers['openai-sentinel-turnstile-token'] = auth_result.turnstile_token
  397. async with session.post(
  398. f"{cls.url}/backend-anon/conversation"
  399. if cls._api_key is None else
  400. f"{cls.url}/backend-api/conversation",
  401. json=data,
  402. headers=headers
  403. ) as response:
  404. cls._update_request_args(auth_result, session)
  405. if response.status == 403:
  406. auth_result.proof_token = None
  407. cls.request_config.proof_token = None
  408. await raise_for_status(response)
  409. buffer = u""
  410. async for line in response.iter_lines():
  411. async for chunk in cls.iter_messages_line(session, auth_result, line, conversation, sources):
  412. if isinstance(chunk, str):
  413. chunk = chunk.replace("\ue203", "").replace("\ue204", "").replace("\ue206", "")
  414. buffer += chunk
  415. if buffer.find(u"\ue200") != -1:
  416. if buffer.find(u"\ue201") != -1:
  417. buffer = buffer.replace("\ue200", "").replace("\ue202", "\n").replace("\ue201", "")
  418. buffer = buffer.replace("navlist\n", "#### ")
  419. def replacer(match):
  420. link = None
  421. if len(sources.list) > int(match.group(1)):
  422. link = sources.list[int(match.group(1))]["url"]
  423. return f"[[{int(match.group(1))+1}]]({link})"
  424. return f" [{int(match.group(1))+1}]"
  425. buffer = re.sub(r'(?:cite\nturn0search|cite\nturn0news|turn0news)(\d+)', replacer, buffer)
  426. else:
  427. continue
  428. yield buffer
  429. buffer = ""
  430. else:
  431. yield chunk
  432. if conversation.finish_reason is not None:
  433. break
  434. if sources.list:
  435. yield sources
  436. if return_conversation:
  437. yield conversation
  438. if not history_disabled and auth_result.api_key is not None:
  439. yield SynthesizeData(cls.__name__, {
  440. "conversation_id": conversation.conversation_id,
  441. "message_id": conversation.message_id,
  442. "voice": "maple",
  443. })
  444. if auto_continue and conversation.finish_reason == "max_tokens":
  445. conversation.finish_reason = None
  446. action = "continue"
  447. await asyncio.sleep(5)
  448. else:
  449. break
  450. yield Parameters(**{
  451. "action": "continue" if conversation.finish_reason == "max_tokens" else "variant",
  452. "conversation": conversation.get_dict(),
  453. "proof_token": cls.request_config.proof_token,
  454. "cookies": cls._cookies,
  455. "headers": cls._headers,
  456. "web_search": web_search,
  457. })
  458. yield FinishReason(conversation.finish_reason)
  459. @classmethod
  460. async def iter_messages_line(cls, session: StreamSession, auth_result: AuthResult, line: bytes, fields: Conversation, sources: Sources) -> AsyncIterator:
  461. if not line.startswith(b"data: "):
  462. return
  463. elif line.startswith(b"data: [DONE]"):
  464. if fields.finish_reason is None:
  465. fields.finish_reason = "error"
  466. return
  467. try:
  468. line = json.loads(line[6:])
  469. except:
  470. return
  471. if not isinstance(line, dict):
  472. return
  473. if "type" in line:
  474. if line["type"] == "title_generation":
  475. yield TitleGeneration(line["title"])
  476. if "v" in line:
  477. v = line.get("v")
  478. if isinstance(v, str) and fields.is_recipient:
  479. if "p" not in line or line.get("p") == "/message/content/parts/0":
  480. yield Reasoning(token=v) if fields.is_thinking else v
  481. elif isinstance(v, list):
  482. for m in v:
  483. if m.get("p") == "/message/content/parts/0" and fields.is_recipient:
  484. yield m.get("v")
  485. elif m.get("p") == "/message/metadata/search_result_groups":
  486. for entry in [p.get("entries") for p in m.get("v")]:
  487. for link in entry:
  488. sources.add_source(link)
  489. elif re.match(r"^/message/metadata/content_references/\d+$", m.get("p")):
  490. sources.add_source(m.get("v"))
  491. elif m.get("p") == "/message/metadata/finished_text":
  492. fields.is_thinking = False
  493. yield Reasoning(status=m.get("v"))
  494. elif m.get("p") == "/message/metadata":
  495. fields.finish_reason = m.get("v", {}).get("finish_details", {}).get("type")
  496. break
  497. elif isinstance(v, dict):
  498. if fields.conversation_id is None:
  499. fields.conversation_id = v.get("conversation_id")
  500. debug.log(f"OpenaiChat: New conversation: {fields.conversation_id}")
  501. m = v.get("message", {})
  502. fields.is_recipient = m.get("recipient", "all") == "all"
  503. if fields.is_recipient:
  504. c = m.get("content", {})
  505. if c.get("content_type") == "text" and m.get("author", {}).get("role") == "tool" and "initial_text" in m.get("metadata", {}):
  506. fields.is_thinking = True
  507. yield Reasoning(status=m.get("metadata", {}).get("initial_text"))
  508. if c.get("content_type") == "multimodal_text":
  509. generated_images = []
  510. for element in c.get("parts"):
  511. if isinstance(element, dict) and element.get("content_type") == "image_asset_pointer":
  512. image = cls.get_generated_image(session, auth_result, element)
  513. generated_images.append(image)
  514. for image_response in await asyncio.gather(*generated_images):
  515. if image_response is not None:
  516. yield image_response
  517. if m.get("author", {}).get("role") == "assistant":
  518. if fields.parent_message_id is None:
  519. fields.parent_message_id = v.get("message", {}).get("id")
  520. fields.message_id = v.get("message", {}).get("id")
  521. return
  522. if "error" in line and line.get("error"):
  523. raise RuntimeError(line.get("error"))
  524. @classmethod
  525. async def synthesize(cls, params: dict) -> AsyncIterator[bytes]:
  526. async for _ in cls.login():
  527. pass
  528. async with StreamSession(
  529. impersonate="chrome",
  530. timeout=0
  531. ) as session:
  532. async with session.get(
  533. f"{cls.url}/backend-api/synthesize",
  534. params=params,
  535. headers=cls._headers
  536. ) as response:
  537. await raise_for_status(response)
  538. async for chunk in response.iter_content():
  539. yield chunk
  540. @classmethod
  541. async def login(
  542. cls,
  543. proxy: str = None,
  544. api_key: str = None,
  545. proof_token: str = None,
  546. cookies: Cookies = None,
  547. headers: dict = None,
  548. **kwargs
  549. ) -> AsyncIterator:
  550. if cls._expires is not None and (cls._expires - 60*10) < time.time():
  551. cls._headers = cls._api_key = None
  552. if cls._headers is None or headers is not None:
  553. cls._headers = {} if headers is None else headers
  554. if proof_token is not None:
  555. cls.request_config.proof_token = proof_token
  556. if cookies is not None:
  557. cls.request_config.cookies = cookies
  558. if api_key is not None:
  559. cls._create_request_args(cls.request_config.cookies, cls.request_config.headers)
  560. cls._set_api_key(api_key)
  561. else:
  562. try:
  563. await get_request_config(cls.request_config, proxy)
  564. cls._create_request_args(cls.request_config.cookies, cls.request_config.headers)
  565. if cls.request_config.access_token is not None or cls.needs_auth:
  566. if not cls._set_api_key(cls.request_config.access_token):
  567. raise NoValidHarFileError(f"Access token is not valid: {cls.request_config.access_token}")
  568. except NoValidHarFileError:
  569. if has_nodriver:
  570. if cls._api_key is None:
  571. login_url = os.environ.get("G4F_LOGIN_URL")
  572. if login_url:
  573. yield RequestLogin(cls.label, login_url)
  574. await cls.nodriver_auth(proxy)
  575. else:
  576. raise
  577. yield Parameters(**{
  578. "api_key": cls._api_key,
  579. "proof_token": cls.request_config.proof_token,
  580. "cookies": cls.request_config.cookies,
  581. })
  582. @classmethod
  583. async def nodriver_auth(cls, proxy: str = None):
  584. browser, stop_browser = await get_nodriver(proxy=proxy)
  585. try:
  586. page = browser.main_tab
  587. def on_request(event: nodriver.cdp.network.RequestWillBeSent):
  588. if event.request.url == start_url or event.request.url.startswith(conversation_url):
  589. cls.request_config.headers = event.request.headers
  590. elif event.request.url in (backend_url, backend_anon_url):
  591. if "OpenAI-Sentinel-Proof-Token" in event.request.headers:
  592. cls.request_config.proof_token = json.loads(base64.b64decode(
  593. event.request.headers["OpenAI-Sentinel-Proof-Token"].split("gAAAAAB", 1)[-1].encode()
  594. ).decode())
  595. if "OpenAI-Sentinel-Turnstile-Token" in event.request.headers:
  596. cls.request_config.turnstile_token = event.request.headers["OpenAI-Sentinel-Turnstile-Token"]
  597. if "Authorization" in event.request.headers:
  598. cls._api_key = event.request.headers["Authorization"].split()[-1]
  599. elif event.request.url == arkose_url:
  600. cls.request_config.arkose_request = arkReq(
  601. arkURL=event.request.url,
  602. arkBx=None,
  603. arkHeader=event.request.headers,
  604. arkBody=event.request.post_data,
  605. userAgent=event.request.headers.get("User-Agent")
  606. )
  607. await page.send(nodriver.cdp.network.enable())
  608. page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
  609. page = await browser.get(cls.url)
  610. user_agent = await page.evaluate("window.navigator.userAgent")
  611. await page.select("#prompt-textarea", 240)
  612. await page.evaluate("document.getElementById('prompt-textarea').innerText = 'Hello'")
  613. await page.evaluate("document.querySelector('[data-testid=\"send-button\"]').click()")
  614. while True:
  615. if cls._api_key is not None or not cls.needs_auth:
  616. break
  617. body = await page.evaluate("JSON.stringify(window.__remixContext)")
  618. if body:
  619. match = re.search(r'"accessToken":"(.*?)"', body)
  620. if match:
  621. cls._api_key = match.group(1)
  622. break
  623. await asyncio.sleep(1)
  624. while True:
  625. if cls.request_config.proof_token:
  626. break
  627. await asyncio.sleep(1)
  628. cls.request_config.data_build = await page.evaluate("document.documentElement.getAttribute('data-build')")
  629. cls.request_config.cookies = await page.send(get_cookies([cls.url]))
  630. await page.close()
  631. cls._create_request_args(cls.request_config.cookies, cls.request_config.headers, user_agent=user_agent)
  632. cls._set_api_key(cls._api_key)
  633. finally:
  634. stop_browser()
  635. @staticmethod
  636. def get_default_headers() -> Dict[str, str]:
  637. return {
  638. **DEFAULT_HEADERS,
  639. "content-type": "application/json",
  640. }
  641. @classmethod
  642. def _create_request_args(cls, cookies: Cookies = None, headers: dict = None, user_agent: str = None):
  643. cls._headers = cls.get_default_headers() if headers is None else headers
  644. if user_agent is not None:
  645. cls._headers["user-agent"] = user_agent
  646. cls._cookies = {} if cookies is None else cookies
  647. cls._update_cookie_header()
  648. @classmethod
  649. def _update_request_args(cls, auth_result: AuthResult, session: StreamSession):
  650. if hasattr(auth_result, "cookies"):
  651. for c in session.cookie_jar if hasattr(session, "cookie_jar") else session.cookies.jar:
  652. auth_result.cookies[getattr(c, "key", getattr(c, "name", ""))] = c.value
  653. cls._cookies = auth_result.cookies
  654. cls._update_cookie_header()
  655. @classmethod
  656. def _set_api_key(cls, api_key: str):
  657. if api_key:
  658. exp = api_key.split(".")[1]
  659. exp = (exp + "=" * (4 - len(exp) % 4)).encode()
  660. cls._expires = json.loads(base64.b64decode(exp)).get("exp")
  661. debug.log(f"OpenaiChat: API key expires at\n {cls._expires} we have:\n {time.time()}")
  662. if time.time() > cls._expires:
  663. debug.log(f"OpenaiChat: API key is expired")
  664. else:
  665. cls._api_key = api_key
  666. cls._headers["authorization"] = f"Bearer {api_key}"
  667. return True
  668. return False
  669. @classmethod
  670. def _update_cookie_header(cls):
  671. if cls._cookies:
  672. cls._headers["cookie"] = format_cookies(cls._cookies)
  673. class Conversation(JsonConversation):
  674. """
  675. Class to encapsulate response fields.
  676. """
  677. def __init__(self, conversation_id: str = None, message_id: str = None, user_id: str = None, finish_reason: str = None, parent_message_id: str = None, is_thinking: bool = False):
  678. self.conversation_id = conversation_id
  679. self.message_id = message_id
  680. self.finish_reason = finish_reason
  681. self.is_recipient = False
  682. self.parent_message_id = message_id if parent_message_id is None else parent_message_id
  683. self.user_id = user_id
  684. self.is_thinking = is_thinking
  685. def get_cookies(
  686. urls: Optional[Iterator[str]] = None
  687. ) -> Generator[Dict, Dict, Dict[str, str]]:
  688. params = {}
  689. if urls is not None:
  690. params['urls'] = [i for i in urls]
  691. cmd_dict = {
  692. 'method': 'Network.getCookies',
  693. 'params': params,
  694. }
  695. json = yield cmd_dict
  696. return {c["name"]: c["value"] for c in json['cookies']} if 'cookies' in json else {}