返回提交历史
Modified
g4f/Provider/Qwen.py
+173
-38
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
代码差异
1 个文件
+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,