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

XFEstudio/gpt4free

Add image caching to Yupp provider (#3246)

* Add image caching to Yupp provider Introduces an image cache to avoid redundant uploads in the Yupp provider. Refactors media attachment handling into a new prepare_files method, improving efficiency and code organization. Updates .gitignore to exclude .idea directory. * Refactor Yupp stream handling and chunk processing Improves stream segmentation in the Yupp provider by introducing buffers for target, variant, quick, thinking, and extra streams. Refactors chunk processing to better handle image-gen, quick responses, and variant outputs, and adds more robust stream ID extraction and routing logic. Yields a consolidated JsonResponse with all stream segments for downstream use. * Handle ClientResponseError in Yupp provider Adds specific handling for aiohttp ClientResponseError in the Yupp provider. Marks account as invalid on 500 Internal Server Error, otherwise increments error count and raises ProviderException for other errors. * Update Yupp.py fix 429 'Too Many Requests' * Update Yupp.py

18fda760
Ammar <ammar.alkotb@gmail.com>
提交于

代码差异

2 个文件 +207 -66
Modified .gitignore +1 -0
@@ -9,3 +9,4 @@ g4f.egg-info
9 9 models/models.json
10 10 pyvenv.cfg
11 11 lib64
12 /.idea
Modified g4f/Provider/Yupp.py +206 -66
@@ -1,3 +1,4 @@
1 import hashlib
1 2 import json
2 3 import time
3 4 import uuid
@@ -5,10 +6,12 @@ import re
5 6 import os
6 7 import asyncio
7 8 import aiohttp
9 from aiohttp import ClientResponseError
8 10
9 11 from ..typing import AsyncResult, Messages, Optional, Dict, Any, List
10 12 from ..providers.base_provider import AsyncGeneratorProvider, ProviderModelMixin
11 from ..providers.response import Reasoning, PlainTextResponse, PreviewResponse, JsonConversation, ImageResponse, ProviderInfo
13 from ..providers.response import Reasoning, PlainTextResponse, PreviewResponse, JsonConversation, ImageResponse, \
14 ProviderInfo, FinishReason, JsonResponse
12 15 from ..errors import RateLimitError, ProviderException, MissingAuthError
13 16 from ..cookies import get_cookies
14 17 from ..tools.auth import AuthManager
@@ -19,9 +22,13 @@ from .helper import get_last_user_message
19 22 from ..debug import log
20 23
21 24 # Global variables to manage Yupp accounts
22 YUPP_ACCOUNTS: List[Dict[str, Any]] = []
25 YUPP_ACCOUNT = Dict[str, Any]
26 YUPP_ACCOUNTS: List[YUPP_ACCOUNT] = []
23 27 account_rotation_lock = asyncio.Lock()
24 28
29 # Global variables to manage Yupp Image Cache
30 ImagesCache:Dict[str, dict] = {}
31
25 32 class YuppAccount:
26 33 """Yupp account representation"""
27 34 def __init__(self, token: str, is_valid: bool = True, error_count: int = 0, last_used: float = 0):
@@ -59,7 +66,7 @@ def create_headers() -> Dict[str, str]:
59 66 "Sec-Fetch-Site": "same-origin",
60 67 }
61 68
62 async def get_best_yupp_account() -> Optional[Dict[str, Any]]:
69 async def get_best_yupp_account() -> Optional[YUPP_ACCOUNT]:
63 70 """Get the best available Yupp account using smart selection algorithm"""
64 71 max_error_count = int(os.getenv("MAX_ERROR_COUNT", "3"))
65 72 error_cooldown = int(os.getenv("ERROR_COOLDOWN", "300"))
@@ -93,7 +100,7 @@ async def get_best_yupp_account() -> Optional[Dict[str, Any]]:
93 100 account["last_used"] = now
94 101 return account
95 102
96 async def claim_yupp_reward(session: aiohttp.ClientSession, account: Dict[str, Any], reward_id: str):
103 async def claim_yupp_reward(session: aiohttp.ClientSession, account: YUPP_ACCOUNT, reward_id: str):
97 104 """Claim Yupp reward asynchronously"""
98 105 try:
99 106 log_debug(f"Claiming reward {reward_id}...")
@@ -102,6 +109,8 @@ async def claim_yupp_reward(session: aiohttp.ClientSession, account: Dict[str, A
102 109 headers = {
103 110 "Content-Type": "application/json",
104 111 "Cookie": f"__Secure-yupp.session-token={account['token']}",
112 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
113
105 114 }
106 115 async with session.post(url, json=payload, headers=headers) as response:
107 116 response.raise_for_status()
@@ -113,7 +122,7 @@ async def claim_yupp_reward(session: aiohttp.ClientSession, account: Dict[str, A
113 122 log_debug(f"Failed to claim reward {reward_id}. Error: {e}")
114 123 return None
115 124
116 async def make_chat_private(session: aiohttp.ClientSession, account: Dict[str, Any], chat_id: str) -> bool:
125 async def make_chat_private(session: aiohttp.ClientSession, account: YUPP_ACCOUNT, chat_id: str) -> bool:
117 126 """Set a Yupp chat's sharing status to PRIVATE"""
118 127 try:
119 128 log_debug(f"Setting chat {chat_id} to PRIVATE...")
@@ -129,6 +138,8 @@ async def make_chat_private(session: aiohttp.ClientSession, account: Dict[str, A
129 138 headers = {
130 139 "Content-Type": "application/json",
131 140 "Cookie": f"__Secure-yupp.session-token={account['token']}",
141 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
142
132 143 }
133 144
134 145 async with session.post(url, json=payload, headers=headers) as response:
@@ -202,6 +213,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
202 213 working = True
203 214 active_by_default = True
204 215 supports_stream = True
216 image_cache = True
205 217
206 218 @classmethod
207 219 def get_models(cls, api_key: str = None, **kwargs) -> List[str]:
@@ -221,6 +233,62 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
221 233 cls.vision_models = [model.get("name") for model in models if "image/*" in model.get("supportedAttachmentMimeTypes", [])]
222 234 return cls.models
223 235
236 @classmethod
237 async def prepare_files(cls, media, session:aiohttp.ClientSession, account:YUPP_ACCOUNT)->list:
238 files = []
239 if not media:
240 return files
241 for file, name in media:
242 data = to_bytes(file)
243 hasher = hashlib.md5()
244 hasher.update(data)
245 image_hash = hasher.hexdigest()
246 file = ImagesCache.get(image_hash)
247 if cls.image_cache and file:
248 log_debug("Using cached image")
249 files.append(file)
250 continue
251 presigned_resp = await session.post(
252 "https://yupp.ai/api/trpc/chat.createPresignedURLForUpload?batch=1",
253 json={
254 "0": {"json": {"fileName": name, "fileSize": len(data), "contentType": is_accepted_format(data)}}},
255 headers={"Content-Type": "application/json",
256 "Cookie": f"__Secure-yupp.session-token={account['token']}",
257 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
258
259 }
260 )
261 presigned_resp.raise_for_status()
262 upload_info = (await presigned_resp.json())[0]["result"]["data"]["json"]
263 upload_url = upload_info["signedUrl"]
264
265 await session.put(
266 upload_url,
267 data=data,
268 headers={
269 "Content-Type": is_accepted_format(data),
270 "Content-Length": str(len(data))
271 }
272 )
273
274 attachment_resp = await session.post(
275 "https://yupp.ai/api/trpc/chat.createAttachmentForUploadedFile?batch=1",
276 json={"0": {"json": {"fileName": name, "contentType": is_accepted_format(data),
277 "fileId": upload_info["fileId"]}}},
278 cookies={"__Secure-yupp.session-token": account["token"]}
279 )
280 attachment_resp.raise_for_status()
281 attachment = (await attachment_resp.json())[0]["result"]["data"]["json"]
282 file = {
283 "fileName": attachment["file_name"],
284 "contentType": attachment["content_type"],
285 "attachmentId": attachment["attachment_id"],
286 "chatMessageId": ""
287 }
288 ImagesCache[image_hash] = file
289 files.append(file)
290 return files
291
224 292 @classmethod
225 293 async def create_async_generator(
226 294 cls,
@@ -266,44 +334,15 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
266 334 try:
267 335 async with aiohttp.ClientSession() as session:
268 336 turn_id = str(uuid.uuid4())
269 files = []
337
270 338
271 339 # Handle media attachments
272 340 media = kwargs.get("media")
273 341 if media:
274 for file, name in list(merge_media(media, messages)):
275 data = to_bytes(file)
276 presigned_resp = await session.post(
277 "https://yupp.ai/api/trpc/chat.createPresignedURLForUpload?batch=1",
278 json={"0": {"json": {"fileName": name, "fileSize": len(data), "contentType": is_accepted_format(data)}}},
279 headers={"Content-Type": "application/json", "Cookie": f"__Secure-yupp.session-token={account['token']}"}
280 )
281 presigned_resp.raise_for_status()
282 upload_info = (await presigned_resp.json())[0]["result"]["data"]["json"]
283 upload_url = upload_info["signedUrl"]
284
285 await session.put(
286 upload_url,
287 data=data,
288 headers={
289 "Content-Type": is_accepted_format(data),
290 "Content-Length": str(len(data))
291 }
292 )
293
294 attachment_resp = await session.post(
295 "https://yupp.ai/api/trpc/chat.createAttachmentForUploadedFile?batch=1",
296 json={"0": {"json": {"fileName": name, "contentType": is_accepted_format(data), "fileId": upload_info["fileId"]}}},
297 cookies={"__Secure-yupp.session-token": account["token"]}
298 )
299 attachment_resp.raise_for_status()
300 attachment = (await attachment_resp.json())[0]["result"]["data"]["json"]
301 files.append({
302 "fileName": attachment["file_name"],
303 "contentType": attachment["content_type"],
304 "attachmentId": attachment["attachment_id"],
305 "chatMessageId": ""
306 })
342 media_ = list(merge_media(media, messages))
343 files = await cls.prepare_files(media_, session=session, account=account)
344 else:
345 files = []
307 346 mode = "image" if model in cls.image_models else "text"
308 347
309 348 # Build payload and URL - FIXED: Use consistent url_uuid handling
@@ -346,6 +385,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
346 385 "content-type": "text/plain;charset=UTF-8",
347 386 "next-action": next_action,
348 387 "cookie": f"__Secure-yupp.session-token={account['token']}",
388 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
349 389 }
350 390
351 391 log_debug(f"Sending request to: {url}")
@@ -377,6 +417,18 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
377 417 else:
378 418 account["error_count"] += 1
379 419 continue
420 except ClientResponseError as e:
421 log_debug(f"Account ...{account['token'][-4:]} failed: {str(e)}")
422 # No Available Yupp credits
423 if e.status == 500 and 'Internal Server Error' in e.message:
424 account["is_valid"] = False
425 # Need User-Agent
426 # elif e.status == 429 and 'Too Many Requests' in e.message:
427 # account["is_valid"] = False
428 else:
429 async with account_rotation_lock:
430 account["error_count"] += 1
431 raise ProviderException(f"Yupp request failed: {str(e)}") from e
380 432 except Exception as e:
381 433 log_debug(f"Unexpected error with account ...{account['token'][-4:]}: {str(e)}")
382 434 async with account_rotation_lock:
@@ -389,7 +441,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
389 441 async def _process_stream_response(
390 442 cls,
391 443 response_content,
392 account: Dict[str, Any],
444 account: YUPP_ACCOUNT,
393 445 session: aiohttp.ClientSession,
394 446 prompt: str,
395 447 model_id: str
@@ -399,9 +451,20 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
399 451 line_pattern = re.compile(b"^([0-9a-fA-F]+):(.*)")
400 452 target_stream_id = None
401 453 reward_info = None
454 # Stream segmentation buffers
402 455 is_thinking = False
403 thinking_content = ""
456 thinking_content = "" # model's "thinking" channel (if activated later)
404 457 normal_content = ""
458 quick_content = "" # quick-response short message
459 variant_text = "" # variant model output (comparison stream)
460 stream = {
461 "target": [],
462 "variant": [],
463 "quick": [],
464 "thinking": [] ,
465 "extra": []
466 }
467 # Holds leftStream / rightStream definitions to determine target/variant
405 468 select_stream = [None, None]
406 469
407 470 def extract_ref_id(ref):
@@ -413,28 +476,44 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
413 476 return False
414 477 return True
415 478
416 async def process_content_chunk(content: str, chunk_id: str, line_count: int):
417 """Process single content chunk"""
418 nonlocal is_thinking, thinking_content, normal_content, session
419
479 async def process_content_chunk(content: str, chunk_id: str, line_count: int, *, for_target: bool = False):
480 """
481 Process a single content chunk from a stream.
482
483 - If for_target=True → chunk belongs to the target model output.
484 """
485 nonlocal is_thinking, thinking_content, normal_content, variant_text, session
486
420 487 if not is_valid_content(content):
421 488 return
422
489
490 # Handle image-gen chunks
423 491 if '<yapp class="image-gen">' in content:
424 content = content.split('<yapp class="image-gen">').pop().split('</yapp>')[0]
492 img_block = content.split('<yapp class="image-gen">').pop().split('</yapp>')[0]
425 493 url = "https://yupp.ai/api/trpc/chat.getSignedImage"
426 async with session.get(url, params={"batch": "1", "input": json.dumps({"0": {"json": {"imageId": json.loads(content).get("image_id")}}})}) as resp:
494 async with session.get(
495 url,
496 params={
497 "batch": "1",
498 "input": json.dumps(
499 {"0": {"json": {"imageId": json.loads(img_block).get("image_id")}}}
500 )
501 }
502 ) as resp:
427 503 resp.raise_for_status()
428 504 data = await resp.json()
429 yield ImageResponse(data[0]["result"]["data"]["json"]["signed_url"], prompt)
505 img = ImageResponse(
506 data[0]["result"]["data"]["json"]["signed_url"],
507 prompt
508 )
509 yield img
430 510 return
431
432 # log_debug(f"Processing chunk #{line_count} with content: '{content[:50]}...'")
433
511 # Optional: thinking-mode support (disabled by default)
434 512 if is_thinking:
435 513 yield Reasoning(content)
436 514 else:
437 normal_content += content
515 if for_target:
516 normal_content += content
438 517 yield content
439 518
440 519 try:
@@ -443,15 +522,23 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
443 522 variant_stream_id = None
444 523 is_started: bool = False
445 524 variant_image: Optional[ImageResponse] = None
446 variant_text = ""
447
525 # "a" use as default then extract from "1"
526 reward_id = "a"
527 routing_id = "e"
528 turn_id = None
529 persisted_turn_id = None
530 left_message_id = None
531 right_message_id = None
532 nudge_new_chat_id = None
533 nudge_new_chat = False
534
448 535 async for line in response_content:
449 536 line_count += 1
450 537
451 538 match = line_pattern.match(line)
452 539 if not match:
453 540 continue
454
541
455 542 chunk_id, chunk_data = match.groups()
456 543 chunk_id = chunk_id.decode()
457 544
@@ -459,9 +546,8 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
459 546 data = json.loads(chunk_data) if chunk_data != b"{}" else {}
460 547 except json.JSONDecodeError:
461 548 continue
462
463 549 # Process reward info
464 if chunk_id == "a":
550 if chunk_id == reward_id and isinstance(data, dict) and "unclaimedRewardInfo" in data:
465 551 reward_info = data
466 552 log_debug(f"Found reward info")
467 553
@@ -471,14 +557,29 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
471 557 if isinstance(data, dict):
472 558 left_stream = data.get("leftStream", {})
473 559 right_stream = data.get("rightStream", {})
474 quick_response_id = extract_ref_id(data.get("quickResponse", {}).get("stream", {}).get("next"))
560 if data.get("quickResponse", {}) != "$undefined":
561 quick_response_id = extract_ref_id(data.get("quickResponse", {}).get("stream", {}).get("next"))
562
563 if data.get("turnId", {}) != "$undefined":
564 turn_id = extract_ref_id(data.get("turnId", {}).get("next"))
565 if data.get("persistedTurn", {}) != "$undefined":
566 persisted_turn_id = extract_ref_id(data.get("persistedTurn", {}).get("next"))
567 if data.get("leftMessageId", {}) != "$undefined":
568 left_message_id = extract_ref_id(data.get("leftMessageId", {}).get("next"))
569 if data.get("rightMessageId", {}) != "$undefined":
570 right_message_id = extract_ref_id(data.get("rightMessageId", {}).get("next"))
571
572 reward_id = extract_ref_id(data.get("pendingRewardActionResult", "")) or reward_id
573 routing_id = extract_ref_id(data.get("routingResultPromise", "")) or routing_id
574 nudge_new_chat_id = extract_ref_id(data.get("nudgeNewChatPromise", "")) or nudge_new_chat_id
475 575 select_stream = [left_stream, right_stream]
476
477 elif chunk_id == "e":
576 # Routing / model selection block
577 elif chunk_id == routing_id:
478 578 yield PlainTextResponse(line.decode(errors="ignore"))
479 579 if isinstance(data, dict):
480 580 provider_info = cls.get_dict()
481 581 provider_info['model'] = model_id
582 # Determine target & variant stream IDs
482 583 for i, selection in enumerate(data.get("modelSelections", [])):
483 584 if selection.get("selectionSource") == "USER_SELECTED":
484 585 target_stream_id = extract_ref_id(select_stream[i].get("next"))
@@ -499,41 +600,80 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
499 600 target_stream_id = extract_ref_id(data.get("next"))
500 601 content = data.get("curr", "")
501 602 if content:
502 async for chunk in process_content_chunk(content, chunk_id, line_count):
603 async for chunk in process_content_chunk(
604 content,
605 chunk_id,
606 line_count,
607 for_target=True
608 ):
609 stream["target"].append(chunk)
503 610 is_started = True
504 611 yield chunk
505
612 # Variant stream (comparison)
506 613 elif variant_stream_id and chunk_id == variant_stream_id:
507 614 yield PlainTextResponse("[Variant] " + line.decode(errors="ignore"))
508 615 if isinstance(data, dict):
509 616 variant_stream_id = extract_ref_id(data.get("next"))
510 617 content = data.get("curr", "")
511 618 if content:
512 async for chunk in process_content_chunk(content, chunk_id, line_count):
619 async for chunk in process_content_chunk(
620 content,
621 chunk_id,
622 line_count,
623 for_target=False
624 ):
625 stream["variant"].append(chunk)
513 626 if isinstance(chunk, ImageResponse):
514 627 yield PreviewResponse(str(chunk))
515 628 else:
516 629 variant_text += str(chunk)
517 630 if not is_started:
518 631 yield PreviewResponse(variant_text)
519
632 # Quick response (short preview)
520 633 elif quick_response_id and chunk_id == quick_response_id:
521 634 yield PlainTextResponse("[Quick] " + line.decode(errors="ignore"))
522 635 if isinstance(data, dict):
523 636 content = data.get("curr", "")
524 637 if content:
638 async for chunk in process_content_chunk(
639 content,
640 chunk_id,
641 line_count,
642 for_target=False
643 ):
644 stream["quick"].append(chunk)
645 quick_content += content
525 646 yield PreviewResponse(content)
526 647
648 elif chunk_id in [turn_id, persisted_turn_id]:
649 ...
650 elif chunk_id == right_message_id:
651 ...
652 elif chunk_id == left_message_id:
653 ...
654 elif chunk_id == nudge_new_chat_id:
655 nudge_new_chat = data
656 # Miscellaneous extra content
527 657 elif isinstance(data, dict) and "curr" in data:
528 658 content = data.get("curr", "")
529 659 if content:
660 async for chunk in process_content_chunk(
661 content,
662 chunk_id,
663 line_count,
664 for_target=False
665 ):
666 stream["extra"].append(chunk)
667 if isinstance(chunk,str) and "<streaming stopped unexpectedly" in chunk:
668 yield FinishReason(chunk)
669
530 670 yield PlainTextResponse("[Extra] " + line.decode(errors="ignore"))
531 671
532 672 if variant_image is not None:
533 673 yield variant_image
534 674 elif variant_text:
535 675 yield PreviewResponse(variant_text)
536
676 yield JsonResponse(**stream)
537 677 log_debug(f"Finished processing {line_count} lines")
538 678
539 679 except: