Perplexity 反向代理服务

pplx-proxy 是一个利用现有 Perplexity Pro/Max 订阅 Cookie 提供 OpenAI 兼容 REST API、MCP 服务器和调试聊天界面的反向代理服务,适用于无需官方 API Key 即可自托管接入多模型并调用实时联网搜索能力的场景。

  • by jamie950315
  • Python
  • 混合部署
  • 29 星标
  • 更新于
在 GitHub 查看 jamie950315/pplx-proxy

使用说明

pplx-proxy

Reverse proxy for Perplexity.ai — use your existing Pro/Max subscription cookie to access all models via standard APIs.

Exposes three interfaces:

  • OpenAI-compatible REST API (/v1/chat/completions, /v1/responses) — streaming, thinking
  • MCP server (Streamable HTTP + SSE) — 5 built-in tools
  • Debug chat UI (/chat) — test everything with real-time OpenAI format validation

Release Status (2026-09-09)

This release includes image input, local file storage, and the experimental Responses function bridge in main. The production deployment target is Pi5, managed by pplx-proxy.service on port 8892. The limitations below still apply.

Feature included in this release Verified status
PNG image input A real image request passed through the official OpenAI SDK.
Local /v1/files storage Upload, read, and delete work; limits are 20 MiB per file and 200 MiB total. Storing a document does not mean Perplexity can read it.
Document input Incomplete. After upload, Perplexity requires /rest/sse/attachment_processing/subscribe; Cloudflare currently returns 403. Document requests fail explicitly with HTTP 502.
Responses function calling Experimental and unreliable. One complete official SDK auto-selection/client-execution/stream-continuation loop passed, but a repeat returned invalid protocol output and failed. This is a prompt bridge, not native Perplexity function calling.
Chat Completions function tools Unsupported; HTTP 400.

How It Works

Perplexity's web frontend talks to its backend through an internal SSE endpoint (/rest/sse/perplexity_ask). This proxy authenticates with your session cookie via curl_cffi (Chrome TLS fingerprinting), translates requests/responses into OpenAI and MCP formats, and keeps your session alive automatically.

No official API key needed — just your subscription.

All queries use search_focus: "internet" — Perplexity's built-in web search is always active, so models return real-time data (stock prices, weather, news) directly in their answers.

Features

  • Full OpenAI format compliance — system_fingerprint, logprobs, proper usage arithmetic, all fields per spec
  • Thinking/reasoning — thinking: true or reasoning_effort param, reasoning streamed as reasoning_content
  • Account tier support — free/pro/max — only exposes models your tier can access
  • Auto-discovery — background task checks model health every 24h, auto-upgrades when versions change
  • Response cleaning — strips Perplexity citations [1][2], <grok:*> tags, <?xml?> declarations, <script> tags
  • Rate limit tracking — tracks Pro Search quota, explicit error when the requested paid model has no remaining quota, notices at every 5th decrement
  • Substitution notice — if Perplexity swaps the requested model, the answer still returns with [Substituted by Perplexity with ...] at the end
  • Session continuity — uses Perplexity backend_uuid for eligible plain-text follow-ups; explicit instructions and function conversations retain their complete context
  • Session keep-alive — validates at startup and every 6 hours, then persists any rotated cookie returned by Perplexity
  • Push notifications — ntfy.sh alerts on cookie expiry or model upgrades
  • Debug chat UI — /chat page with tools toggle, thinking toggle, streaming toggle, and OpenAI format validator
  • Dynamic model management — add/remove models at runtime via admin API
  • Full input validation — proper error messages for every malformed request

Quick Start

Choose one startup mode:

Mode Command Includes
Manual Python venv/bin/uvicorn server:app --host 0.0.0.0 --port 8892 pplx-proxy only
Docker Compose docker compose up -d --build pplx-proxy, FlareSolverr, persistent runtime volume

