返回提交历史
Modified
g4f/Provider/Perplexity.py
+190
-53
Modified
g4f/cookies.py
+1
-0
XFEstudio/gpt4free
Enhance Perplexity provider with HAR file support for improved authentication management and update cookie handling for conversation continuity
db299525
代码差异
2 个文件
+191
-53
@@ -1,22 +1,42 @@
1
1
from __future__ import annotations
2
2
3
import random
3
import os
4
4
import uuid
5
import json
6
from typing import AsyncIterator
5
7
6
8
from ..typing import AsyncResult, Messages, Cookies
7
9
from ..requests import StreamSession, raise_for_status, sse_stream
8
from ..cookies import get_cookies
9
from ..providers.response import ProviderInfo, JsonConversation, JsonRequest, JsonResponse, Reasoning, Sources, SuggestedFollowups, ImageResponse, PreviewResponse, YouTubeResponse
10
from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
10
from ..cookies import get_cookies, get_cookies_dir
11
from ..providers.response import (
12
ProviderInfo, JsonConversation, JsonRequest, JsonResponse,
13
Reasoning, Sources, SuggestedFollowups, ImageResponse,
14
VariantResponse, YouTubeResponse, TitleGeneration
15
)
16
from ..providers.base_provider import AsyncGeneratorProvider, ProviderModelMixin
11
17
from .. import debug
12
18
19
# Perplexity API endpoints
20
PERPLEXITY_URL = "https://www.perplexity.ai"
21
PERPLEXITY_DOMAIN = ".perplexity.ai"
22
AUTH_ENDPOINT = f"{PERPLEXITY_URL}/api/auth/session"
23
QUERY_ENDPOINT = f"{PERPLEXITY_URL}/rest/sse/perplexity_ask"
24
13
25
class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
26
"""
27
Perplexity provider using browser emulation with HAR file support.
28
29
This provider extends the base Perplexity implementation with HAR file support
30
for easier authentication management. It uses curl_cffi's Chrome impersonation
31
for realistic browser-like requests.
32
"""
33
14
34
label = "Perplexity"
15
url = "https://www.perplexity.ai"
16
cookie_domain = ".perplexity.ai"
35
url = PERPLEXITY_URL
36
cookie_domain = PERPLEXITY_DOMAIN
17
37
working = True
18
38
active_by_default = True
19
39
20
40
default_model = "auto"
21
41
models = [
22
42
default_model,
@@ -72,82 +92,125 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
72
92
"gpt-5-thinking": "gpt5_thinking",
73
93
"r1-1776": "r1",
74
94
}
75
95
76
96
@classmethod
77
97
async def create_async_generator(
78
98
cls,
79
99
model: str,
80
100
messages: Messages,
81
101
cookies: Cookies = None,
82
conversation: JsonConversation = None,
83
102
proxy: str = None,
103
conversation: JsonConversation = None,
84
104
**kwargs
85
105
) -> AsyncResult:
106
"""
107
Create async generator for Perplexity requests with HAR file support.
108
109
Authentication priority:
110
1. HAR file cookies (har_and_cookies/perplexity*.har)
111
2. Cookie jar from get_cookies()
112
"""
86
113
if not model:
87
114
model = cls.default_model
115
116
# Try to get cookies from HAR file first
88
117
if cookies is None:
89
118
cookies = get_cookies(cls.cookie_domain, False)
119
if cookies:
120
debug.log(f"Perplexity: Using {len(cookies)} cookies from cookie jar")
121
122
# Initialize conversation if needed
90
123
if conversation is None:
91
124
conversation = JsonConversation(
92
125
frontend_uid=str(uuid.uuid4()),
93
126
frontend_context_uuid=str(uuid.uuid4()),
94
127
visitor_id=str(uuid.uuid4()),
95
128
user_id=None,
129
thread_url_slug=None, # For conversation continuity via Referer header
96
130
)
131
97
132
request_id = str(uuid.uuid4())
98
133
134
# Set referer based on thread_url_slug for conversation continuity
135
referer = f"{cls.url}/"
136
if hasattr(conversation, 'thread_url_slug') and conversation.thread_url_slug:
137
referer = f"{cls.url}/search/{conversation.thread_url_slug}"
138
# debug.log(f"Perplexity: Using conversation referer: {referer}")
139
99
140
headers = {
100
141
"accept": "text/event-stream",
101
142
"accept-language": "en-US,en;q=0.9",
102
143
"cache-control": "no-cache",
103
144
"content-type": "application/json",
104
145
"origin": cls.url,
105
"referer": f"{cls.url}/",
146
"referer": referer,
106
147
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
107
148
"x-perplexity-request-reason": "perplexity-query-state-provider",
108
149
"x-request-id": request_id,
109
150
}
110
111
# Extract the last user message as the query
151
152
# Extract query from messages
112
153
query = ""
113
154
for message in reversed(messages):
114
155
if message["role"] == "user":
115
156
query = message["content"]
116
157
break
117
158
159
# Use StreamSession with Chrome impersonation
118
160
async with StreamSession(headers=headers, cookies=cookies, proxy=proxy, impersonate="chrome") as session:
161
# Get user info if needed
119
162
if conversation.user_id is None:
120
async with session.get(f"{cls.url}/api/auth/session") as response:
121
await raise_for_status(response)
122
user = await response.json()
123
conversation.user_id = user.get("user", {}).get("id")
124
debug.log(f"Perplexity user id: {conversation.user_id}")
163
try:
164
async with session.get(f"{cls.url}/api/auth/session") as response:
165
await raise_for_status(response)
166
user = await response.json()
167
conversation.user_id = user.get("user", {}).get("id")
168
debug.log(f"Perplexity: User ID: {conversation.user_id}")
169
except Exception as e:
170
debug.error(f"Perplexity: Failed to get user info: {e}")
171
125
172
yield conversation
173
174
# Determine model
126
175
if model == "auto" or model == "perplexity":
127
176
model = "pplx_pro" if conversation.user_id else "turbo"
177
128
178
yield ProviderInfo(**cls.get_dict(), model=model)
179
129
180
if model in cls.model_aliases:
130
181
model = cls.model_aliases[model]
131
if conversation.user_id is None:
182
183
# Build request data (same as original Perplexity)
184
# Check if this is a followup request (has session tokens)
185
is_followup = hasattr(conversation, 'last_backend_uuid') and conversation.last_backend_uuid
186
187
# debug.log(f"Perplexity: is_followup={is_followup}")
188
if is_followup:
189
debug.log(f"Perplexity: followup with last_backend_uuid={conversation.last_backend_uuid}, read_write_token={getattr(conversation, 'read_write_token', None)}")
190
191
# Generate new frontend_uuid for followup requests (browser does this)
192
if is_followup:
193
conversation.frontend_uid = str(uuid.uuid4())
194
195
if not is_followup:
132
196
data = {
133
197
"params": {
134
198
"attachments": [],
135
199
"language": "en-US",
136
"timezone": "America/New_York",
200
"timezone": "America/Los_Angeles",
137
201
"search_focus": "internet",
138
202
"sources": ["web"],
139
203
"search_recency_filter": None,
140
204
"frontend_uuid": conversation.frontend_uid,
141
"mode": "concise",
205
"mode": "copilot", # Match HAR - use copilot mode
142
206
"model_preference": model,
143
207
"is_related_query": False,
144
208
"is_sponsored": False,
145
"visitor_id": conversation.visitor_id,
146
209
"frontend_context_uuid": conversation.frontend_context_uuid,
147
210
"prompt_source": "user",
148
211
"query_source": "home",
149
212
"is_incognito": False,
150
"time_from_first_type": 0,
213
"time_from_first_type": 18361, # Match HAR value
151
214
"local_search_enabled": False,
152
215
"use_schematized_api": True,
153
216
"send_back_text_in_streaming_api": False,
@@ -158,29 +221,43 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
158
221
"inline_entity_cards",
159
222
"place_widgets",
160
223
"finance_widgets",
224
"prediction_market_widgets",
161
225
"sports_widgets",
226
"flight_status_widgets",
227
"news_widgets",
162
228
"shopping_widgets",
163
229
"jobs_widgets",
164
230
"search_result_widgets",
165
"clarification_responses",
166
231
"inline_images",
167
232
"inline_assets",
168
"inline_finance_widgets",
169
233
"placeholder_cards",
170
234
"diff_blocks",
171
235
"inline_knowledge_cards",
172
236
"entity_group_v2",
173
237
"refinement_filters",
174
"canvas_mode"
238
"canvas_mode",
239
"maps_preview",
240
"answer_tabs",
241
"price_comparison_widgets",
242
"preserve_latex",
243
"generic_onboarding_widgets",
244
"in_context_suggestions",
245
"inline_claims"
175
246
],
176
247
"client_coordinates": None,
177
248
"mentions": [],
178
249
"dsl_query": query,
179
"skip_search_enabled": False,
250
"skip_search_enabled": True,
180
251
"is_nav_suggestions_disabled": False,
252
"source": "default",
181
253
"always_search_override": False,
182
254
"override_no_search": False,
183
"comet_max_assistant_enabled": False,
255
"should_ask_for_mcp_tool_confirmation": True,
256
"browser_agent_allow_once_from_toggle": False,
257
"force_enable_browser_agent": False,
258
"supported_features": [
259
"browser_agent_permission_banner_v1.1"
260
],
184
261
"version": "2.18"
185
262
},
186
263
"query_str": query
@@ -188,26 +265,24 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
188
265
else:
189
266
data = {
190
267
"params": {
191
"last_backend_uuid": None,
192
"read_write_token": None,
268
"last_backend_uuid": getattr(conversation, 'last_backend_uuid', None),
269
"read_write_token": getattr(conversation, 'read_write_token', None),
193
270
"attachments": [],
194
271
"language": "en-US",
195
"timezone": "America/New_York",
272
"timezone": "America/Los_Angeles",
196
273
"search_focus": "internet",
197
"sources": [
198
"web"
199
],
200
"frontend_uuid": conversation.frontend_uid,
201
"mode": "copilot",
274
"sources": ["web"],
275
"search_recency_filter": None,
276
"frontend_uuid": conversation.frontend_uid, # New UUID for followup
277
"mode": "copilot", # Match HAR - use copilot mode
202
278
"model_preference": model,
203
279
"is_related_query": False,
204
280
"is_sponsored": False,
205
"visitor_id": conversation.visitor_id,
206
"user_nextauth_id": conversation.user_id,
207
281
"prompt_source": "user",
208
282
"query_source": "followup",
283
"followup_source": "link", # Critical for conversation continuity
209
284
"is_incognito": False,
210
"time_from_first_type": random.randint(0, 1000),
285
"time_from_first_type": 8758, # Match HAR value
211
286
"local_search_enabled": False,
212
287
"use_schematized_api": True,
213
288
"send_back_text_in_streaming_api": False,
@@ -218,75 +293,129 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
218
293
"inline_entity_cards",
219
294
"place_widgets",
220
295
"finance_widgets",
296
"prediction_market_widgets",
221
297
"sports_widgets",
298
"flight_status_widgets",
299
"news_widgets",
222
300
"shopping_widgets",
223
301
"jobs_widgets",
224
302
"search_result_widgets",
225
"clarification_responses",
226
303
"inline_images",
227
304
"inline_assets",
228
"inline_finance_widgets",
229
305
"placeholder_cards",
230
306
"diff_blocks",
231
307
"inline_knowledge_cards",
232
308
"entity_group_v2",
233
309
"refinement_filters",
234
"canvas_mode"
310
"canvas_mode",
311
"maps_preview",
312
"answer_tabs",
313
"price_comparison_widgets",
314
"preserve_latex",
315
"generic_onboarding_widgets",
316
"in_context_suggestions",
317
"inline_claims"
235
318
],
236
319
"client_coordinates": None,
237
320
"mentions": [],
321
"dsl_query": query,
238
322
"skip_search_enabled": True,
239
323
"is_nav_suggestions_disabled": False,
240
"followup_source": "link",
324
"source": "default",
241
325
"always_search_override": False,
242
326
"override_no_search": False,
243
"comet_max_assistant_enabled": False,
327
"should_ask_for_mcp_tool_confirmation": True,
328
"force_enable_browser_agent": False,
329
"supported_features": [
330
"browser_agent_permission_banner_v1.1"
331
],
244
332
"version": "2.18"
245
333
},
246
334
"query_str": query
247
335
}
336
248
337
yield JsonRequest.from_dict(data)
249
async with session.post(
250
f"{cls.url}/rest/sse/perplexity_ask",
251
json=data,
252
) as response:
338
339
# Log full request data for debugging
340
# debug.log(f"Perplexity: Request data: {json.dumps(data, indent=2, default=str)[:1000]}")
341
342
# Send request
343
# debug.log(f"Perplexity: Sending request to {QUERY_ENDPOINT}")
344
345
async with session.post(QUERY_ENDPOINT, json=data) as response:
346
# Process SSE stream
347
# debug.log(f"Perplexity: Processing response...")
253
348
await raise_for_status(response)
349
254
350
full_response = ""
255
351
full_reasoning = ""
352
sources = []
353
256
354
async for json_data in sse_stream(response):
257
355
yield JsonResponse.from_dict(json_data)
356
357
# Capture session tokens for conversation continuity
358
# Note: The 'backend_uuid' field in responses is the backend UUID we need for followups
359
if 'backend_uuid' in json_data:
360
conversation.last_backend_uuid = json_data['backend_uuid']
361
362
# Only capture read_write_token if we don't have one yet (like a session cookie)
363
if 'read_write_token' in json_data and not hasattr(conversation, 'read_write_token'):
364
conversation.read_write_token = json_data['read_write_token']
365
366
# Capture thread_url_slug for conversation continuity via Referer header
367
if 'thread_url_slug' in json_data and (not hasattr(conversation, 'thread_url_slug') or not conversation.thread_url_slug):
368
conversation.thread_url_slug = json_data.get('thread_url_slug')
369
370
if 'thread_title' in json_data:
371
conversation.thread_title = json_data['thread_title']
372
yield TitleGeneration(json_data['thread_title'])
258
373
for block in json_data.get("blocks", []):
374
# Handle sources
259
375
if block.get("intended_usage") == "sources_answer_mode":
260
yield Sources(block.get("sources_mode_block", {}).get("web_results", []))
376
sources = block.get("sources_mode_block", {}).get("web_results", [])
261
377
continue
378
379
# Handle media items
262
380
if block.get("intended_usage") == "media_items":
263
yield PreviewResponse([
381
yield VariantResponse("".join([chunk.to_string() if hasattr(chunk, "to_string") else str(chunk) for chunk in [
264
382
ImageResponse(item.get("url"), item.get("name"), {
265
383
"height": item.get("image_height"),
266
384
"width": item.get("image_width"),
267
385
**item
268
386
}) if item.get("medium") == "image" else YouTubeResponse(item.get("url").split("=").pop())
269
387
for item in block.get("media_block", {}).get("media_items", [])
270
])
388
]]))
271
389
continue
390
391
# Handle response text
272
392
for patch in block.get("diff_block", {}).get("patches", []):
273
393
if patch.get("path") == "/progress":
274
394
continue
395
275
396
value = patch.get("value", "")
397
398
# Handle reasoning
276
399
if isinstance(value, dict) and "chunks" in value:
277
400
value = "".join(value.get("chunks", []))
401
278
402
if patch.get("path").startswith("/goals"):
279
403
if isinstance(value, str):
280
404
if value.startswith(full_reasoning):
281
405
value = value[len(full_reasoning):]
282
yield Reasoning(value)
283
full_reasoning += value
406
if value:
407
yield Reasoning(value)
408
full_reasoning += value
284
409
else:
285
410
yield Reasoning(status="")
286
411
continue
412
413
# Handle regular response
287
414
if block.get("diff_block").get("field") != "markdown_block":
288
415
continue
416
289
417
value = value.get("answer", "") if isinstance(value, dict) else value
418
290
419
if value and isinstance(value, str):
291
420
if value.startswith(full_response):
292
421
value = value[len(full_response):]
@@ -295,8 +424,16 @@ class Perplexity(AsyncGeneratorProvider, ProviderModelMixin):
295
424
if value:
296
425
full_response += value
297
426
yield value
427
428
# Handle follow-ups
298
429
if "related_query_items" in json_data:
299
430
followups = []
300
431
for item in json_data["related_query_items"]:
301
432
followups.append(item.get("text", ""))
302
433
yield SuggestedFollowups(followups)
434
if sources:
435
yield Sources([{"name": f"Perplexity - {conversation.thread_title}", "url": f"{cls.url}/search/{conversation.thread_url_slug}"}] + sources)
436
yield conversation
437
438
# debug.log("Perplexity: Request completed successfully")
439
# debug.log(f"Perplexity: last_backend_uuid={getattr(conversation, 'last_backend_uuid', None)}, read_write_token={getattr(conversation, 'read_write_token', None)}")
@@ -74,6 +74,7 @@ COOKIE_DOMAINS = (
74
74
"github.com",
75
75
"yupp.ai",
76
76
"chat.deepseek.com",
77
".perplexity.ai"
77
78
)
78
79
79
80
if has_browser_cookie3 and os.environ.get("DBUS_SESSION_BUS_ADDRESS", "/dev/null") == "/dev/null":