XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 1
返回提交历史

XFEstudio/gpt4free

Add image caching to OpenaiChat provider

Introduces an image cache mechanism to OpenaiChat for uploaded images, reducing redundant uploads and improving efficiency. Also refactors code for clarity, updates type hints, and makes minor formatting improvements throughout the file.

9d2cdf9b
Ammar <ammar.alkotb@gmail.com>
提交于

代码差异

2 个文件 +230 -172
Modified g4f/Provider/Qwen.py +1 -1
@@ -32,7 +32,7 @@ try:
32 32 except ImportError:
33 33 has_curl_cffi = False
34 34
35 # Global variables to manage Yupp Image Cache
35 # Global variables to manage Qwen Image Cache
36 36 ImagesCache: Dict[str, dict] = {}
37 37
38 38
Modified g4f/Provider/needs_auth/OpenaiChat.py +229 -171
@@ -1,18 +1,20 @@
1 1 from __future__ import annotations
2 2
3 import os
4 import re
5 3 import asyncio
6 import uuid
7 import json
8 4 import base64
9 import time
5 import hashlib
6 import json
7 import os
10 8 import random
11 from typing import AsyncIterator, Iterator, Optional, Generator, Dict, Union, List, Any
9 import re
10 import time
11 import uuid
12 12 from copy import copy
13 from typing import AsyncIterator, Iterator, Optional, Generator, Dict, Union, List, Any
13 14
14 15 try:
15 16 import nodriver
17
16 18 has_nodriver = True
17 19 except ImportError:
18 20 has_nodriver = False
@@ -22,15 +24,17 @@ from ...typing import AsyncResult, Messages, Cookies, MediaListType
22 24 from ...requests.raise_for_status import raise_for_status
23 25 from ...requests import StreamSession
24 26 from ...requests import get_nodriver_session
25 from ...image import ImageRequest, to_image, to_bytes, is_accepted_format, detect_file_type
27 from ...image import ImageRequest, to_image, to_bytes, detect_file_type
26 28 from ...errors import MissingAuthError, NoValidHarFileError, ModelNotFoundError
27 from ...providers.response import JsonConversation, FinishReason, SynthesizeData, AuthResult, ImageResponse, ImagePreview, ResponseType, JsonRequest, format_link
29 from ...providers.response import JsonConversation, FinishReason, SynthesizeData, AuthResult, ImageResponse, \
30 ImagePreview, ResponseType, JsonRequest, format_link
28 31 from ...providers.response import TitleGeneration, RequestLogin, Reasoning
29 32 from ...tools.media import merge_media
30 33 from ..helper import format_cookies, format_media_prompt, to_string
31 34 from ..openai.models import default_model, default_image_model, models, image_models, text_models, model_aliases
32 35 from ..openai.har_file import get_request_config
33 from ..openai.har_file import RequestConfig, arkReq, arkose_url, start_url, conversation_url, backend_url, prepare_url, backend_anon_url
36 from ..openai.har_file import RequestConfig, arkReq, arkose_url, start_url, conversation_url, backend_url, prepare_url, \
37 backend_anon_url
34 38 from ..openai.proofofwork import generate_proof_token
35 39 from ..openai.new import get_requirements_token, get_config
36 40 from ... import debug
@@ -87,6 +91,9 @@ UPLOAD_HEADERS = {
87 91 "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"
88 92 }
89 93
94 ImagesCache: Dict[str, dict] = {}
95
96
90 97 class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
91 98 """A class for creating and managing conversations with OpenAI chat service"""
92 99
@@ -95,6 +102,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
95 102 working = True
96 103 active_by_default = True
97 104 use_nodriver = True
105 image_cache = True
98 106 supports_gpt_4 = True
99 107 supports_message_history = True
100 108 supports_system_message = True
@@ -127,11 +135,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
127 135
128 136 @classmethod
129 137 async def upload_files(
130 cls,
131 session: StreamSession,
132 auth_result: AuthResult,
133 media: MediaListType,
134 ) -> list[ImageRequest]:
138 cls,
139 session: StreamSession,
140 auth_result: AuthResult,
141 media: MediaListType,
142 ) -> List[ImageRequest]:
135 143 """
136 144 Upload an image to the service and get the download URL
137 145
@@ -143,11 +151,20 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
143 151 Returns:
144 152 An ImageRequest object that contains the download URL, file name, and other data
145 153 """
146 async def upload_file(file, image_name=None):
154
155 async def upload_file(file, image_name=None) -> ImageRequest:
147 156 debug.log(f"Uploading file: {image_name}")
148 157 file_data = {}
149 158
150 159 data_bytes = to_bytes(file)
160 # Check Cache
161 hasher = hashlib.md5()
162 hasher.update(data_bytes)
163 image_hash = hasher.hexdigest()
164 cache_file = ImagesCache.get(image_hash)
165 if cls.image_cache and file:
166 debug.log("Using cached image")
167 return ImageRequest(cache_file)
151 168 extension, mime_type = detect_file_type(data_bytes)
152 169 if "image" in mime_type:
153 170 # Convert the image to a PIL Image object
@@ -181,30 +198,31 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
181 198 # Put the image bytes to the upload URL and check the status
182 199 await asyncio.sleep(1)
183 200 async with session.put(
184 file_data["upload_url"],
185 data=data_bytes,
186 headers={
187 **UPLOAD_HEADERS,
188 "Content-Type": file_data["mime_type"],
189 "x-ms-blob-type": "BlockBlob",
190 "x-ms-version": "2020-04-08",
191 "Origin": "https://chatgpt.com",
192 }
201 file_data["upload_url"],
202 data=data_bytes,
203 headers={
204 **UPLOAD_HEADERS,
205 "Content-Type": file_data["mime_type"],
206 "x-ms-blob-type": "BlockBlob",
207 "x-ms-version": "2020-04-08",
208 "Origin": "https://chatgpt.com",
209 }
193 210 ) as response:
194 211 await raise_for_status(response)
195 212 # Post the file ID to the service and get the download URL
196 213 async with session.post(
197 f"{cls.url}/backend-api/files/{file_data['file_id']}/uploaded",
198 json={},
199 headers=auth_result.headers
214 f"{cls.url}/backend-api/files/{file_data['file_id']}/uploaded",
215 json={},
216 headers=auth_result.headers
200 217 ) as response:
201 218 cls._update_request_args(auth_result, session)
202 219 await raise_for_status(response, "Get download url failed")
203 220 uploaded_data = await response.json()
204 221 file_data["download_url"] = uploaded_data["download_url"]
222 ImagesCache[image_hash] = file_data.copy()
205 223 return ImageRequest(file_data)
206 224
207 medias = []
225 medias: List["ImageRequest"] = []
208 226 for item in media:
209 227 item = item if isinstance(item, tuple) else (item,)
210 228 __uploaded_media = await upload_file(*item)
@@ -242,7 +260,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
242 260 "id": str(uuid.uuid4()),
243 261 "author": {"role": message["role"]},
244 262 "content": {"content_type": "text", "parts": [to_string(message["content"])]},
245 "metadata": {"serialization_metadata": {"custom_symbol_offsets": []}, **({"system_hints": system_hints} if system_hints else {})},
263 "metadata": {"serialization_metadata": {"custom_symbol_offsets": []},
264 **({"system_hints": system_hints} if system_hints else {})},
246 265 "create_time": time.time(),
247 266 } for message in messages]
248 267 # Check if there is an image response
@@ -256,11 +275,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
256 275 "size_bytes": image_request.get("file_size"),
257 276 "width": image_request.get("width"),
258 277 }
259 for image_request in image_requests
278 for image_request in image_requests
260 279 # Add For Images Only
261 280 if image_request.get("use_case") == "multimodal"
262 281 ],
263 messages[-1]["content"]["parts"][0]]
282 messages[-1]["content"]["parts"][0]]
264 283 }
265 284 # Add the metadata object with the attachments
266 285 messages[-1]["metadata"] = {
@@ -278,12 +297,14 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
278 297 else {}
279 298 ),
280 299 }
281 for image_request in image_requests]
300 for image_request in image_requests]
282 301 }
283 302 return messages
284 303
285 304 @classmethod
286 async def get_generated_image(cls, session: StreamSession, auth_result: AuthResult, element: Union[dict, str], prompt: str = None, conversation_id: str = None, status: Optional[str]=None) -> ImagePreview|ImageResponse|None:
305 async def get_generated_image(cls, session: StreamSession, auth_result: AuthResult, element: Union[dict, str],
306 prompt: str = None, conversation_id: str = None,
307 status: Optional[str] = None) -> ImagePreview | ImageResponse | None:
287 308 download_urls = []
288 309 is_sediment = False
289 310 if prompt is None:
@@ -292,7 +313,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
292 313 except KeyError:
293 314 pass
294 315 if "asset_pointer" in element:
295 element = element["asset_pointer"]
316 element = element["asset_pointer"]
296 317 if isinstance(element, str) and element.startswith("file-service://"):
297 318 element = element.split("file-service://", 1)[-1]
298 319 elif isinstance(element, str) and element.startswith("sediment://"):
@@ -303,7 +324,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
303 324 if is_sediment:
304 325 url = f"{cls.url}/backend-api/conversation/{conversation_id}/attachment/{element}/download"
305 326 else:
306 url =f"{cls.url}/backend-api/files/{element}/download"
327 url = f"{cls.url}/backend-api/files/{element}/download"
307 328 try:
308 329 async with session.get(url, headers=auth_result.headers) as response:
309 330 cls._update_request_args(auth_result, session)
@@ -327,23 +348,23 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
327 348
328 349 @classmethod
329 350 async def create_authed(
330 cls,
331 model: str,
332 messages: Messages,
333 auth_result: AuthResult,
334 proxy: str = None,
335 timeout: int = 360,
336 auto_continue: bool = False,
337 action: Optional[str] = None,
338 conversation: Conversation = None,
339 media: MediaListType = None,
340 return_conversation: bool = True,
341 web_search: bool = False,
342 prompt: str = None,
343 conversation_mode: Optional[dict] = None,
344 temporary: Optional[bool] = None,
345 conversation_id: Optional[str] = None,
346 **kwargs
351 cls,
352 model: str,
353 messages: Messages,
354 auth_result: AuthResult,
355 proxy: str = None,
356 timeout: int = 360,
357 auto_continue: bool = False,
358 action: Optional[str] = None,
359 conversation: Conversation = None,
360 media: MediaListType = None,
361 return_conversation: bool = True,
362 web_search: bool = False,
363 prompt: str = None,
364 conversation_mode: Optional[dict] = None,
365 temporary: Optional[bool] = None,
366 conversation_id: Optional[str] = None,
367 **kwargs
347 368 ) -> AsyncResult:
348 369 """
349 370 Create an asynchronous generator for the conversation.
@@ -371,12 +392,12 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
371 392 if action is None:
372 393 action = "next"
373 394 async with StreamSession(
374 proxy=proxy,
375 impersonate="chrome",
376 timeout=timeout
395 proxy=proxy,
396 impersonate="chrome",
397 timeout=timeout
377 398 ) as session:
378 399 image_requests = None
379 media = merge_media(media, messages)
400 media = merge_media(media, messages)
380 401 if not cls.needs_auth and not media:
381 402 if cls._headers is None:
382 403 cls._create_request_args(cls._cookies)
@@ -440,18 +461,19 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
440 461 if temporary:
441 462 data["history_and_training_disabled"] = True
442 463 async with session.post(
443 prepare_url,
444 json=data,
445 headers=cls._headers
464 prepare_url,
465 json=data,
466 headers=cls._headers
446 467 ) as response:
447 468 await raise_for_status(response)
448 469 conduit_token = (await response.json())["conduit_token"]
449 470 async with session.post(
450 f"{cls.url}/backend-anon/sentinel/chat-requirements"
451 if cls._api_key is None else
452 f"{cls.url}/backend-api/sentinel/chat-requirements",
453 json={"p": None if not getattr(auth_result, "proof_token", None) else get_requirements_token(getattr(auth_result, "proof_token", None))},
454 headers=cls._headers
471 f"{cls.url}/backend-anon/sentinel/chat-requirements"
472 if cls._api_key is None else
473 f"{cls.url}/backend-api/sentinel/chat-requirements",
474 json={"p": None if not getattr(auth_result, "proof_token", None) else get_requirements_token(
475 getattr(auth_result, "proof_token", None))},
476 headers=cls._headers
455 477 ) as response:
456 478 if response.status in (401, 403):
457 479 raise MissingAuthError(f"Response status: {response.status}")
@@ -460,10 +482,10 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
460 482 await raise_for_status(response)
461 483 chat_requirements = await response.json()
462 484 need_turnstile = chat_requirements.get("turnstile", {}).get("required", False)
463 need_arkose = chat_requirements.get("arkose", {}).get("required", False)
464 chat_token = chat_requirements.get("token")
485 need_arkose = chat_requirements.get("arkose", {}).get("required", False)
486 chat_token = chat_requirements.get("token")
465 487
466 # if need_arkose and cls.request_config.arkose_token is None:
488 # if need_arkose and cls.request_config.arkose_token is None:
467 489 # await get_request_config(proxy)
468 490 # cls._create_request_args(auth_result.cookies, auth_result.headers)
469 491 # cls._set_api_key(auth_result.access_token)
@@ -480,23 +502,25 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
480 502 proof_token=proof_token
481 503 )
482 504 # [debug.log(text) for text in (
483 #f"Arkose: {'False' if not need_arkose else auth_result.arkose_token[:12]+'...'}",
484 #f"Proofofwork: {'False' if proofofwork is None else proofofwork[:12]+'...'}",
485 #f"AccessToken: {'False' if cls._api_key is None else cls._api_key[:12]+'...'}",
505 # f"Arkose: {'False' if not need_arkose else auth_result.arkose_token[:12]+'...'}",
506 # f"Proofofwork: {'False' if proofofwork is None else proofofwork[:12]+'...'}",
507 # f"AccessToken: {'False' if cls._api_key is None else cls._api_key[:12]+'...'}",
486 508 # )]
487 509 data = {
488 510 "action": "next",
489 511 "parent_message_id": conversation.message_id,
490 512 "model": model,
491 "timezone_offset_min":-120,
492 "timezone":"Europe/Berlin",
493 "conversation_mode":{"kind":"primary_assistant"},
494 "enable_message_followups":True,
513 "timezone_offset_min": -120,
514 "timezone": "Europe/Berlin",
515 "conversation_mode": {"kind": "primary_assistant"},
516 "enable_message_followups": True,
495 517 "system_hints": ["search"] if web_search else None,
496 "supports_buffering":True,
497 "supported_encodings":["v1"],
498 "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},
499 "paragen_cot_summary_display_override":"allow"
518 "supports_buffering": True,
519 "supported_encodings": ["v1"],
520 "client_contextual_info": {"is_dark_mode": False, "time_since_loaded": random.randint(20, 500),
521 "page_height": 578, "page_width": 1850, "pixel_ratio": 1,
522 "screen_height": 1080, "screen_width": 1920},
523 "paragen_cot_summary_display_override": "allow"
500 524 }
501 525 if temporary:
502 526 data["history_and_training_disabled"] = True
@@ -516,7 +540,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
516 540 new_messages = []
517 541 else:
518 542 new_messages.append(message)
519 data["messages"] = cls.create_messages(new_messages, image_requests, ["search"] if web_search else None)
543 data["messages"] = cls.create_messages(new_messages, image_requests,
544 ["search"] if web_search else None)
520 545 yield JsonRequest.from_dict(data)
521 546 headers = {
522 547 **cls._headers,
@@ -525,18 +550,18 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
525 550 "openai-sentinel-chat-requirements-token": chat_token,
526 551 **({} if conduit_token is None else {"x-conduit-token": conduit_token})
527 552 }
528 #if cls.request_config.arkose_token:
553 # if cls.request_config.arkose_token:
529 554 # headers["openai-sentinel-arkose-token"] = cls.request_config.arkose_token
530 555 if proofofwork is not None:
531 556 headers["openai-sentinel-proof-token"] = proofofwork
532 557 if need_turnstile and getattr(auth_result, "turnstile_token", None) is not None:
533 558 headers['openai-sentinel-turnstile-token'] = auth_result.turnstile_token
534 559 async with session.post(
535 backend_anon_url
536 if cls._api_key is None else
537 backend_url,
538 json=data,
539 headers=headers
560 backend_anon_url
561 if cls._api_key is None else
562 backend_url,
563 json=data,
564 headers=headers
540 565 ) as response:
541 566 cls._update_request_args(auth_result, session)
542 567 if response.status in (401, 403, 429, 500):
@@ -552,10 +577,12 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
552 577 if match.group(0) in matches:
553 578 continue
554 579 matches.append(match.group(0))
555 generated_image = await cls.get_generated_image(session, auth_result, match.group(0), prompt)
580 generated_image = await cls.get_generated_image(session, auth_result, match.group(0),
581 prompt)
556 582 if generated_image is not None:
557 583 yield generated_image
558 async for chunk in cls.iter_messages_line(session, auth_result, line, conversation, sources, references):
584 async for chunk in cls.iter_messages_line(session, auth_result, line, conversation, sources,
585 references):
559 586 if isinstance(chunk, str):
560 587 chunk = chunk.replace("\ue203", "").replace("\ue204", "").replace("\ue206", "")
561 588 buffer += chunk
@@ -565,9 +592,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
565 592 def citation_replacer(match: re.Match[str]):
566 593 ref_type = match.group(1)
567 594 ref_index = int(match.group(2))
568 if ((ref_type == "image" and is_image_embedding) or
569 is_video_embedding or
570 ref_type == "forecast"):
595 if ((ref_type == "image" and is_image_embedding) or
596 is_video_embedding or
597 ref_type == "forecast"):
571 598
572 599 reference = references.get_reference({
573 600 "ref_index": ref_index,
@@ -575,7 +602,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
575 602 })
576 603 if not reference:
577 604 return ""
578
605
579 606 if ref_type == "forecast":
580 607 if reference.get("alt"):
581 608 return reference.get("alt")
@@ -584,11 +611,13 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
584 611
585 612 if is_image_embedding and reference.get("content_url", ""):
586 613 return f"![{reference.get('title', '')}]({reference.get('content_url')})"
587
614
588 615 if is_video_embedding:
589 if reference.get("url", "") and reference.get("thumbnail_url", ""):
616 if reference.get("url", "") and reference.get("thumbnail_url",
617 ""):
590 618 return f"[![{reference.get('title', '')}]({reference['thumbnail_url']})]({reference['url']})"
591 video_match = re.match(r"video\n(.*?)\nturn[0-9]+", match.group(0))
619 video_match = re.match(r"video\n(.*?)\nturn[0-9]+",
620 match.group(0))
592 621 if video_match:
593 622 return video_match.group(1)
594 623 return ""
@@ -599,9 +628,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
599 628 })
600 629 if source_index is not None and len(sources.list) > source_index:
601 630 link = sources.list[source_index]["url"]
602 return f"[[{source_index+1}]]({link})"
631 return f"[[{source_index + 1}]]({link})"
603 632 return f""
604
633
605 634 def products_replacer(match: re.Match[str]):
606 635 try:
607 636 products_data = json.loads(match.group(1))
@@ -616,25 +645,30 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
616 645 return ""
617 646
618 647 sequence_content = match.group(1)
619 sequence_content = sequence_content.replace("\ue200", "").replace("\ue202", "\n").replace("\ue201", "")
648 sequence_content = sequence_content.replace("\ue200", "").replace("\ue202",
649 "\n").replace(
650 "\ue201", "")
620 651 sequence_content = sequence_content.replace("navlist\n", "#### ")
621
652
622 653 # Handle search, news, view and image citations
623 654 is_image_embedding = sequence_content.startswith("i\nturn")
624 655 is_video_embedding = sequence_content.startswith("video\n")
625 656 sequence_content = re.sub(
626 r'(?:cite\nturn[0-9]+|forecast\nturn[0-9]+|video\n.*?\nturn[0-9]+|i?\n?turn[0-9]+)(search|news|view|image|forecast)(\d+)',
627 citation_replacer,
657 r'(?:cite\nturn[0-9]+|forecast\nturn[0-9]+|video\n.*?\nturn[0-9]+|i?\n?turn[0-9]+)(search|news|view|image|forecast)(\d+)',
658 citation_replacer,
628 659 sequence_content
629 660 )
630 sequence_content = re.sub(r'products\n(.*)', products_replacer, sequence_content)
631 sequence_content = re.sub(r'product_entity\n\[".*","(.*)"\]', lambda x: x.group(1), sequence_content)
661 sequence_content = re.sub(r'products\n(.*)', products_replacer,
662 sequence_content)
663 sequence_content = re.sub(r'product_entity\n\[".*","(.*)"\]',
664 lambda x: x.group(1), sequence_content)
632 665 return sequence_content
633
666
634 667 # process only completed sequences and do not touch start of next not completed sequence
635 buffer = re.sub(r'\ue200(.*?)\ue201', sequence_replacer, buffer, flags=re.DOTALL)
636
637 if buffer.find(u"\ue200") != -1: # still have uncompleted sequence
668 buffer = re.sub(r'\ue200(.*?)\ue201', sequence_replacer, buffer,
669 flags=re.DOTALL)
670
671 if buffer.find(u"\ue200") != -1: # still have uncompleted sequence
638 672 continue
639 673 else:
640 674 # do not yield to consume rest part of special sequence
@@ -651,7 +685,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
651 685 if sources.list:
652 686 yield sources
653 687 if conversation.generated_images:
654 yield ImageResponse(conversation.generated_images.urls, conversation.prompt, {"headers": auth_result.headers})
688 yield ImageResponse(conversation.generated_images.urls, conversation.prompt,
689 {"headers": auth_result.headers})
655 690 conversation.generated_images = None
656 691 conversation.prompt = None
657 692 if return_conversation:
@@ -671,7 +706,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
671 706 yield FinishReason(conversation.finish_reason)
672 707
673 708 @classmethod
674 async def iter_messages_line(cls, session: StreamSession, auth_result: AuthResult, line: bytes, fields: Conversation, sources: OpenAISources, references: ContentReferences) -> AsyncIterator:
709 async def iter_messages_line(cls, session: StreamSession, auth_result: AuthResult, line: bytes,
710 fields: Conversation, sources: OpenAISources,
711 references: ContentReferences) -> AsyncIterator:
675 712 if not line.startswith(b"data: "):
676 713 return
677 714 elif line.startswith(b"data: [DONE]"):
@@ -710,10 +747,14 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
710 747 elif m.get("p") == "/message/metadata/image_gen_title":
711 748 fields.prompt = m.get("v")
712 749 elif m.get("p") == "/message/content/parts/0/asset_pointer":
713 status = next(filter(lambda x:x.get("p") == '/message/status' ,v), {}).get('v', None)
714 generated_images = fields.generated_images = await cls.get_generated_image(session, auth_result, m.get("v"), fields.prompt, fields.conversation_id, status)
750 status = next(filter(lambda x: x.get("p") == '/message/status', v), {}).get('v', None)
751 generated_images = fields.generated_images = await cls.get_generated_image(session, auth_result,
752 m.get("v"),
753 fields.prompt,
754 fields.conversation_id,
755 status)
715 756 if generated_images is not None:
716 if buffer:
757 if buffer:
717 758 yield buffer
718 759 yield generated_images
719 760 elif m.get("p") == "/message/metadata/search_result_groups":
@@ -740,41 +781,48 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
740 781 if match and m.get("o") == "append" and isinstance(m.get("v"), dict):
741 782 idx = int(match.group(1))
742 783 references.merge_reference(idx, m.get("v"))
743 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/fallback_items$", m.get("p")) and isinstance(m.get("v"), list):
784 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/fallback_items$",
785 m.get("p")) and isinstance(m.get("v"), list):
744 786 for link in m.get("v", []) or []:
745 787 sources.add_source(link)
746 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/items$", m.get("p")) and isinstance(m.get("v"), list):
788 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/items$",
789 m.get("p")) and isinstance(m.get("v"), list):
747 790 for link in m.get("v", []) or []:
748 791 sources.add_source(link)
749 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/refs$", m.get("p")) and isinstance(m.get("v"), list):
792 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/refs$",
793 m.get("p")) and isinstance(m.get("v"), list):
750 794 match = re.match(r"^/message/metadata/content_references/(\d+)/refs$", m.get("p"))
751 795 if match:
752 796 idx = int(match.group(1))
753 797 references.update_reference(idx, m.get("o"), "refs", m.get("v"))
754 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/alt$", m.get("p")) and isinstance(m.get("v"), list):
798 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/alt$",
799 m.get("p")) and isinstance(m.get("v"), list):
755 800 match = re.match(r"^/message/metadata/content_references/(\d+)/alt$", m.get("p"))
756 801 if match:
757 802 idx = int(match.group(1))
758 803 references.update_reference(idx, m.get("o"), "alt", m.get("v"))
759 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/prompt_text$", m.get("p")) and isinstance(m.get("v"), list):
804 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/prompt_text$",
805 m.get("p")) and isinstance(m.get("v"), list):
760 806 match = re.match(r"^/message/metadata/content_references/(\d+)/prompt_text$", m.get("p"))
761 807 if match:
762 808 idx = int(match.group(1))
763 809 references.update_reference(idx, m.get("o"), "prompt_text", m.get("v"))
764 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/refs/\d+$", m.get("p")) and isinstance(m.get("v"), dict):
810 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/refs/\d+$",
811 m.get("p")) and isinstance(m.get("v"), dict):
765 812 match = re.match(r"^/message/metadata/content_references/(\d+)/refs/(\d+)$", m.get("p"))
766 813 if match:
767 814 reference_idx = int(match.group(1))
768 815 ref_idx = int(match.group(2))
769 816 references.update_reference(reference_idx, m.get("o"), "refs", m.get("v"), ref_idx)
770 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/images$", m.get("p")) and isinstance(m.get("v"), list):
817 elif m.get("p") and re.match(r"^/message/metadata/content_references/\d+/images$",
818 m.get("p")) and isinstance(m.get("v"), list):
771 819 match = re.match(r"^/message/metadata/content_references/(\d+)/images$", m.get("p"))
772 820 if match:
773 821 idx = int(match.group(1))
774 822 references.update_reference(idx, m.get("o"), "images", m.get("v"))
775 823 elif m.get("p") == "/message/metadata/finished_text":
776 824 fields.is_thinking = False
777 if buffer:
825 if buffer:
778 826 yield buffer
779 827 yield Reasoning(status=m.get("v"))
780 828 elif m.get("p") == "/message/metadata" and fields.recipient == "all":
@@ -790,10 +838,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
790 838 fields.recipient = m.get("recipient", fields.recipient)
791 839 if fields.recipient == "all":
792 840 c = m.get("content", {})
793 if c.get("content_type") == "text" and m.get("author", {}).get("role") == "tool" and "initial_text" in m.get("metadata", {}):
841 if c.get("content_type") == "text" and m.get("author", {}).get(
842 "role") == "tool" and "initial_text" in m.get("metadata", {}):
794 843 fields.is_thinking = True
795 844 yield Reasoning(status=m.get("metadata", {}).get("initial_text"))
796 #if c.get("content_type") == "multimodal_text":
845 # if c.get("content_type") == "multimodal_text":
797 846 # for part in c.get("parts"):
798 847 # if isinstance(part, dict) and part.get("content_type") == "image_asset_pointer":
799 848 # yield await cls.get_generated_image(session, auth_result, part, fields.prompt, fields.conversation_id)
@@ -808,13 +857,13 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
808 857 @classmethod
809 858 async def synthesize(cls, params: dict) -> AsyncIterator[bytes]:
810 859 async with StreamSession(
811 impersonate="chrome",
812 timeout=0
860 impersonate="chrome",
861 timeout=0
813 862 ) as session:
814 863 async with session.get(
815 f"{cls.url}/backend-api/synthesize",
816 params=params,
817 headers=cls._headers
864 f"{cls.url}/backend-api/synthesize",
865 params=params,
866 headers=cls._headers
818 867 ) as response:
819 868 await raise_for_status(response)
820 869 async for chunk in response.iter_content():
@@ -822,15 +871,15 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
822 871
823 872 @classmethod
824 873 async def login(
825 cls,
826 proxy: str = None,
827 api_key: str = None,
828 proof_token: str = None,
829 cookies: Cookies = None,
830 headers: dict = None,
831 **kwargs
874 cls,
875 proxy: str = None,
876 api_key: str = None,
877 proof_token: str = None,
878 cookies: Cookies = None,
879 headers: dict = None,
880 **kwargs
832 881 ) -> AsyncIterator:
833 if cls._expires is not None and (cls._expires - 60*10) < time.time():
882 if cls._expires is not None and (cls._expires - 60 * 10) < time.time():
834 883 cls._headers = cls._api_key = None
835 884 if cls._headers is None or headers is not None:
836 885 cls._headers = {} if headers is None else headers
@@ -863,6 +912,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
863 912 async def nodriver_auth(cls, proxy: str = None):
864 913 async with get_nodriver_session(proxy=proxy) as browser:
865 914 page = await browser.get(cls.url)
915
866 916 def on_request(event: nodriver.cdp.network.RequestWillBeSent, page=None):
867 917 if event.request.url == start_url or event.request.url.startswith(conversation_url):
868 918 if cls.request_config.headers is None:
@@ -871,9 +921,10 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
871 921 cls.request_config.headers[key.lower()] = value
872 922 elif event.request.url in (backend_url, backend_anon_url):
873 923 if "OpenAI-Sentinel-Proof-Token" in event.request.headers:
874 cls.request_config.proof_token = json.loads(base64.b64decode(
875 event.request.headers["OpenAI-Sentinel-Proof-Token"].split("gAAAAAB", 1)[-1].split("~")[0].encode()
876 ).decode())
924 cls.request_config.proof_token = json.loads(base64.b64decode(
925 event.request.headers["OpenAI-Sentinel-Proof-Token"].split("gAAAAAB", 1)[-1].split("~")[
926 0].encode()
927 ).decode())
877 928 if "OpenAI-Sentinel-Turnstile-Token" in event.request.headers:
878 929 cls.request_config.turnstile_token = event.request.headers["OpenAI-Sentinel-Turnstile-Token"]
879 930 if "Authorization" in event.request.headers:
@@ -886,6 +937,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
886 937 arkBody=event.request.post_data,
887 938 userAgent=event.request.headers.get("User-Agent")
888 939 )
940
889 941 await page.send(nodriver.cdp.network.enable())
890 942 page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
891 943 await page.reload()
@@ -917,7 +969,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
917 969 if cls._api_key is not None or not cls.needs_auth:
918 970 break
919 971 await asyncio.sleep(1)
920 debug.log(f"OpenaiChat: Access token: {'False' if cls._api_key is None else cls._api_key[:12]+'...'}")
972 debug.log(f"OpenaiChat: Access token: {'False' if cls._api_key is None else cls._api_key[:12] + '...'}")
921 973 while True:
922 974 if cls.request_config.proof_token:
923 975 break
@@ -975,11 +1027,14 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
975 1027 if cls._cookies:
976 1028 cls._headers["cookie"] = format_cookies(cls._cookies)
977 1029
1030
978 1031 class Conversation(JsonConversation):
979 1032 """
980 1033 Class to encapsulate response fields.
981 1034 """
982 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):
1035
1036 def __init__(self, conversation_id: str = None, message_id: str = None, user_id: str = None,
1037 finish_reason: str = None, parent_message_id: str = None, is_thinking: bool = False):
983 1038 self.conversation_id = conversation_id
984 1039 self.message_id = message_id
985 1040 self.finish_reason = finish_reason
@@ -992,8 +1047,9 @@ class Conversation(JsonConversation):
992 1047 self.prompt = None
993 1048 self.generated_images: ImagePreview = None
994 1049
1050
995 1051 def get_cookies(
996 urls: Optional[Iterator[str]] = None
1052 urls: Optional[Iterator[str]] = None
997 1053 ) -> Generator[Dict, Dict, Dict[str, str]]:
998 1054 params = {}
999 1055 if urls is not None:
@@ -1005,6 +1061,7 @@ def get_cookies(
1005 1061 json = yield cmd_dict
1006 1062 return {c["name"]: c["value"] for c in json['cookies']} if 'cookies' in json else {}
1007 1063
1064
1008 1065 class OpenAISources(ResponseType):
1009 1066 list: List[Dict[str, str]]
1010 1067
@@ -1030,7 +1087,7 @@ class OpenAISources(ResponseType):
1030 1087 if existing_source and idx is not None:
1031 1088 self.list[idx] = source
1032 1089 return
1033
1090
1034 1091 existing_source, idx = self.find_by_url(source["url"])
1035 1092 if existing_source and idx is not None:
1036 1093 self.list[idx] = source
@@ -1043,53 +1100,54 @@ class OpenAISources(ResponseType):
1043 1100 if not self.list:
1044 1101 return ""
1045 1102 return "\n\n\n\n" + ("\n>\n".join([
1046 f"> [{idx+1}] {format_link(link['url'], link.get('title', ''))}"
1103 f"> [{idx + 1}] {format_link(link['url'], link.get('title', ''))}"
1047 1104 for idx, link in enumerate(self.list)
1048 1105 ]))
1049
1050 def get_ref_info(self, source: Dict[str, str]) -> dict[str, str|int] | None:
1106
1107 def get_ref_info(self, source: Dict[str, str]) -> dict[str, str | int] | None:
1051 1108 ref_index = source.get("ref_id", {}).get("ref_index", None)
1052 1109 ref_type = source.get("ref_id", {}).get("ref_type", None)
1053 1110 if isinstance(ref_index, int):
1054 1111 return {
1055 "ref_index": ref_index,
1112 "ref_index": ref_index,
1056 1113 "ref_type": ref_type,
1057 1114 }
1058
1115
1059 1116 for ref_info in source.get('refs') or []:
1060 1117 ref_index = ref_info.get("ref_index", None)
1061 1118 ref_type = ref_info.get("ref_type", None)
1062 1119 if isinstance(ref_index, int):
1063 1120 return {
1064 "ref_index": ref_index,
1121 "ref_index": ref_index,
1065 1122 "ref_type": ref_type,
1066 1123 }
1067
1124
1068 1125 return None
1069 1126
1070 def find_by_ref_info(self, ref_info: dict[str, str|int]):
1127 def find_by_ref_info(self, ref_info: dict[str, str | int]):
1071 1128 for idx, source in enumerate(self.list):
1072 1129 source_ref_info = self.get_ref_info(source)
1073 if (source_ref_info and
1074 source_ref_info["ref_index"] == ref_info["ref_index"] and
1075 source_ref_info["ref_type"] == ref_info["ref_type"]):
1076 return source, idx
1130 if (source_ref_info and
1131 source_ref_info["ref_index"] == ref_info["ref_index"] and
1132 source_ref_info["ref_type"] == ref_info["ref_type"]):
1133 return source, idx
1077 1134
1078 1135 return None, None
1079
1136
1080 1137 def find_by_url(self, url: str):
1081 1138 for idx, source in enumerate(self.list):
1082 1139 if source["url"] == url:
1083 1140 return source, idx
1084 return None, None
1141 return None, None
1085 1142
1086 def get_index(self, ref_info: dict[str, str|int]) -> int | None:
1143 def get_index(self, ref_info: dict[str, str | int]) -> int | None:
1087 1144 _, index = self.find_by_ref_info(ref_info)
1088 1145 if index is not None:
1089 return index
1146 return index
1090 1147
1091 1148 return None
1092 1149
1150
1093 1151 class ContentReferences:
1094 1152 def __init__(self) -> None:
1095 1153 self.list: List[Dict[str, Any]] = []
@@ -1103,16 +1161,16 @@ class ContentReferences:
1103 1161
1104 1162 self.list[idx] = {**self.list[idx], **reference_part}
1105 1163
1106 def update_reference(self, idx: int, operation: str, field: str, value: Any, ref_idx = None) -> None:
1164 def update_reference(self, idx: int, operation: str, field: str, value: Any, ref_idx=None) -> None:
1107 1165 while len(self.list) <= idx:
1108 1166 self.list.append({})
1109
1167
1110 1168 if operation == "append" or operation == "add":
1111 1169 if not isinstance(self.list[idx].get(field, None), list):
1112 1170 self.list[idx][field] = []
1113 1171 if isinstance(value, list):
1114 1172 self.list[idx][field].extend(value)
1115 else:
1173 else:
1116 1174 self.list[idx][field].append(value)
1117 1175
1118 1176 if operation == "replace" and ref_idx is not None:
@@ -1128,10 +1186,10 @@ class ContentReferences:
1128 1186 self.list[idx][field] = value
1129 1187
1130 1188 def get_ref_info(
1131 self,
1132 source: Dict[str, str],
1133 target_ref_info: Dict[str, Union[str, int]]
1134 ) -> dict[str, str|int] | None:
1189 self,
1190 source: Dict[str, str],
1191 target_ref_info: Dict[str, Union[str, int]]
1192 ) -> dict[str, str | int] | None:
1135 1193 for idx, ref_info in enumerate(source.get("refs", [])) or []:
1136 1194 if not isinstance(ref_info, dict):
1137 1195 continue
@@ -1139,11 +1197,11 @@ class ContentReferences:
1139 1197 ref_index = ref_info.get("ref_index", None)
1140 1198 ref_type = ref_info.get("ref_type", None)
1141 1199 if isinstance(ref_index, int) and isinstance(ref_type, str):
1142 if (not target_ref_info or
1143 (target_ref_info["ref_index"] == ref_index and
1144 target_ref_info["ref_type"] == ref_type)):
1200 if (not target_ref_info or
1201 (target_ref_info["ref_index"] == ref_index and
1202 target_ref_info["ref_type"] == ref_type)):
1145 1203 return {
1146 "ref_index": ref_index,
1204 "ref_index": ref_index,
1147 1205 "ref_type": ref_type,
1148 1206 "idx": idx
1149 1207 }
@@ -1154,9 +1212,9 @@ class ContentReferences:
1154 1212 for reference in self.list:
1155 1213 reference_ref_info = self.get_ref_info(reference, ref_info)
1156 1214
1157 if (not reference_ref_info or
1158 reference_ref_info["ref_index"] != ref_info["ref_index"] or
1159 reference_ref_info["ref_type"] != ref_info["ref_type"]):
1215 if (not reference_ref_info or
1216 reference_ref_info["ref_index"] != ref_info["ref_index"] or
1217 reference_ref_info["ref_type"] != ref_info["ref_type"]):
1160 1218 continue
1161 1219
1162 1220 if ref_info["ref_type"] != "image":