OpenaiChat.py 37 KB

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