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

XFEstudio/gpt4free

feat: add Qwen provider with conversation support and stream handling

- Added `Qwen` to `g4f/Provider/__init__.py` for provider registration - Created new Qwen provider in `g4f/Provider/Qwen.py` using `AsyncGeneratorProvider` - Implemented conversation state via new `JsonConversation` argument - Replaced raw `print` statements with `debug.log` for internal logging - Introduced `get_last_user_message()` for improved prompt extraction - Added support for `Reasoning` and `Usage` response types during SSE parsing - Replaced manual SSE parsing with `sse_stream()` utility from `requests` - Added `active_by_default = True` to `Qwen` and modified related headers - Tracked message and parent IDs for contextual threading - Updated `Usage` class in `g4f/providers/response.py` to support `input_tokens` and `output_tokens` - Refactored Nvidia provider: removed unused attributes and set `models_needs_auth = True

31fee02c
hlohaus <983577+hlohaus@users.noreply.github.com>
提交于

代码差异

4 个文件 +78 -67
Modified g4f/Provider/Qwen.py +68 -59
@@ -1,3 +1,5 @@
1 from __future__ import annotations
2
1 3 import asyncio
2 4 import json
3 5 import re
@@ -6,8 +8,12 @@ from time import time
6 8
7 9 import aiohttp
8 10 from ..errors import RateLimitError
9 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
10 11 from ..typing import AsyncResult, Messages
12 from ..providers.response import JsonConversation, Reasoning, Usage
13 from ..requests import sse_stream
14 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
15 from .helper import get_last_user_message
16 from .. import debug
11 17
12 18 class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
13 19 """
@@ -16,6 +22,7 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
16 22 """
17 23 url = "https://chat.qwen.ai"
18 24 working = True
25 active_by_default = True
19 26 supports_stream = True
20 27 supports_message_history = False
21 28
@@ -46,6 +53,7 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
46 53 cls,
47 54 model: str,
48 55 messages: Messages,
56 conversation: JsonConversation = None,
49 57 proxy: str = None,
50 58 timeout: int = 120,
51 59 stream: bool = True,
@@ -70,13 +78,13 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
70 78 'Source': 'web'
71 79 }
72 80
73 prompt = messages[-1]["content"]
81 prompt = get_last_user_message(messages)
74 82
75 83 async with aiohttp.ClientSession(headers=headers) as session:
76 84 for attempt in range(5):
77 85 try:
78 86 if not cls._midtoken:
79 print("[Qwen] INFO: No active midtoken. Fetching a new one...")
87 debug.log("[Qwen] INFO: No active midtoken. Fetching a new one...")
80 88 async with session.get('https://sg-wum.alibaba.com/w/wu.json', proxy=proxy) as r:
81 89 r.raise_for_status()
82 90 text = await r.text()
@@ -85,42 +93,48 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
85 93 raise RuntimeError("Failed to extract bx-umidtoken.")
86 94 cls._midtoken = match.group(1)
87 95 cls._midtoken_uses = 1
88 print(f"[Qwen] INFO: New midtoken obtained. Use count: {cls._midtoken_uses}. Midtoken: {cls._midtoken}")
96 debug.log(f"[Qwen] INFO: New midtoken obtained. Use count: {cls._midtoken_uses}. Midtoken: {cls._midtoken}")
89 97 else:
90 98 cls._midtoken_uses += 1
91 print(f"[Qwen] INFO: Reusing midtoken. Use count: {cls._midtoken_uses}")
99 debug.log(f"[Qwen] INFO: Reusing midtoken. Use count: {cls._midtoken_uses}")
92 100
93 101 req_headers = session.headers.copy()
94 102 req_headers['bx-umidtoken'] = cls._midtoken
95 103 req_headers['bx-v'] = '2.5.31'
96
97 chat_payload = {
98 "title": "New Chat",
99 "models": [model_name],
100 "chat_mode": "normal",
101 "chat_type": "t2t",
102 "timestamp": int(time() * 1000)
103 }
104 async with session.post(
105 f'{cls.url}/api/v2/chats/new', json=chat_payload, headers=req_headers, proxy=proxy
106 ) as resp:
107 resp.raise_for_status()
108 data = await resp.json()
109 if not (data.get('success') and data['data'].get('id')):
110 raise RuntimeError(f"Failed to create chat: {data}")
111 chat_id = data['data']['id']
112
104 message_id = str(uuid.uuid4())
105 parent_id = None
106 if conversation is None:
107 chat_payload = {
108 "title": "New Chat",
109 "models": [model_name],
110 "chat_mode": "normal",
111 "chat_type": "t2t",
112 "timestamp": int(time() * 1000)
113 }
114 async with session.post(
115 f'{cls.url}/api/v2/chats/new', json=chat_payload, headers=req_headers, proxy=proxy
116 ) as resp:
117 resp.raise_for_status()
118 data = await resp.json()
119 if not (data.get('success') and data['data'].get('id')):
120 raise RuntimeError(f"Failed to create chat: {data}")
121 conversation = JsonConversation(
122 chat_id=data['data']['id'],
123 cookies={key: value for key, value in resp.cookies.items()},
124 parent_id=None
125 )
126
113 127 msg_payload = {
114 128 "stream": stream,
115 129 "incremental_output": stream,
116 "chat_id": chat_id,
130 "chat_id": conversation.chat_id,
117 131 "chat_mode": "normal",
118 132 "model": model_name,
119 "parent_id": None,
133 "parent_id": conversation.parent_id,
120 134 "messages": [
121 135 {
122 "fid": str(uuid.uuid4()),
123 "parentId": None,
136 "fid": message_id,
137 "parentId": conversation.parent_id,
124 138 "childrenIds": [],
125 139 "role": "user",
126 140 "content": prompt,
@@ -145,50 +159,45 @@ class Qwen(AsyncGeneratorProvider, ProviderModelMixin):
145 159 }
146 160
147 161 async with session.post(
148 f'{cls.url}/api/v2/chat/completions?chat_id={chat_id}', json=msg_payload,
149 headers=req_headers, proxy=proxy, timeout=timeout
162 f'{cls.url}/api/v2/chat/completions?chat_id={conversation.chat_id}', json=msg_payload,
163 headers=req_headers, proxy=proxy, timeout=timeout, cookies=conversation.cookies
150 164 ) as resp:
151 165 first_line = await resp.content.readline()
152 166 line_str = first_line.decode().strip()
153 167 if line_str.startswith('{'):
154 error_data = json.loads(line_str)
155 if error_data.get("data", {}).get("code") == "RateLimited":
156 raise RuntimeError("RateLimited by JSON response")
157
158 buffer = first_line
168 data = json.loads(line_str)
169 if data.get("data", {}).get("code"):
170 raise RuntimeError(f"Response: {data}")
171 conversation.parent_id = data.get("response.created", {}).get("response_id")
172 yield conversation
173
159 174 thinking_started = False
160 async for chunk in resp.content:
161 buffer += chunk
162 while b'\n' in buffer:
163 line, buffer = buffer.split(b'\n', 1)
164 line_str = line.decode().strip()
165 if not line_str.startswith("data: "): continue
166 try:
167 data_json = json.loads(line_str.lstrip("data: "))
168 choices = data_json.get("choices", [])
169 if not choices: continue
170 delta = choices[0].get("delta", {})
171 phase = delta.get("phase")
172 content = delta.get("content")
173 if phase == "think" and not thinking_started:
174 yield "<think>"
175 thinking_started = True
176 elif phase == "answer" and thinking_started:
177 yield "</think>"
178 thinking_started = False
179 if content:
180 yield content
181 except (json.JSONDecodeError, KeyError, IndexError):
182 continue
183 if thinking_started:
184 yield "</think>"
175 usage = None
176 async for chunk in sse_stream(resp):
177 try:
178 usage = chunk.get("usage", usage)
179 choices = chunk.get("choices", [])
180 if not choices: continue
181 delta = choices[0].get("delta", {})
182 phase = delta.get("phase")
183 content = delta.get("content")
184 if phase == "think" and not thinking_started:
185 thinking_started = True
186 elif phase == "answer" and thinking_started:
187 thinking_started = False
188 if content:
189 yield Reasoning(content) if thinking_started else content
190 except (json.JSONDecodeError, KeyError, IndexError):
191 continue
192 if usage:
193 yield Usage(**usage)
185 194 return
186 195
187 196 except (aiohttp.ClientResponseError, RuntimeError) as e:
188 197 is_rate_limit = (isinstance(e, aiohttp.ClientResponseError) and e.status == 429) or \
189 198 ("RateLimited" in str(e))
190 199 if is_rate_limit:
191 print(f"[Qwen] WARNING: Rate limit detected (attempt {attempt + 1}/5). Invalidating current midtoken.")
200 debug.log(f"[Qwen] WARNING: Rate limit detected (attempt {attempt + 1}/5). Invalidating current midtoken.")
192 201 cls._midtoken = None
193 202 cls._midtoken_uses = 0
194 203 await asyncio.sleep(2)
Modified g4f/Provider/__init__.py +1 -0
@@ -54,6 +54,7 @@ from .PerplexityLabs import PerplexityLabs
54 54 from .PollinationsAI import PollinationsAI
55 55 from .PollinationsImage import PollinationsImage
56 56 from .Startnest import Startnest
57 from .Qwen import Qwen
57 58 from .TeachAnything import TeachAnything
58 59 from .WeWordle import WeWordle
59 60 from .YouTube import YouTube
Modified g4f/Provider/needs_auth/Nvidia.py +3 -8
@@ -9,12 +9,7 @@ class Nvidia(OpenaiTemplate):
9 9 login_url = "https://google.com"
10 10 url = "https://build.nvidia.com"
11 11 working = True
12 active_by_default = True
12 13 needs_auth = True
13 supports_stream = True
14 supports_system_message = True
15 supports_message_history = True
16 default_model = DEFAULT_MODEL.split("/")[-1]
17
18 @classmethod
19 def get_model(cls, model: str, **kwargs) -> str:
20 return super().get_model(model, **kwargs)
14 models_needs_auth = True
15 default_model = DEFAULT_MODEL
Modified g4f/providers/response.py +6 -0
@@ -170,12 +170,18 @@ class Usage(JsonMixin, HiddenResponse):
170 170 self,
171 171 promptTokens: int = None,
172 172 completionTokens: int = None,
173 input_tokens: int = None,
174 output_tokens: int = None,
173 175 **kwargs
174 176 ):
175 177 if promptTokens is not None:
176 178 kwargs["prompt_tokens"] = promptTokens
177 179 if completionTokens is not None:
178 180 kwargs["completion_tokens"] = completionTokens
181 if input_tokens is not None:
182 kwargs["prompt_tokens"] = input_tokens
183 if output_tokens is not None:
184 kwargs["completion_tokens"] = output_tokens
179 185 if "total_tokens" not in kwargs and "prompt_tokens" in kwargs and "completion_tokens" in kwargs:
180 186 kwargs["total_tokens"] = kwargs["prompt_tokens"] + kwargs["completion_tokens"]
181 187 return super().__init__(**kwargs)