返回提交历史
Modified
g4f/Provider/Qwen.py
+1
-1
Modified
g4f/Provider/needs_auth/OpenaiChat.py
+229
-171
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
代码差异
2 个文件
+230
-172
@@ -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
@@ -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"})"
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['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":