Manual Python is best for local development. Docker Compose is the full self-hosted stack and is recommended when you want quota data in /health.

Manual Python

git clone https://github.com/jamie950315/pplx-proxy.git
cd pplx-proxy
python3 -m venv venv
venv/bin/pip install -r requirements.txt

cp .env.example .env
# Edit .env — set PPLX_COOKIE and ACCOUNT_TYPE

venv/bin/uvicorn server:app --host 0.0.0.0 --port 8892

Then open http://localhost:8892/chat to test with the debug UI.

Docker Compose

For the complete setup, including pplx-proxy, FlareSolverr, and persistent runtime storage:

cp .env.example .env
# Edit .env — set PPLX_COOKIE, PPLX_PROXY_API_KEY, and ACCOUNT_TYPE
docker compose up -d --build

This starts:

  • pplx-proxy on http://localhost:8892
  • flaresolverr inside the Compose network at http://flaresolverr:8191
  • pplx-data volume for .cookie_cache.json, .models.json, and .responses_store.json

FlareSolverr is optional for chat, streaming, MCP, and Responses API. Without it, /health still reports service and cookie age, but remaining_pro and remaining_research stay null and quota exhaustion cannot be checked locally.

Getting Your Cookie

  1. Log in to perplexity.ai
  2. F12 → Application → Cookies → www.perplexity.ai
  3. Copy __Secure-next-auth.session-token
  4. Set PPLX_COOKIE=<value> in .env

Models

Model ID Backend Tier Thinking Variant
auto Perplexity Best free+ —
sonar Sonar 2 pro+ —
gpt / gpt-5.6-terra GPT-5.6 Terra pro+ gpt56_terra_thinking
gpt-5.6-sol GPT-5.6 Sol max gpt56_sol_thinking
gpt-5.5 GPT-5.5 pro+ gpt55_thinking
gpt-mini GPT-5 Mini pro+ —
gpt-nano GPT-5 Nano pro+ —
sonnet / sonnet-5 Claude Sonnet 5 pro+ claude50sonnetthinking
sonnet-4.6 Claude Sonnet 4.6 pro+ claude46sonnetthinking
gemini Gemini 3.1 Pro pro+ —
gemini-flash Gemini 3.5 Flash pro+ —
grok / grok-4.6 Grok 4.6 pro+ —
grok-4.5 Grok 4.5 pro+ grok45medium
grok-4 Grok 4 pro+ —
grok-reasoning Grok 4.20 Reasoning pro+ —
grok-non-reasoning Grok 4.20 Non Reasoning pro+ —
nemotron Nemotron 3 Ultra pro+ —
nemotron-3-super Nemotron 3 Super pro+ —
glm-5.2 GLM-5.2 pro+ —
kimi-k2.6 Kimi K2.6 pro+ kimik26thinking
kimi-k3 Kimi K3 pro+ —
opus / opus-4.8 Claude Opus 4.8 max claude48opusthinking
opus-4.7 Claude Opus 4.7 max claude47opusthinking
opus-4.6 Claude Opus 4.6 max claude46opusthinking

Thinking variants are activated via thinking: true or reasoning_effort parameter — no separate model names needed.

Tracked candidates such as Claude Haiku 4.5, Gemini 3.1 Flash Lite, and Grok 4.20 Multi-Agent remain disabled and are skipped by discovery. Custom model IDs added through /admin/update-models are available on Pro and Max accounts; Free accounts only expose auto.

API Endpoints

