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

XFEstudio/gpt4free

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.

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

代码差异

1 个文件 +173 -38
Modified g4f/Provider/Qwen.py +173 -38
@@ -1,23 +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
12 from ..requests.aiohttp import StreamSession
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
13 21 from ..errors import RateLimitError, ResponseError
14 from ..typing import AsyncResult, Messages, MediaListType
15 22 from ..providers.response import JsonConversation, Reasoning, Usage, ImageResponse, FinishReason
16 23 from ..requests import sse_stream
24 from ..requests.aiohttp import StreamSession
17 25 from ..tools.media import merge_media
18 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
19 from .helper import get_last_user_message
20 from .. import debug
26 from ..typing import AsyncResult, Messages, MediaListType
21 27
22 28 try:
23 29 import curl_cffi
@@ -26,6 +32,56 @@ try:
26 32 except ImportError:
27 33 has_curl_cffi = False
28 34
35 # Global variables to manage Yupp 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
29 85 text_models = [
30 86 'qwen3-max-preview', 'qwen-plus-2025-09-11', 'qwen3-235b-a22b', 'qwen3-coder-plus', 'qwen3-30b-a3b',
31 87 'qwen3-coder-30b-a3b-instruct', 'qwen-max-latest', 'qwen-plus-2025-01-25', 'qwq-32b', 'qwen-turbo-2025-02-11',
@@ -61,12 +117,12 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
61 117 active_by_default = True
62 118 supports_stream = True
63 119 supports_message_history = False
64
120 image_cache = True
65 121 _models_loaded = True
66 122 image_models = image_models
67 123 text_models = text_models
68 124 vision_models = vision_models
69 models = models
125 models: list = models
70 126 default_model = "qwen3-235b-a22b"
71 127
72 128 _midtoken: str = None
@@ -98,34 +154,106 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
98 154 return cls.models
99 155
100 156 @classmethod
101 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 = {}
102 160 files = []
103 for _file, file_name in media:
104 file_type, _ = mimetypes.guess_type(file_name)
105 file_class: Literal["default", "vision", "video", "audio", "document"] = "default"
106 _type: Literal["file", "image", "video", "audio"] = "file"
107 showType: Literal["file", "image", "video", "audio"] = "file"
108
109 if isinstance(_file, str) and _file.startswith('http'):
110 if chat_type == "image_edit" or (file_type and file_type.startswith("image")):
111 file_class = "vision"
112 _type = "image"
113 if not file_type:
114 # Try to infer from file extension, fallback to generic
115 ext = file_name.split('.')[-1].lower() if '.' in file_name else ''
116 file_type = mimetypes.types_map.get(f'.{ext}', 'application/octet-stream')
117 showType = "image"
118
119 files.append(
120 {
121 "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": {
122 234 "name": file_name,
123 "file_type": file_type,
124 "showType": showType,
125 "file_class": file_class,
126 "url": _file
127 }
128 )
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)
129 257 return files
130 258
131 259 @classmethod
@@ -158,7 +286,7 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
158 286 """
159 287
160 288 model_name = cls.get_model(model)
161
289 token = kwargs.get("token")
162 290 headers = {
163 291 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36',
164 292 'Accept': '*/*',
@@ -170,13 +298,19 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
170 298 'Sec-Fetch-Mode': 'cors',
171 299 'Sec-Fetch-Site': 'same-origin',
172 300 'Connection': 'keep-alive',
173 'Authorization': 'Bearer',
301 'Authorization': f'Bearer {token}' if token else "Bearer",
174 302 'Source': 'web'
175 303 }
176 304
177 305 prompt = get_last_user_message(messages)
178 306
179 307 async with StreamSession(headers=headers) as session:
308 try:
309 async with session.get('https://chat.qwen.ai/api/v1/auths/', proxy=proxy) as user_info_res:
310 user_info_res.raise_for_status()
311 debug.log(await user_info_res.json())
312 except:
313 ...
180 314 for attempt in range(5):
181 315 try:
182 316 if not cls._midtoken:
@@ -222,7 +356,8 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
222 356 files = []
223 357 media = list(merge_media(media, messages))
224 358 if media:
225 files = await cls.prepare_files(media, chat_type=chat_type)
359 files = await cls.prepare_files(media, session=session,
360 headers=req_headers)
226 361
227 362 msg_payload = {
228 363 "stream": stream,