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

XFEstudio/gpt4free

Add unitests for the client Fix: async generator ignored GeneratorExit Fix: ResourceWarning: unclosed event loop

e1a0b3ff
Heiner Lohaus <hlohaus@users.noreply.github.com>
提交于

代码差异

8 个文件 +173 -133
Modified etc/unittest/asyncio.py +8 -2
@@ -8,6 +8,7 @@ import unittest
8 8
9 9 import g4f
10 10 from g4f import ChatCompletion
11 from g4f.client import Client
11 12 from .mocks import ProviderMock, AsyncProviderMock, AsyncGeneratorProviderMock
12 13
13 14 DEFAULT_MESSAGES = [{'role': 'user', 'content': 'Hello'}]
@@ -24,11 +25,16 @@ class TestChatCompletion(unittest.TestCase):
24 25
25 26 def test_create(self):
26 27 result = ChatCompletion.create(g4f.models.default, DEFAULT_MESSAGES, AsyncProviderMock)
27 self.assertEqual("Mock",result)
28 self.assertEqual("Mock", result)
28 29
29 30 def test_create_generator(self):
30 31 result = ChatCompletion.create(g4f.models.default, DEFAULT_MESSAGES, AsyncGeneratorProviderMock)
31 self.assertEqual("Mock",result)
32 self.assertEqual("Mock", result)
33
34 def test_await_callback(self):
35 client = Client(provider=AsyncGeneratorProviderMock)
36 response = client.chat.completions.create(DEFAULT_MESSAGES, "", max_tokens=0)
37 self.assertEqual("Mock", response.choices[0].message.content)
32 38
33 39 class TestChatCompletionAsync(unittest.IsolatedAsyncioTestCase):
34 40
Added etc/unittest/client.py +54 -0
@@ -0,0 +1,54 @@
1 import unittest
2
3 from g4f.client import Client, ChatCompletion, ChatCompletionChunk
4 from .mocks import AsyncGeneratorProviderMock, ModelProviderMock, YieldProviderMock
5
6 DEFAULT_MESSAGES = [{'role': 'user', 'content': 'Hello'}]
7
8 class TestPassModel(unittest.TestCase):
9
10 def test_response(self):
11 client = Client(provider=AsyncGeneratorProviderMock)
12 response = client.chat.completions.create(DEFAULT_MESSAGES, "")
13 self.assertIsInstance(response, ChatCompletion)
14 self.assertEqual("Mock", response.choices[0].message.content)
15
16 def test_pass_model(self):
17 client = Client(provider=ModelProviderMock)
18 response = client.chat.completions.create(DEFAULT_MESSAGES, "Hello")
19 self.assertIsInstance(response, ChatCompletion)
20 self.assertEqual("Hello", response.choices[0].message.content)
21
22 def test_max_tokens(self):
23 client = Client(provider=YieldProviderMock)
24 messages = [{'role': 'user', 'content': chunk} for chunk in ["How ", "are ", "you", "?"]]
25 response = client.chat.completions.create(messages, "Hello", max_tokens=1)
26 self.assertIsInstance(response, ChatCompletion)
27 self.assertEqual("How ", response.choices[0].message.content)
28 response = client.chat.completions.create(messages, "Hello", max_tokens=2)
29 self.assertIsInstance(response, ChatCompletion)
30 self.assertEqual("How are ", response.choices[0].message.content)
31
32 def test_max_stream(self):
33 client = Client(provider=YieldProviderMock)
34 messages = [{'role': 'user', 'content': chunk} for chunk in ["How ", "are ", "you", "?"]]
35 response = client.chat.completions.create(messages, "Hello", stream=True)
36 for chunk in response:
37 self.assertIsInstance(chunk, ChatCompletionChunk)
38 self.assertIsInstance(chunk.choices[0].delta.content, str)
39 messages = [{'role': 'user', 'content': chunk} for chunk in ["You ", "You ", "Other", "?"]]
40 response = client.chat.completions.create(messages, "Hello", stream=True, max_tokens=2)
41 response = list(response)
42 self.assertEqual(len(response), 2)
43 for chunk in response:
44 self.assertEqual(chunk.choices[0].delta.content, "You ")
45
46 def no_test_stop(self):
47 client = Client(provider=YieldProviderMock)
48 messages = [{'role': 'user', 'content': chunk} for chunk in ["How ", "are ", "you", "?"]]
49 response = client.chat.completions.create(messages, "Hello", stop=["and"])
50 self.assertIsInstance(response, ChatCompletion)
51 self.assertEqual("How are you?", response.choices[0].message.content)
52
53 if __name__ == '__main__':
54 unittest.main()
Modified etc/unittest/mocks.py +14 -5
@@ -7,10 +7,10 @@ class ProviderMock(AbstractProvider):
7 7 model, messages, stream, **kwargs
8 8 ):
9 9 yield "Mock"
10
10
11 11 class AsyncProviderMock(AsyncProvider):
12 12 working = True
13
13
14 14 async def create_async(
15 15 model, messages, **kwargs
16 16 ):
@@ -18,16 +18,25 @@ class AsyncProviderMock(AsyncProvider):
18 18
19 19 class AsyncGeneratorProviderMock(AsyncGeneratorProvider):
20 20 working = True
21
21
22 22 async def create_async_generator(
23 23 model, messages, stream, **kwargs
24 24 ):
25 25 yield "Mock"
26
26
27 27 class ModelProviderMock(AbstractProvider):
28 28 working = True
29 29
30 30 def create_completion(
31 31 model, messages, stream, **kwargs
32 32 ):
33 yield model
33 yield model
34
35 class YieldProviderMock(AsyncGeneratorProvider):
36 working = True
37
38 async def create_async_generator(
39 model, messages, stream, **kwargs
40 ):
41 for message in messages:
42 yield message["content"]
Modified g4f/Provider/base_provider.py +14 -9
@@ -196,15 +196,20 @@ class AsyncGeneratorProvider(AsyncProvider):
196 196 generator = cls.create_async_generator(model, messages, stream=stream, **kwargs)
197 197 gen = generator.__aiter__()
198 198
199 while True:
200 try:
201 yield loop.run_until_complete(gen.__anext__())
202 except StopAsyncIteration:
203 break
204
205 if new_loop:
206 loop.close()
207 asyncio.set_event_loop(None)
199 # Fix for RuntimeError: async generator ignored GeneratorExit
200 async def await_callback(callback):
201 return await callback()
202
203 try:
204 while True:
205 yield loop.run_until_complete(await_callback(gen.__anext__))
206 except StopAsyncIteration:
207 ...
208 # Fix for: ResourceWarning: unclosed event loop
209 finally:
210 if new_loop:
211 loop.close()
212 asyncio.set_event_loop(None)
208 213
209 214 @classmethod
210 215 async def create_async(
Modified g4f/Provider/needs_auth/OpenaiChat.py +1 -1
@@ -385,7 +385,7 @@ class OpenaiChat(AsyncGeneratorProvider, ProviderModelMixin):
385 385 }
386 386 ) as response:
387 387 if not response.ok:
388 raise RuntimeError(f"Response {response.status_code}: {await response.text()}")
388 raise RuntimeError(f"Response {response.status}: {await response.text()}")
389 389 last_message: int = 0
390 390 async for line in response.iter_lines():
391 391 if not line.startswith(b"data: "):
Modified g4f/client.py +35 -116
@@ -2,9 +2,9 @@ from __future__ import annotations
2 2
3 3 import re
4 4
5 from .typing import Union, Generator, AsyncGenerator, Messages, ImageType
5 from .stubs import ChatCompletion, ChatCompletionChunk, Image, ImagesResponse
6 from .typing import Union, Generator, Messages, ImageType
6 7 from .base_provider import BaseProvider, ProviderType
7 from .Provider.base_provider import AsyncGeneratorProvider
8 8 from .image import ImageResponse as ImageProviderResponse
9 9 from .Provider import BingCreateImages, Gemini, OpenaiChat
10 10 from .errors import NoImageResponseError
@@ -36,14 +36,14 @@ def iter_response(
36 36 stop: list = None
37 37 ) -> Generator:
38 38 content = ""
39 idx = 1
40 chunk = None
41 finish_reason = "stop"
39 finish_reason = None
40 last_chunk = None
42 41 for idx, chunk in enumerate(response):
42 if last_chunk is not None:
43 yield ChatCompletionChunk(last_chunk, finish_reason)
43 44 content += str(chunk)
44 if max_tokens is not None and idx > max_tokens:
45 if max_tokens is not None and idx + 1 >= max_tokens:
45 46 finish_reason = "max_tokens"
46 break
47 47 first = -1
48 48 word = None
49 49 if stop is not None:
@@ -52,98 +52,30 @@ def iter_response(
52 52 if first != -1:
53 53 content = content[:first]
54 54 break
55 if stream:
55 if stream and first != -1:
56 first = chunk.find(word)
56 57 if first != -1:
57 first = chunk.find(word)
58 if first != -1:
59 chunk = chunk[:first]
60 else:
61 first = 0
62 yield ChatCompletionChunk([ChatCompletionDeltaChoice(ChatCompletionDelta(chunk))])
58 chunk = chunk[:first]
59 else:
60 first = 0
63 61 if first != -1:
62 finish_reason = "stop"
63 if stream:
64 last_chunk = chunk
65 if finish_reason is not None:
64 66 break
67 if last_chunk is not None:
68 yield ChatCompletionChunk(last_chunk, finish_reason)
65 69 if not stream:
66 70 if response_format is not None and "type" in response_format:
67 71 if response_format["type"] == "json_object":
68 72 response = read_json(response)
69 yield ChatCompletion([ChatCompletionChoice(ChatCompletionMessage(response, finish_reason))])
70
71 async def aiter_response(
72 response: aiter,
73 stream: bool,
74 response_format: dict = None,
75 max_tokens: int = None,
76 stop: list = None
77 ) -> AsyncGenerator:
78 content = ""
79 try:
80 idx = 0
81 chunk = None
82 async for chunk in response:
83 content += str(chunk)
84 if max_tokens is not None and idx > max_tokens:
85 break
86 first = -1
87 word = None
88 if stop is not None:
89 for word in list(stop):
90 first = content.find(word)
91 if first != -1:
92 content = content[:first]
93 break
94 if stream:
95 if first != -1:
96 first = chunk.find(word)
97 if first != -1:
98 chunk = chunk[:first]
99 else:
100 first = 0
101 yield ChatCompletionChunk([ChatCompletionDeltaChoice(ChatCompletionDelta(chunk))])
102 if first != -1:
103 break
104 idx += 1
105 except:
106 ...
107 if not stream:
108 if response_format is not None and "type" in response_format:
109 if response_format["type"] == "json_object":
110 response = read_json(response)
111 yield ChatCompletion([ChatCompletionChoice(ChatCompletionMessage(response))])
112
113 class Model():
114 def __getitem__(self, item):
115 return getattr(self, item)
116
117 class ChatCompletion(Model):
118 def __init__(self, choices: list):
119 self.choices = choices
120
121 class ChatCompletionChunk(Model):
122 def __init__(self, choices: list):
123 self.choices = choices
124
125 class ChatCompletionChoice(Model):
126 def __init__(self, message: ChatCompletionMessage):
127 self.message = message
128
129 class ChatCompletionMessage(Model):
130 def __init__(self, content: str, finish_reason: str):
131 self.content = content
132 self.finish_reason = finish_reason
133 self.index = 0
134 self.logprobs = None
135
136 class ChatCompletionDelta(Model):
137 def __init__(self, content: str):
138 self.content = content
139
140 class ChatCompletionDeltaChoice(Model):
141 def __init__(self, delta: ChatCompletionDelta):
142 self.delta = delta
73 yield ChatCompletion(content, finish_reason)
143 74
144 75 class Client():
145 76 proxies: Proxies = None
146 77 chat: Chat
78 images: Images
147 79
148 80 def __init__(
149 81 self,
@@ -152,9 +84,9 @@ class Client():
152 84 proxies: Proxies = None,
153 85 **kwargs
154 86 ) -> None:
155 self.proxies: Proxies = proxies
156 self.images = Images(self, image_provider)
157 87 self.chat = Chat(self, provider)
88 self.images = Images(self, image_provider)
89 self.proxies: Proxies = proxies
158 90
159 91 def get_proxy(self) -> Union[str, None]:
160 92 if isinstance(self.proxies, str) or self.proxies is None:
@@ -178,13 +110,13 @@ class Completions():
178 110 stream: bool = False,
179 111 response_format: dict = None,
180 112 max_tokens: int = None,
181 stop: list = None,
113 stop: Union[list. str] = None,
182 114 **kwargs
183 ) -> Union[dict, Generator]:
115 ) -> Union[ChatCompletion, Generator[ChatCompletionChunk]]:
184 116 if max_tokens is not None:
185 117 kwargs["max_tokens"] = max_tokens
186 118 if stop:
187 kwargs["stop"] = list(stop)
119 kwargs["stop"] = stop
188 120 model, provider = get_model_and_provider(
189 121 model,
190 122 self.provider if provider is None else provider,
@@ -192,10 +124,8 @@ class Completions():
192 124 **kwargs
193 125 )
194 126 response = provider.create_completion(model, messages, stream=stream, **kwargs)
195 if isinstance(provider, type) and issubclass(provider, AsyncGeneratorProvider):
196 response = iter_response(response, stream, response_format) # max_tokens, stop
197 else:
198 response = iter_response(response, stream, response_format, max_tokens, stop)
127 stop = [stop] if isinstance(stop, str) else stop
128 response = iter_response(response, stream, response_format, max_tokens, stop)
199 129 return response if stream else next(response)
200 130
201 131 class Chat():
@@ -203,7 +133,7 @@ class Chat():
203 133
204 134 def __init__(self, client: Client, provider: ProviderType = None):
205 135 self.completions = Completions(client, provider)
206
136
207 137 class ImageModels():
208 138 gemini = Gemini
209 139 openai = OpenaiChat
@@ -212,21 +142,9 @@ class ImageModels():
212 142 self.client = client
213 143 self.default = BingCreateImages(proxy=self.client.get_proxy())
214 144
215 def get(self, name: str) -> ImageProvider:
216 return getattr(self, name) if hasattr(self, name) else self.default
145 def get(self, name: str, default: ImageProvider = None) -> ImageProvider:
146 return getattr(self, name) if hasattr(self, name) else default or self.default
217 147
218 class ImagesResponse(Model):
219 data: list[Image]
220
221 def __init__(self, data: list) -> None:
222 self.data = data
223
224 class Image(Model):
225 url: str
226
227 def __init__(self, url: str) -> None:
228 self.url = url
229
230 148 class Images():
231 149 def __init__(self, client: Client, provider: ImageProvider = None):
232 150 self.client: Client = client
@@ -234,7 +152,7 @@ class Images():
234 152 self.models: ImageModels = ImageModels(client)
235 153
236 154 def generate(self, prompt, model: str = None, **kwargs):
237 provider = self.models.get(model) if model else self.provider or self.models.get(model)
155 provider = self.models.get(model, self.provider)
238 156 if isinstance(provider, BaseProvider) or isinstance(provider, type) and issubclass(provider, BaseProvider):
239 157 prompt = f"create a image: {prompt}"
240 158 response = provider.create_completion(
@@ -246,14 +164,15 @@ class Images():
246 164 )
247 165 else:
248 166 response = provider.create(prompt)
249
167
250 168 for chunk in response:
251 169 if isinstance(chunk, ImageProviderResponse):
252 return ImagesResponse([Image(image)for image in list(chunk.images)])
170 images = [chunk.images] if isinstance(chunk.images, str) else chunk.images
171 return ImagesResponse([Image(image) for image in images])
253 172 raise NoImageResponseError()
254 173
255 174 def create_variation(self, image: ImageType, model: str = None, **kwargs):
256 provider = self.models.get(model) if model else self.provider
175 provider = self.models.get(model, self.provider)
257 176 result = None
258 177 if isinstance(provider, type) and issubclass(provider, BaseProvider):
259 178 response = provider.create_completion(
Added g4f/stubs.py +44 -0
@@ -0,0 +1,44 @@
1
2 from __future__ import annotations
3
4 class Model():
5 def __getitem__(self, item):
6 return getattr(self, item)
7
8 class ChatCompletion(Model):
9 def __init__(self, content: str, finish_reason: str):
10 self.choices = [ChatCompletionChoice(ChatCompletionMessage(content, finish_reason))]
11
12 class ChatCompletionChunk(Model):
13 def __init__(self, content: str, finish_reason: str):
14 self.choices = [ChatCompletionDeltaChoice(ChatCompletionDelta(content, finish_reason))]
15
16 class ChatCompletionMessage(Model):
17 def __init__(self, content: str, finish_reason: str):
18 self.content = content
19 self.finish_reason = finish_reason
20
21 class ChatCompletionChoice(Model):
22 def __init__(self, message: ChatCompletionMessage):
23 self.message = message
24
25 class ChatCompletionDelta(Model):
26 def __init__(self, content: str, finish_reason: str):
27 self.content = content
28 self.finish_reason = finish_reason
29
30 class ChatCompletionDeltaChoice(Model):
31 def __init__(self, delta: ChatCompletionDelta):
32 self.delta = delta
33
34 class Image(Model):
35 url: str
36
37 def __init__(self, url: str) -> None:
38 self.url = url
39
40 class ImagesResponse(Model):
41 data: list[Image]
42
43 def __init__(self, data: list) -> None:
44 self.data = data
Added image.html +3 -0
@@ -0,0 +1,3 @@
1 <style type="text/css">#designer_attribute_container{display:flex;justify-content:space-between;width:100%;margin-top:10px}#designer_attribute_container .des_attr_i{width:16px;margin-right:2px}#designer_attribute_container .des_attr_txt{height:18px;line-height:18px;font-size:14px;font-weight:400;font-family:"Roboto",Helvetica,sans-serif}#designer_attribute_container #dalle_attribute_container{margin-left:auto}#designer_attribute_container #dalle_attribute_container .des_attr_dal{display:flex;justify-content:center;height:14px;border-radius:4px;background-color:#5f5f5e;padding:2px 8px}#designer_attribute_container #dalle_attribute_container .des_attr_dal_txt{height:14px;line-height:14px;font-size:11px;font-weight:400;font-family:"Roboto",Helvetica,sans-serif;color:#fff}.des_attr_txt{color:#fff}</style><div id="gir_async"
2 class="giric gir_1" data-rewriteurl="/images/create/a-serene-garden-filled-with-colorful-flowers-in-fu/1-65c8a550c2d34e67a93b016cd1f3ade3?FORM=GENCRE" data-cis="512" data-vimgseturl="/images/create/async/viewimageset/1-65c8a550c2d34e67a93b016cd1f3ade3&amp;IG=062C548EC8DD4047A2AAE63FD928194A&amp;IID=images.vis"
3 fir-th="OIG2.EPxx_.JFG402kzMQYYhj" data-ctc="Image copied to clipboard" data-wide="" data-wide-mobile=""><a class="single-img-link" target="_blank" href="/images/create/a-serene-garden-filled-with-colorful-flowers-in-fu/1-65c8a550c2d34e67a93b016cd1f3ade3?id=LrsKPoRLQud1%2bT8YdxQDhA%3d%3d&amp;view=detailv2&amp;idpp=genimg&amp;FORM=GCRIDP" h="ID=images,5015.1"><img class="gir_mmimg" src="https://tse4.mm.bing.net/th/id/OIG2.EPxx_.JFG402kzMQYYhj?w=270&amp;h=270&amp;c=6&amp;r=0&amp;o=5&amp;pid=ImgGn" alt="a serene garden filled with colorful flowers in full bloom"/></a><div id="designer_attribute_container"><img class="des_attr_i rms_img" alt="Designer" src="https://r.bing.com/rp/gmZtdJVd-klWl3XWpa6-ni1FU3M.svg" /><span class="des_attr_txt des_attr_txt_clr">Designer</span><div id="dalle_attribute_container"><div class="des_attr_dal"><span class="des_attr_dal_txt">Powered by DALL&#183;E 3</span></div></div></div></div>