diff --git a/.env.example b/.env.example index 78d1bd5acf..6a443a9c2d 100644 --- a/.env.example +++ b/.env.example @@ -1415,6 +1415,14 @@ CURSOR_USER_AGENT="Cursor/3.4" # OMNIROUTE_PPLX_TLS_TIMEOUT_MS=30000 # OMNIROUTE_PPLX_TLS_GRACE_MS=10000 +# ── Perplexity web: built-in-search hint ── +# Used by: open-sse/executors/perplexity-web/protocol.ts — appends "You have +# built-in web search. Answer questions directly using search results." to the +# caller's system message. Off by default: Perplexity's answer engine searches +# anyway, and for coding clients the sentence leaks into replies as +# meta-commentary. Set to 1/true/yes/on to restore the old behavior. +# OMNIROUTE_PPLX_SEARCH_HINT=0 + # ── Grok web TLS sidecar (Chrome-fingerprinted client) ── # Used by: open-sse/services/grokTlsClient.ts — wire-level timeout for the # bogdanfinn/tls-client koffi binding and the JS-side grace window layered on diff --git a/changelog.d/fixes/10902-pplx-search-hint-optin.md b/changelog.d/fixes/10902-pplx-search-hint-optin.md new file mode 100644 index 0000000000..233fb61f19 --- /dev/null +++ b/changelog.d/fixes/10902-pplx-search-hint-optin.md @@ -0,0 +1 @@ +- **fix(perplexity-web):** make the built-in-search hint appended to every system message opt-in via `OMNIROUTE_PPLX_SEARCH_HINT` (off by default) — Perplexity's answer engine searches anyway, and the hint leaked into replies as meta-commentary for coding clients ([#10902](https://github.com/diegosouzapw/OmniRoute/pull/10902), extracted from [#8634](https://github.com/diegosouzapw/OmniRoute/pull/8634)) — thanks @danscMax diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index bf3fbd68d2..3d0e74ff32 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -755,6 +755,7 @@ REQUEST_TIMEOUT_MS (global override) | `OMNIROUTE_CLAUDE_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | | `OMNIROUTE_PPLX_TLS_TIMEOUT_MS` | `30000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`perplexityTlsClient.ts`). | | `OMNIROUTE_PPLX_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | +| `OMNIROUTE_PPLX_SEARCH_HINT` | `0` (off) | Appends "You have built-in web search. Answer questions directly using search results." to the caller's system message (`perplexity-web/protocol.ts`). Off by default — Perplexity searches anyway, and the sentence leaks into replies as meta-commentary for coding clients. Set `1`/`true`/`yes`/`on` to restore. | | `OMNIROUTE_GROK_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`grokTlsClient.ts`). | | `OMNIROUTE_GROK_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | | `OMNIROUTE_NOTION_TLS_TIMEOUT_MS` | `30000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`notionTlsClient.ts`); the `notion-web` executor raises it per-request to `180000` for long generations. | diff --git a/open-sse/executors/perplexity-web/protocol.ts b/open-sse/executors/perplexity-web/protocol.ts index f98680c4ff..12e98ccdc4 100644 --- a/open-sse/executors/perplexity-web/protocol.ts +++ b/open-sse/executors/perplexity-web/protocol.ts @@ -370,15 +370,29 @@ export function buildPplxRequestBody( }; } +const SEARCH_HINT = "You have built-in web search. Answer questions directly using search results."; + +/** + * Whether to append {@link SEARCH_HINT} to the caller's system message. + * + * It used to be unconditional. Perplexity's answer engine is search-first anyway, and + * for coding clients the sentence leaks into replies as meta-commentary ("I need to + * search before responding per my instructions"), so it is now opt-in via + * `OMNIROUTE_PPLX_SEARCH_HINT`. Read per call rather than at module load so the flag + * can be flipped without restarting the server (and so tests can toggle it). + */ +function searchHintEnabled(): boolean { + return /^(1|true|yes|on)$/i.test(process.env.OMNIROUTE_PPLX_SEARCH_HINT ?? ""); +} + export function buildQuery(parsed: ParsedMessages, followUpUuid: string | null): string { if (followUpUuid) return parsed.currentMsg; const obj: Record = {}; if (parsed.systemMsg.trim()) { - obj.instructions = [ - parsed.systemMsg.trim(), - "You have built-in web search. Answer questions directly using search results.", - ]; + obj.instructions = searchHintEnabled() + ? [parsed.systemMsg.trim(), SEARCH_HINT] + : [parsed.systemMsg.trim()]; } if (parsed.history.length > 0) { obj.history = parsed.history; diff --git a/tests/unit/perplexity-web.test.ts b/tests/unit/perplexity-web.test.ts index eec23be974..f6f4b7f7a7 100644 --- a/tests/unit/perplexity-web.test.ts +++ b/tests/unit/perplexity-web.test.ts @@ -912,6 +912,35 @@ test("Model mapping: thinking mode uses thinking variant", async () => { } }); +// ─── The search hint is opt-in ────────────────────────────────────────────── +// It used to be appended to every system message and leaked into answers as +// meta-commentary, which is noise for coding clients. + +test("buildQuery: search hint is off by default and opt-in via env", async () => { + const { buildQuery } = await import("../../open-sse/executors/perplexity-web/protocol.ts"); + const parsed = { systemMsg: "You are terse.", history: [], currentMsg: "hi" }; + const HINT = "built-in web search"; + const prev = process.env.OMNIROUTE_PPLX_SEARCH_HINT; + + try { + delete process.env.OMNIROUTE_PPLX_SEARCH_HINT; + const off = JSON.parse(buildQuery(parsed, null)); + assert.deepEqual(off.instructions, ["You are terse."]); + assert.equal(off.query, "hi"); + + process.env.OMNIROUTE_PPLX_SEARCH_HINT = "1"; + const on = JSON.parse(buildQuery(parsed, null)); + assert.equal(on.instructions.length, 2); + assert.ok(on.instructions[1].includes(HINT)); + + process.env.OMNIROUTE_PPLX_SEARCH_HINT = "0"; + assert.equal(JSON.parse(buildQuery(parsed, null)).instructions.length, 1); + } finally { + if (prev === undefined) delete process.env.OMNIROUTE_PPLX_SEARCH_HINT; + else process.env.OMNIROUTE_PPLX_SEARCH_HINT = prev; + } +}); + // ─── Test: Live multi-step stream (no COMPLETED; text_completed + diffs) ──── test("Live multi-step: reconstructs answer without status COMPLETED", async () => {