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

XFEstudio/gpt4free

Refactor Yupp provider to use asyncio and aiohttp for asynchronous operations

0bc0ce7d
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

1 个文件 +175 -238
Modified g4f/Provider/Yupp.py +175 -238
@@ -3,11 +3,11 @@ import time
3 3 import uuid
4 4 import re
5 5 import os
6 from typing import Iterable, Optional, Dict, Any, Generator, List
7 import threading
8 import requests
6 import asyncio
7 import aiohttp
9 8
10 from ..providers.base_provider import AbstractProvider, ProviderModelMixin
9 from ..typing import AsyncResult, Messages, Optional, Dict, Any, List
10 from ..providers.base_provider import AsyncGeneratorProvider, ProviderModelMixin
11 11 from ..providers.response import Reasoning, PlainTextResponse, PreviewResponse, JsonConversation, ImageResponse, ProviderInfo
12 12 from ..errors import RateLimitError, ProviderException, MissingAuthError
13 13 from ..cookies import get_cookies
@@ -15,13 +15,12 @@ from ..tools.auth import AuthManager
15 15 from ..tools.media import merge_media
16 16 from ..image import is_accepted_format, to_bytes
17 17 from .yupp.models import YuppModelManager
18 from .helper import get_last_user_message
18 from .helper import get_last_user_message, format_prompt
19 19 from ..debug import log
20 20
21 # Global variables to manage Yupp accounts (should be set by your main application)
21 # Global variables to manage Yupp accounts
22 22 YUPP_ACCOUNTS: List[Dict[str, Any]] = []
23 YUPP_MODELS: List[Dict[str, Any]] = []
24 account_rotation_lock = threading.Lock()
23 account_rotation_lock = asyncio.Lock()
25 24
26 25 class YuppAccount:
27 26 """Yupp account representation"""
@@ -32,7 +31,7 @@ class YuppAccount:
32 31 self.last_used = last_used
33 32
34 33 def load_yupp_accounts(tokens_str: str):
35 """Load Yupp accounts from token string (compatible with your existing system)"""
34 """Load Yupp accounts from token string"""
36 35 global YUPP_ACCOUNTS
37 36 if not tokens_str:
38 37 return
@@ -48,11 +47,9 @@ def load_yupp_accounts(tokens_str: str):
48 47 for token in tokens
49 48 ]
50 49
51 def create_requests_session():
52 """Create a requests session with proper headers"""
53 import requests
54 session = requests.Session()
55 session.headers.update({
50 def create_headers() -> Dict[str, str]:
51 """Create headers for requests"""
52 return {
56 53 "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",
57 54 "Accept": "text/x-component, */*",
58 55 "Accept-Encoding": "gzip, deflate, br, zstd",
@@ -60,15 +57,14 @@ def create_requests_session():
60 57 "Sec-Fetch-Dest": "empty",
61 58 "Sec-Fetch-Mode": "cors",
62 59 "Sec-Fetch-Site": "same-origin",
63 })
64 return session
60 }
65 61
66 def get_best_yupp_account() -> Optional[Dict[str, Any]]:
67 """Get the best available Yupp account using a smart selection algorithm."""
62 async def get_best_yupp_account() -> Optional[Dict[str, Any]]:
63 """Get the best available Yupp account using smart selection algorithm"""
68 64 max_error_count = int(os.getenv("MAX_ERROR_COUNT", "3"))
69 65 error_cooldown = int(os.getenv("ERROR_COOLDOWN", "300"))
70 66
71 with account_rotation_lock:
67 async with account_rotation_lock:
72 68 now = time.time()
73 69 valid_accounts = [
74 70 acc
@@ -83,7 +79,7 @@ def get_best_yupp_account() -> Optional[Dict[str, Any]]:
83 79 if not valid_accounts:
84 80 return None
85 81
86 # Reset error count for accounts that have been in cooldown
82 # Reset error count for accounts in cooldown
87 83 for acc in valid_accounts:
88 84 if (
89 85 acc["error_count"] >= max_error_count
@@ -91,16 +87,15 @@ def get_best_yupp_account() -> Optional[Dict[str, Any]]:
91 87 ):
92 88 acc["error_count"] = 0
93 89
94 # Sort by last used (oldest first) and error count (lowest first)
90 # Sort by last used and error count
95 91 valid_accounts.sort(key=lambda x: (x["last_used"], x["error_count"]))
96 92 account = valid_accounts[0]
97 93 account["last_used"] = now
98 94 return account
99 95
100 def claim_yupp_reward(account: Dict[str, Any], reward_id: str):
101 """Claim Yupp reward synchronously"""
96 async def claim_yupp_reward(session: aiohttp.ClientSession, account: Dict[str, Any], reward_id: str):
97 """Claim Yupp reward asynchronously"""
102 98 try:
103 import requests
104 99 log_debug(f"Claiming reward {reward_id}...")
105 100 url = "https://yupp.ai/api/trpc/reward.claim?batch=1"
106 101 payload = {"0": {"json": {"rewardId": reward_id}}}
@@ -108,22 +103,18 @@ def claim_yupp_reward(account: Dict[str, Any], reward_id: str):
108 103 "Content-Type": "application/json",
109 104 "Cookie": f"__Secure-yupp.session-token={account['token']}",
110 105 }
111 session = create_requests_session()
112 response = session.post(url, json=payload, headers=headers)
113 response.raise_for_status()
114 data = response.json()
115 balance = data[0]["result"]["data"]["json"]["currentCreditBalance"]
116 log_debug(f"Reward claimed successfully. New balance: {balance}")
117 return balance
106 async with session.post(url, json=payload, headers=headers) as response:
107 response.raise_for_status()
108 data = await response.json()
109 balance = data[0]["result"]["data"]["json"]["currentCreditBalance"]
110 log_debug(f"Reward claimed successfully. New balance: {balance}")
111 return balance
118 112 except Exception as e:
119 113 log_debug(f"Failed to claim reward {reward_id}. Error: {e}")
120 114 return None
121 115
122 def make_chat_private(account: Dict[str, Any], chat_id: str) -> bool:
123 """
124 Set a Yupp chat's sharing status to PRIVATE.
125 Returns True if successful, False otherwise.
126 """
116 async def make_chat_private(session: aiohttp.ClientSession, account: Dict[str, Any], chat_id: str) -> bool:
117 """Set a Yupp chat's sharing status to PRIVATE"""
127 118 try:
128 119 log_debug(f"Setting chat {chat_id} to PRIVATE...")
129 120 url = "https://yupp.ai/api/trpc/chat.updateSharingSettings?batch=1"
@@ -140,34 +131,31 @@ def make_chat_private(account: Dict[str, Any], chat_id: str) -> bool:
140 131 "Cookie": f"__Secure-yupp.session-token={account['token']}",
141 132 }
142 133
143 session = create_requests_session()
144 response = session.post(url, json=payload, headers=headers)
145 response.raise_for_status()
146
147 data = response.json()
148 # Expected: [{"result":{"data":{"json":{}}}}]
149 if (
150 isinstance(data, list) and len(data) > 0
151 and "json" in data[0].get("result", {}).get("data", {})
152 ):
153 log_debug(f"Chat {chat_id} is now PRIVATE ✅")
154 return True
134 async with session.post(url, json=payload, headers=headers) as response:
135 response.raise_for_status()
136 data = await response.json()
137 if (
138 isinstance(data, list) and len(data) > 0
139 and "json" in data[0].get("result", {}).get("data", {})
140 ):
141 log_debug(f"Chat {chat_id} is now PRIVATE ✅")
142 return True
155 143
156 log_debug(f"Unexpected response while setting chat private: {data}")
157 return False
144 log_debug(f"Unexpected response while setting chat private: {data}")
145 return False
158 146
159 147 except Exception as e:
160 148 log_debug(f"Failed to make chat {chat_id} private: {e}")
161 149 return False
162 150
163 151 def log_debug(message: str):
164 """Debug logging (can be replaced with your logging system)"""
152 """Debug logging"""
165 153 if os.getenv("DEBUG_MODE", "false").lower() == "true":
166 154 print(f"[DEBUG] {message}")
167 155 else:
168 156 log(f"[Yupp] {message}")
169 157
170 def format_messages_for_yupp(messages: List[Dict[str, str]]) -> str:
158 def format_messages_for_yupp(messages: Messages) -> str:
171 159 """Format multi-turn conversation for Yupp single-turn format"""
172 160 if not messages:
173 161 return ""
@@ -198,13 +186,12 @@ def format_messages_for_yupp(messages: List[Dict[str, str]]) -> str:
198 186 formatted.append("\n\nAssistant:")
199 187
200 188 result = "".join(formatted)
201 # Remove leading \n\n if present
202 189 if result.startswith("\n\n"):
203 190 result = result[2:]
204 191
205 192 return result
206 193
207 class Yupp(AbstractProvider, ProviderModelMixin):
194 class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
208 195 """
209 196 Yupp.ai Provider for g4f
210 197 Uses multiple account rotation and smart error handling
@@ -215,6 +202,7 @@ class Yupp(AbstractProvider, ProviderModelMixin):
215 202 working = True
216 203 active_by_default = True
217 204 supports_stream = True
205
218 206 @classmethod
219 207 def get_models(cls, api_key: str = None, **kwargs) -> List[str]:
220 208 if not cls.models:
@@ -234,25 +222,18 @@ class Yupp(AbstractProvider, ProviderModelMixin):
234 222 return cls.models
235 223
236 224 @classmethod
237 def create_completion(
225 async def create_async_generator(
238 226 cls,
239 227 model: str,
240 messages: List[Dict[str, str]] = None,
241 stream: bool = False,
242 api_key: Optional[str] = None,
243 prompt: Optional[str] = None,
244 conversation: JsonConversation = None,
245 mode:str = "text",
246 timeout:float = 60,
228 messages: Messages,
229 proxy: str = None,
247 230 **kwargs,
248 ) -> Generator[str, Any, None]:
231 ) -> AsyncResult:
249 232 """
250 Create completion using Yupp.ai API with account rotation
251 :mode: Mode can be 'text' or 'image'
252
253
233 Create async completion using Yupp.ai API with account rotation
254 234 """
255 # Initialize Yupp accounts and models
235 # Initialize Yupp accounts
236 api_key = kwargs.get("api_key")
256 237 if not api_key:
257 238 api_key = get_cookies("yupp.ai", False).get("__Secure-yupp.session-token")
258 239 if api_key:
@@ -261,135 +242,136 @@ class Yupp(AbstractProvider, ProviderModelMixin):
261 242 else:
262 243 raise MissingAuthError("No Yupp accounts configured. Set YUPP_API_KEY environment variable.")
263 244
264 if messages is None:
265 messages = []
266
267 # Format messages - use the new format_messages_for_yupp function
268 url_uuid = None
269 if conversation is not None:
270 url_uuid = conversation.url_uuid
271
272 # Determine the prompt based on conversation context
245 # Format messages
246 conversation = kwargs.get("conversation")
247 url_uuid = conversation.url_uuid if conversation else None
273 248 is_new_conversation = url_uuid is None
249
250 prompt = kwargs.get("prompt")
274 251 if prompt is None:
275 252 if is_new_conversation:
276 # New conversation - format all messages
277 253 prompt = format_messages_for_yupp(messages)
278 254 else:
279 # Continuing conversation - use only the last user message
280 255 prompt = get_last_user_message(messages, prompt)
281
256
282 257 log_debug(f"Use url_uuid: {url_uuid}, Formatted prompt length: {len(prompt)}, Is new conversation: {is_new_conversation}")
283 258
284 259 # Try all accounts with rotation
285 260 max_attempts = len(YUPP_ACCOUNTS)
286 261 for attempt in range(max_attempts):
287 account = get_best_yupp_account()
262 account = await get_best_yupp_account()
288 263 if not account:
289 264 raise ProviderException("No valid Yupp accounts available")
290 265
291 266 try:
292 # Prepare the request
293 session = create_requests_session()
294 turn_id = str(uuid.uuid4())
295 files = []
296
297 # Handle media attachments if any
298 media = kwargs.get("media", None)
299 if media:
300 for file, name in list(merge_media(media, messages)):
301 data = to_bytes(file)
302 presigned_resp = session.post(
303 "https://yupp.ai/api/trpc/chat.createPresignedURLForUpload?batch=1",
304 json={"0": {"json": {"fileName": name, "fileSize": len(data), "contentType": is_accepted_format(data)}}},
305 headers={"Content-Type": "application/json", "Cookie": f"__Secure-yupp.session-token={account['token']}"}
306 )
307 presigned_resp.raise_for_status()
308 upload_info = presigned_resp.json()[0]["result"]["data"]["json"]
309 upload_url = upload_info["signedUrl"]
310 session.put(upload_url, data=data, headers={"Content-Type": is_accepted_format(data), "Content-Length": str(len(data))})
311 attachment_resp = session.post(
312 "https://yupp.ai/api/trpc/chat.createAttachmentForUploadedFile?batch=1",
313 json={"0": {"json": {"fileName": name, "contentType": is_accepted_format(data), "fileId": upload_info["fileId"]}}},
314 cookies={"__Secure-yupp.session-token": account["token"]}
315 )
316 attachment_resp.raise_for_status()
317 attachment = attachment_resp.json()[0]["result"]["data"]["json"]
318 files.append({
319 "fileName": attachment["file_name"],
320 "contentType": attachment["content_type"],
321 "attachmentId": attachment["attachment_id"],
322 "chatMessageId": ""
323 })
324
325 # Build payload and URL - FIXED: Use consistent url_uuid handling
326 if is_new_conversation:
327 url_uuid = str(uuid.uuid4())
328 payload = [
329 url_uuid,
330 turn_id,
331 prompt,
332 "$undefined",
333 "$undefined",
334 files,
335 "$undefined",
336 [{"modelName": model, "promptModifierId": "$undefined"}] if model else "none",
337 mode,
338 True,
339 "$undefined",
340 ]
341 url = f"https://yupp.ai/chat/{url_uuid}?stream=true"
342 # Yield the conversation info first
343 yield JsonConversation(url_uuid=url_uuid)
344 next_action = kwargs.get("next_action", "7f2a2308b5fc462a2c26df714cb2cccd02a9c10fbb")
345 else:
346 # Continuing existing conversation
347 payload = [
348 url_uuid,
349 turn_id,
350 prompt,
351 False,
352 [],
353 [{"modelName": model, "promptModifierId": "$undefined"}] if model else [],
354 mode,
355 files
356 ]
357 url = f"https://yupp.ai/chat/{url_uuid}?stream=true"
358 next_action = kwargs.get("next_action", "7f1e9eec4ab22c8bfc73a50c026db603cd8380f87d")
359
360 headers = {
361 "accept": "text/x-component",
362 "content-type": "text/plain;charset=UTF-8",
363 "next-action": next_action,
364 "cookie": f"__Secure-yupp.session-token={account['token']}",
365 }
366
367 log_debug(f"Sending request to: {url}")
368 log_debug(f"Payload structure: {type(payload)}, length: {len(str(payload))}")
369
370 # Send request
371 response = session.post(url, data=json.dumps(payload), headers=headers, stream=True, timeout=timeout)
372 response.raise_for_status()
373
374 # Attempt to make chat private
375 try:
376 make_chat_private(account, url_uuid)
377 except Exception as e:
378 log_debug(f"Failed to set chat private for {url_uuid}: {e}")
379
380 # Yield streaming responses
381 yield from cls._process_stream_response(response.iter_lines(), account, session, prompt, model)
382
383 return # Exit after successful completion
267 async with aiohttp.ClientSession() as session:
268 turn_id = str(uuid.uuid4())
269 files = []
270
271 # Handle media attachments
272 media = kwargs.get("media")
273 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 })
307 mode = "image" if model in cls.image_models else "text"
308
309 # Build payload and URL - FIXED: Use consistent url_uuid handling
310 if is_new_conversation:
311 url_uuid = str(uuid.uuid4())
312 payload = [
313 url_uuid,
314 turn_id,
315 prompt,
316 "$undefined",
317 "$undefined",
318 files,
319 "$undefined",
320 [{"modelName": model, "promptModifierId": "$undefined"}] if model else "none",
321 mode,
322 True,
323 "$undefined",
324 ]
325 url = f"https://yupp.ai/chat/{url_uuid}?stream=true"
326 # Yield the conversation info first
327 yield JsonConversation(url_uuid=url_uuid)
328 next_action = kwargs.get("next_action", "7f2a2308b5fc462a2c26df714cb2cccd02a9c10fbb")
329 else:
330 # Continuing existing conversation
331 payload = [
332 url_uuid,
333 turn_id,
334 prompt,
335 False,
336 [],
337 [{"modelName": model, "promptModifierId": "$undefined"}] if model else [],
338 mode,
339 files
340 ]
341 url = f"https://yupp.ai/chat/{url_uuid}?stream=true"
342 next_action = kwargs.get("next_action", "7f1e9eec4ab22c8bfc73a50c026db603cd8380f87d")
343
344 headers = {
345 "accept": "text/x-component",
346 "content-type": "text/plain;charset=UTF-8",
347 "next-action": next_action,
348 "cookie": f"__Secure-yupp.session-token={account['token']}",
349 }
350
351 log_debug(f"Sending request to: {url}")
352 log_debug(f"Payload structure: {type(payload)}, length: {len(str(payload))}")
353
354 # Send request
355 async with session.post(url, json=payload, headers=headers, proxy=proxy) as response:
356 response.raise_for_status()
357
358 # Make chat private in background
359 asyncio.create_task(make_chat_private(session, account, url_uuid))
360
361 # Process stream
362 async for chunk in cls._process_stream_response(response.content, account, session, prompt, model):
363 yield chunk
364
365 return
384 366
385 367 except RateLimitError:
386 368 log_debug(f"Account ...{account['token'][-4:]} hit rate limit, rotating")
387 with account_rotation_lock:
369 async with account_rotation_lock:
388 370 account["error_count"] += 1
389 371 continue
390 372 except ProviderException as e:
391 373 log_debug(f"Account ...{account['token'][-4:]} failed: {str(e)}")
392 with account_rotation_lock:
374 async with account_rotation_lock:
393 375 if "auth" in str(e).lower() or "401" in str(e) or "403" in str(e):
394 376 account["is_valid"] = False
395 377 else:
@@ -397,25 +379,24 @@ class Yupp(AbstractProvider, ProviderModelMixin):
397 379 continue
398 380 except Exception as e:
399 381 log_debug(f"Unexpected error with account ...{account['token'][-4:]}: {str(e)}")
400 with account_rotation_lock:
382 async with account_rotation_lock:
401 383 account["error_count"] += 1
402 384 raise ProviderException(f"Yupp request failed: {str(e)}") from e
403 385
404 386 raise ProviderException("All Yupp accounts failed after rotation attempts")
405 387
406 388 @classmethod
407 def _process_stream_response(
389 async def _process_stream_response(
408 390 cls,
409 response_lines: Iterable[bytes],
391 response_content,
410 392 account: Dict[str, Any],
411 session: requests.Session,
393 session: aiohttp.ClientSession,
412 394 prompt: str,
413 395 model_id: str
414 ) -> Generator[str, Any, None]:
415 """Process Yupp stream response and convert to OpenAI format"""
396 ) -> AsyncResult:
397 """Process Yupp stream response asynchronously"""
416 398
417 399 line_pattern = re.compile(b"^([0-9a-fA-F]+):(.*)")
418 chunks = {}
419 400 target_stream_id = None
420 401 reward_info = None
421 402 is_thinking = False
@@ -426,15 +407,13 @@ class Yupp(AbstractProvider, ProviderModelMixin):
426 407 def extract_ref_id(ref):
427 408 """Extract ID from reference string, e.g., from '$@123' extract '123'"""
428 409 return ref[2:] if ref and isinstance(ref, str) and ref.startswith("$@") else None
429
430 410 def is_valid_content(content: str) -> bool:
431 """Check if content is valid, avoid over-filtering"""
411 """Check if content is valid"""
432 412 if not content or content in [None, "", "$undefined"]:
433 413 return False
434
435 414 return True
436 415
437 def process_content_chunk(content: str, chunk_id: str, line_count: int):
416 async def process_content_chunk(content: str, chunk_id: str, line_count: int):
438 417 """Process single content chunk"""
439 418 nonlocal is_thinking, thinking_content, normal_content, session
440 419
@@ -443,10 +422,11 @@ class Yupp(AbstractProvider, ProviderModelMixin):
443 422
444 423 if '<yapp class="image-gen">' in content:
445 424 content = content.split('<yapp class="image-gen">').pop().split('</yapp>')[0]
446 url = f"https://yupp.ai/api/trpc/chat.getSignedImage"
447 response = session.get(url, params={"batch": "1", "input": json.dumps({"0": {"json": {"imageId": json.loads(content).get("image_id")}}})})
448 response.raise_for_status()
449 yield ImageResponse(response.json()[0]["result"]["data"]["json"]["signed_url"], prompt)
425 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:
427 resp.raise_for_status()
428 data = await resp.json()
429 yield ImageResponse(data[0]["result"]["data"]["json"]["signed_url"], prompt)
450 430 return
451 431
452 432 # log_debug(f"Processing chunk #{line_count} with content: '{content[:50]}...'")
@@ -458,7 +438,6 @@ class Yupp(AbstractProvider, ProviderModelMixin):
458 438 yield content
459 439
460 440 try:
461 # log_debug("Starting to process Yupp stream response...")
462 441 line_count = 0
463 442 quick_response_id = None
464 443 variant_stream_id = None
@@ -466,13 +445,11 @@ class Yupp(AbstractProvider, ProviderModelMixin):
466 445 variant_image: Optional[ImageResponse] = None
467 446 variant_text = ""
468 447
469 for line in response_lines:
470
448 async for line in response_content:
471 449 line_count += 1
472 450
473 451 match = line_pattern.match(line)
474 452 if not match:
475 log_debug(f"Line {line_count}: No pattern match")
476 453 continue
477 454
478 455 chunk_id, chunk_data = match.groups()
@@ -480,9 +457,7 @@ class Yupp(AbstractProvider, ProviderModelMixin):
480 457
481 458 try:
482 459 data = json.loads(chunk_data) if chunk_data != b"{}" else {}
483 chunks[chunk_id] = data
484 460 except json.JSONDecodeError:
485 log_debug(f"Failed to parse JSON for chunk {chunk_id}")
486 461 continue
487 462
488 463 # Process reward info
@@ -524,7 +499,7 @@ class Yupp(AbstractProvider, ProviderModelMixin):
524 499 target_stream_id = extract_ref_id(data.get("next"))
525 500 content = data.get("curr", "")
526 501 if content:
527 for chunk in process_content_chunk(content, chunk_id, line_count):
502 async for chunk in process_content_chunk(content, chunk_id, line_count):
528 503 is_started = True
529 504 yield chunk
530 505
@@ -534,10 +509,9 @@ class Yupp(AbstractProvider, ProviderModelMixin):
534 509 variant_stream_id = extract_ref_id(data.get("next"))
535 510 content = data.get("curr", "")
536 511 if content:
537 for chunk in process_content_chunk(content, chunk_id, line_count):
512 async for chunk in process_content_chunk(content, chunk_id, line_count):
538 513 if isinstance(chunk, ImageResponse):
539 variant_image = chunk
540 yield PreviewResponse(str(variant_image))
514 yield PreviewResponse(str(chunk))
541 515 else:
542 516 variant_text += str(chunk)
543 517 if not is_started:
@@ -550,7 +524,6 @@ class Yupp(AbstractProvider, ProviderModelMixin):
550 524 if content:
551 525 yield PreviewResponse(content)
552 526
553 # Fallback: process any chunk with "curr"
554 527 elif isinstance(data, dict) and "curr" in data:
555 528 content = data.get("curr", "")
556 529 if content:
@@ -571,40 +544,4 @@ class Yupp(AbstractProvider, ProviderModelMixin):
571 544 if reward_info and "unclaimedRewardInfo" in reward_info:
572 545 reward_id = reward_info["unclaimedRewardInfo"].get("rewardId")
573 546 if reward_id:
574 try:
575 claim_yupp_reward(account, reward_id)
576 except Exception as e:
577 log_debug(f"Failed to claim reward: {e}")
578
579 # log_debug(f"Stream completed. Content length: {len(normal_content)}")
580
581 # Initialize the provider
582 def init_yupp_provider():
583 """Initialize Yupp provider with environment configuration"""
584 tokens = os.getenv("YUPP_TOKENS", "")
585 if tokens:
586 load_yupp_accounts(tokens)
587
588 log_debug(f"Yupp provider initialized: {len(YUPP_ACCOUNTS)} accounts, {len(YUPP_MODELS)} models")
589 return Yupp
590
591 # Example usage and testing
592 if __name__ == "__main__":
593 # Set up environment for testing
594 os.environ["DEBUG_MODE"] = "true"
595
596 # Initialize provider
597 provider = init_yupp_provider()
598
599 # Test stream completion
600 try:
601 print("\nTesting stream completion...")
602 for chunk in provider.create_completion(
603 model="claude-sonnet-4-5-20250929<>thinking",
604 messages=[{"role": "user", "content": "What is Python?"}],
605 stream=True
606 ):
607 if isinstance(chunk, str) and chunk.strip():
608 print(chunk, end="")
609 except Exception as e:
610 print(f"\nStream test failed: {e}")
547 await claim_yupp_reward(session, account, reward_id)