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

XFEstudio/gpt4free

Implement automatic NextAction token swapping for Yupp provider (#3345)

* WIP: Implement automatic NextAction token swapping for Yupp provider This PR adds automatic token extraction and swapping when NextAction tokens expire. - Add token_extractor.py with multi-strategy extraction - Add constants.py with configuration - Integrate into Yupp.py with automatic failure detection - Tested and verified working * fix: Add 404 detection to token failure handlers - Detect token failures in generic exception handler - Improves extraction triggering for invalid/expired tokens - Verified working with invalid token test * refactor: Address code review feedback - Remove unused EXTRACTION_COOLDOWN constant - Add MIN_REQUIRED_TOKENS constant - Fix race condition in mark_token_failed() - Remove unused hashlib import - Add type hints for scraper parameter - Add debug stack trace logging for exceptions - Fix line length issues - Reorder imports to PEP 8 - Update docstring for get_token() - Standardize error logging format * style: Remove duplicate docstring and unused import - Remove duplicate module docstring - Remove unused 'time' import * fix: Remove gpt-oss-120b from DeepInfra DeepInfra no longer lists openai/gpt-oss-120b in their featured models API. This fixes the failing test test_provider_has_model. - Removed from DeepInfra.vision_models - Removed from gpt_oss_120b best_provider list

80edb02b
Max Luecke <max@plainskill.net>
提交于

代码差异

5 个文件 +723 -155
Modified g4f/Provider/DeepInfra.py +0 -2
@@ -17,8 +17,6 @@ class DeepInfra(OpenaiTemplate):
17 17 vision_models = [
18 18 default_vision_model,
19 19 'meta-llama/Llama-3.2-90B-Vision-Instruct',
20 'openai/gpt-oss-120b',
21 'openai/gpt-oss-20b',
22 20 ]
23 21
24 22 model_aliases = {
Modified g4f/Provider/Yupp.py +305 -152
@@ -11,16 +11,24 @@ from typing import Optional, Dict, Any, List
11 11 try:
12 12 import cloudscraper
13 13 from cloudscraper import CloudScraper
14
14 15 has_cloudscraper = True
15 16 except ImportError:
16 17 from typing import Type as CloudScraper
18
17 19 has_cloudscraper = False
18 20
19 21 from .helper import get_last_user_message
20 22 from .yupp.models import YuppModelManager
23 from .yupp.token_extractor import get_token_extractor
21 24 from ..cookies import get_cookies
22 25 from ..debug import log
23 from ..errors import RateLimitError, ProviderException, MissingAuthError, MissingRequirementsError
26 from ..errors import (
27 RateLimitError,
28 ProviderException,
29 MissingAuthError,
30 MissingRequirementsError,
31 )
24 32 from ..image import is_accepted_format, to_bytes
25 33 from ..providers.base_provider import AsyncGeneratorProvider, ProviderModelMixin
26 34 from ..providers.response import (
@@ -49,25 +57,27 @@ MAX_CACHE_SIZE = 1000
49 57 def create_scraper():
50 58 scraper = cloudscraper.create_scraper(
51 59 browser={
52 'browser': 'chrome',
53 'platform': 'windows',
54 'desktop': True,
55 'mobile': False
60 "browser": "chrome",
61 "platform": "windows",
62 "desktop": True,
63 "mobile": False,
56 64 },
57 65 delay=10,
58 interpreter='nodejs'
66 interpreter="nodejs",
67 )
68 scraper.headers.update(
69 {
70 "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",
71 "Accept": "text/x-component, */*",
72 "Accept-Language": "en-US,en;q=0.9",
73 "Sec-Fetch-Dest": "empty",
74 "Sec-Fetch-Mode": "cors",
75 "Sec-Fetch-Site": "same-origin",
76 "Sec-Ch-Ua": '"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
77 "Sec-Ch-Ua-Mobile": "?0",
78 "Sec-Ch-Ua-Platform": '"Windows"',
79 }
59 80 )
60 scraper.headers.update({
61 "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",
62 "Accept": "text/x-component, */*",
63 "Accept-Language": "en-US,en;q=0.9",
64 "Sec-Fetch-Dest": "empty",
65 "Sec-Fetch-Mode": "cors",
66 "Sec-Fetch-Site": "same-origin",
67 "Sec-Ch-Ua": '"Microsoft Edge";v="137", "Chromium";v="137", "Not/A)Brand";v="24"',
68 "Sec-Ch-Ua-Mobile": "?0",
69 "Sec-Ch-Ua-Platform": '"Windows"'
70 })
71 81 return scraper
72 82
73 83
@@ -77,14 +87,9 @@ def load_yupp_accounts(tokens_str: str):
77 87 return
78 88 if not tokens_str:
79 89 return
80 tokens = [token.strip() for token in tokens_str.split(',') if token.strip()]
90 tokens = [token.strip() for token in tokens_str.split(",") if token.strip()]
81 91 YUPP_ACCOUNTS = [
82 {
83 "token": token,
84 "is_valid": True,
85 "error_count": 0,
86 "last_used": 0.0
87 }
92 {"token": token, "is_valid": True, "error_count": 0, "last_used": 0.0}
88 93 for token in tokens
89 94 ]
90 95 _accounts_loaded = True
@@ -122,12 +127,14 @@ async def get_best_yupp_account() -> Optional[Dict[str, Any]]:
122 127 return account
123 128
124 129
125 def sync_claim_yupp_reward(scraper: CloudScraper, account: Dict[str, Any], eval_id: str):
130 def sync_claim_yupp_reward(
131 scraper: CloudScraper, account: Dict[str, Any], eval_id: str
132 ):
126 133 try:
127 134 log_debug(f"Claiming reward {eval_id}...")
128 135 url = "https://yupp.ai/api/trpc/reward.claim?batch=1"
129 136 payload = {"0": {"json": {"evalId": eval_id}}}
130 scraper.cookies.set("__Secure-yupp.session-token", account['token'])
137 scraper.cookies.set("__Secure-yupp.session-token", account["token"])
131 138 response = scraper.post(url, json=payload)
132 139 response.raise_for_status()
133 140 data = response.json()
@@ -139,9 +146,13 @@ def sync_claim_yupp_reward(scraper: CloudScraper, account: Dict[str, Any], eval_
139 146 return None
140 147
141 148
142 async def claim_yupp_reward(scraper: CloudScraper, account: Dict[str, Any], eval_id: str):
149 async def claim_yupp_reward(
150 scraper: CloudScraper, account: Dict[str, Any], eval_id: str
151 ):
143 152 loop = asyncio.get_event_loop()
144 return await loop.run_in_executor(_executor, sync_claim_yupp_reward, scraper, account, eval_id)
153 return await loop.run_in_executor(
154 _executor, sync_claim_yupp_reward, scraper, account, eval_id
155 )
145 156
146 157
147 158 def sync_record_model_feedback(
@@ -149,7 +160,7 @@ def sync_record_model_feedback(
149 160 account: Dict[str, Any],
150 161 turn_id: str,
151 162 left_message_id: str,
152 right_message_id: str
163 right_message_id: str,
153 164 ) -> Optional[str]:
154 165 try:
155 166 log_debug(f"Recording model feedback for turn {turn_id}...")
@@ -163,20 +174,16 @@ def sync_record_model_feedback(
163 174 {
164 175 "messageId": right_message_id,
165 176 "rating": "GOOD",
166 "reasons": ["Fast"]
177 "reasons": ["Fast"],
167 178 },
168 {
169 "messageId": left_message_id,
170 "rating": "BAD",
171 "reasons": []
172 }
179 {"messageId": left_message_id, "rating": "BAD", "reasons": []},
173 180 ],
174 181 "comment": "",
175 "requireReveal": False
182 "requireReveal": False,
176 183 }
177 184 }
178 185 }
179 scraper.cookies.set("__Secure-yupp.session-token", account['token'])
186 scraper.cookies.set("__Secure-yupp.session-token", account["token"])
180 187 response = scraper.post(url, json=payload)
181 188 response.raise_for_status()
182 189 data = response.json()
@@ -200,25 +207,34 @@ async def record_model_feedback(
200 207 account: Dict[str, Any],
201 208 turn_id: str,
202 209 left_message_id: str,
203 right_message_id: str
210 right_message_id: str,
204 211 ) -> Optional[str]:
205 212 loop = asyncio.get_event_loop()
206 213 return await loop.run_in_executor(
207 _executor, sync_record_model_feedback, scraper, account, turn_id, left_message_id, right_message_id
214 _executor,
215 sync_record_model_feedback,
216 scraper,
217 account,
218 turn_id,
219 left_message_id,
220 right_message_id,
208 221 )
209 222
210 223
211 def sync_delete_chat(scraper: CloudScraper, account: Dict[str, Any], chat_id: str) -> bool:
224 def sync_delete_chat(
225 scraper: CloudScraper, account: Dict[str, Any], chat_id: str
226 ) -> bool:
212 227 try:
213 228 log_debug(f"Deleting chat {chat_id}...")
214 229 url = "https://yupp.ai/api/trpc/chat.deleteChat?batch=1"
215 230 payload = {"0": {"json": {"chatId": chat_id}}}
216 scraper.cookies.set("__Secure-yupp.session-token", account['token'])
231 scraper.cookies.set("__Secure-yupp.session-token", account["token"])
217 232 response = scraper.post(url, json=payload)
218 233 response.raise_for_status()
219 234 data = response.json()
220 235 if (
221 isinstance(data, list) and len(data) > 0
236 isinstance(data, list)
237 and len(data) > 0
222 238 and data[0].get("result", {}).get("data", {}).get("json") is None
223 239 ):
224 240 log_debug(f"Chat {chat_id} deleted successfully")
@@ -230,29 +246,29 @@ def sync_delete_chat(scraper: CloudScraper, account: Dict[str, Any], chat_id: st
230 246 return False
231 247
232 248
233 async def delete_chat(scraper: CloudScraper, account: Dict[str, Any], chat_id: str) -> bool:
249 async def delete_chat(
250 scraper: CloudScraper, account: Dict[str, Any], chat_id: str
251 ) -> bool:
234 252 loop = asyncio.get_event_loop()
235 return await loop.run_in_executor(_executor, sync_delete_chat, scraper, account, chat_id)
253 return await loop.run_in_executor(
254 _executor, sync_delete_chat, scraper, account, chat_id
255 )
236 256
237 257
238 def sync_make_chat_private(scraper: CloudScraper, account: Dict[str, Any], chat_id: str) -> bool:
258 def sync_make_chat_private(
259 scraper: CloudScraper, account: Dict[str, Any], chat_id: str
260 ) -> bool:
239 261 try:
240 262 log_debug(f"Setting chat {chat_id} to PRIVATE...")
241 263 url = "https://yupp.ai/api/trpc/chat.updateSharingSettings?batch=1"
242 payload = {
243 "0": {
244 "json": {
245 "chatId": chat_id,
246 "status": "PRIVATE"
247 }
248 }
249 }
250 scraper.cookies.set("__Secure-yupp.session-token", account['token'])
264 payload = {"0": {"json": {"chatId": chat_id, "status": "PRIVATE"}}}
265 scraper.cookies.set("__Secure-yupp.session-token", account["token"])
251 266 response = scraper.post(url, json=payload)
252 267 response.raise_for_status()
253 268 data = response.json()
254 269 if (
255 isinstance(data, list) and len(data) > 0
270 isinstance(data, list)
271 and len(data) > 0
256 272 and "json" in data[0].get("result", {}).get("data", {})
257 273 ):
258 274 log_debug(f"Chat {chat_id} is now PRIVATE")
@@ -264,9 +280,13 @@ def sync_make_chat_private(scraper: CloudScraper, account: Dict[str, Any], chat_
264 280 return False
265 281
266 282
267 async def make_chat_private(scraper: CloudScraper, account: Dict[str, Any], chat_id: str) -> bool:
283 async def make_chat_private(
284 scraper: CloudScraper, account: Dict[str, Any], chat_id: str
285 ) -> bool:
268 286 loop = asyncio.get_event_loop()
269 return await loop.run_in_executor(_executor, sync_make_chat_private, scraper, account, chat_id)
287 return await loop.run_in_executor(
288 _executor, sync_make_chat_private, scraper, account, chat_id
289 )
270 290
271 291
272 292 def log_debug(message: str):
@@ -285,13 +305,17 @@ def format_messages_for_yupp(messages: Messages) -> str:
285 305
286 306 formatted = []
287 307
288 system_messages = [msg for msg in messages if msg.get("role") in ["developer", "system"]]
308 system_messages = [
309 msg for msg in messages if msg.get("role") in ["developer", "system"]
310 ]
289 311 if system_messages:
290 312 for sys_msg in system_messages:
291 313 content = sys_msg.get("content", "")
292 314 formatted.append(content)
293 315
294 user_assistant_msgs = [msg for msg in messages if msg.get("role") in ["user", "assistant"]]
316 user_assistant_msgs = [
317 msg for msg in messages if msg.get("role") in ["user", "assistant"]
318 ]
295 319 for msg in user_assistant_msgs:
296 320 role = "Human" if msg.get("role") == "user" else "Assistant"
297 321 content = msg.get("content", "")
@@ -312,7 +336,9 @@ def format_messages_for_yupp(messages: Messages) -> str:
312 336 def evict_cache_if_needed():
313 337 global ImagesCache
314 338 if len(ImagesCache) > MAX_CACHE_SIZE:
315 keys_to_remove = list(ImagesCache.keys())[:len(ImagesCache) - MAX_CACHE_SIZE + 100]
339 keys_to_remove = list(ImagesCache.keys())[
340 : len(ImagesCache) - MAX_CACHE_SIZE + 100
341 ]
316 342 for key in keys_to_remove:
317 343 del ImagesCache[key]
318 344
@@ -331,23 +357,37 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
331 357 if not api_key:
332 358 api_key = AuthManager.load_api_key(cls)
333 359 if not api_key:
334 api_key = get_cookies("yupp.ai", False).get("__Secure-yupp.session-token")
360 api_key = get_cookies("yupp.ai", False).get(
361 "__Secure-yupp.session-token"
362 )
335 363 if not api_key:
336 raise MissingAuthError("No Yupp accounts configured. Set YUPP_API_KEY environment variable.")
364 raise MissingAuthError(
365 "No Yupp accounts configured. Set YUPP_API_KEY environment variable."
366 )
337 367 manager = YuppModelManager(api_key=api_key, session=create_scraper())
338 368 models = manager.client.fetch_models()
339 369 if models:
340 cls.models_tags = {model.get("name"): manager.processor.generate_tags(model) for model in models}
370 cls.models_tags = {
371 model.get("name"): manager.processor.generate_tags(model)
372 for model in models
373 }
341 374 cls.models = [model.get("name") for model in models]
342 cls.image_models = [model.get("name") for model in models if model.get("isImageGeneration")]
375 cls.image_models = [
376 model.get("name")
377 for model in models
378 if model.get("isImageGeneration")
379 ]
343 380 cls.vision_models = [
344 model.get("name") for model in models
381 model.get("name")
382 for model in models
345 383 if "image/*" in model.get("supportedAttachmentMimeTypes", [])
346 384 ]
347 385 return cls.models
348 386
349 387 @classmethod
350 def sync_prepare_files(cls, media, scraper: CloudScraper, account: Dict[str, Any]) -> list:
388 def sync_prepare_files(
389 cls, media, scraper: CloudScraper, account: Dict[str, Any]
390 ) -> list:
351 391 files = []
352 392 if not media:
353 393 return files
@@ -362,13 +402,19 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
362 402 files.append(cached_file)
363 403 continue
364 404
365 scraper.cookies.set("__Secure-yupp.session-token", account['token'])
405 scraper.cookies.set("__Secure-yupp.session-token", account["token"])
366 406 presigned_resp = scraper.post(
367 407 "https://yupp.ai/api/trpc/chat.createPresignedURLForUpload?batch=1",
368 408 json={
369 "0": {"json": {"fileName": name, "fileSize": len(data), "contentType": is_accepted_format(data)}}
409 "0": {
410 "json": {
411 "fileName": name,
412 "fileSize": len(data),
413 "contentType": is_accepted_format(data),
414 }
415 }
370 416 },
371 headers={"Content-Type": "application/json"}
417 headers={"Content-Type": "application/json"},
372 418 )
373 419 presigned_resp.raise_for_status()
374 420 upload_info = presigned_resp.json()[0]["result"]["data"]["json"]
@@ -379,8 +425,8 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
379 425 data=data,
380 426 headers={
381 427 "Content-Type": is_accepted_format(data),
382 "Content-Length": str(len(data))
383 }
428 "Content-Length": str(len(data)),
429 },
384 430 )
385 431
386 432 attachment_resp = scraper.post(
@@ -390,11 +436,11 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
390 436 "json": {
391 437 "fileName": name,
392 438 "contentType": is_accepted_format(data),
393 "fileId": upload_info["fileId"]
439 "fileId": upload_info["fileId"],
394 440 }
395 441 }
396 442 },
397 cookies={"__Secure-yupp.session-token": account["token"]}
443 cookies={"__Secure-yupp.session-token": account["token"]},
398 444 )
399 445 attachment_resp.raise_for_status()
400 446 attachment = attachment_resp.json()[0]["result"]["data"]["json"]
@@ -402,7 +448,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
402 448 "fileName": attachment["file_name"],
403 449 "contentType": attachment["content_type"],
404 450 "attachmentId": attachment["attachment_id"],
405 "chatMessageId": ""
451 "chatMessageId": "",
406 452 }
407 453 evict_cache_if_needed()
408 454 ImagesCache[image_hash] = file_info
@@ -410,9 +456,13 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
410 456 return files
411 457
412 458 @classmethod
413 async def prepare_files(cls, media, scraper: CloudScraper, account: Dict[str, Any]) -> list:
459 async def prepare_files(
460 cls, media, scraper: CloudScraper, account: Dict[str, Any]
461 ) -> list:
414 462 loop = asyncio.get_event_loop()
415 return await loop.run_in_executor(_executor, cls.sync_prepare_files, media, scraper, account)
463 return await loop.run_in_executor(
464 _executor, cls.sync_prepare_files, media, scraper, account
465 )
416 466
417 467 @classmethod
418 468 def sync_get_signed_image(cls, scraper: CloudScraper, image_id: str) -> str:
@@ -421,10 +471,8 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
421 471 url,
422 472 params={
423 473 "batch": "1",
424 "input": json.dumps(
425 {"0": {"json": {"imageId": image_id}}}
426 )
427 }
474 "input": json.dumps({"0": {"json": {"imageId": image_id}}}),
475 },
428 476 )
429 477 resp.raise_for_status()
430 478 data = resp.json()[0]["result"]["data"]["json"]
@@ -433,11 +481,17 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
433 481 @classmethod
434 482 async def get_signed_image(cls, scraper: CloudScraper, image_id: str) -> str:
435 483 loop = asyncio.get_event_loop()
436 return await loop.run_in_executor(_executor, cls.sync_get_signed_image, scraper, image_id)
484 return await loop.run_in_executor(
485 _executor, cls.sync_get_signed_image, scraper, image_id
486 )
437 487
438 488 @classmethod
439 def sync_stream_request(cls, scraper: CloudScraper, url: str, payload: list, headers: dict, timeout: int):
440 response = scraper.post(url, json=payload, headers=headers, stream=True, timeout=timeout)
489 def sync_stream_request(
490 cls, scraper: CloudScraper, url: str, payload: list, headers: dict, timeout: int
491 ):
492 response = scraper.post(
493 url, json=payload, headers=headers, stream=True, timeout=timeout
494 )
441 495 response.raise_for_status()
442 496 return response
443 497
@@ -462,7 +516,9 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
462 516 load_yupp_accounts(api_key)
463 517 log_debug(f"Yupp provider initialized with {len(YUPP_ACCOUNTS)} accounts")
464 518 else:
465 raise MissingAuthError("No Yupp accounts configured. Set YUPP_API_KEY environment variable.")
519 raise MissingAuthError(
520 "No Yupp accounts configured. Set YUPP_API_KEY environment variable."
521 )
466 522
467 523 conversation = kwargs.get("conversation")
468 524 url_uuid = conversation.url_uuid if conversation else None
@@ -488,17 +544,21 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
488 544 try:
489 545 scraper = create_scraper()
490 546 if proxy:
491 scraper.proxies = {
492 "http": proxy,
493 "https": proxy
494 }
547 scraper.proxies = {"http": proxy, "https": proxy}
548
549 # Initialize token extractor for automatic token swapping
550 token_extractor = get_token_extractor(
551 jwt_token=account["token"], scraper=scraper
552 )
495 553
496 554 turn_id = str(uuid.uuid4())
497 555
498 556 media = kwargs.get("media")
499 557 if media:
500 558 media_ = list(merge_media(media, messages))
501 files = await cls.prepare_files(media_, scraper=scraper, account=account)
559 files = await cls.prepare_files(
560 media_, scraper=scraper, account=account
561 )
502 562 else:
503 563 files = []
504 564
@@ -514,14 +574,19 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
514 574 "$undefined",
515 575 files,
516 576 "$undefined",
517 [{"modelName": model, "promptModifierId": "$undefined"}] if model else "none",
577 [{"modelName": model, "promptModifierId": "$undefined"}]
578 if model
579 else "none",
518 580 mode,
519 581 True,
520 582 "$undefined",
521 583 ]
522 584 url = f"https://yupp.ai/chat/{url_uuid}?stream=true"
523 585 yield JsonConversation(url_uuid=url_uuid)
524 next_action = kwargs.get("next_action", "7f7de0a21bc8dc3cee8ba8b6de632ff16f769649dd")
586 next_action = kwargs.get(
587 "next_action",
588 await token_extractor.get_token("new_conversation"),
589 )
525 590 else:
526 591 payload = [
527 592 url_uuid,
@@ -529,12 +594,17 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
529 594 prompt,
530 595 False,
531 596 [],
532 [{"modelName": model, "promptModifierId": "$undefined"}] if model else [],
597 [{"modelName": model, "promptModifierId": "$undefined"}]
598 if model
599 else [],
533 600 mode,
534 files
601 files,
535 602 ]
536 603 url = f"https://yupp.ai/chat/{url_uuid}?stream=true"
537 next_action = kwargs.get("next_action", "7f9ec99a63cbb61f69ef18c0927689629bda07f1bf")
604 next_action = kwargs.get(
605 "next_action",
606 await token_extractor.get_token("existing_conversation"),
607 )
538 608
539 609 headers = {
540 610 "accept": "text/x-component",
@@ -544,7 +614,9 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
544 614 }
545 615
546 616 log_debug(f"Sending request to: {url}")
547 log_debug(f"Payload structure: {type(payload)}, length: {len(str(payload))}")
617 log_debug(
618 f"Payload structure: {type(payload)}, length: {len(str(payload))}"
619 )
548 620
549 621 _timeout = kwargs.get("timeout")
550 622 if isinstance(_timeout, (int, float)):
@@ -560,11 +632,13 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
560 632 url,
561 633 payload,
562 634 headers,
563 timeout
635 timeout,
564 636 )
565 637
566 638 try:
567 async for chunk in cls._process_stream_response(response, account, scraper, prompt, model):
639 async for chunk in cls._process_stream_response(
640 response, account, scraper, prompt, model
641 ):
568 642 yield chunk
569 643 finally:
570 644 response.close()
@@ -573,23 +647,66 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
573 647 return
574 648
575 649 except RateLimitError:
576 log_debug(f"Account ...{account['token'][-4:]} hit rate limit, rotating")
650 log_debug(
651 f"Account ...{account['token'][-4:]} hit rate limit, rotating"
652 )
577 653 async with account_rotation_lock:
578 654 account["error_count"] += 1
579 655 continue
580 656
581 657 except ProviderException as e:
582 658 log_debug(f"Account ...{account['token'][-4:]} failed: {str(e)}")
659 error_msg = str(e).lower()
660
661 # Check if this is a token-related error
662 if any(
663 x in error_msg
664 for x in [
665 "auth",
666 "401",
667 "403",
668 "404",
669 "invalid action",
670 "action",
671 "next-action",
672 ]
673 ):
674 # Mark token as failed to trigger extraction
675 token_type = (
676 "new_conversation"
677 if is_new_conversation
678 else "existing_conversation"
679 )
680 await token_extractor.mark_token_failed(token_type, next_action)
681 log_debug(
682 f"Token failure detected, marked for extraction: {token_type}"
683 )
684
583 685 async with account_rotation_lock:
584 if "auth" in str(e).lower() or "401" in str(e) or "403" in str(e):
686 if "auth" in error_msg or "401" in error_msg or "403" in error_msg:
585 687 account["is_valid"] = False
586 688 else:
587 689 account["error_count"] += 1
588 690 continue
589 691
590 692 except Exception as e:
591 log_debug(f"Unexpected error with account ...{account['token'][-4:]}: {str(e)}")
693 log_debug(
694 f"Unexpected error with account ...{account['token'][-4:]}: {str(e)}"
695 )
592 696 error_str = str(e).lower()
697
698 # Check for token-related errors in generic exceptions too
699 if any(x in error_str for x in ["404", "401", "403", "invalid action"]):
700 token_type = (
701 "new_conversation"
702 if is_new_conversation
703 else "existing_conversation"
704 )
705 await token_extractor.mark_token_failed(token_type, next_action)
706 log_debug(
707 f"Token failure detected in exception handler: {token_type}"
708 )
709
593 710 if "500" in error_str or "internal server error" in error_str:
594 711 async with account_rotation_lock:
595 712 account["error_count"] += 1
@@ -607,7 +724,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
607 724 account: Dict[str, Any],
608 725 scraper: CloudScraper,
609 726 prompt: str,
610 model_id: str
727 model_id: str,
611 728 ) -> AsyncResult:
612 729 line_pattern = re.compile(b"^([0-9a-fA-F]+):(.*)")
613 730 target_stream_id = None
@@ -617,13 +734,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
617 734 normal_content = ""
618 735 quick_content = ""
619 736 variant_text = ""
620 stream = {
621 "target": [],
622 "variant": [],
623 "quick": [],
624 "thinking": [],
625 "extra": []
626 }
737 stream = {"target": [], "variant": [], "quick": [], "thinking": [], "extra": []}
627 738 select_stream = [None, None]
628 739 capturing_ref_id: Optional[str] = None
629 740 capturing_lines: List[bytes] = []
@@ -631,7 +742,11 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
631 742 image_blocks: Dict[str, str] = {}
632 743
633 744 def extract_ref_id(ref):
634 return ref[2:] if ref and isinstance(ref, str) and ref.startswith("$@") else None
745 return (
746 ref[2:]
747 if ref and isinstance(ref, str) and ref.startswith("$@")
748 else None
749 )
635 750
636 751 def extract_ref_name(ref: str) -> Optional[str]:
637 752 if not isinstance(ref, str):
@@ -647,14 +762,18 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
647 762 return False
648 763 return True
649 764
650 async def process_content_chunk(content: str, chunk_id: str, line_count: int, *, for_target: bool = False):
765 async def process_content_chunk(
766 content: str, chunk_id: str, line_count: int, *, for_target: bool = False
767 ):
651 768 nonlocal normal_content
652 769
653 770 if not is_valid_content(content):
654 771 return
655 772
656 773 if '<yapp class="image-gen">' in content:
657 img_block = content.split('<yapp class="image-gen">').pop().split('</yapp>')[0]
774 img_block = (
775 content.split('<yapp class="image-gen">').pop().split("</yapp>")[0]
776 )
658 777 image_id = json.loads(img_block).get("image_id")
659 778 signed_url = await cls.get_signed_image(scraper, image_id)
660 779 img = ImageResponse(signed_url, prompt)
@@ -674,7 +793,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
674 793 think_start = text.find("<think>")
675 794 think_end = text.find("</think>")
676 795 if think_start != -1 and think_end != -1 and think_end > think_start:
677 inner = text[think_start + len("<think>"):think_end].strip()
796 inner = text[think_start + len("<think>") : think_end].strip()
678 797 if inner:
679 798 think_blocks[ref_id] = inner
680 799
@@ -682,7 +801,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
682 801 if yapp_start != -1:
683 802 yapp_end = text.find("</yapp>", yapp_start)
684 803 if yapp_end != -1:
685 yapp_block = text[yapp_start:yapp_end + len("</yapp>")]
804 yapp_block = text[yapp_start : yapp_end + len("</yapp>")]
686 805 image_blocks[ref_id] = yapp_block
687 806
688 807 try:
@@ -712,7 +831,9 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
712 831
713 832 while True:
714 833 try:
715 line = await loop.run_in_executor(_executor, lambda: next(lines_iterator, None))
834 line = await loop.run_in_executor(
835 _executor, lambda: next(lines_iterator, None)
836 )
716 837 if line is None:
717 838 break
718 839 except StopIteration:
@@ -728,7 +849,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
728 849
729 850 if b"</yapp>" in line:
730 851 idx = line.find(b"</yapp>")
731 suffix = line[idx + len(b"</yapp>"):]
852 suffix = line[idx + len(b"</yapp>") :]
732 853 finalize_capture_block(capturing_ref_id, capturing_lines)
733 854 capturing_ref_id = None
734 855 capturing_lines = []
@@ -762,7 +883,11 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
762 883 except json.JSONDecodeError:
763 884 continue
764 885
765 if chunk_id == reward_id and isinstance(data, dict) and "unclaimedRewardInfo" in data:
886 if (
887 chunk_id == reward_id
888 and isinstance(data, dict)
889 and "unclaimedRewardInfo" in data
890 ):
766 891 reward_info = data
767 892 log_debug(f"Found reward info")
768 893
@@ -773,39 +898,68 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
773 898 right_stream = data.get("rightStream", {})
774 899 if data.get("quickResponse", {}) != "$undefined":
775 900 quick_response_id = extract_ref_id(
776 data.get("quickResponse", {}).get("stream", {}).get("next")
901 data.get("quickResponse", {})
902 .get("stream", {})
903 .get("next")
777 904 )
778 905
779 906 if data.get("turnId", {}) != "$undefined":
780 907 turn_id = extract_ref_id(data.get("turnId", {}).get("next"))
781 908 if data.get("persistedTurn", {}) != "$undefined":
782 persisted_turn_id = extract_ref_id(data.get("persistedTurn", {}).get("next"))
909 persisted_turn_id = extract_ref_id(
910 data.get("persistedTurn", {}).get("next")
911 )
783 912 if data.get("leftMessageId", {}) != "$undefined":
784 left_message_id = extract_ref_id(data.get("leftMessageId", {}).get("next"))
913 left_message_id = extract_ref_id(
914 data.get("leftMessageId", {}).get("next")
915 )
785 916 if data.get("rightMessageId", {}) != "$undefined":
786 right_message_id = extract_ref_id(data.get("rightMessageId", {}).get("next"))
917 right_message_id = extract_ref_id(
918 data.get("rightMessageId", {}).get("next")
919 )
787 920
788 reward_id = extract_ref_id(data.get("pendingRewardActionResult", "")) or reward_id
789 routing_id = extract_ref_id(data.get("routingResultPromise", "")) or routing_id
790 nudge_new_chat_id = extract_ref_id(data.get("nudgeNewChatPromise", "")) or nudge_new_chat_id
921 reward_id = (
922 extract_ref_id(data.get("pendingRewardActionResult", ""))
923 or reward_id
924 )
925 routing_id = (
926 extract_ref_id(data.get("routingResultPromise", ""))
927 or routing_id
928 )
929 nudge_new_chat_id = (
930 extract_ref_id(data.get("nudgeNewChatPromise", ""))
931 or nudge_new_chat_id
932 )
791 933 select_stream = [left_stream, right_stream]
792 934
793 935 elif chunk_id == routing_id:
794 936 yield PlainTextResponse(line.decode(errors="ignore"))
795 937 if isinstance(data, dict):
796 938 provider_info = cls.get_dict()
797 provider_info['model'] = model_id
939 provider_info["model"] = model_id
798 940 for i, selection in enumerate(data.get("modelSelections", [])):
799 941 if selection.get("selectionSource") == "USER_SELECTED":
800 target_stream_id = extract_ref_id(select_stream[i].get("next"))
801 provider_info["modelLabel"] = selection.get("shortLabel")
942 target_stream_id = extract_ref_id(
943 select_stream[i].get("next")
944 )
945 provider_info["modelLabel"] = selection.get(
946 "shortLabel"
947 )
802 948 provider_info["modelUrl"] = selection.get("externalUrl")
803 949 log_debug(f"Found target stream ID: {target_stream_id}")
804 950 else:
805 variant_stream_id = extract_ref_id(select_stream[i].get("next"))
806 provider_info["variantLabel"] = selection.get("shortLabel")
807 provider_info["variantUrl"] = selection.get("externalUrl")
808 log_debug(f"Found variant stream ID: {variant_stream_id}")
951 variant_stream_id = extract_ref_id(
952 select_stream[i].get("next")
953 )
954 provider_info["variantLabel"] = selection.get(
955 "shortLabel"
956 )
957 provider_info["variantUrl"] = selection.get(
958 "externalUrl"
959 )
960 log_debug(
961 f"Found variant stream ID: {variant_stream_id}"
962 )
809 963 yield ProviderInfo.from_dict(provider_info)
810 964
811 965 elif target_stream_id and chunk_id == target_stream_id:
@@ -815,7 +969,9 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
815 969 content = data.get("curr", "")
816 970 if content:
817 971 ref_name = extract_ref_name(content)
818 if ref_name and (ref_name in think_blocks or ref_name in image_blocks):
972 if ref_name and (
973 ref_name in think_blocks or ref_name in image_blocks
974 ):
819 975 if ref_name in think_blocks:
820 976 t_text = think_blocks[ref_name]
821 977 if t_text:
@@ -828,17 +984,14 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
828 984 img_block_text,
829 985 ref_name,
830 986 line_count,
831 for_target=True
987 for_target=True,
832 988 ):
833 989 stream["target"].append(chunk)
834 990 is_started = True
835 991 yield chunk
836 992 else:
837 993 async for chunk in process_content_chunk(
838 content,
839 chunk_id,
840 line_count,
841 for_target=True
994 content, chunk_id, line_count, for_target=True
842 995 ):
843 996 stream["target"].append(chunk)
844 997 is_started = True
@@ -851,10 +1004,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
851 1004 content = data.get("curr", "")
852 1005 if content:
853 1006 async for chunk in process_content_chunk(
854 content,
855 chunk_id,
856 line_count,
857 for_target=False
1007 content, chunk_id, line_count, for_target=False
858 1008 ):
859 1009 stream["variant"].append(chunk)
860 1010 if isinstance(chunk, ImageResponse):
@@ -870,10 +1020,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
870 1020 content = data.get("curr", "")
871 1021 if content:
872 1022 async for chunk in process_content_chunk(
873 content,
874 chunk_id,
875 line_count,
876 for_target=False
1023 content, chunk_id, line_count, for_target=False
877 1024 ):
878 1025 stream["quick"].append(chunk)
879 1026 quick_content += content
@@ -895,16 +1042,18 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
895 1042 content = data.get("curr", "")
896 1043 if content:
897 1044 async for chunk in process_content_chunk(
898 content,
899 chunk_id,
900 line_count,
901 for_target=False
1045 content, chunk_id, line_count, for_target=False
902 1046 ):
903 1047 stream["extra"].append(chunk)
904 if isinstance(chunk, str) and "<streaming stopped unexpectedly" in chunk:
1048 if (
1049 isinstance(chunk, str)
1050 and "<streaming stopped unexpectedly" in chunk
1051 ):
905 1052 yield FinishReason(chunk)
906 1053
907 yield PlainTextResponse("[Extra] " + line.decode(errors="ignore"))
1054 yield PlainTextResponse(
1055 "[Extra] " + line.decode(errors="ignore")
1056 )
908 1057
909 1058 if variant_image is not None:
910 1059 yield variant_image
@@ -915,13 +1064,17 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
915 1064
916 1065 finally:
917 1066 log_debug(f"Get Reward: {reward_kw}")
918 if reward_kw.get("turn_id") and reward_kw.get("left_message_id") and reward_kw.get("right_message_id"):
1067 if (
1068 reward_kw.get("turn_id")
1069 and reward_kw.get("left_message_id")
1070 and reward_kw.get("right_message_id")
1071 ):
919 1072 eval_id = await record_model_feedback(
920 1073 scraper,
921 1074 account,
922 1075 reward_kw["turn_id"],
923 1076 reward_kw["left_message_id"],
924 reward_kw["right_message_id"]
1077 reward_kw["right_message_id"],
925 1078 )
926 1079 if eval_id:
927 1080 await claim_yupp_reward(scraper, account, eval_id)
Added g4f/Provider/yupp/constants.py +33 -0
@@ -0,0 +1,33 @@
1 """
2 Constants for Yupp AI NextAction token management
3 """
4
5 # Default fallback tokens (hardcoded)
6 NEXT_ACTION_TOKENS = {
7 "new_conversation": "7f7de0a21bc8dc3cee8ba8b6de632ff16f769649dd",
8 "existing_conversation": "7f9ec99a63cbb61f69ef18c0927689629bda07f1bf",
9 }
10
11 # Cache settings
12 TOKEN_CACHE_TTL = 3600 # 1 hour in seconds
13 MAX_EXTRACTION_RETRIES = 3
14 MIN_REQUIRED_TOKENS = 2 # Minimum tokens needed to update cache
15
16 # URLs
17 YUPP_BASE_URL = "https://yupp.ai"
18 YUPP_CHAT_URL = "https://yupp.ai/chat"
19
20 # Regex patterns for token extraction
21 TOKEN_PATTERNS = [
22 # Standard patterns
23 r'next-action["\']?\s*[:=]\s*["\']?([a-f0-9]{40,42})',
24 r'"next-action"\s*:\s*"([a-f0-9]{40,42})"',
25 r'"actionId"\s*:\s*"([a-f0-9]{40,42})"',
26 r'nextAction["\']?\s*:\s*["\']?([a-f0-9]{40,42})',
27 # Broader patterns for various formats
28 r'["\']?action["\']?\s*[:=]\s*["\']?([a-f0-9]{40,42})',
29 r'["\']?new_conversation["\']?\s*[:=]\s*["\']?([a-f0-9]{40,42})',
30 r'["\']?existing_conversation["\']?\s*[:=]\s*["\']?([a-f0-9]{40,42})',
31 r'["\']?new["\']?\s*[:=]\s*["\']?([a-f0-9]{40,42})',
32 r'["\']?existing["\']?\s*[:=]\s*["\']?([a-f0-9]{40,42})',
33 ]
Added g4f/Provider/yupp/token_extractor.py +384 -0
@@ -0,0 +1,384 @@
1 """
2 Yupp AI NextAction Token Extractor
3 Smart extraction with multiple fallback strategies
4 Only attempts extraction on token failure
5 """
6
7 import asyncio
8 import json
9 import os
10 import re
11 from dataclasses import dataclass, field
12 from datetime import datetime, timedelta
13 from typing import Dict, List, Optional
14
15 from .constants import (
16 MAX_EXTRACTION_RETRIES,
17 MIN_REQUIRED_TOKENS,
18 NEXT_ACTION_TOKENS,
19 TOKEN_CACHE_TTL,
20 TOKEN_PATTERNS,
21 YUPP_BASE_URL,
22 YUPP_CHAT_URL,
23 )
24
25
26 @dataclass
27 class TokenCache:
28 """Cache for NextAction tokens"""
29
30 tokens: Dict[str, str] = field(default_factory=dict)
31 last_updated: Optional[datetime] = None
32 failed_attempts: int = 0
33
34 def is_expired(self) -> bool:
35 """Check if cache is expired"""
36 if self.last_updated is None:
37 return True
38 return datetime.now() - self.last_updated > timedelta(seconds=TOKEN_CACHE_TTL)
39
40 def is_valid(self) -> bool:
41 """Check if cache has valid tokens"""
42 return (
43 not self.is_expired()
44 and len(self.tokens) >= MIN_REQUIRED_TOKENS
45 and all(
46 k in self.tokens for k in ["new_conversation", "existing_conversation"]
47 )
48 )
49
50
51 class TokenExtractor:
52 """
53 Extracts NextAction tokens from Yupp AI
54 Uses multiple strategies and only attempts extraction on failure
55 """
56
57 def __init__(
58 self,
59 jwt_token: Optional[str] = None,
60 scraper: Optional["cloudscraper.CloudScraper"] = None,
61 ):
62 self.jwt_token = jwt_token or os.getenv("YUPP_JWT") or os.getenv("YUPP_API_KEY")
63 self.scraper = scraper
64 self._cache = TokenCache()
65 self._extraction_in_progress = False
66 self._lock = asyncio.Lock()
67
68 async def get_token(self, token_type: str) -> str:
69 """
70 Get a NextAction token from cache or fallback.
71
72 This method does NOT trigger extraction - it only returns cached
73 tokens or fallbacks. Extraction is only triggered by mark_token_failed().
74
75 Args:
76 token_type: Type of token to retrieve ("new_conversation" or
77 "existing_conversation")
78
79 Returns:
80 The token string from cache if valid, otherwise the fallback token
81 """
82 # Return cached token if valid
83 if self._cache.is_valid() and token_type in self._cache.tokens:
84 return self._cache.tokens[token_type]
85
86 # Return fallback token
87 return NEXT_ACTION_TOKENS.get(
88 token_type, NEXT_ACTION_TOKENS["new_conversation"]
89 )
90
91 async def mark_token_failed(self, token_type: str, token_value: str) -> None:
92 """
93 Mark a token as failed - this triggers extraction attempt
94 Only extracts if we haven't tried too many times recently
95 """
96 async with self._lock:
97 # Check if this is actually a cached token that failed
98 cached_value = self._cache.tokens.get(token_type)
99
100 # If the failed token matches our cache, increment failures
101 if cached_value == token_value:
102 self._cache.failed_attempts += 1
103 elif token_value in NEXT_ACTION_TOKENS.values():
104 # Hardcoded token failed - definitely need to extract
105 self._cache.failed_attempts += 1
106
107 # Only attempt extraction if we haven't failed too many times
108 if self._cache.failed_attempts < MAX_EXTRACTION_RETRIES:
109 if not self._extraction_in_progress:
110 # Set flag immediately to prevent race conditions
111 self._extraction_in_progress = True
112 # Start extraction in background
113 asyncio.create_task(self._attempt_extraction())
114
115 async def _attempt_extraction(self) -> bool:
116 """
117 Attempt to extract fresh tokens from Yupp AI
118 Uses multiple strategies for robustness
119 """
120 async with self._lock:
121 if self._extraction_in_progress:
122 return False
123 self._extraction_in_progress = True
124
125 try:
126 # Try multiple extraction methods
127 extracted_tokens = await self._extract_from_chat_page()
128
129 if not extracted_tokens:
130 extracted_tokens = await self._extract_from_main_page()
131
132 if not extracted_tokens:
133 extracted_tokens = await self._extract_from_js_bundles()
134
135 if extracted_tokens and len(extracted_tokens) >= MIN_REQUIRED_TOKENS:
136 # Update cache with extracted tokens
137 async with self._lock:
138 self._cache.tokens = {
139 "new_conversation": extracted_tokens[0],
140 "existing_conversation": extracted_tokens[1]
141 if len(extracted_tokens) > 1
142 else extracted_tokens[0],
143 }
144 self._cache.last_updated = datetime.now()
145 self._cache.failed_attempts = 0
146 return True
147
148 return False
149
150 except Exception as e:
151 print(f"[Yupp TokenExtractor] Extraction failed: {e}")
152 if os.getenv("DEBUG_MODE", "").lower() == "true":
153 import traceback
154
155 traceback.print_exc()
156 return False
157 finally:
158 async with self._lock:
159 self._extraction_in_progress = False
160
161 async def _extract_from_chat_page(self) -> List[str]:
162 """Extract tokens from chat page HTML"""
163 try:
164 headers = self._get_headers()
165
166 if self.scraper:
167 response = self.scraper.get(YUPP_CHAT_URL, headers=headers, timeout=10)
168 text = response.text
169 else:
170 # Try to create a scraper if not provided
171 try:
172 import cloudscraper
173
174 scraper = cloudscraper.create_scraper(
175 browser={
176 "browser": "chrome",
177 "platform": "windows",
178 "desktop": True,
179 "mobile": False,
180 },
181 delay=10,
182 )
183 scraper.headers.update(headers)
184 if self.jwt_token:
185 scraper.cookies.set(
186 "__Secure-yupp.session-token", self.jwt_token
187 )
188 response = scraper.get(YUPP_CHAT_URL, timeout=10)
189 text = response.text
190 except ImportError:
191 import aiohttp
192
193 async with aiohttp.ClientSession() as session:
194 async with session.get(
195 YUPP_CHAT_URL,
196 headers=headers,
197 timeout=aiohttp.ClientTimeout(total=10),
198 ) as response:
199 text = await response.text()
200
201 tokens = self._extract_tokens_from_html(text)
202 if tokens:
203 print(
204 f"[Yupp TokenExtractor] Extracted {len(tokens)} tokens "
205 f"from chat page"
206 )
207 return tokens
208
209 except Exception as e:
210 print(f"[Yupp TokenExtractor] Chat page extraction failed: {e}")
211 if os.getenv("DEBUG_MODE", "").lower() == "true":
212 import traceback
213
214 traceback.print_exc()
215
216 return []
217
218 async def _extract_from_main_page(self) -> List[str]:
219 """Extract tokens from main page HTML"""
220 try:
221 headers = self._get_headers()
222
223 if self.scraper:
224 response = self.scraper.get(YUPP_BASE_URL, headers=headers, timeout=10)
225 text = response.text
226 else:
227 # Try to create a scraper if not provided
228 try:
229 import cloudscraper
230
231 scraper = cloudscraper.create_scraper(
232 browser={
233 "browser": "chrome",
234 "platform": "windows",
235 "desktop": True,
236 "mobile": False,
237 },
238 delay=10,
239 )
240 scraper.headers.update(headers)
241 if self.jwt_token:
242 scraper.cookies.set(
243 "__Secure-yupp.session-token", self.jwt_token
244 )
245 response = scraper.get(YUPP_BASE_URL, timeout=10)
246 text = response.text
247 except ImportError:
248 import aiohttp
249
250 async with aiohttp.ClientSession() as session:
251 async with session.get(
252 YUPP_BASE_URL,
253 headers=headers,
254 timeout=aiohttp.ClientTimeout(total=10),
255 ) as response:
256 text = await response.text()
257
258 tokens = self._extract_tokens_from_html(text)
259 if tokens:
260 print(
261 f"[Yupp TokenExtractor] Extracted {len(tokens)} tokens "
262 f"from main page"
263 )
264 return tokens
265
266 except Exception as e:
267 print(f"[Yupp TokenExtractor] Main page extraction failed: {e}")
268 if os.getenv("DEBUG_MODE", "").lower() == "true":
269 import traceback
270
271 traceback.print_exc()
272
273 return []
274
275 async def _extract_from_js_bundles(self) -> List[str]:
276 """Extract tokens from JavaScript bundles"""
277 try:
278 import aiohttp
279
280 # Common Next.js bundle patterns
281 bundle_patterns = [
282 "/_next/static/chunks/",
283 "/_next/static/app/",
284 ]
285
286 headers = self._get_headers()
287
288 async with aiohttp.ClientSession() as session:
289 # Try to fetch a page and extract script URLs
290 async with session.get(
291 YUPP_BASE_URL,
292 headers=headers,
293 timeout=aiohttp.ClientTimeout(total=10),
294 ) as response:
295 text = await response.text()
296
297 # Extract script URLs
298 script_urls = re.findall(r'src="([^"]*\.js[^"]*)"', text)
299
300 for script_url in script_urls:
301 if any(pattern in script_url for pattern in bundle_patterns):
302 try:
303 full_url = (
304 script_url
305 if script_url.startswith("http")
306 else f"{YUPP_BASE_URL}{script_url}"
307 )
308 async with session.get(
309 full_url,
310 headers=headers,
311 timeout=aiohttp.ClientTimeout(total=5),
312 ) as js_response:
313 js_text = await js_response.text()
314
315 tokens = self._extract_tokens_from_html(js_text)
316 if tokens and len(tokens) >= MIN_REQUIRED_TOKENS:
317 print(
318 f"[Yupp TokenExtractor] Extracted tokens "
319 f"from JS bundle: {script_url}"
320 )
321 return tokens
322 except Exception:
323 continue
324
325 except Exception as e:
326 print(f"[Yupp TokenExtractor] JS bundle extraction failed: {e}")
327 if os.getenv("DEBUG_MODE", "").lower() == "true":
328 import traceback
329
330 traceback.print_exc()
331
332 return []
333
334 def _extract_tokens_from_html(self, html: str) -> List[str]:
335 """Extract tokens from HTML/JS using multiple patterns"""
336 all_tokens = set()
337
338 for pattern in TOKEN_PATTERNS:
339 matches = re.findall(pattern, html, re.IGNORECASE)
340 all_tokens.update(matches)
341
342 # Filter to only 40-42 character hex strings (likely action tokens)
343 filtered_tokens = [
344 token
345 for token in all_tokens
346 if re.match(r"^[a-f0-9]{40,42}$", token.lower())
347 ]
348
349 return list(filtered_tokens)
350
351 def _get_headers(self) -> Dict[str, str]:
352 """Get request headers"""
353 headers = {
354 "User-Agent": (
355 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
356 "AppleWebKit/537.36 (KHTML, like Gecko) "
357 "Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0"
358 ),
359 "Accept": (
360 "text/html,application/xhtml+xml,application/xml;q=0.9,"
361 "image/webp,*/*;q=0.8"
362 ),
363 "Accept-Language": "en-US,en;q=0.9",
364 }
365
366 if self.jwt_token:
367 headers["Cookie"] = f"__Secure-yupp.session-token={self.jwt_token}"
368
369 return headers
370
371
372 # Global singleton instance
373 _token_extractor: Optional[TokenExtractor] = None
374
375
376 def get_token_extractor(
377 jwt_token: Optional[str] = None,
378 scraper: Optional["cloudscraper.CloudScraper"] = None,
379 ) -> TokenExtractor:
380 """Get or create the global token extractor instance"""
381 global _token_extractor
382 if _token_extractor is None:
383 _token_extractor = TokenExtractor(jwt_token=jwt_token, scraper=scraper)
384 return _token_extractor
Modified g4f/models.py +1 -1
@@ -287,7 +287,7 @@ gpt_oss_120b = Model(
287 287 name = 'gpt-oss-120b',
288 288 long_name = 'openai/gpt-oss-120b',
289 289 base_provider = 'OpenAI',
290 best_provider = IterListProvider([Together, DeepInfra, HuggingFace, OpenRouter, Groq])
290 best_provider = IterListProvider([Together, HuggingFace, OpenRouter, Groq])
291 291 )
292 292
293 293 # dall-e