Method Path Auth Description
GET /health No Health check
GET /chat No Debug chat UI with OpenAI format validator
GET /v1/models Yes List tier-available models
POST /v1/chat/completions Yes Chat (streaming + non-streaming + thinking)
POST /v1/responses Yes OpenAI Responses API (stream, store, previous_response_id; experimental function bridge and image input)
GET /v1/responses/{id} Yes Retrieve a stored response
DELETE /v1/responses/{id} Yes Delete a stored response
POST /v1/responses/{id}/cancel Yes Cancel an in-progress background response
GET /v1/responses/{id}/input_items Yes List input items for a stored response
POST / GET /v1/files Yes Upload/list locally stored files
GET / DELETE /v1/files/{id} Yes Retrieve metadata/delete a stored file
GET /v1/files/{id}/content Yes Read stored bytes
POST /<api-key>/mcp Key in URL MCP Streamable HTTP
GET /<api-key>/sse Key in URL MCP SSE
GET /admin/models Yes Full model map
POST /admin/update-models Yes Add/replace models
POST /admin/refresh-cookie Yes Inject new session token
POST /admin/discover-models Yes Run model discovery

Usage

Responses API

POST /v1/responses accepts OpenAI Responses API requests (input, instructions, previous_response_id, stream, store, reasoning, text.format). Responses are stored by default so you can GET / DELETE them and continue a thread with previous_response_id. With store: false, they are not retained or retrievable.

Built-in web search is always on. Responses web-search tools are accepted because they use that existing search behavior. Background requests require store: true; only active background requests can be cancelled. Custom sampling values, output-token limits, automatic truncation, and strict text-output JSON schemas are rejected. JSON-object and non-strict text schema modes are prompt-based requests, not guaranteed structured output. File search, code interpreter, computer use, and image generation remain unsupported.

Function definitions in tools enable an experimental prompt-mediated bridge. One full SDK loop succeeded, but a subsequent auto-selection test returned non-JSON output and failed with tool_protocol_error. Reliability is not established; passing unit and container tests verifies the proxy behavior, not model compliance. No retry or fallback hides these failures.

The bridge works as follows: Perplexity proposes a JSON call, the proxy validates its name and arguments against the supplied schema, and the client application executes it. Malformed JSON, invalid arguments, or a violated tool choice produce HTTP 502 (tool_protocol_error), or response.failed after streaming has started. There is no ordinary-text fallback and the proxy never executes client functions. Streaming sends function events only after the entire upstream answer has passed validation.

Function tool_choice supports auto, required, none, and {"type":"function","name":"..."}. parallel_tool_calls: false limits the result to one call. Function parameter schemas, including strict: true, are checked locally; this does not provide native constrained generation. Send each result as function_call_output with the returned call_id. When tool definitions are omitted on a stored continuation, the proxy preserves the function context but uses tool_choice: "none"; resend the definitions to enable more calls.

Function loop with the official Python SDK

