使用说明
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, properusagearithmetic, all fields per spec - Thinking/reasoning —
thinking: trueorreasoning_effortparam, reasoning streamed asreasoning_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_uuidfor 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 —
/chatpage 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-proxyonhttp://localhost:8892flaresolverrinside the Compose network athttp://flaresolverr:8191pplx-datavolume 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
- Log in to perplexity.ai
- F12 → Application → Cookies →
www.perplexity.ai - Copy
__Secure-next-auth.session-token - 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_reasonusage.total_tokens=prompt_tokens+completion_tokens- Successful streaming: consistent
id,system_fingerprintin 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:
-
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. -
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.
-
LobeHub requests always prepend local
CUSTOM_PROMPTS. The proxy still detectsrole: developerand system-prompt-like user messages so it can classify the request source, but those upstream prompt blocks are never forwarded. Each LobeHub turn sendsinstructions=[CUSTOM_PROMPTS]plus preservedhistoryand currentquery. -
Rate limit tracking uses FlareSolverr (
FLARESOLVERR_URL, defaulthttp://localhost:8191) to poll Perplexity's/rest/rate-limit/allendpoint with the session cookie. Requires FlareSolverr for quota fields in/health. Whenremaining_proreaches 0, non-auto requests receive HTTP 429; selectautoexplicitly 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.