File size: 11,404 Bytes
e3596d7
a503c73
096dbd4
 
d3cddbc
096dbd4
ae95de2
 
 
1b7f3dd
511f8f6
546eb40
a639253
ae95de2
096dbd4
a639253
 
 
 
 
 
ae95de2
b73bb4c
 
ae95de2
511f8f6
ae95de2
511f8f6
ae95de2
511f8f6
ae95de2
b73bb4c
511f8f6
b73bb4c
 
ae95de2
 
 
 
 
 
 
 
 
e3596d7
511f8f6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae95de2
3eec23c
 
 
511f8f6
3eec23c
511f8f6
3eec23c
1b7f3dd
511f8f6
 
 
 
 
 
 
b9e956c
ae95de2
511f8f6
ae95de2
b9e956c
a503c73
 
 
 
 
 
 
096dbd4
a503c73
 
 
 
2ceda68
096dbd4
a503c73
096dbd4
 
a503c73
ae95de2
511f8f6
 
ae95de2
e3596d7
0e13636
a639253
 
ee90599
45eb20f
1b7f3dd
10d3a03
4a5e212
 
da88775
4a5e212
 
10d3a03
4a5e212
ae95de2
b9e956c
511f8f6
b9e956c
 
 
 
 
 
 
 
 
 
 
 
511f8f6
 
b9e956c
511f8f6
 
 
b9e956c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511f8f6
b9e956c
 
 
 
 
 
511f8f6
 
 
 
 
b9e956c
 
 
 
511f8f6
b9e956c
 
b73bb4c
 
511f8f6
b9e956c
 
511f8f6
b9e956c
568f598
ec67b57
 
511f8f6
b9e956c
 
511f8f6
 
b9e956c
 
 
ae95de2
511f8f6
b9e956c
 
 
511f8f6
ae95de2
 
 
 
511f8f6
096dbd4
a503c73
a639253
511f8f6
ae95de2
10d3a03
4a5e212
 
 
 
511f8f6
 
 
4a5e212
a639253
4a5e212
 
 
 
db9ffff
 
 
 
 
 
 
 
 
4a5e212
 
 
511f8f6
4a5e212
b73bb4c
 
 
 
 
 
 
 
4a5e212
511f8f6
4a5e212
a639253
4a5e212
 
511f8f6
4a5e212
a639253
4a5e212
 
 
 
1b7f3dd
7164dd1
4a5e212
a639253
4a5e212
a639253
8eb01dc
4a5e212
 
8eb01dc
4a5e212
a639253
 
4a5e212
 
73e1543
b9e956c
4a5e212
 
a639253
511f8f6
4a5e212
 
 
 
ae95de2
 
511f8f6
096dbd4
ae95de2
 
a639253
511f8f6
 
 
 
 
 
b9e956c
096dbd4
 
ae95de2
a639253
faf18e1
a639253
511f8f6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
import os
import json
import uuid
from datetime import datetime
from flask import Flask, request, Response, jsonify
import socketio
import requests
import logging
from threading import Event
import re
from functools import wraps

# 创建 Flask 应用
app = Flask(__name__)

# 自定义日志格式
log_format = "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
logging.basicConfig(level=logging.INFO, format=log_format)
app_logger = logging.getLogger('app')

# 从环境变量中获取 API 密钥
API_KEY = os.environ.get('PPLX_KEY')

# 代理设置
proxy_url = os.environ.get('PROXY_URL')
transport = requests.Session()
if proxy_url:
    transport.proxies.update({'http': proxy_url, 'https': proxy_url})

sio = socketio.Client(http_session=transport, logger=True, engineio_logger=True)

# 连接选项
connect_opts = {'transports': ['websocket', 'polling']}

# 其他选项
sio_opts = {
    'extraHeaders': {
        'Cookie': os.environ.get('PPLX_COOKIE'),
        'User-Agent': os.environ.get('USER_AGENT'),
        'Accept': '*/*',
        'priority': 'u=1, i',
        'Referer': 'https://www.perplexity.ai/',
    }
}

