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

XFEstudio/gpt4free

Improve Yupp provider account handling , request timeout and get byte from url (#3249)

* 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 * Improve Yupp provider account handling and request timeout Refactored account loading to preserve account history and error counts when updating tokens. Enhanced request logic to support custom timeouts using aiohttp's ClientTimeout, allowing for more flexible timeout configuration. * Update __init__.py * Handle multi-line <think> and <yapp> blocks in Yupp Added logic to capture and process multi-line <think> and <yapp class="image-gen"> blocks referenced by special IDs. Introduced block storage and extraction functions, enabling reasoning and image-gen content to be handled via references in the response stream. * Update LMArena.py Not Found Model error * Refactor to use StreamSession in Qwen and Yupp providers Replaced aiohttp.ClientSession with StreamSession in Qwen.py and Yupp.py for improved session handling. Updated exception and timeout references in Yupp.py to use aiohttp types. Improved default argument handling in StreamSession initialization. * Update Yupp.py * Add status parameter to get_generated_image method Introduces a 'status' parameter to the get_generated_image method to allow passing image generation status. Updates method calls and response objects to include status in their metadata for improved tracking of image generation progress. * Update OpenaiChat.py * Refactor Qwen image upload and caching logic and token Reworked the image upload flow in Qwen provider to use direct file uploads with OSS headers, added caching for uploaded images, and improved file type detection. Updated prepare_files to handle uploads via session and cache results, and added utility for generating OSS headers. Minor imports and typing adjustments included and token support. * Refactor Qwen and Yupp providers for improved async handling Updated Qwen provider to handle timeout via kwargs and improved type annotations. Refactored Yupp provider for better code organization, formatting, and async account rotation logic. Enhanced readability and maintainability by reordering imports, adding whitespace, and clarifying function implementations. * 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.

7771cf3d
Ammar <ammar.alkotb@gmail.com>
提交于

代码差异

6 个文件 +623 -289
Modified g4f/Provider/Qwen.py +181 -41
@@ -1,22 +1,29 @@
1 1 from __future__ import annotations
2 2
3 3 import asyncio
4 import datetime
5 import hashlib
6 import hmac
4 7 import json
5 import mimetypes
6 8 import re
7 9 import uuid
8 10 from time import time
9 from typing import Literal, Optional
11 from typing import Literal, Optional, Dict
12 from urllib.parse import quote
10 13
11 14 import aiohttp
15
16 from g4f.image import to_bytes, detect_file_type
17 from g4f.requests import raise_for_status
18 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
19 from .helper import get_last_user_message
20 from .. import debug
12 21 from ..errors import RateLimitError, ResponseError
13 from ..typing import AsyncResult, Messages, MediaListType
14 22 from ..providers.response import JsonConversation, Reasoning, Usage, ImageResponse, FinishReason
15 23 from ..requests import sse_stream
24 from ..requests.aiohttp import StreamSession
16 25 from ..tools.media import merge_media
17 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
18 from .helper import get_last_user_message
19 from .. import debug
26 from ..typing import AsyncResult, Messages, MediaListType
20 27
21 28 try:
22 29 import curl_cffi
@@ -25,6 +32,56 @@ try:
25 32 except ImportError:
26 33 has_curl_cffi = False
27 34
35 # Global variables to manage Qwen Image Cache
36 ImagesCache: Dict[str, dict] = {}
37
38
39 def get_oss_headers(method: str, date_str: str, sts_data: dict, content_type: str) -> dict[str, str]:
40 bucket_name = sts_data.get('bucketname', 'qwen-webui-prod')
41 file_path = sts_data.get('file_path', '')
42 access_key_id = sts_data.get('access_key_id')
43 access_key_secret = sts_data.get('access_key_secret')
44 security_token = sts_data.get('security_token')
45 headers = {
46 'Content-Type': content_type,
47 'x-oss-content-sha256': 'UNSIGNED-PAYLOAD',
48 'x-oss-date': date_str,
49 'x-oss-security-token': security_token,
50 'x-oss-user-agent': 'aliyun-sdk-js/6.23.0 Chrome 132.0.0.0 on Windows 10 64-bit'
51 }
52 headers_lower = {k.lower(): v for k, v in headers.items()}
53
54 canonical_headers_list = []
55 signed_headers_list = []
56 required_headers = ['content-md5', 'content-type', 'x-oss-content-sha256', 'x-oss-date', 'x-oss-security-token',
57 'x-oss-user-agent']
58 for header_name in sorted(required_headers):
59 if header_name in headers_lower:
60 canonical_headers_list.append(f"{header_name}:{headers_lower[header_name]}")
61 signed_headers_list.append(header_name)
62
63 canonical_headers = '\n'.join(canonical_headers_list) + '\n'
64 canonical_uri = f"/{bucket_name}/{quote(file_path, safe='/')}"
65
66 canonical_request = f"{method}\n{canonical_uri}\n\n{canonical_headers}\n\nUNSIGNED-PAYLOAD"
67
68 date_parts = date_str.split('T')
69 date_scope = f"{date_parts[0]}/ap-southeast-1/oss/aliyun_v4_request"
70 string_to_sign = f"OSS4-HMAC-SHA256\n{date_str}\n{date_scope}\n{hashlib.sha256(canonical_request.encode()).hexdigest()}"
71
72 def sign(key, msg):
73 return hmac.new(key, msg.encode() if isinstance(msg, str) else msg, hashlib.sha256).digest()
74
75 date_key = sign(f"aliyun_v4{access_key_secret}".encode(), date_parts[0])
76 region_key = sign(date_key, "ap-southeast-1")
77 service_key = sign(region_key, "oss")
78 signing_key = sign(service_key, "aliyun_v4_request")
79 signature = hmac.new(signing_key, string_to_sign.encode(), hashlib.sha256).hexdigest()
80
81 headers['authorization'] = f"OSS4-HMAC-SHA256 Credential={access_key_id}/{date_scope},Signature={signature}"
82 return headers
83
84
28 85 text_models = [
29 86 'qwen3-max-preview', 'qwen-plus-2025-09-11', 'qwen3-235b-a22b', 'qwen3-coder-plus', 'qwen3-30b-a3b',
30 87 'qwen3-coder-30b-a3b-instruct', 'qwen-max-latest', 'qwen-plus-2025-01-25', 'qwq-32b', 'qwen-turbo-2025-02-11',
@@ -60,19 +117,19 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
60 117 active_by_default = True
61 118 supports_stream = True
62 119 supports_message_history = False
63
120 image_cache = True
64 121 _models_loaded = True
65 122 image_models = image_models
66 123 text_models = text_models
67 124 vision_models = vision_models
68 models = models
125 models: list[str] = models
69 126 default_model = "qwen3-235b-a22b"
70 127
71 128 _midtoken: str = None
72 129 _midtoken_uses: int = 0
73 130
74 131 @classmethod
75 def get_models(cls) -> list[str]:
132 def get_models(cls, **kwargs) -> list[str]:
76 133 if not cls._models_loaded and has_curl_cffi:
77 134 response = curl_cffi.get(f"{cls.url}/api/models")
78 135 if response.ok:
@@ -97,34 +154,106 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
97 154 return cls.models
98 155
99 156 @classmethod
100 async def prepare_files(cls, media, chat_type="")->list:
157 async def prepare_files(cls, media, session: aiohttp.ClientSession, headers=None) -> list:
158 if headers is None:
159 headers = {}
101 160 files = []
102 for _file, file_name in media:
103 file_type, _ = mimetypes.guess_type(file_name)
104 file_class: Literal["default", "vision", "video", "audio", "document"] = "default"
105 _type: Literal["file", "image", "video", "audio"] = "file"
106 showType: Literal["file", "image", "video", "audio"] = "file"
107
108 if isinstance(_file, str) and _file.startswith('http'):
109 if chat_type == "image_edit" or (file_type and file_type.startswith("image")):
110 file_class = "vision"
111 _type = "image"
112 if not file_type:
113 # Try to infer from file extension, fallback to generic
114 ext = file_name.split('.')[-1].lower() if '.' in file_name else ''
115 file_type = mimetypes.types_map.get(f'.{ext}', 'application/octet-stream')
116 showType = "image"
117
118 files.append(
119 {
120 "type": _type,
161 for index, (_file, file_name) in enumerate(media):
162
163 data_bytes = to_bytes(_file)
164 # Check Cache
165 hasher = hashlib.md5()
166 hasher.update(data_bytes)
167 image_hash = hasher.hexdigest()
168 file = ImagesCache.get(image_hash)
169 if cls.image_cache and file:
170 debug.log("Using cached image")
171 files.append(file)
172 continue
173
174 extension, file_type = detect_file_type(data_bytes)
175 file_name = file_name or f"file-{len(data_bytes)}{extension}"
176 file_size = len(data_bytes)
177
178 # Get File Url
179 async with session.post(
180 f'{cls.url}/api/v2/files/getstsToken',
181 json={"filename": file_name,
182 "filesize": file_size, "filetype": file_type},
183 headers=headers
184
185 ) as r:
186 await raise_for_status(r, "Create file failed")
187 res_data = await r.json()
188 data = res_data.get("data")
189
190 if res_data["success"] is False:
191 raise RateLimitError(f"{data['code']}:{data['details']}")
192 file_url = data.get("file_url")
193 file_id = data.get("file_id")
194
195 # Put File into Url
196 str_date = datetime.datetime.now(datetime.UTC).strftime('%Y%m%dT%H%M%SZ')
197 headers = get_oss_headers('PUT', str_date, data, file_type)
198 async with session.put(
199 file_url.split("?")[0],
200 data=data_bytes,
201 headers=headers
202 ) as response:
203 await raise_for_status(response)
204
205 file_class: Literal["default", "vision", "video", "audio", "document"]
206 _type: Literal["file", "image", "video", "audio"]
207 show_type: Literal["file", "image", "video", "audio"]
208 if "image" in file_type:
209 _type = "image"
210 show_type = "image"
211 file_class = "vision"
212 elif "video" in file_type:
213 _type = "video"
214 show_type = "video"
215 file_class = "video"
216 elif "audio" in file_type:
217 _type = "audio"
218 show_type = "audio"
219 file_class = "audio"
220 else:
221 _type = "file"
222 show_type = "file"
223 file_class = "document"
224
225 file = {
226 "type": _type,
227 "file": {
228 "created_at": int(time() * 1000),
229 "data": {},
230 "filename": file_name,
231 "hash": None,
232 "id": file_id,
233 "meta": {
121 234 "name": file_name,
122 "file_type": file_type,
123 "showType": showType,
124 "file_class": file_class,
125 "url": _file
126 }
127 )
235 "size": file_size,
236 "content_type": file_type
237 },
238 "update_at": int(time() * 1000),
239 },
240 "id": file_id,
241 "url": file_url,
242 "name": file_name,
243 "collection_name": "",
244 "progress": 0,
245 "status": "uploaded",
246 "greenNet": "success",
247 "size": file_size,
248 "error": "",
249 "itemId": str(uuid.uuid4()),
250 "file_type": file_type,
251 "showType": show_type,
252 "file_class": file_class,
253 "uploadTaskId": str(uuid.uuid4())
254 }
255 ImagesCache[image_hash] = file
256 files.append(file)
128 257 return files
129 258
130 259 @classmethod
@@ -135,7 +264,6 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
135 264 media: MediaListType = None,
136 265 conversation: JsonConversation = None,
137 266 proxy: str = None,
138 timeout: int = 120,
139 267 stream: bool = True,
140 268 enable_thinking: bool = True,
141 269 chat_type: Literal[
@@ -157,7 +285,7 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
157 285 """
158 286
159 287 model_name = cls.get_model(model)
160
288 token = kwargs.get("token")
161 289 headers = {
162 290 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
163 291 'Accept': '*/*',
@@ -169,13 +297,24 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
169 297 'Sec-Fetch-Mode': 'cors',
170 298 'Sec-Fetch-Site': 'same-origin',
171 299 'Connection': 'keep-alive',
172 'Authorization': 'Bearer',
300 'Authorization': f'Bearer {token}' if token else "Bearer",
173 301 'Source': 'web'
174 302 }
175 303
176 304 prompt = get_last_user_message(messages)
177
178 async with aiohttp.ClientSession(headers=headers) as session:
305 _timeout = kwargs.get("timeout")
306 if isinstance(_timeout, aiohttp.ClientTimeout):
307 timeout = _timeout
308 else:
309 total = float(_timeout) if isinstance(_timeout, (int, float)) else 5 * 60
310 timeout = aiohttp.ClientTimeout(total=total)
311 async with StreamSession(headers=headers) as session:
312 try:
313 async with session.get('https://chat.qwen.ai/api/v1/auths/', proxy=proxy) as user_info_res:
314 user_info_res.raise_for_status()
315 debug.log(await user_info_res.json())
316 except:
317 ...
179 318 for attempt in range(5):
180 319 try:
181 320 if not cls._midtoken:
@@ -221,7 +360,8 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
221 360 files = []
222 361 media = list(merge_media(media, messages))
223 362 if media:
224 files = await cls.prepare_files(media, chat_type=chat_type)
363 files = await cls.prepare_files(media, session=session,
364 headers=req_headers)
225 365
226 366 msg_payload = {
227 367 "stream": stream,
Modified g4f/Provider/Yupp.py +190 -72
@@ -1,25 +1,26 @@
1 import asyncio
1 2 import hashlib
2 3 import json
4 import os
5 import re
3 6 import time
4 7 import uuid
5 import re
6 import os
7 import asyncio
8
8 9 import aiohttp
9 from aiohttp import ClientResponseError
10 10
11 from ..typing import AsyncResult, Messages, Optional, Dict, Any, List
11 from .helper import get_last_user_message
12 from .yupp.models import YuppModelManager
13 from ..cookies import get_cookies
14 from ..debug import log
15 from ..errors import RateLimitError, ProviderException, MissingAuthError
16 from ..image import is_accepted_format, to_bytes
12 17 from ..providers.base_provider import AsyncGeneratorProvider, ProviderModelMixin
13 18 from ..providers.response import Reasoning, PlainTextResponse, PreviewResponse, JsonConversation, ImageResponse, \
14 19 ProviderInfo, FinishReason, JsonResponse
15 from ..errors import RateLimitError, ProviderException, MissingAuthError
16 from ..cookies import get_cookies
20 from ..requests.aiohttp import StreamSession
17 21 from ..tools.auth import AuthManager
18 22 from ..tools.media import merge_media
19 from ..image import is_accepted_format, to_bytes
20 from .yupp.models import YuppModelManager
21 from .helper import get_last_user_message
22 from ..debug import log
23 from ..typing import AsyncResult, Messages, Optional, Dict, Any, List
23 24
24 25 # Global variables to manage Yupp accounts
25 26 YUPP_ACCOUNT = Dict[str, Any]
@@ -27,22 +28,24 @@ YUPP_ACCOUNTS: List[YUPP_ACCOUNT] = []
27 28 account_rotation_lock = asyncio.Lock()
28 29
29 30 # Global variables to manage Yupp Image Cache
30 ImagesCache:Dict[str, dict] = {}
31 ImagesCache: Dict[str, dict] = {}
32
31 33
32 34 class YuppAccount:
33 35 """Yupp account representation"""
36
34 37 def __init__(self, token: str, is_valid: bool = True, error_count: int = 0, last_used: float = 0):
35 38 self.token = token
36 39 self.is_valid = is_valid
37 40 self.error_count = error_count
38 41 self.last_used = last_used
39 42
43
40 44 def load_yupp_accounts(tokens_str: str):
41 45 """Load Yupp accounts from token string"""
42 46 global YUPP_ACCOUNTS
43 47 if not tokens_str:
44 48 return
45
46 49 tokens = [token.strip() for token in tokens_str.split(',') if token.strip()]
47 50 YUPP_ACCOUNTS = [
48 51 {
@@ -54,6 +57,7 @@ def load_yupp_accounts(tokens_str: str):
54 57 for token in tokens
55 58 ]
56 59
60
57 61 def create_headers() -> Dict[str, str]:
58 62 """Create headers for requests"""
59 63 return {
@@ -66,6 +70,7 @@ def create_headers() -> Dict[str, str]:
66 70 "Sec-Fetch-Site": "same-origin",
67 71 }
68 72
73
69 74 async def get_best_yupp_account() -> Optional[YUPP_ACCOUNT]:
70 75 """Get the best available Yupp account using smart selection algorithm"""
71 76 max_error_count = int(os.getenv("MAX_ERROR_COUNT", "3"))
@@ -77,10 +82,10 @@ async def get_best_yupp_account() -> Optional[YUPP_ACCOUNT]:
77 82 acc
78 83 for acc in YUPP_ACCOUNTS
79 84 if acc["is_valid"]
80 and (
81 acc["error_count"] < max_error_count
82 or now - acc["last_used"] > error_cooldown
83 )
85 and (
86 acc["error_count"] < max_error_count
87 or now - acc["last_used"] > error_cooldown
88 )
84 89 ]
85 90
86 91 if not valid_accounts:
@@ -89,8 +94,8 @@ async def get_best_yupp_account() -> Optional[YUPP_ACCOUNT]:
89 94 # Reset error count for accounts in cooldown
90 95 for acc in valid_accounts:
91 96 if (
92 acc["error_count"] >= max_error_count
93 and now - acc["last_used"] > error_cooldown
97 acc["error_count"] >= max_error_count
98 and now - acc["last_used"] > error_cooldown
94 99 ):
95 100 acc["error_count"] = 0
96 101
@@ -100,6 +105,7 @@ async def get_best_yupp_account() -> Optional[YUPP_ACCOUNT]:
100 105 account["last_used"] = now
101 106 return account
102 107
108
103 109 async def claim_yupp_reward(session: aiohttp.ClientSession, account: YUPP_ACCOUNT, reward_id: str):
104 110 """Claim Yupp reward asynchronously"""
105 111 try:
@@ -109,7 +115,7 @@ async def claim_yupp_reward(session: aiohttp.ClientSession, account: YUPP_ACCOUN
109 115 headers = {
110 116 "Content-Type": "application/json",
111 117 "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",
118 "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 119
114 120 }
115 121 async with session.post(url, json=payload, headers=headers) as response:
@@ -122,6 +128,7 @@ async def claim_yupp_reward(session: aiohttp.ClientSession, account: YUPP_ACCOUN
122 128 log_debug(f"Failed to claim reward {reward_id}. Error: {e}")
123 129 return None
124 130
131
125 132 async def make_chat_private(session: aiohttp.ClientSession, account: YUPP_ACCOUNT, chat_id: str) -> bool:
126 133 """Set a Yupp chat's sharing status to PRIVATE"""
127 134 try:
@@ -138,7 +145,7 @@ async def make_chat_private(session: aiohttp.ClientSession, account: YUPP_ACCOUN
138 145 headers = {
139 146 "Content-Type": "application/json",
140 147 "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",
148 "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 149
143 150 }
144 151
@@ -146,8 +153,8 @@ async def make_chat_private(session: aiohttp.ClientSession, account: YUPP_ACCOUN
146 153 response.raise_for_status()
147 154 data = await response.json()
148 155 if (
149 isinstance(data, list) and len(data) > 0
150 and "json" in data[0].get("result", {}).get("data", {})
156 isinstance(data, list) and len(data) > 0
157 and "json" in data[0].get("result", {}).get("data", {})
151 158 ):
152 159 log_debug(f"Chat {chat_id} is now PRIVATE ✅")
153 160 return True
@@ -159,6 +166,7 @@ async def make_chat_private(session: aiohttp.ClientSession, account: YUPP_ACCOUN
159 166 log_debug(f"Failed to make chat {chat_id} private: {e}")
160 167 return False
161 168
169
162 170 def log_debug(message: str):
163 171 """Debug logging"""
164 172 if os.getenv("DEBUG_MODE", "false").lower() == "true":
@@ -166,11 +174,12 @@ def log_debug(message: str):
166 174 else:
167 175 log(f"[Yupp] {message}")
168 176
177
169 178 def format_messages_for_yupp(messages: Messages) -> str:
170 179 """Format multi-turn conversation for Yupp single-turn format"""
171 180 if not messages:
172 181 return ""
173
182
174 183 if len(messages) == 1 and isinstance(messages[0].get("content"), str):
175 184 return messages[0].get("content", "").strip()
176 185
@@ -202,6 +211,7 @@ def format_messages_for_yupp(messages: Messages) -> str:
202 211
203 212 return result
204 213
214
205 215 class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
206 216 """
207 217 Yupp.ai Provider for g4f
@@ -214,7 +224,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
214 224 active_by_default = True
215 225 supports_stream = True
216 226 image_cache = True
217
227
218 228 @classmethod
219 229 def get_models(cls, api_key: str = None, **kwargs) -> List[str]:
220 230 if not cls.models:
@@ -230,11 +240,12 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
230 240 cls.models_tags = {model.get("name"): manager.processor.generate_tags(model) for model in models}
231 241 cls.models = [model.get("name") for model in models]
232 242 cls.image_models = [model.get("name") for model in models if model.get("isImageGeneration")]
233 cls.vision_models = [model.get("name") for model in models if "image/*" in model.get("supportedAttachmentMimeTypes", [])]
243 cls.vision_models = [model.get("name") for model in models if
244 "image/*" in model.get("supportedAttachmentMimeTypes", [])]
234 245 return cls.models
235 246
236 247 @classmethod
237 async def prepare_files(cls, media, session:aiohttp.ClientSession, account:YUPP_ACCOUNT)->list:
248 async def prepare_files(cls, media, session: aiohttp.ClientSession, account: YUPP_ACCOUNT) -> list:
238 249 files = []
239 250 if not media:
240 251 return files
@@ -291,11 +302,11 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
291 302
292 303 @classmethod
293 304 async def create_async_generator(
294 cls,
295 model: str,
296 messages: Messages,
297 proxy: str = None,
298 **kwargs,
305 cls,
306 model: str,
307 messages: Messages,
308 proxy: str = None,
309 **kwargs,
299 310 ) -> AsyncResult:
300 311 """
301 312 Create async completion using Yupp.ai API with account rotation
@@ -314,15 +325,16 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
314 325 conversation = kwargs.get("conversation")
315 326 url_uuid = conversation.url_uuid if conversation else None
316 327 is_new_conversation = url_uuid is None
317
328
318 329 prompt = kwargs.get("prompt")
319 330 if prompt is None:
320 331 if is_new_conversation:
321 332 prompt = format_messages_for_yupp(messages)
322 333 else:
323 334 prompt = get_last_user_message(messages, prompt)
324
325 log_debug(f"Use url_uuid: {url_uuid}, Formatted prompt length: {len(prompt)}, Is new conversation: {is_new_conversation}")
335
336 log_debug(
337 f"Use url_uuid: {url_uuid}, Formatted prompt length: {len(prompt)}, Is new conversation: {is_new_conversation}")
326 338
327 339 # Try all accounts with rotation
328 340 max_attempts = len(YUPP_ACCOUNTS)
@@ -332,10 +344,9 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
332 344 raise ProviderException("No valid Yupp accounts available")
333 345
334 346 try:
335 async with aiohttp.ClientSession() as session:
347 async with StreamSession() as session:
336 348 turn_id = str(uuid.uuid4())
337 349
338
339 350 # Handle media attachments
340 351 media = kwargs.get("media")
341 352 if media:
@@ -390,16 +401,23 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
390 401
391 402 log_debug(f"Sending request to: {url}")
392 403 log_debug(f"Payload structure: {type(payload)}, length: {len(str(payload))}")
393
404 _timeout = kwargs.get("timeout")
405 if isinstance(_timeout, aiohttp.ClientTimeout):
406 timeout = _timeout
407 else:
408 total = float(_timeout) if isinstance(_timeout, (int, float)) else 5 * 60
409 timeout = aiohttp.ClientTimeout(total=total)
394 410 # Send request
395 async with session.post(url, json=payload, headers=headers, proxy=proxy) as response:
411 async with session.post(url, json=payload, headers=headers, proxy=proxy,
412 timeout=timeout) as response:
396 413 response.raise_for_status()
397 414
398 415 # Make chat private in background
399 416 asyncio.create_task(make_chat_private(session, account, url_uuid))
400 417
401 418 # Process stream
402 async for chunk in cls._process_stream_response(response.content, account, session, prompt, model):
419 async for chunk in cls._process_stream_response(response.content, account, session, prompt,
420 model):
403 421 yield chunk
404 422
405 423 return
@@ -417,7 +435,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
417 435 else:
418 436 account["error_count"] += 1
419 437 continue
420 except ClientResponseError as e:
438 except aiohttp.ClientResponseError as e:
421 439 log_debug(f"Account ...{account['token'][-4:]} failed: {str(e)}")
422 440 # No Available Yupp credits
423 441 if e.status == 500 and 'Internal Server Error' in e.message:
@@ -439,12 +457,12 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
439 457
440 458 @classmethod
441 459 async def _process_stream_response(
442 cls,
443 response_content,
444 account: YUPP_ACCOUNT,
445 session: aiohttp.ClientSession,
446 prompt: str,
447 model_id: str
460 cls,
461 response_content,
462 account: YUPP_ACCOUNT,
463 session: aiohttp.ClientSession,
464 prompt: str,
465 model_id: str
448 466 ) -> AsyncResult:
449 467 """Process Yupp stream response asynchronously"""
450 468
@@ -461,15 +479,33 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
461 479 "target": [],
462 480 "variant": [],
463 481 "quick": [],
464 "thinking": [] ,
482 "thinking": [],
465 483 "extra": []
466 484 }
467 485 # Holds leftStream / rightStream definitions to determine target/variant
468 486 select_stream = [None, None]
469
487 # State for capturing a multi-line <think> + <yapp> block (fa-style)
488 capturing_ref_id: Optional[str] = None
489 capturing_lines: List[bytes] = []
490
491 # Storage for special referenced blocks like $fa
492 think_blocks: Dict[str, str] = {}
493 image_blocks: Dict[str, str] = {}
494
470 495 def extract_ref_id(ref):
471 496 """Extract ID from reference string, e.g., from '$@123' extract '123'"""
472 497 return ref[2:] if ref and isinstance(ref, str) and ref.startswith("$@") else None
498
499 def extract_ref_name(ref: str) -> Optional[str]:
500 """Extract simple ref name from '$fa' → 'fa'"""
501 if not isinstance(ref, str):
502 return None
503 if ref.startswith("$@"):
504 return ref[2:]
505 if ref.startswith("$") and len(ref) > 1:
506 return ref[1:]
507 return None
508
473 509 def is_valid_content(content: str) -> bool:
474 510 """Check if content is valid"""
475 511 if not content or content in [None, "", "$undefined"]:
@@ -515,7 +551,27 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
515 551 if for_target:
516 552 normal_content += content
517 553 yield content
518
554
555 def finalize_capture_block(ref_id: str, lines: List[bytes]):
556 """Parse captured <think> + <yapp> block for a given ref ID."""
557 text = b"".join(lines).decode("utf-8", errors="ignore")
558
559 # Extract <think>...</think>
560 think_start = text.find("<think>")
561 think_end = text.find("</think>")
562 if think_start != -1 and think_end != -1 and think_end > think_start:
563 inner = text[think_start + len("<think>"):think_end].strip()
564 if inner:
565 think_blocks[ref_id] = inner
566
567 # Extract <yapp class="image-gen">...</yapp>
568 yapp_start = text.find('<yapp class="image-gen">')
569 if yapp_start != -1:
570 yapp_end = text.find("</yapp>", yapp_start)
571 if yapp_end != -1:
572 yapp_block = text[yapp_start:yapp_end + len("</yapp>")]
573 image_blocks[ref_id] = yapp_block
574
519 575 try:
520 576 line_count = 0
521 577 quick_response_id = None
@@ -531,17 +587,55 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
531 587 right_message_id = None
532 588 nudge_new_chat_id = None
533 589 nudge_new_chat = False
534
535 590 async for line in response_content:
536 591 line_count += 1
537
592 # If we are currently capturing a think/image block for some ref ID
593 if capturing_ref_id is not None:
594 capturing_lines.append(line)
595
596 # Check if this line closes the <yapp> block; after that, block is complete
597 if b"</yapp>" in line: # or b':{"curr"' in line:
598 # We may have trailing "2:{...}" after </yapp> on the same line
599 # Get id using re
600 idx = line.find(b"</yapp>")
601 suffix = line[idx + len(b"</yapp>"):]
602
603 # Finalize captured block for this ref ID
604 finalize_capture_block(capturing_ref_id, capturing_lines)
605 capturing_ref_id = None
606 capturing_lines = []
607
608 # If there is trailing content (e.g. '2:{"curr":"$fa"...}')
609 if suffix.strip():
610 # Process suffix as a new "line" in the same iteration
611 line = suffix
612 else:
613 # Nothing more on this line
614 continue
615 else:
616 # Still inside captured block; skip normal processing
617 continue
618
619 # Detect start of a <think> block assigned to a ref like 'fa:...<think>'
620 if b"<think>" in line:
621 m = line_pattern.match(line)
622 if m:
623 capturing_ref_id = m.group(1).decode()
624 capturing_lines = [line]
625 # Skip normal parsing; the rest of the block will be captured until </yapp>
626 continue
627
538 628 match = line_pattern.match(line)
539 629 if not match:
540 630 continue
541 631
542 632 chunk_id, chunk_data = match.groups()
543 633 chunk_id = chunk_id.decode()
544
634
635 if nudge_new_chat_id and chunk_id == nudge_new_chat_id:
636 nudge_new_chat = chunk_data.decode()
637 continue
638
545 639 try:
546 640 data = json.loads(chunk_data) if chunk_data != b"{}" else {}
547 641 except json.JSONDecodeError:
@@ -550,7 +644,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
550 644 if chunk_id == reward_id and isinstance(data, dict) and "unclaimedRewardInfo" in data:
551 645 reward_info = data
552 646 log_debug(f"Found reward info")
553
647
554 648 # Process initial setup
555 649 elif chunk_id == "1":
556 650 yield PlainTextResponse(line.decode(errors="ignore"))
@@ -558,7 +652,8 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
558 652 left_stream = data.get("leftStream", {})
559 653 right_stream = data.get("rightStream", {})
560 654 if data.get("quickResponse", {}) != "$undefined":
561 quick_response_id = extract_ref_id(data.get("quickResponse", {}).get("stream", {}).get("next"))
655 quick_response_id = extract_ref_id(
656 data.get("quickResponse", {}).get("stream", {}).get("next"))
562 657
563 658 if data.get("turnId", {}) != "$undefined":
564 659 turn_id = extract_ref_id(data.get("turnId", {}).get("next"))
@@ -592,7 +687,7 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
592 687 provider_info["variantUrl"] = selection.get("externalUrl")
593 688 log_debug(f"Found variant stream ID: {variant_stream_id}")
594 689 yield ProviderInfo.from_dict(provider_info)
595
690
596 691 # Process target stream content
597 692 elif target_stream_id and chunk_id == target_stream_id:
598 693 yield PlainTextResponse(line.decode(errors="ignore"))
@@ -600,15 +695,41 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
600 695 target_stream_id = extract_ref_id(data.get("next"))
601 696 content = data.get("curr", "")
602 697 if content:
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)
610 is_started = True
611 yield chunk
698 # Handle special "$fa" / "$<id>" reference
699 ref_name = extract_ref_name(content)
700 if ref_name and (ref_name in think_blocks or ref_name in image_blocks):
701 # Thinking block
702 if ref_name in think_blocks:
703 t_text = think_blocks[ref_name]
704 if t_text:
705 reasoning = Reasoning(t_text)
706 # thinking_content += t_text
707 stream["thinking"].append(reasoning)
708 # yield reasoning
709
710 # Image-gen block
711 if ref_name in image_blocks:
712 img_block_text = image_blocks[ref_name]
713 async for chunk in process_content_chunk(
714 img_block_text,
715 ref_name,
716 line_count,
717 for_target=True
718 ):
719 stream["target"].append(chunk)
720 is_started = True
721 yield chunk
722 else:
723 # Normal textual chunk
724 async for chunk in process_content_chunk(
725 content,
726 chunk_id,
727 line_count,
728 for_target=True
729 ):
730 stream["target"].append(chunk)
731 is_started = True
732 yield chunk
612 733 # Variant stream (comparison)
613 734 elif variant_stream_id and chunk_id == variant_stream_id:
614 735 yield PlainTextResponse("[Variant] " + line.decode(errors="ignore"))
@@ -651,8 +772,6 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
651 772 ...
652 773 elif chunk_id == left_message_id:
653 774 ...
654 elif chunk_id == nudge_new_chat_id:
655 nudge_new_chat = data
656 775 # Miscellaneous extra content
657 776 elif isinstance(data, dict) and "curr" in data:
658 777 content = data.get("curr", "")
@@ -664,24 +783,23 @@ class Yupp(AsyncGeneratorProvider, ProviderModelMixin):
664 783 for_target=False
665 784 ):
666 785 stream["extra"].append(chunk)
667 if isinstance(chunk,str) and "<streaming stopped unexpectedly" in chunk:
786 if isinstance(chunk, str) and "<streaming stopped unexpectedly" in chunk:
668 787 yield FinishReason(chunk)
669 788
670 789 yield PlainTextResponse("[Extra] " + line.decode(errors="ignore"))
671
790
672 791 if variant_image is not None:
673 792 yield variant_image
674 793 elif variant_text:
675 794 yield PreviewResponse(variant_text)
676 795 yield JsonResponse(**stream)
677 796 log_debug(f"Finished processing {line_count} lines")
678
679 797 except:
680 798 raise
681
799
682 800 finally:
683 801 # Claim reward in background
684 802 if reward_info and "unclaimedRewardInfo" in reward_info:
685 803 reward_id = reward_info["unclaimedRewardInfo"].get("rewardId")
686 804 if reward_id:
687 await claim_yupp_reward(session, account, reward_id)
805 await claim_yupp_reward(session, account, reward_id)
Modified g4f/Provider/needs_auth/LMArena.py +1 -1
@@ -642,7 +642,7 @@ class LMArena(AsyncGeneratorProvider, ProviderModelMixin, AuthFileMixin):
642 642
643 643 if not cls._models_loaded:
644 644 cls.get_models()
645 is_image_model = model in image_models
645 is_image_model = model in cls.image_models
646 646 if not model:
647 647 model = cls.default_model
648 648 if model in cls.model_aliases:
Modified g4f/Provider/needs_auth/OpenaiChat.py +234 -171
@@ -1,18 +1,20 @@
1 1 from __future__ import annotations
2 2
3 import os
4 import re
5 3 import asyncio
6 import uuid
7 import json
8 4 import base64
9 import time
5 import hashlib
6 import json
7 import os
10 8 import random
11 from typing import AsyncIterator, Iterator, Optional, Generator, Dict, Union, List, Any
9 import re
10 import time
11 import uuid
12 12 from copy import copy
13 from typing import AsyncIterator, Iterator, Optional, Generator, Dict, Union, List, Any
13 14
14 15 try:
15 16 import nodriver
17
16 18 has_nodriver = True
17 19 except ImportError:
18 20 has_nodriver = False
@@ -22,15 +24,17 @@ from ...typing import AsyncResult, Messages, Cookies, MediaListType
22 24 from ...requests.raise_for_status import raise_for_status
23 25 from ...requests import StreamSession
24 26 from ...requests import get_nodriver_session
25 from ...image import ImageRequest, to_image, to_bytes, is_accepted_format, detect_file_type
27 from ...image import ImageRequest, to_image, to_bytes, detect_file_type
26 28 from ...errors import MissingAuthError, NoValidHarFileError, ModelNotFoundError
27 from ...providers.response import JsonConversation, FinishReason, SynthesizeData, AuthResult, ImageResponse, ImagePreview, ResponseType, JsonRequest, format_link
29 from ...providers.response import JsonConversation, FinishReason, SynthesizeData, AuthResult, ImageResponse, \
30 ImagePreview, ResponseType, JsonRequest, format_link
28 31 from ...providers.response import TitleGeneration, RequestLogin, Reasoning
29 32 from ...tools.media import merge_media
30 33 from ..helper import format_cookies, format_media_prompt, to_string
31 34 from ..openai.models import default_model, default_image_model, models, image_models, text_models, model_aliases
32 35 from ..openai.har_file import get_request_config
33 from ..openai.har_file import RequestConfig, arkReq, arkose_url, start_url, conversation_url, backend_url, prepare_url, backend_anon_url
36 from ..openai.har_file import RequestConfig, arkReq, arkose_url, start_url, conversation_url, backend_url, prepare_url, \
37 backend_anon_url
34 38 from ..openai.proofofwork import generate_proof_token
35 39 from ..openai.new import get_requirements_token, get_config
36 40 from ... import debug
@@ -87,6 +91,9 @@ UPLOAD_HEADERS = {
87 91 "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
88 92 }
89 93
94 ImagesCache: Dict[str, dict] = {}
95
96
90 97 class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
91 98 """A class for creating and managing conversations with OpenAI chat service"""
92 99
@@ -95,6 +102,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
95 102 working = True
96 103 active_by_default = True
97 104 use_nodriver = True
105 image_cache = True
98 106 supports_gpt_4 = True
99 107 supports_message_history = True
100 108 supports_system_message = True
@@ -127,11 +135,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
127 135
128 136 @classmethod
129 137 async def upload_files(
130 cls,
131 session: StreamSession,
132 auth_result: AuthResult,
133 media: MediaListType,
134 ) -> list[ImageRequest]:
138 cls,
139 session: StreamSession,
140 auth_result: AuthResult,
141 media: MediaListType,
142 ) -> List[ImageRequest]:
135 143 """
136 144 Upload an image to the service and get the download URL
137 145
@@ -143,11 +151,20 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
143 151 Returns:
144 152 An ImageRequest object that contains the download URL, file name, and other data
145 153 """
146 async def upload_file(file, image_name=None):
154
155 async def upload_file(file, image_name=None) -> ImageRequest:
147 156 debug.log(f"Uploading file: {image_name}")
148 157 file_data = {}
149 158
150 159 data_bytes = to_bytes(file)
160 # Check Cache
161 hasher = hashlib.md5()
162 hasher.update(data_bytes)
163 image_hash = hasher.hexdigest()
164 cache_file = ImagesCache.get(image_hash)
165 if cls.image_cache and file:
166 debug.log("Using cached image")
167 return ImageRequest(cache_file)
151 168 extension, mime_type = detect_file_type(data_bytes)
152 169 if "image" in mime_type:
153 170 # Convert the image to a PIL Image object
@@ -181,30 +198,31 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
181 198 # Put the image bytes to the upload URL and check the status
182 199 await asyncio.sleep(1)
183 200 async with session.put(
184 file_data["upload_url"],
185 data=data_bytes,
186 headers={
187 **UPLOAD_HEADERS,
188 "Content-Type": file_data["mime_type"],
189 "x-ms-blob-type": "BlockBlob",
190 "x-ms-version": "2020-04-08",
191 "Origin": "https://chatgpt.com",
192 }
201 file_data["upload_url"],
202 data=data_bytes,
203 headers={
204 **UPLOAD_HEADERS,
205 "Content-Type": file_data["mime_type"],
206 "x-ms-blob-type": "BlockBlob",
207 "x-ms-version": "2020-04-08",
208 "Origin": "https://chatgpt.com",
209 }
193 210 ) as response:
194 211 await raise_for_status(response)
195 212 # Post the file ID to the service and get the download URL
196 213 async with session.post(
197 f"{cls.url}/backend-api/files/{file_data['file_id']}/uploaded",
198 json={},
199 headers=auth_result.headers
214 f"{cls.url}/backend-api/files/{file_data['file_id']}/uploaded",
215 json={},
216 headers=auth_result.headers
200 217 ) as response:
201 218 cls._update_request_args(auth_result, session)
202 219 await raise_for_status(response, "Get download url failed")
203 220 uploaded_data = await response.json()
204 221 file_data["download_url"] = uploaded_data["download_url"]
222 ImagesCache[image_hash] = file_data.copy()
205 223 return ImageRequest(file_data)
206 224
207 medias = []
225 medias: List["ImageRequest"] = []
208 226 for item in media:
209 227 item = item if isinstance(item, tuple) else (item,)
210 228 __uploaded_media = await upload_file(*item)
@@ -242,7 +260,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
242 260 "id": str(uuid.uuid4()),
243 261 "author": {"role": message["role"]},
244 262 "content": {"content_type": "text", "parts": [to_string(message["content"])]},
245 "metadata": {"serialization_metadata": {"custom_symbol_offsets": []}, **({"system_hints": system_hints} if system_hints else {})},
263 "metadata": {"serialization_metadata": {"custom_symbol_offsets": []},
264 **({"system_hints": system_hints} if system_hints else {})},
246 265 "create_time": time.time(),
247 266 } for message in messages]
248 267 # Check if there is an image response
@@ -256,11 +275,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
256 275 "size_bytes": image_request.get("file_size"),
257 276 "width": image_request.get("width"),
258 277 }
259 for image_request in image_requests
278 for image_request in image_requests
260 279 # Add For Images Only
261 280 if image_request.get("use_case") == "multimodal"
262 281 ],
263 messages[-1]["content"]["parts"][0]]
282 messages[-1]["content"]["parts"][0]]
264 283 }
265 284 # Add the metadata object with the attachments
266 285 messages[-1]["metadata"] = {
@@ -278,12 +297,14 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
278 297 else {}
279 298 ),
280 299 }
281 for image_request in image_requests]
300 for image_request in image_requests]
282 301 }
283 302 return messages
284 303
285 304 @classmethod
286 async def get_generated_image(cls, session: StreamSession, auth_result: AuthResult, element: Union[dict, str], prompt: str = None, conversation_id: str = None) -> 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)
@@ -319,27 +340,31 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
319 340 debug.error("OpenaiChat: Download image failed")
320 341 debug.error(e)
321 342 if download_urls:
322 return ImagePreview(download_urls, prompt, {"headers": auth_result.headers}) if is_sediment else ImageResponse(download_urls, prompt, {"headers": auth_result.headers})
343 # status = None, finished_successfully
344 if is_sediment and status is None:
345 return ImagePreview(download_urls, prompt, {"status": status, "headers": auth_result.headers})
346 else:
347 return ImageResponse(download_urls, prompt, {"status": status, "headers": auth_result.headers})
323 348
324 349 @classmethod
325 350 async def create_authed(
326 cls,
327 model: str,
328 messages: Messages,
329 auth_result: AuthResult,
330 proxy: str = None,
331 timeout: int = 360,
332 auto_continue: bool = False,
333 action: Optional[str] = None,
334 conversation: Conversation = None,
335 media: MediaListType = None,
336 return_conversation: bool = True,
337 web_search: bool = False,
338 prompt: str = None,
339 conversation_mode: Optional[dict] = None,
340 temporary: Optional[bool] = None,
341 conversation_id: Optional[str] = None,
342 **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
343 368 ) -> AsyncResult:
344 369 """
345 370 Create an asynchronous generator for the conversation.
@@ -367,12 +392,12 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
367 392 if action is None:
368 393 action = "next"
369 394 async with StreamSession(
370 proxy=proxy,
371 impersonate="chrome",
372 timeout=timeout
395 proxy=proxy,
396 impersonate="chrome",
397 timeout=timeout
373 398 ) as session:
374 399 image_requests = None
375 media = merge_media(media, messages)
400 media = merge_media(media, messages)
376 401 if not cls.needs_auth and not media:
377 402 if cls._headers is None:
378 403 cls._create_request_args(cls._cookies)
@@ -436,18 +461,19 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
436 461 if temporary:
437 462 data["history_and_training_disabled"] = True
438 463 async with session.post(
439 prepare_url,
440 json=data,
441 headers=cls._headers
464 prepare_url,
465 json=data,
466 headers=cls._headers
442 467 ) as response:
443 468 await raise_for_status(response)
444 469 conduit_token = (await response.json())["conduit_token"]
445 470 async with session.post(
446 f"{cls.url}/backend-anon/sentinel/chat-requirements"
447 if cls._api_key is None else
448 f"{cls.url}/backend-api/sentinel/chat-requirements",
449 json={"p": None if not getattr(auth_result, "proof_token", None) else get_requirements_token(getattr(auth_result, "proof_token", None))},
450 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
451 477 ) as response:
452 478 if response.status in (401, 403):
453 479 raise MissingAuthError(f"Response status: {response.status}")
@@ -456,10 +482,10 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
456 482 await raise_for_status(response)
457 483 chat_requirements = await response.json()
458 484 need_turnstile = chat_requirements.get("turnstile", {}).get("required", False)
459 need_arkose = chat_requirements.get("arkose", {}).get("required", False)
460 chat_token = chat_requirements.get("token")
485 need_arkose = chat_requirements.get("arkose", {}).get("required", False)
486 chat_token = chat_requirements.get("token")
461 487
462 # if need_arkose and cls.request_config.arkose_token is None:
488 # if need_arkose and cls.request_config.arkose_token is None:
463 489 # await get_request_config(proxy)
464 490 # cls._create_request_args(auth_result.cookies, auth_result.headers)
465 491 # cls._set_api_key(auth_result.access_token)
@@ -476,23 +502,25 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
476 502 proof_token=proof_token
477 503 )
478 504 # [debug.log(text) for text in (
479 #f"Arkose: {'False' if not need_arkose else auth_result.arkose_token[:12]+'...'}",
480 #f"Proofofwork: {'False' if proofofwork is None else proofofwork[:12]+'...'}",
481 #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]+'...'}",
482 508 # )]
483 509 data = {
484 510 "action": "next",
485 511 "parent_message_id": conversation.message_id,
486 512 "model": model,
487 "timezone_offset_min":-120,
488 "timezone":"Europe/Berlin",
489 "conversation_mode":{"kind":"primary_assistant"},
490 "enable_message_followups":True,
513 "timezone_offset_min": -120,
514 "timezone": "Europe/Berlin",
515 "conversation_mode": {"kind": "primary_assistant"},
516 "enable_message_followups": True,
491 517 "system_hints": ["search"] if web_search else None,
492 "supports_buffering":True,
493 "supported_encodings":["v1"],
494 "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},
495 "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"
496 524 }
497 525 if temporary:
498 526 data["history_and_training_disabled"] = True
@@ -512,7 +540,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
512 540 new_messages = []
513 541 else:
514 542 new_messages.append(message)
515 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)
516 545 yield JsonRequest.from_dict(data)
517 546 headers = {
518 547 **cls._headers,
@@ -521,18 +550,18 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
521 550 "openai-sentinel-chat-requirements-token": chat_token,
522 551 **({} if conduit_token is None else {"x-conduit-token": conduit_token})
523 552 }
524 #if cls.request_config.arkose_token:
553 # if cls.request_config.arkose_token:
525 554 # headers["openai-sentinel-arkose-token"] = cls.request_config.arkose_token
526 555 if proofofwork is not None:
527 556 headers["openai-sentinel-proof-token"] = proofofwork
528 557 if need_turnstile and getattr(auth_result, "turnstile_token", None) is not None:
529 558 headers['openai-sentinel-turnstile-token'] = auth_result.turnstile_token
530 559 async with session.post(
531 backend_anon_url
532 if cls._api_key is None else
533 backend_url,
534 json=data,
535 headers=headers
560 backend_anon_url
561 if cls._api_key is None else
562 backend_url,
563 json=data,
564 headers=headers
536 565 ) as response:
537 566 cls._update_request_args(auth_result, session)
538 567 if response.status in (401, 403, 429, 500):
@@ -548,10 +577,12 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
548 577 if match.group(0) in matches:
549 578 continue
550 579 matches.append(match.group(0))
551 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)
552 582 if generated_image is not None:
553 583 yield generated_image
554 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):
555 586 if isinstance(chunk, str):
556 587 chunk = chunk.replace("\ue203", "").replace("\ue204", "").replace("\ue206", "")
557 588 buffer += chunk
@@ -561,9 +592,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
561 592 def citation_replacer(match: re.Match[str]):
562 593 ref_type = match.group(1)
563 594 ref_index = int(match.group(2))
564 if ((ref_type == "image" and is_image_embedding) or
565 is_video_embedding or
566 ref_type == "forecast"):
595 if ((ref_type == "image" and is_image_embedding) or
596 is_video_embedding or
597 ref_type == "forecast"):
567 598
568 599 reference = references.get_reference({
569 600 "ref_index": ref_index,
@@ -571,7 +602,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
571 602 })
572 603 if not reference:
573 604 return ""
574
605
575 606 if ref_type == "forecast":
576 607 if reference.get("alt"):
577 608 return reference.get("alt")
@@ -580,11 +611,13 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
580 611
581 612 if is_image_embedding and reference.get("content_url", ""):
582 613 return f"![{reference.get('title', '')}]({reference.get('content_url')})"
583
614
584 615 if is_video_embedding:
585 if reference.get("url", "") and reference.get("thumbnail_url", ""):
616 if reference.get("url", "") and reference.get("thumbnail_url",
617 ""):
586 618 return f"[![{reference.get('title', '')}]({reference['thumbnail_url']})]({reference['url']})"
587 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))
588 621 if video_match:
589 622 return video_match.group(1)
590 623 return ""
@@ -595,9 +628,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
595 628 })
596 629 if source_index is not None and len(sources.list) > source_index:
597 630 link = sources.list[source_index]["url"]
598 return f"[[{source_index+1}]]({link})"
631 return f"[[{source_index + 1}]]({link})"
599 632 return f""
600
633
601 634 def products_replacer(match: re.Match[str]):
602 635 try:
603 636 products_data = json.loads(match.group(1))
@@ -612,25 +645,30 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
612 645 return ""
613 646
614 647 sequence_content = match.group(1)
615 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", "")
616 651 sequence_content = sequence_content.replace("navlist\n", "#### ")
617
652
618 653 # Handle search, news, view and image citations
619 654 is_image_embedding = sequence_content.startswith("i\nturn")
620 655 is_video_embedding = sequence_content.startswith("video\n")
621 656 sequence_content = re.sub(
622 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+)',
623 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,
624 659 sequence_content
625 660 )
626 sequence_content = re.sub(r'products\n(.*)', products_replacer, sequence_content)
627 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)
628 665 return sequence_content
629
666
630 667 # process only completed sequences and do not touch start of next not completed sequence
631 buffer = re.sub(r'\ue200(.*?)\ue201', sequence_replacer, buffer, flags=re.DOTALL)
632
633 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
634 672 continue
635 673 else:
636 674 # do not yield to consume rest part of special sequence
@@ -647,7 +685,8 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
647 685 if sources.list:
648 686 yield sources
649 687 if conversation.generated_images:
650 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})
651 690 conversation.generated_images = None
652 691 conversation.prompt = None
653 692 if return_conversation:
@@ -667,7 +706,9 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
667 706 yield FinishReason(conversation.finish_reason)
668 707
669 708 @classmethod
670 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:
671 712 if not line.startswith(b"data: "):
672 713 return
673 714 elif line.startswith(b"data: [DONE]"):
@@ -706,9 +747,14 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
706 747 elif m.get("p") == "/message/metadata/image_gen_title":
707 748 fields.prompt = m.get("v")
708 749 elif m.get("p") == "/message/content/parts/0/asset_pointer":
709 generated_images = fields.generated_images = await cls.get_generated_image(session, auth_result, m.get("v"), fields.prompt, fields.conversation_id)
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)
710 756 if generated_images is not None:
711 if buffer:
757 if buffer:
712 758 yield buffer
713 759 yield generated_images
714 760 elif m.get("p") == "/message/metadata/search_result_groups":
@@ -735,41 +781,48 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
735 781 if match and m.get("o") == "append" and isinstance(m.get("v"), dict):
736 782 idx = int(match.group(1))
737 783 references.merge_reference(idx, m.get("v"))
738 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):
739 786 for link in m.get("v", []) or []:
740 787 sources.add_source(link)
741 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):
742 790 for link in m.get("v", []) or []:
743 791 sources.add_source(link)
744 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):
745 794 match = re.match(r"^/message/metadata/content_references/(\d+)/refs$", m.get("p"))
746 795 if match:
747 796 idx = int(match.group(1))
748 797 references.update_reference(idx, m.get("o"), "refs", m.get("v"))
749 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):
750 800 match = re.match(r"^/message/metadata/content_references/(\d+)/alt$", m.get("p"))
751 801 if match:
752 802 idx = int(match.group(1))
753 803 references.update_reference(idx, m.get("o"), "alt", m.get("v"))
754 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):
755 806 match = re.match(r"^/message/metadata/content_references/(\d+)/prompt_text$", m.get("p"))
756 807 if match:
757 808 idx = int(match.group(1))
758 809 references.update_reference(idx, m.get("o"), "prompt_text", m.get("v"))
759 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):
760 812 match = re.match(r"^/message/metadata/content_references/(\d+)/refs/(\d+)$", m.get("p"))
761 813 if match:
762 814 reference_idx = int(match.group(1))
763 815 ref_idx = int(match.group(2))
764 816 references.update_reference(reference_idx, m.get("o"), "refs", m.get("v"), ref_idx)
765 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):
766 819 match = re.match(r"^/message/metadata/content_references/(\d+)/images$", m.get("p"))
767 820 if match:
768 821 idx = int(match.group(1))
769 822 references.update_reference(idx, m.get("o"), "images", m.get("v"))
770 823 elif m.get("p") == "/message/metadata/finished_text":
771 824 fields.is_thinking = False
772 if buffer:
825 if buffer:
773 826 yield buffer
774 827 yield Reasoning(status=m.get("v"))
775 828 elif m.get("p") == "/message/metadata" and fields.recipient == "all":
@@ -785,10 +838,11 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
785 838 fields.recipient = m.get("recipient", fields.recipient)
786 839 if fields.recipient == "all":
787 840 c = m.get("content", {})
788 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", {}):
789 843 fields.is_thinking = True
790 844 yield Reasoning(status=m.get("metadata", {}).get("initial_text"))
791 #if c.get("content_type") == "multimodal_text":
845 # if c.get("content_type") == "multimodal_text":
792 846 # for part in c.get("parts"):
793 847 # if isinstance(part, dict) and part.get("content_type") == "image_asset_pointer":
794 848 # yield await cls.get_generated_image(session, auth_result, part, fields.prompt, fields.conversation_id)
@@ -803,13 +857,13 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
803 857 @classmethod
804 858 async def synthesize(cls, params: dict) -> AsyncIterator[bytes]:
805 859 async with StreamSession(
806 impersonate="chrome",
807 timeout=0
860 impersonate="chrome",
861 timeout=0
808 862 ) as session:
809 863 async with session.get(
810 f"{cls.url}/backend-api/synthesize",
811 params=params,
812 headers=cls._headers
864 f"{cls.url}/backend-api/synthesize",
865 params=params,
866 headers=cls._headers
813 867 ) as response:
814 868 await raise_for_status(response)
815 869 async for chunk in response.iter_content():
@@ -817,15 +871,15 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
817 871
818 872 @classmethod
819 873 async def login(
820 cls,
821 proxy: str = None,
822 api_key: str = None,
823 proof_token: str = None,
824 cookies: Cookies = None,
825 headers: dict = None,
826 **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
827 881 ) -> AsyncIterator:
828 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():
829 883 cls._headers = cls._api_key = None
830 884 if cls._headers is None or headers is not None:
831 885 cls._headers = {} if headers is None else headers
@@ -858,6 +912,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
858 912 async def nodriver_auth(cls, proxy: str = None):
859 913 async with get_nodriver_session(proxy=proxy) as browser:
860 914 page = await browser.get(cls.url)
915
861 916 def on_request(event: nodriver.cdp.network.RequestWillBeSent, page=None):
862 917 if event.request.url == start_url or event.request.url.startswith(conversation_url):
863 918 if cls.request_config.headers is None:
@@ -866,9 +921,10 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
866 921 cls.request_config.headers[key.lower()] = value
867 922 elif event.request.url in (backend_url, backend_anon_url):
868 923 if "OpenAI-Sentinel-Proof-Token" in event.request.headers:
869 cls.request_config.proof_token = json.loads(base64.b64decode(
870 event.request.headers["OpenAI-Sentinel-Proof-Token"].split("gAAAAAB", 1)[-1].split("~")[0].encode()
871 ).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())
872 928 if "OpenAI-Sentinel-Turnstile-Token" in event.request.headers:
873 929 cls.request_config.turnstile_token = event.request.headers["OpenAI-Sentinel-Turnstile-Token"]
874 930 if "Authorization" in event.request.headers:
@@ -881,6 +937,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
881 937 arkBody=event.request.post_data,
882 938 userAgent=event.request.headers.get("User-Agent")
883 939 )
940
884 941 await page.send(nodriver.cdp.network.enable())
885 942 page.add_handler(nodriver.cdp.network.RequestWillBeSent, on_request)
886 943 await page.reload()
@@ -912,7 +969,7 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
912 969 if cls._api_key is not None or not cls.needs_auth:
913 970 break
914 971 await asyncio.sleep(1)
915 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] + '...'}")
916 973 while True:
917 974 if cls.request_config.proof_token:
918 975 break
@@ -970,11 +1027,14 @@ class OpenaiChat(AsyncAuthedProvider, ProviderModelMixin):
970 1027 if cls._cookies:
971 1028 cls._headers["cookie"] = format_cookies(cls._cookies)
972 1029
1030
973 1031 class Conversation(JsonConversation):
974 1032 """
975 1033 Class to encapsulate response fields.
976 1034 """
977 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):
978 1038 self.conversation_id = conversation_id
979 1039 self.message_id = message_id
980 1040 self.finish_reason = finish_reason
@@ -987,8 +1047,9 @@ class Conversation(JsonConversation):
987 1047 self.prompt = None
988 1048 self.generated_images: ImagePreview = None
989 1049
1050
990 1051 def get_cookies(
991 urls: Optional[Iterator[str]] = None
1052 urls: Optional[Iterator[str]] = None
992 1053 ) -> Generator[Dict, Dict, Dict[str, str]]:
993 1054 params = {}
994 1055 if urls is not None:
@@ -1000,6 +1061,7 @@ def get_cookies(
1000 1061 json = yield cmd_dict
1001 1062 return {c["name"]: c["value"] for c in json['cookies']} if 'cookies' in json else {}
1002 1063
1064
1003 1065 class OpenAISources(ResponseType):
1004 1066 list: List[Dict[str, str]]
1005 1067
@@ -1025,7 +1087,7 @@ class OpenAISources(ResponseType):
1025 1087 if existing_source and idx is not None:
1026 1088 self.list[idx] = source
1027 1089 return
1028
1090
1029 1091 existing_source, idx = self.find_by_url(source["url"])
1030 1092 if existing_source and idx is not None:
1031 1093 self.list[idx] = source
@@ -1038,53 +1100,54 @@ class OpenAISources(ResponseType):
1038 1100 if not self.list:
1039 1101 return ""
1040 1102 return "\n\n\n\n" + ("\n>\n".join([
1041 f"> [{idx+1}] {format_link(link['url'], link.get('title', ''))}"
1103 f"> [{idx + 1}] {format_link(link['url'], link.get('title', ''))}"
1042 1104 for idx, link in enumerate(self.list)
1043 1105 ]))
1044
1045 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:
1046 1108 ref_index = source.get("ref_id", {}).get("ref_index", None)
1047 1109 ref_type = source.get("ref_id", {}).get("ref_type", None)
1048 1110 if isinstance(ref_index, int):
1049 1111 return {
1050 "ref_index": ref_index,
1112 "ref_index": ref_index,
1051 1113 "ref_type": ref_type,
1052 1114 }
1053
1115
1054 1116 for ref_info in source.get('refs') or []:
1055 1117 ref_index = ref_info.get("ref_index", None)
1056 1118 ref_type = ref_info.get("ref_type", None)
1057 1119 if isinstance(ref_index, int):
1058 1120 return {
1059 "ref_index": ref_index,
1121 "ref_index": ref_index,
1060 1122 "ref_type": ref_type,
1061 1123 }
1062
1124
1063 1125 return None
1064 1126
1065 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]):
1066 1128 for idx, source in enumerate(self.list):
1067 1129 source_ref_info = self.get_ref_info(source)
1068 if (source_ref_info and
1069 source_ref_info["ref_index"] == ref_info["ref_index"] and
1070 source_ref_info["ref_type"] == ref_info["ref_type"]):
1071 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
1072 1134
1073 1135 return None, None
1074
1136
1075 1137 def find_by_url(self, url: str):
1076 1138 for idx, source in enumerate(self.list):
1077 1139 if source["url"] == url:
1078 1140 return source, idx
1079 return None, None
1141 return None, None
1080 1142
1081 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:
1082 1144 _, index = self.find_by_ref_info(ref_info)
1083 1145 if index is not None:
1084 return index
1146 return index
1085 1147
1086 1148 return None
1087 1149
1150
1088 1151 class ContentReferences:
1089 1152 def __init__(self) -> None:
1090 1153 self.list: List[Dict[str, Any]] = []
@@ -1098,16 +1161,16 @@ class ContentReferences:
1098 1161
1099 1162 self.list[idx] = {**self.list[idx], **reference_part}
1100 1163
1101 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:
1102 1165 while len(self.list) <= idx:
1103 1166 self.list.append({})
1104
1167
1105 1168 if operation == "append" or operation == "add":
1106 1169 if not isinstance(self.list[idx].get(field, None), list):
1107 1170 self.list[idx][field] = []
1108 1171 if isinstance(value, list):
1109 1172 self.list[idx][field].extend(value)
1110 else:
1173 else:
1111 1174 self.list[idx][field].append(value)
1112 1175
1113 1176 if operation == "replace" and ref_idx is not None:
@@ -1123,10 +1186,10 @@ class ContentReferences:
1123 1186 self.list[idx][field] = value
1124 1187
1125 1188 def get_ref_info(
1126 self,
1127 source: Dict[str, str],
1128 target_ref_info: Dict[str, Union[str, int]]
1129 ) -> 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:
1130 1193 for idx, ref_info in enumerate(source.get("refs", [])) or []:
1131 1194 if not isinstance(ref_info, dict):
1132 1195 continue
@@ -1134,11 +1197,11 @@ class ContentReferences:
1134 1197 ref_index = ref_info.get("ref_index", None)
1135 1198 ref_type = ref_info.get("ref_type", None)
1136 1199 if isinstance(ref_index, int) and isinstance(ref_type, str):
1137 if (not target_ref_info or
1138 (target_ref_info["ref_index"] == ref_index and
1139 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)):
1140 1203 return {
1141 "ref_index": ref_index,
1204 "ref_index": ref_index,
1142 1205 "ref_type": ref_type,
1143 1206 "idx": idx
1144 1207 }
@@ -1149,9 +1212,9 @@ class ContentReferences:
1149 1212 for reference in self.list:
1150 1213 reference_ref_info = self.get_ref_info(reference, ref_info)
1151 1214
1152 if (not reference_ref_info or
1153 reference_ref_info["ref_index"] != ref_info["ref_index"] or
1154 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"]):
1155 1218 continue
1156 1219
1157 1220 if ref_info["ref_type"] != "image":
Modified g4f/image/__init__.py +9 -0
@@ -9,6 +9,8 @@ from pathlib import Path
9 9 from typing import Optional
10 10 from urllib.parse import urlparse
11 11
12 import requests
13
12 14 try:
13 15 from PIL import Image, ImageOps
14 16 has_requirements = True
@@ -383,6 +385,13 @@ def to_bytes(image: ImageType) -> bytes:
383 385 return Path(path).read_bytes()
384 386 else:
385 387 raise FileNotFoundError(f"File not found: {path}")
388 else:
389 resp = requests.get(image, headers={
390 "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",
391 })
392 if resp.ok and is_accepted_format(resp.content):
393 return resp.content
394 raise ValueError("Invalid image url. Expected bytes, str, or PIL Image.")
386 395 else:
387 396 raise ValueError("Invalid image format. Expected bytes, str, or PIL Image.")
388 397 elif isinstance(image, Image.Image):
Modified g4f/requests/aiohttp.py +8 -4
@@ -31,17 +31,21 @@ class StreamResponse(ClientResponse):
31 31 except json.JSONDecodeError:
32 32 continue
33 33
34 class StreamSession():
34 class StreamSession:
35 35 def __init__(
36 36 self,
37 headers: dict = {},
37 headers=None,
38 38 timeout: int = None,
39 39 connector: BaseConnector = None,
40 40 proxy: str = None,
41 proxies: dict = {},
41 proxies=None,
42 42 impersonate = None,
43 43 **kwargs
44 44 ):
45 if proxies is None:
46 proxies = {}
47 if headers is None:
48 headers = {}
45 49 if impersonate:
46 50 headers = {
47 51 **DEFAULT_HEADERS,
@@ -49,7 +53,7 @@ class StreamSession():
49 53 }
50 54 connect = None
51 55 if isinstance(timeout, tuple):
52 connect, timeout = timeout;
56 connect, timeout = timeout
53 57 if timeout is not None:
54 58 timeout = ClientTimeout(timeout, connect)
55 59 if proxy is None: