返回提交历史
Modified
g4f/Provider/Perplexity.py
+117
-35
Modified
g4f/Provider/PollinationsAI.py
+2
-1
Modified
g4f/Provider/template/OpenaiTemplate.py
+4
-1
Modified
g4f/client/__init__.py
+25
-101
Modified
g4f/gui/server/api.py
+4
-0
Modified
g4f/providers/response.py
+27
-0
XFEstudio/gpt4free
Enhance Perplexity provider with additional models and improved conversation handling; add JsonRequest and JsonResponse classes for better response management
ddadc36f
代码差异
6 个文件
+179
-138
@@ -6,7 +6,7 @@ import uuid
6
6
from ..typing import AsyncResult, Messages, Cookies
7
7
from ..requests import StreamSession, raise_for_status, sse_stream
8
8
from ..cookies import get_cookies
9
from ..providers.response import ProviderInfo
9
from ..providers.response import ProviderInfo, JsonConversation, JsonRequest, JsonResponse, Reasoning
10
10
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
11
11
from .. import debug
12
12
@@ -21,21 +21,63 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
21
21
models = [
22
22
default_model,
23
23
"turbo",
24
"gpt41",
25
"gpt5",
26
"gpt5_thinking",
27
"o3",
28
"o3pro",
29
"claude2",
30
"claude37sonnetthinking",
31
"claude40opus",
32
"claude40opusthinking",
33
"claude41opusthinking",
34
"claude45sonnet",
35
"claude45sonnetthinking",
36
"experimental",
37
"grok",
38
"grok4",
39
"gemini2flash",
24
40
"pplx_pro",
25
"gpt-5",
41
"pplx_pro_upgraded",
42
"pplx_alpha",
43
"pplx_beta",
44
"comet_max_assistant",
45
"o3_research",
46
"o3pro_research",
47
"claude40sonnet_research",
48
"claude40sonnetthinking_research",
49
"claude40opus_research",
50
"claude40opusthinking_research",
51
"o3_labs",
52
"o3pro_labs",
53
"claude40sonnetthinking_labs",
54
"claude40opusthinking_labs",
55
"o4mini",
56
"o1",
57
"gpt4o",
58
"gpt45",
59
"gpt4",
60
"o3mini",
61
"claude35haiku",
62
"llama_x_large",
63
"mistral",
64
"claude3opus",
65
"gemini",
66
"pplx_reasoning",
67
"r1"
26
68
]
27
69
model_aliases = {
28
70
"gpt-5": "gpt5",
71
"gpt-5-thinking": "gpt5_thinking",
29
72
}
30
73
31
_user_id = None
32
33
74
@classmethod
34
75
async def create_async_generator(
35
76
cls,
36
77
model: str,
37
78
messages: Messages,
38
79
cookies: Cookies = None,
80
conversation: JsonConversation = None,
39
81
proxy: str = None,
40
82
**kwargs
41
83
) -> AsyncResult:
@@ -43,13 +85,13 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
43
85
model = cls.default_model
44
86
if cookies is None:
45
87
cookies = get_cookies(cls.cookie_domain, False)
46
else:
47
cls._user_id = None
48
49
# Generate UUIDs for request tracking
50
frontend_uuid = str(uuid.uuid4())
51
frontend_context_uuid = str(uuid.uuid4())
52
visitor_id = str(uuid.uuid4())
88
if conversation is None:
89
conversation = JsonConversation(
90
frontend_uid=str(uuid.uuid4()),
91
frontend_context_uuid=str(uuid.uuid4()),
92
visitor_id=str(uuid.uuid4()),
93
user_id=None,
94
)
53
95
request_id = str(uuid.uuid4())
54
96
55
97
headers = {
@@ -63,7 +105,7 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
63
105
"x-perplexity-request-reason": "perplexity-query-state-provider",
64
106
"x-request-id": request_id,
65
107
}
66
108
67
109
# Extract the last user message as the query
68
110
query = ""
69
111
for message in reversed(messages):
@@ -72,18 +114,19 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
72
114
break
73
115
74
116
async with StreamSession(headers=headers, cookies=cookies, proxy=proxy, impersonate="chrome") as session:
75
if cls._user_id is None:
117
if conversation.user_id is None:
76
118
async with session.get(f"{cls.url}/api/auth/session") as response:
77
119
await raise_for_status(response)
78
120
user = await response.json()
79
cls._user_id = user.get("user", {}).get("id")
80
debug.log(f"Perplexity user id: {cls._user_id}")
121
conversation.user_id = user.get("user", {}).get("id")
122
debug.log(f"Perplexity user id: {conversation.user_id}")
123
yield conversation
81
124
if model == "auto":
82
model = "pplx_pro" if cls._user_id else "turbo"
125
model = "pplx_pro" if conversation.user_id else "turbo"
83
126
yield ProviderInfo(**cls.get_dict(), model=model)
84
127
if model in cls.model_aliases:
85
128
model = cls.model_aliases[model]
86
if cls._user_id is None:
129
if conversation.user_id is None:
87
130
data = {
88
131
"params": {
89
132
"attachments": [],
@@ -92,13 +135,13 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
92
135
"search_focus": "internet",
93
136
"sources": ["web"],
94
137
"search_recency_filter": None,
95
"frontend_uuid": frontend_uuid,
138
"frontend_uuid": conversation.frontend_uid,
96
139
"mode": "concise",
97
140
"model_preference": model,
98
141
"is_related_query": False,
99
142
"is_sponsored": False,
100
"visitor_id": visitor_id,
101
"frontend_context_uuid": frontend_context_uuid,
143
"visitor_id": conversation.visitor_id,
144
"frontend_context_uuid": conversation.frontend_context_uuid,
102
145
"prompt_source": "user",
103
146
"query_source": "home",
104
147
"is_incognito": False,
@@ -144,53 +187,92 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
144
187
data = {
145
188
"params": {
146
189
"last_backend_uuid": None,
147
"read_write_token": "457a2d3d-c53f-4065-8554-7645a36fc220",
190
"read_write_token": None,
148
191
"attachments": [],
149
192
"language": "en-US",
150
193
"timezone": "America/New_York",
151
194
"search_focus": "internet",
152
"sources": ["web"],
153
"frontend_uuid": frontend_uuid,
195
"sources": [
196
"web"
197
],
198
"frontend_uuid": conversation.frontend_uid,
154
199
"mode": "copilot",
155
"model_preference": "gpt5",
200
"model_preference": model,
156
201
"is_related_query": False,
157
202
"is_sponsored": False,
158
"visitor_id": visitor_id,
159
"user_nextauth_id": cls._user_id,
203
"visitor_id": conversation.visitor_id,
204
"user_nextauth_id": conversation.user_id,
160
205
"prompt_source": "user",
161
"query_source":"followup",
206
"query_source": "followup",
162
207
"is_incognito": False,
163
208
"time_from_first_type": random.randint(0, 1000),
164
209
"local_search_enabled": False,
165
210
"use_schematized_api": True,
166
211
"send_back_text_in_streaming_api": False,
167
"supported_block_use_cases": ["answer_modes", "media_items", "knowledge_cards", "inline_entity_cards", "place_widgets", "finance_widgets", "sports_widgets", "shopping_widgets", "jobs_widgets", "search_result_widgets", "clarification_responses", "inline_images", "inline_assets", "inline_finance_widgets", "placeholder_cards", "diff_blocks", "inline_knowledge_cards", "entity_group_v2", "refinement_filters", "canvas_mode"],
212
"supported_block_use_cases": [
213
"answer_modes",
214
"media_items",
215
"knowledge_cards",
216
"inline_entity_cards",
217
"place_widgets",
218
"finance_widgets",
219
"sports_widgets",
220
"shopping_widgets",
221
"jobs_widgets",
222
"search_result_widgets",
223
"clarification_responses",
224
"inline_images",
225
"inline_assets",
226
"inline_finance_widgets",
227
"placeholder_cards",
228
"diff_blocks",
229
"inline_knowledge_cards",
230
"entity_group_v2",
231
"refinement_filters",
232
"canvas_mode"
233
],
168
234
"client_coordinates": None,
169
235
"mentions": [],
170
236
"skip_search_enabled": True,
171
237
"is_nav_suggestions_disabled": False,
172
238
"followup_source": "link",
239
"always_search_override": False,
240
"override_no_search": False,
241
"comet_max_assistant_enabled": False,
173
242
"version": "2.18"
174
243
},
175
244
"query_str": query
176
245
}
246
yield JsonRequest.from_dict(data)
177
247
async with session.post(
178
248
f"{cls.url}/rest/sse/perplexity_ask",
179
249
json=data,
180
250
) as response:
181
251
await raise_for_status(response)
182
252
full_response = ""
183
last_response = ""
253
full_reasoning = ""
184
254
async for json_data in sse_stream(response):
255
yield JsonResponse.from_dict(json_data)
185
256
for block in json_data.get("blocks", []):
186
257
for patch in block.get("diff_block", {}).get("patches", []):
258
if patch.get("path") == "/progress":
259
continue
187
260
value = patch.get("value", "")
261
if patch.get("path").startswith("/goals"):
262
if isinstance(value, str):
263
if value.startswith(full_reasoning):
264
value = value[len(full_reasoning):]
265
yield Reasoning(value)
266
full_reasoning += value
267
else:
268
yield Reasoning(status="")
269
continue
270
if block.get("diff_block").get("field") != "markdown_block":
271
continue
188
272
value = value.get("answer", "") if isinstance(value, dict) else value
189
if value:
273
if value and isinstance(value, str):
190
274
if value.startswith(full_response):
191
275
value = value[len(full_response):]
192
if value.startswith(last_response):
193
value = value[len(last_response):]
194
last_response = value
195
full_response += value
196
yield value
276
if value:
277
full_response += value
278
yield value
@@ -17,7 +17,7 @@ from ..errors import MissingAuthError
17
17
from ..requests.raise_for_status import raise_for_status
18
18
from ..requests.aiohttp import get_connector
19
19
from ..image import use_aspect_ratio
20
from ..providers.response import ImageResponse, Reasoning, TitleGeneration, SuggestedFollowups
20
from ..providers.response import ImageResponse, Reasoning, TitleGeneration, SuggestedFollowups, JsonRequest
21
21
from ..tools.media import render_messages
22
22
from ..config import STATIC_URL
23
23
from .template.OpenaiTemplate import read_response
@@ -461,6 +461,7 @@ class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
461
461
headers = {"referer": referrer}
462
462
if api_key:
463
463
headers["authorization"] = f"Bearer {api_key}"
464
yield JsonRequest.from_dict(data)
464
465
async with session.post(cls.openai_endpoint, json=data, headers=headers) as response:
465
466
if response.status in (400, 500):
466
467
debug.error(f"Error: {response.status} - Bad Request: {data}")
@@ -8,7 +8,7 @@ from ...typing import Union, AsyncResult, Messages, MediaListType
8
8
from ...requests import StreamSession, StreamResponse, raise_for_status, sse_stream
9
9
from ...image import use_aspect_ratio
10
10
from ...image.copy_images import save_response_media
11
from ...providers.response import FinishReason, ToolCalls, Usage, ImageResponse, ProviderInfo, AudioResponse, Reasoning, JsonConversation
11
from ...providers.response import *
12
12
from ...tools.media import render_messages
13
13
from ...tools.run_tools import AuthManager
14
14
from ...errors import MissingAuthError
@@ -150,6 +150,7 @@ class OpenaiTemplate(AsyncGeneratorProvider, ProviderModelMixin, RaiseErrorMixin
150
150
api_endpoint = f"{api_base.rstrip('/')}/chat/completions"
151
151
if api_endpoint is None:
152
152
api_endpoint = cls.api_endpoint
153
yield JsonRequest.from_dict(data)
153
154
async with session.post(api_endpoint, json=data, ssl=cls.ssl) as response:
154
155
async for chunk in read_response(response, stream, prompt, cls.get_dict(), download_media):
155
156
yield chunk
@@ -170,6 +171,7 @@ async def read_response(response: StreamResponse, stream: bool, prompt: str, pro
170
171
content_type = response.headers.get("content-type", "text/event-stream" if stream else "application/json")
171
172
if content_type.startswith("application/json"):
172
173
data = await response.json()
174
yield JsonResponse.from_dict(data)
173
175
OpenaiTemplate.raise_error(data, response.status)
174
176
await raise_for_status(response)
175
177
model = data.get("model")
@@ -206,6 +208,7 @@ async def read_response(response: StreamResponse, stream: bool, prompt: str, pro
206
208
first = True
207
209
model_returned = False
208
210
async for data in sse_stream(response):
211
yield JsonResponse.from_dict(data)
209
212
OpenaiTemplate.raise_error(data)
210
213
model = data.get("model")
211
214
if not model_returned and model:
@@ -261,53 +261,6 @@ async def async_iter_response(
261
261
finally:
262
262
await safe_aclose(response)
263
263
264
async def async_response(
265
response: AsyncIterator[Union[str, ResponseType]]
266
) -> ClientResponse:
267
content = ""
268
response_id = ''.join(random.choices(string.ascii_letters + string.digits, k=28))
269
idx = 0
270
usage = None
271
provider: ProviderInfo = None
272
conversation: JsonConversation = None
273
274
async for chunk in response:
275
if isinstance(chunk, FinishReason):
276
continue
277
elif isinstance(chunk, JsonConversation):
278
conversation = chunk
279
continue
280
elif isinstance(chunk, ToolCalls):
281
continue
282
elif isinstance(chunk, Usage):
283
usage = chunk
284
continue
285
elif isinstance(chunk, ProviderInfo):
286
provider = chunk
287
continue
288
elif isinstance(chunk, HiddenResponse):
289
continue
290
elif isinstance(chunk, Exception):
291
continue
292
293
content = add_chunk(content, chunk)
294
if not content:
295
continue
296
idx += 1
297
298
if usage is None:
299
usage = UsageModel.model_construct(completion_tokens=idx, total_tokens=idx)
300
else:
301
usage = UsageModel.model_construct(**usage.get_dict())
302
303
response = ClientResponse.model_construct(
304
content, response_id, int(time.time()), usage=usage, conversation=conversation
305
)
306
if provider is not None:
307
response.provider = provider.name
308
response.model = provider.model
309
return response
310
311
264
async def async_iter_append_model_and_provider(
312
265
response: AsyncChatCompletionResponseType,
313
266
last_model: str,
@@ -361,6 +314,7 @@ class Completions:
361
314
stop: Optional[Union[list[str], str]] = None,
362
315
api_key: Optional[str] = None,
363
316
ignore_stream: Optional[bool] = False,
317
raw: Optional[bool] = False,
364
318
**kwargs
365
319
) -> ChatCompletion:
366
320
if isinstance(messages, str):
@@ -392,12 +346,21 @@ class Completions:
392
346
**kwargs
393
347
)
394
348
349
if raw:
350
def filter_raw(response):
351
for chunk in response:
352
if isinstance(chunk, JsonResponse):
353
yield chunk
354
raw_response = filter_raw(response)
355
if stream:
356
return raw_response
357
return next(raw_response)
358
395
359
response = iter_response(response, stream, response_format, max_tokens, stop)
396
360
response = iter_append_model_and_provider(response, model, provider)
397
361
if stream:
398
362
return response
399
else:
400
return next(response)
363
return next(response)
401
364
402
365
def stream(
403
366
self,
@@ -655,7 +618,6 @@ class AsyncClient(BaseClient):
655
618
self.models: ClientModels = ClientModels(self, provider, media_provider)
656
619
self.images: AsyncImages = AsyncImages(self, media_provider)
657
620
self.media: AsyncImages = self.images
658
self.responses: AsyncResponses = AsyncResponses(self, provider)
659
621
660
622
class AsyncChat:
661
623
completions: AsyncCompletions
@@ -682,6 +644,7 @@ class AsyncCompletions:
682
644
stop: Optional[Union[list[str], str]] = None,
683
645
api_key: Optional[str] = None,
684
646
ignore_stream: Optional[bool] = False,
647
raw: Optional[bool] = False,
685
648
**kwargs
686
649
) -> Awaitable[ChatCompletion]:
687
650
if isinstance(messages, str):
@@ -713,13 +676,22 @@ class AsyncCompletions:
713
676
**kwargs
714
677
)
715
678
679
if raw:
680
async def filter_raw(response):
681
async for chunk in response:
682
if isinstance(chunk, JsonResponse):
683
yield chunk
684
raw_response = filter_raw(response)
685
if stream:
686
return raw_response
687
return next(raw_response)
688
716
689
response = async_iter_response(response, stream, response_format, max_tokens, stop)
717
690
response = async_iter_append_model_and_provider(response, model, provider)
718
691
719
692
if stream:
720
693
return response
721
else:
722
return anext(response)
694
return anext(response)
723
695
724
696
def stream(
725
697
self,
@@ -754,52 +726,4 @@ class AsyncImages(Images):
754
726
) -> ImagesResponse:
755
727
return await self.async_create_variation(
756
728
image=image, model=model, provider=provider, response_format=response_format, **kwargs
757
)
758
759
class AsyncResponses():
760
def __init__(self, client: AsyncClient, provider: Optional[ProviderType] = None):
761
self.client: AsyncClient = client
762
self.provider: ProviderType = provider
763
764
async def create(
765
self,
766
input: str,
767
model: str = "",
768
provider: Optional[ProviderType] = None,
769
instructions: Optional[str] = None,
770
proxy: Optional[str] = None,
771
api_key: Optional[str] = None,
772
**kwargs
773
) -> ClientResponse:
774
if isinstance(input, str):
775
input = [{"role": "user", "content": input}]
776
if instructions is not None:
777
input = [{"role": "developer", "content": instructions}] + input
778
for idx, message in enumerate(input):
779
if isinstance(message["content"], list):
780
for key, value in enumerate(message["content"]):
781
if isinstance(value, dict) and value.get("type") == "input_text":
782
message["content"][key] = {"type": "text", "text": value.get("text")}
783
input[idx] = {"role": message["role"], "content": message["content"]}
784
resolve_media(kwargs)
785
if hasattr(model, "name"):
786
model = model.get_long_name()
787
if provider is None:
788
provider = self.provider
789
if provider is None:
790
provider = AnyProvider
791
if isinstance(provider, str):
792
provider = convert_to_provider(provider)
793
794
response = async_iter_run_tools(
795
provider,
796
model=model,
797
messages=input,
798
**filter_none(
799
proxy=self.client.proxy if proxy is None else proxy,
800
api_key=self.client.api_key if api_key is None else api_key
801
),
802
**kwargs
803
)
804
805
return await async_response(response)
729
)
@@ -274,6 +274,10 @@ class Api:
274
274
yield self._format_json("continue", chunk.log)
275
275
elif isinstance(chunk, RawResponse):
276
276
yield self._format_json(chunk.type, **chunk.get_dict())
277
elif isinstance(chunk, JsonRequest):
278
yield self._format_json("request", chunk.get_dict())
279
elif isinstance(chunk, JsonResponse):
280
yield self._format_json("response", chunk.get_dict())
277
281
else:
278
282
yield self._format_json("content", str(chunk))
279
283
except MissingAuthError as e:
@@ -151,6 +151,33 @@ class JsonMixin:
151
151
class RawResponse(ResponseType, JsonMixin):
152
152
pass
153
153
154
class ObjectMixin:
155
def __init__(self, **kwargs) -> None:
156
"""Initialize with keyword arguments as attributes."""
157
for key, value in kwargs.items():
158
setattr(self, key, ObjectMixin.from_dict(value) if isinstance(value, dict) else [ObjectMixin.from_dict(v) if isinstance(v, dict) else v for v in value] if isinstance(value, list) else value)
159
160
def get_dict(self) -> Dict:
161
"""Return a dictionary of non-private attributes."""
162
return {
163
key: value.get_dict() if isinstance(value, ObjectMixin) else [v.get_dict() if isinstance(v, ObjectMixin) else v for v in value] if isinstance(value, list) else value
164
for key, value in self.__dict__.items()
165
if not key.startswith("__")
166
}
167
168
@classmethod
169
def from_dict(cls, data: Dict) -> JsonMixin:
170
"""Create an instance from a dictionary."""
171
return cls(**data)
172
173
class JsonResponse(ResponseType, ObjectMixin):
174
def __str__(self) -> str:
175
return str(self.get_dict())
176
177
class JsonRequest(ResponseType, ObjectMixin):
178
def __str__(self) -> str:
179
return str(self.get_dict())
180
154
181
class HiddenResponse(ResponseType):
155
182
def __str__(self) -> str:
156
183
"""Hidden responses return an empty string."""