Set PPLX_PROXY_BASE_URL to your deployed proxy, including /v1 (for this deployment, https://pplx.0ruka.dev/v1). Use the existing proxy API key. Function calling remains experimental; clients must handle explicit failures.

import json
import os
from openai import OpenAI

client = OpenAI(
    base_url=os.environ["PPLX_PROXY_BASE_URL"],
    api_key=os.environ["PPLX_PROXY_API_KEY"],
)
tools = [{
    "type": "function", "name": "lookup_inventory",
    "description": "Read the application's current stock count for a SKU.",
    "parameters": {
        "type": "object", "properties": {"sku": {"type": "string"}},
        "required": ["sku"], "additionalProperties": False,
    },
    "strict": True,
}]
first = client.responses.create(
    model="gpt", tools=tools, tool_choice="auto",
    input="Use lookup_inventory to check the current stock count for DEMO.",
)
calls = [item for item in first.output if item.type == "function_call"]
if not calls:
    raise RuntimeError("The model did not request the inventory lookup")
results = []
for call in calls:
    if call.name != "lookup_inventory":
        raise RuntimeError("Unexpected function")
    sku = json.loads(call.arguments)["sku"]
    # Client-side execution; replace this sample data with your own lookup.
    quantity = {"DEMO": 7}[sku]
    results.append({"type": "function_call_output", "call_id": call.call_id,
                    "output": json.dumps({"sku": sku, "quantity": quantity})})
for event in client.responses.create(
    model="gpt", previous_response_id=first.id, input=results, stream=True,
):
    if event.type == "response.output_text.delta":
        print(event.delta, end="", flush=True)
    elif event.type == "response.failed":
        raise RuntimeError(event.response.error)

Image and file input

A real PNG input has passed an official SDK test. Use a Responses input_image content part alongside input_text. /v1/files accepts multipart file and purpose fields and returns a local file_id; its read/delete endpoints manage those stored bytes. Limits are 20 MiB per file and 200 MiB total local storage.

Document upload and model-readable document input are different steps. Document processing must finish through Perplexity's /rest/sse/attachment_processing/subscribe endpoint before a query can use it. That endpoint currently receives Cloudflare 403, so document input is not working and returns an explicit 502. Do not treat successful local file upload as a successful document-reading test.

OpenAI API

# Basic chat
curl -X POST http://localhost:8892/v1/chat/completions \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "sonnet", "messages": [{"role": "user", "content": "Hello"}], "stream": true}'

# With thinking
curl -X POST http://localhost:8892/v1/chat/completions \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt", "messages": [{"role": "user", "content": "Analyze X"}], "thinking": true}'

Debug Chat UI

Open http://localhost:8892/chat (or https://your-domain/chat) in a browser:

  • Toggle Test unsupported tools to verify that function-tool requests produce a visible error
  • Toggle thinking to test reasoning mode
  • Toggle stream for streaming vs non-streaming
  • Raw tab: shows full request/response JSON
  • Format ✓ tab: validates every response field against the OpenAI spec with PASS/FAIL badges

MCP

The API key is part of the URL path for MCP authentication:

# Claude Code
claude mcp add pplx-proxy --transport http http://localhost:8892/YOUR_API_KEY/mcp

# SSE transport
# Connect to http://localhost:8892/YOUR_API_KEY/sse

Without PPLX_PROXY_API_KEY set, MCP falls back to unauthenticated /mcp/mcp and /sse/sse.

MCP Tools:

Tool Description
perplexity_search Pro Search with model/source selection
perplexity_ask Quick auto-mode Q&A
perplexity_reason Reasoning with model selection
perplexity_research Deep Research
perplexity_models List available models for your tier

OpenAI Format Compliance

All responses strictly match the OpenAI Chat Completions API spec:

  • id (chatcmpl-*), object, created, model, system_fingerprint (null)
  • choices[].index, choices[].logprobs (null), choices[].finish_reason
  • usage.total_tokens = prompt_tokens + completion_tokens
  • Successful streaming: consistent id, system_fingerprint in every chunk, proper [DONE] termination

Use /chat to visually verify — the Format ✓ tab runs 20+ checks per response.

Auto-Discovery

Every PROBE_INTERVAL_HOURS (default 24h), pplx-proxy checks if models are still alive. If one dies, it increments the version number (e.g., gpt54 → gpt55 → ... up to +1.0, capped at 10 probes) and auto-upgrades. It also probes known new model names that are missing from a persisted .models.json, so newly added families such as Grok, Haiku, Flash, Mini, and Nano can be added instead of only version-bumping old IDs. Thinking variants are auto-derived from _THINKING_MAP.

Manual trigger: POST /admin/discover-models

Configuration

Variable Default Description
PPLX_COOKIE — Session token (required)
PPLX_PROXY_API_KEY — Bearer auth (empty = no auth)
ACCOUNT_TYPE pro free, pro, or max
DEFAULT_MODEL gpt Default when not specified
PPLX_PROXY_PORT 8892 Listen port
DATA_DIR . Runtime file directory for .cookie_cache.json, .models.json, and .responses_store.json
CUSTOM_PROMPTS file Local prompt block prepended to every LobeHub request
KEEPALIVE_HOURS 6 Session ping interval
PROBE_INTERVAL_HOURS 24 Auto-discovery interval
FLARESOLVERR_URL http://localhost:8191 FlareSolverr endpoint for /health quota data
NTFY_TOPIC disabled Unique ntfy.sh topic: maximum 64 characters using letters, numbers, _, or -; the shared public pplx-proxy name is rejected
NTFY_URL https://ntfy.sh ntfy server URL
NTFY_COOLDOWN_SECS 3600 Min interval between alerts
PUBLIC_URL http://localhost:8892 URL in ntfy messages
PPLX_API_VERSION 2.18 Perplexity internal API ver
PPLX_IMPERSONATE chrome curl_cffi TLS fingerprint
USER_AGENT Chrome/130 HTTP User-Agent
COOKIE_MAX_AGE_HOURS 168 Cookie cache max age
LOG_LEVEL INFO Logging level

On macOS, copy the configured private topic without exposing it or adding a trailing newline:

ssh YOUR_PI 'value=$(sed -n "s/^NTFY_TOPIC=//p" ~/pplx-proxy/.env); printf %s "$value"' | pbcopy

Deployment (systemd)

sudo cp pplx-proxy.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now pplx-proxy

Cookie Lifecycle

Manual inject → validate at startup and every 6h → persist returned cookie rotation → restart uses the latest session
                                      ↓ (if Perplexity force-revokes)
                                      ntfy alert → manual re-inject

Re-inject without SSH:

curl -X POST https://your-domain/admin/refresh-cookie \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"session_token": "NEW_TOKEN"}'

Critical Implementation Notes

Why models say "I can't access real-time data": This proxy must handle three issues that cause Perplexity models to ignore their own search results:

  1. search_focus: "internet" must be set in every request. Without it, Perplexity defaults to "writing" mode where models don't incorporate search results. This is the single most important parameter.

  2. System prompts must be stripped or replaced before sending to Perplexity. Perplexity searches ALL query text — if the system prompt says "You are an AI assistant", Perplexity finds chatbot tutorial pages and the model gets confused. Generic clients keep only whitelist-approved lines; LobeHub requests discard upstream prompt content entirely.

  3. LobeHub requests always prepend local CUSTOM_PROMPTS. The proxy still detects role: developer and system-prompt-like user messages so it can classify the request source, but those upstream prompt blocks are never forwarded. Each LobeHub turn sends instructions=[CUSTOM_PROMPTS] plus preserved history and current query.

  4. Rate limit tracking uses FlareSolverr (FLARESOLVERR_URL, default http://localhost:8191) to poll Perplexity's /rest/rate-limit/all endpoint with the session cookie. Requires FlareSolverr for quota fields in /health. When remaining_pro reaches 0, non-auto requests receive HTTP 429; select auto explicitly to use the free model.

See AGENTS.md for the full technical breakdown and MANUAL.md troubleshooting section for diagnosis steps.

Disclaimer

Unofficial reverse proxy for personal use. Relies on Perplexity's internal web API which may change without notice. Use responsibly.

License

MIT

Verification and session updates

Run ./test.sh [base-url] with the project's Python environment installed. It validates health, models, complete chat responses, streaming completion, and Responses output. HTTP errors, malformed output, and interrupted streams cause a nonzero exit status.

Run node --test test_chat.js for debug-page protocol checks.

Run ./inject_cookie.sh for a hidden token prompt, or pipe a token on standard input. The helper reads the project .env, calls the running local service, and only replaces the session after validation. It does not restart the service. Avoid placing tokens in shell command arguments or history.

Requests exceeding the 96,000-character query limit receive an explicit error; the proxy does not silently truncate conversation content. /health reports cached quota immediately and schedules a refresh when needed. A successful chat stream ends with [DONE]; a failed stream emits an error instead of reporting success.

Chat Completions rejects unsupported generation controls with HTTP 400, including custom sampling, output-token limits, structured response formats, multiple choices, stop sequences, nondefault penalties, seeds, logprobs, and streaming usage requests. These options are not silently ignored.