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

XFEstudio/gpt4free

Restore AsyncClient streaming return type Improve error handling in DDG use OpenaiAPI provider in DeepInfraChat Support api_base and response_type parameters in API

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

代码差异

10 个文件 +95 -145
Modified docs/async_client.md +4 -5
@@ -149,8 +149,8 @@ from g4f.client import AsyncClient
149 149
150 150 async def main():
151 151 client = AsyncClient()
152
153 stream = await client.chat.completions.create(
152
153 stream = client.chat.completions.stream(
154 154 model="gpt-4",
155 155 messages=[
156 156 {
@@ -158,10 +158,9 @@ async def main():
158 158 "content": "Say this is a test"
159 159 }
160 160 ],
161 stream=True,
162 161 web_search = False
163 162 )
164
163
165 164 async for chunk in stream:
166 165 if chunk.choices and chunk.choices[0].delta.content:
167 166 print(chunk.choices[0].delta.content, end="")
@@ -366,7 +365,7 @@ async def main():
366 365 "query": "Latest advancements in AI",
367 366 "max_results": 5,
368 367 "max_words": 2500,
369 "backend": "api",
368 "backend": "auto",
370 369 "add_text": True,
371 370 "timeout": 5
372 371 },
Modified docs/client.md +1 -1
@@ -386,7 +386,7 @@ tool_calls = [
386 386 "query": "Latest advancements in AI",
387 387 "max_results": 5,
388 388 "max_words": 2500,
389 "backend": "api",
389 "backend": "auto",
390 390 "add_text": True,
391 391 "timeout": 5
392 392 },
Modified etc/unittest/client.py +2 -2
@@ -37,14 +37,14 @@ class AsyncTestPassModel(unittest.IsolatedAsyncioTestCase):
37 37 async def test_max_stream(self):
38 38 client = AsyncClient(provider=YieldProviderMock)
39 39 messages = [{'role': 'user', 'content': chunk} for chunk in ["How ", "are ", "you", "?"]]
40 response = await client.chat.completions.create(messages, "Hello", stream=True)
40 response = client.chat.completions.create(messages, "Hello", stream=True)
41 41 async for chunk in response:
42 42 chunk: ChatCompletionChunk = chunk
43 43 self.assertIsInstance(chunk, ChatCompletionChunk)
44 44 if chunk.choices[0].delta.content is not None:
45 45 self.assertIsInstance(chunk.choices[0].delta.content, str)
46 46 messages = [{'role': 'user', 'content': chunk} for chunk in ["You ", "You ", "Other", "?"]]
47 response = await client.chat.completions.create(messages, "Hello", stream=True, max_tokens=2)
47 response = client.chat.completions.create(messages, "Hello", stream=True, max_tokens=2)
48 48 response_list = []
49 49 async for chunk in response:
50 50 response_list.append(chunk)
Modified g4f/Provider/DDG.py +60 -84
@@ -11,19 +11,11 @@ from ..requests.raise_for_status import raise_for_status
11 11 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
12 12 from .helper import format_prompt
13 13 from ..providers.response import FinishReason, JsonConversation
14 from ..errors import ModelNotSupportedError, ResponseStatusError, RateLimitError, TimeoutError, ConversationLimitError
14 15
15 16 class DuckDuckGoSearchException(Exception):
16 17 """Base exception class for duckduckgo_search."""
17 18
18 class RatelimitException(DuckDuckGoSearchException):
19 """Raised for rate limit exceeded errors during API requests."""
20
21 class TimeoutException(DuckDuckGoSearchException):
22 """Raised for timeout errors during API requests."""
23
24 class ConversationLimitException(DuckDuckGoSearchException):
25 """Raised for conversation limit during API requests to AI endpoint."""
26
27 19 class Conversation(JsonConversation):
28 20 vqd: str = None
29 21 message_history: Messages = []
@@ -61,7 +53,7 @@ class DDG(AsyncGeneratorProvider, ProviderModelMixin):
61 53 if model in cls.model_aliases:
62 54 model = cls.model_aliases[model]
63 55 if model not in cls.models:
64 raise ValueError(f"Model {model} not supported. Available models: {cls.models}")
56 raise ModelNotSupportedError(f"Model {model} not supported. Available models: {cls.models}")
65 57 return model
66 58
67 59 @classmethod
@@ -88,25 +80,20 @@ class DDG(AsyncGeneratorProvider, ProviderModelMixin):
88 80 try:
89 81 await cls.sleep()
90 82 async with session.get(cls.status_url, headers=headers) as response:
91 if response.status == 200:
92 vqd = response.headers.get("x-vqd-4", "")
93 if vqd:
94 return vqd
95 elif response.status == 429:
96 if attempt < max_retries - 1:
97 wait_time = random.uniform(1, 3) * (attempt + 1)
98 await asyncio.sleep(wait_time)
99 continue
83 await raise_for_status(response)
84 vqd = response.headers.get("x-vqd-4", "")
85 if vqd:
86 return vqd
100 87 response_text = await response.text()
101 raise Exception(f"Failed to fetch VQD token: {response.status} {response_text}")
102 except Exception as e:
88 raise RuntimeError(f"Failed to fetch VQD token: {response.status} {response_text}")
89 except ResponseStatusError as e:
103 90 if attempt < max_retries - 1:
104 91 wait_time = random.uniform(1, 3) * (attempt + 1)
105 92 await asyncio.sleep(wait_time)
106 93 else:
107 raise Exception(f"Failed to fetch VQD token after {max_retries} attempts: {str(e)}")
108
109 raise Exception("Failed to fetch VQD token: Maximum retries exceeded")
94 raise RuntimeError(f"Failed to fetch VQD token after {max_retries} attempts: {str(e)}")
95
96 raise RuntimeError("Failed to fetch VQD token: Maximum retries exceeded")
110 97
111 98 @classmethod
112 99 async def create_async_generator(
@@ -121,10 +108,10 @@ class DDG(AsyncGeneratorProvider, ProviderModelMixin):
121 108 **kwargs
122 109 ) -> AsyncResult:
123 110 model = cls.validate_model(model)
124
111
125 112 if cookies is None and conversation is not None:
126 113 cookies = conversation.cookies
127
114
128 115 try:
129 116 async with ClientSession(timeout=ClientTimeout(total=timeout), cookies=cookies) as session:
130 117 if conversation is None:
@@ -140,67 +127,56 @@ class DDG(AsyncGeneratorProvider, ProviderModelMixin):
140 127 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
141 128 "x-vqd-4": conversation.vqd,
142 129 }
143
130
144 131 data = {
145 132 "model": model,
146 133 "messages": conversation.message_history,
147 134 }
148 135
149 136 await cls.sleep()
150 try:
151 async with session.post(cls.api_endpoint, json=data, headers=headers, proxy=proxy) as response:
152 await raise_for_status(response)
153 reason = None
154 full_message = ""
155
156 async for line in response.content:
157 line = line.decode("utf-8").strip()
158 if line.startswith("data:"):
159 try:
160 message = json.loads(line[5:].strip())
161
162 if "action" in message and message["action"] == "error":
163 error_type = message.get("type", "")
164 if message.get("status") == 429:
165 if error_type == "ERR_CONVERSATION_LIMIT":
166 raise ConversationLimitException(error_type)
167 raise RatelimitException(error_type)
168 raise DuckDuckGoSearchException(error_type)
169
170 if "message" in message:
171 if message["message"]:
172 yield message["message"]
173 full_message += message["message"]
174 reason = "length"
175 else:
176 reason = "stop"
177 except json.JSONDecodeError:
178 continue
179
180 if return_conversation:
181 conversation.message_history.append({"role": "assistant", "content": full_message})
182 conversation.vqd = response.headers.get("x-vqd-4", conversation.vqd)
183 conversation.cookies = {
184 n: c.value
185 for n, c in session.cookie_jar.filter_cookies(cls.url).items()
186 }
187
188 if reason is not None:
189 yield FinishReason(reason)
190
191 if return_conversation:
192 yield conversation
193
194 except asyncio.TimeoutError as e:
195 raise TimeoutException(f"Request timed out: {str(e)}")
196 except Exception as e:
197 if "time" in str(e).lower():
198 raise TimeoutException(f"Request timed out: {str(e)}")
199 raise DuckDuckGoSearchException(f"Request failed: {str(e)}")
200
201 except Exception as e:
202 if isinstance(e, (RatelimitException, TimeoutException, ConversationLimitException)):
203 raise
204 if "time" in str(e).lower():
205 raise TimeoutException(f"Request timed out: {str(e)}")
206 raise DuckDuckGoSearchException(f"Request failed: {str(e)}")
137 async with session.post(cls.api_endpoint, json=data, headers=headers, proxy=proxy) as response:
138 await raise_for_status(response)
139 reason = None
140 full_message = ""
141
142 async for line in response.content:
143 line = line.decode("utf-8").strip()
144
145 if line.startswith("data:"):
146 try:
147 message = json.loads(line[5:].strip())
148 except json.JSONDecodeError:
149 continue
150
151 if "action" in message and message["action"] == "error":
152 error_type = message.get("type", "")
153 if message.get("status") == 429:
154 if error_type == "ERR_CONVERSATION_LIMIT":
155 raise ConversationLimitError(error_type)
156 raise RateLimitError(error_type)
157 raise DuckDuckGoSearchException(error_type)
158
159 if "message" in message:
160 if message["message"]:
161 yield message["message"]
162 full_message += message["message"]
163 reason = "length"
164 else:
165 reason = "stop"
166
167 if return_conversation:
168 conversation.message_history.append({"role": "assistant", "content": full_message})
169 conversation.vqd = response.headers.get("x-vqd-4", conversation.vqd)
170 conversation.cookies = {
171 n: c.value
172 for n, c in session.cookie_jar.filter_cookies(cls.url).items()
173 }
174
175 if reason is not None:
176 yield FinishReason(reason)
177
178 if return_conversation:
179 yield conversation
180
181 except asyncio.TimeoutError as e:
182 raise TimeoutError(f"Request timed out: {str(e)}")
Modified g4f/Provider/DeepInfraChat.py +9 -40
@@ -1,15 +1,13 @@
1 1 from __future__ import annotations
2 2
3 import json
4 from aiohttp import ClientSession
5
6 3 from ..typing import AsyncResult, Messages
7 from ..requests.raise_for_status import raise_for_status
8 from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
4 from .needs_auth import OpenaiAPI
9 5
10 class DeepInfraChat(AsyncGeneratorProvider, ProviderModelMixin):
6 class DeepInfraChat(OpenaiAPI):
11 7 url = "https://deepinfra.com/chat"
12 api_endpoint = "https://api.deepinfra.com/v1/openai/chat/completions"
8 login_url = None
9 needs_auth = False
10 api_base = "https://api.deepinfra.com/v1/openai"
13 11
14 12 working = True
15 13 supports_stream = True
@@ -47,44 +45,15 @@ class DeepInfraChat(AsyncGeneratorProvider, ProviderModelMixin):
47 45 cls,
48 46 model: str,
49 47 messages: Messages,
50 proxy: str = None,
48 headers: dict = {},
51 49 **kwargs
52 50 ) -> AsyncResult:
53 model = cls.get_model(model)
54
55 51 headers = {
56 52 'Accept-Language': 'en-US,en;q=0.9',
57 'Content-Type': 'application/json',
58 53 'Origin': 'https://deepinfra.com',
59 54 'Referer': 'https://deepinfra.com/',
60 55 'X-Deepinfra-Source': 'web-page',
61 'accept': 'text/event-stream',
56 **headers
62 57 }
63 async with ClientSession(headers=headers) as session:
64 data = {
65 "model": model,
66 "messages": messages,
67 "stream": True
68 }
69 async with session.post(cls.api_endpoint, json=data, proxy=proxy) as response:
70 await raise_for_status(response)
71 async for chunk in response.content:
72 if chunk:
73 chunk_text = chunk.decode(errors="ignore")
74 try:
75 # Handle streaming response
76 if chunk_text.startswith("data: "):
77 if chunk_text.strip() == "data: [DONE]":
78 continue
79 chunk_data = json.loads(chunk_text[6:])
80 content = chunk_data["choices"][0]["delta"].get("content")
81 if content:
82 yield content
83 # Handle non-streaming response
84 else:
85 chunk_data = json.loads(chunk_text)
86 content = chunk_data["choices"][0]["message"].get("content")
87 if content:
88 yield content
89 except (json.JSONDecodeError, KeyError):
90 continue
58 async for chunk in super().create_async_generator(model, messages, headers=headers, **kwargs):
59 yield chunk
Modified g4f/Provider/PollinationsAI.py +1 -1
@@ -40,7 +40,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
40 40 default_vision_model = "gpt-4o"
41 41 extra_image_models = ["midjourney", "dall-e-3", "flux-pro", "flux-realism", "flux-cablyai", "flux-anime", "flux-3d"]
42 42 vision_models = [default_vision_model, "gpt-4o-mini"]
43 extra_text_models = [*vision_models, "claude", "karma", "command-r", "llamalight", "mistral-large", "sur", "sur-mistral", "any-dark"]
43 extra_text_models = [*vision_models, "claude", "claude-email", "karma", "command-r", "llamalight", "mistral-large", "sur", "sur-mistral", "any-dark"]
44 44 model_aliases = {
45 45 "qwen-2-72b": "qwen",
46 46 "qwen-2.5-coder-32b": "qwen-coder",
Modified g4f/Provider/needs_auth/OpenaiAPI.py +2 -0
@@ -140,6 +140,8 @@ class OpenaiAPI(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin):
140 140 if delta:
141 141 first = False
142 142 yield delta
143 if "usage" in data and data["usage"]:
144 yield Usage(**data["usage"])
143 145 if "finish_reason" in choice and choice["finish_reason"] is not None:
144 146 yield FinishReason(choice["finish_reason"])
145 147 break
Modified g4f/api/stubs.py +3 -1
@@ -21,6 +21,7 @@ class ChatCompletionsConfig(BaseModel):
21 21 max_tokens: Optional[int] = None
22 22 stop: Union[list[str], str, None] = None
23 23 api_key: Optional[str] = None
24 api_base: str = None
24 25 web_search: Optional[bool] = None
25 26 proxy: Optional[str] = None
26 27 conversation_id: Optional[str] = None
@@ -31,13 +32,14 @@ class ChatCompletionsConfig(BaseModel):
31 32 tool_calls: list = Field(default=[], examples=[[
32 33 {
33 34 "function": {
34 "arguments": {"query":"search query", "max_results":5, "max_words": 2500, "backend": "api", "add_text": True, "timeout": 5},
35 "arguments": {"query":"search query", "max_results":5, "max_words": 2500, "backend": "auto", "add_text": True, "timeout": 5},
35 36 "name": "search_tool"
36 37 },
37 38 "type": "function"
38 39 }
39 40 ]])
40 41 tools: list = None
42 response_format: Optional[dict] = None
41 43
42 44 class ImageGenerationConfig(BaseModel):
43 45 prompt: str
Modified g4f/client/__init__.py +6 -10
@@ -237,7 +237,6 @@ class Completions:
237 237 max_tokens: Optional[int] = None,
238 238 stop: Optional[Union[list[str], str]] = None,
239 239 api_key: Optional[str] = None,
240 ignored: Optional[list[str]] = None,
241 240 ignore_working: Optional[bool] = False,
242 241 ignore_stream: Optional[bool] = False,
243 242 **kwargs
@@ -268,8 +267,6 @@ class Completions:
268 267 ),
269 268 **kwargs
270 269 )
271 if not hasattr(response, '__iter__'):
272 response = [response]
273 270
274 271 response = iter_response(response, stream, response_format, max_tokens, stop)
275 272 response = iter_append_model_and_provider(response, model, provider)
@@ -471,7 +468,7 @@ class Images:
471 468 elif response_format == "b64_json":
472 469 # Convert URLs directly to base64 without saving
473 470 async def get_b64_from_url(url: str) -> Image:
474 async with aiohttp.ClientSession() as session:
471 async with aiohttp.ClientSession(cookies=response.get("cookies")) as session:
475 472 async with session.get(url, proxy=proxy) as resp:
476 473 if resp.status == 200:
477 474 image_data = await resp.read()
@@ -512,7 +509,7 @@ class AsyncCompletions:
512 509 self.client: AsyncClient = client
513 510 self.provider: ProviderType = provider
514 511
515 async def create(
512 def create(
516 513 self,
517 514 messages: Messages,
518 515 model: str,
@@ -525,11 +522,10 @@ class AsyncCompletions:
525 522 max_tokens: Optional[int] = None,
526 523 stop: Optional[Union[list[str], str]] = None,
527 524 api_key: Optional[str] = None,
528 ignored: Optional[list[str]] = None,
529 525 ignore_working: Optional[bool] = False,
530 526 ignore_stream: Optional[bool] = False,
531 527 **kwargs
532 ) -> Awaitable[ChatCompletion, AsyncIterator[ChatCompletionChunk]]:
528 ) -> Awaitable[ChatCompletion]:
533 529 model, provider = get_model_and_provider(
534 530 model,
535 531 self.provider if provider is None else provider,
@@ -556,14 +552,14 @@ class AsyncCompletions:
556 552 ),
557 553 **kwargs
558 554 )
559
555
560 556 response = async_iter_response(response, stream, response_format, max_tokens, stop)
561 557 response = async_iter_append_model_and_provider(response, model, provider)
562
558
563 559 if stream:
564 560 return response
565 561 else:
566 return await anext(response)
562 return anext(response)
567 563
568 564 def stream(
569 565 self,
Modified g4f/errors.py +7 -1
@@ -47,4 +47,10 @@ class RateLimitError(ResponseStatusError):
47 47 ...
48 48
49 49 class NoValidHarFileError(Exception):
50 ...
50 ...
51
52 class TimeoutError(Exception):
53 """Raised for timeout errors during API requests."""
54
55 class ConversationLimitError(Exception):
56 """Raised for conversation limit during API requests to AI endpoint."""