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

XFEstudio/gpt4free

Enhance Perplexity provider with additional models and improved conversation handling; add JsonRequest and JsonResponse classes for better response management

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

代码差异

6 个文件 +179 -138
Modified g4f/Provider/Perplexity.py +117 -35
@@ -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
Modified g4f/Provider/PollinationsAI.py +2 -1
@@ -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}")
Modified g4f/Provider/template/OpenaiTemplate.py +4 -1
@@ -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:
Modified g4f/client/__init__.py +25 -101
@@ -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 )
Modified g4f/gui/server/api.py +4 -0
@@ -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:
Modified g4f/providers/response.py +27 -0
@@ -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."""