def log_request(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = datetime.now()
        response = func(*args, **kwargs)
        duration = (datetime.now() - start_time).total_seconds()
        app_logger.info(f"{request.remote_addr} - {request.method} {request.path} - {response.status_code} - {duration:.2f}s")
        return response
    return wrapper

def validate_api_key(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        api_key = request.headers.get('x-api-key')
        if api_key != API_KEY:
            app_logger.warning(f"Invalid API key attempt from {request.remote_addr}")
            return jsonify({"error": "Invalid API key"}), 401
        return func(*args, **kwargs)
    return wrapper

def normalize_content(content):
    if isinstance(content, str):
        return content
    elif isinstance(content, (dict, list)):
        return json.dumps(content, ensure_ascii=False)
    return str(content)

def calculate_tokens(text):
    return len(re.findall(r'\w+|[^\w\s]', text, re.UNICODE))

def create_json_response(data, status_code=200):
    response = jsonify(data)
    response.status_code = status_code
    app_logger.debug(f"Sending JSON response: {json.dumps(data, ensure_ascii=False)}")
    return response

@app.route('/')
@log_request
def root():
    return create_json_response({
        "message": "Welcome to the Perplexity AI Proxy API",
        "endpoints": {
            "/ai/v1/messages": {
                "method": "POST",
                "description": "Send a message to the AI",
                "headers": {
                    "x-api-key": "Your API key (required)",
                    "Content-Type": "application/json"
                },
                "body": {
                    "messages": "Array of message objects",
                    "stream": "Boolean (true for streaming response)",
                    "model": "Model to be used (optional, defaults to claude-3-opus-20240229)"
                }
            }
        }
    })

@app.route('/ai/v1/messages', methods=['POST'])
@log_request
@validate_api_key
def messages():
    try:
        json_body = request.json
        model = json_body.get('model', 'claude-3-opus-20240229')
        stream = json_body.get('stream', True)

        previous_messages = "\n\n".join([normalize_content(msg['content']) for msg in json_body['messages']])
        input_tokens = calculate_tokens(previous_messages)

        msg_id = str(uuid.uuid4())
        response_event = Event()
        response_text = []

        if not stream:
            return handle_non_stream(previous_messages, msg_id, model, input_tokens)

        def generate():
            try:
                yield create_sse_event("message_start", {
                    "type": "message_start",
                    "message": {
                        "id": msg_id,
                        "type": "message",
                        "role": "assistant",
                        "content": [],
                        "model": model,
                        "stop_reason": None,
                        "stop_sequence": None,
                        "usage": {"input_tokens": input_tokens, "output_tokens": 1},
                    },
                })
                yield create_sse_event("content_block_start", {"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}})
                yield create_sse_event("ping", {"type": "ping"})

                sio.connect('wss://www.perplexity.ai/', **connect_opts, headers=sio_opts['extraHeaders'])
                
                @sio.on('connect')
                def on_connect():
                    app_logger.info("Connected to Perplexity AI")
                    emit_data = {
                        "version": "2.9",
                        "source": "default",
                        "attachments": [],
                        "language": "en-GB",
                        "timezone": "Europe/London",
                        "mode": "concise",
                        "is_related_query": False,
                        "is_default_related_query": False,
                        "visitor_id": str(uuid.uuid4()),
                        "frontend_context_uuid": str(uuid.uuid4()),
                        "prompt_source": "user",
                        "query_source": "home"
                    }
                    sio.emit('perplexity_ask', (previous_messages, emit_data))

                @sio.on('query_progress')
                def on_query_progress(data):
                    if 'text' in data:
                        text = json.loads(data['text'])
                        chunk = text['chunks'][-1] if text['chunks'] else None
                        if chunk:
                            response_text.append(chunk)
                            yield create_sse_event("content_block_delta", {
                                "type": "content_block_delta",
                                "index": 0,
                                "delta": {"type": "text_delta", "text": chunk},
                            })

                    if data.get('final', False):
                        response_event.set()

                @sio.on('disconnect')
                def on_disconnect():
                    app_logger.info("Disconnected from Perplexity AI")
                    response_event.set()

                @sio.on('connect_error')
                def on_connect_error(data):
                    app_logger.error(f"Connection error: {data}")
                    yield create_sse_event("error", {"type": "error", "message": f"Error connecting to Perplexity AI: {data}"})
                    response_event.set()

                while not response_event.is_set():
                    sio.sleep(0.1)

                output_tokens = calculate_tokens(''.join(response_text))

                yield create_sse_event("content_block_stop", {"type": "content_block_stop", "index": 0})
                yield create_sse_event("message_delta", {
                    "type": "message_delta",
                    "delta": {"stop_reason": "end_turn", "stop_sequence": None},
                    "usage": {"input_tokens": input_tokens, "output_tokens": output_tokens},
                })
                yield create_sse_event("message_stop", {"type": "message_stop"})

            except Exception as e:
                app_logger.error(f"Error in generate function: {str(e)}")
                yield create_sse_event("error", {"type": "error", "message": str(e)})
            finally:
                if sio.connected:
                    sio.disconnect()

        return Response(generate(), content_type='text/event-stream; charset=utf-8')

    except Exception as e:
        app_logger.error(f"Request error: {str(e)}")
        return create_json_response({"error": str(e)}, 400)

def handle_non_stream(previous_messages, msg_id, model, input_tokens):
    try:
        response_event = Event()
        response_text = []

        sio.connect('wss://www.perplexity.ai/', **connect_opts, headers=sio_opts['extraHeaders'])

        @sio.on('connect')
        def on_connect():
            app_logger.info("Connected to Perplexity AI")
            emit_data = {
                "version": "2.9",
                "source": "default",
                "attachments": [],
                "language": "en-GB",
                "timezone": "Europe/London",
                "mode": "concise",
                "is_related_query": False,
                "is_default_related_query": False,
                "visitor_id": str(uuid.uuid4()),
                "frontend_context_uuid": str(uuid.uuid4()),
                "prompt_source": "user",
                "query_source": "home"
            }
            sio.emit('perplexity_ask', (previous_messages, emit_data))

        @sio.on('query_progress')
        def on_query_progress(data):
            if 'text' in data:
                text = json.loads(data['text'])
                chunk = text['chunks'][-1] if text['chunks'] else None
                if chunk:
                    response_text.append(chunk)

            if data.get('final', False):
                response_event.set()

        @sio.on('disconnect')
        def on_disconnect():
            app_logger.info("Disconnected from Perplexity AI")
            response_event.set()

        @sio.on('connect_error')
        def on_connect_error(data):
            app_logger.error(f"Connection error: {data}")
            response_text.append(f"Error connecting to Perplexity AI: {data}")
            response_event.set()

        response_event.wait(timeout=30)
        output_tokens = calculate_tokens(''.join(response_text))

        full_response = {
            "content": [{"text": ''.join(response_text), "type": "text"}],
            "id": msg_id,
            "model": model,
            "role": "assistant",
            "stop_reason": "end_turn",
            "stop_sequence": None,
            "type": "message",
            "usage": {
                "input_tokens": input_tokens,
                "output_tokens": output_tokens,
            },
        }
        
        return create_json_response(full_response)

    except Exception as e:
        app_logger.error(f"Error during socket connection: {str(e)}")
        return create_json_response({"error": str(e)}, 500)
    finally:
        if sio.connected:
            sio.disconnect()

@app.errorhandler(404)
def not_found(error):
    return create_json_response({"error": "Not Found"}, 404)

@app.errorhandler(500)
def server_error(error):
    app_logger.error(f"Server error: {str(error)}")
    return create_json_response({"error": "Internal Server Error"}, 500)

def create_sse_event(event, data):
    json_data = json.dumps(data, ensure_ascii=False)
    event_str = f"event: {event}\ndata: {json_data}\n\n"
    app_logger.debug(f"Sending SSE event: {event_str}")
    return event_str

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 8081))
    app_logger.info(f"Perplexity proxy listening on port {port}")
    if not API_KEY:
        app_logger.warning("Warning: PPLX_KEY environment variable is not set. API key validation will fail.")
    app.run(host='0.0.0.0', port=port, debug=False)