From 9629693a3c2ca5fbdd824e69ebfde5a6f85f51bb Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sat, 22 Aug 2026 14:28:14 -0300 Subject: [PATCH] fix(providers): filter Perplexity model import to the Sonar family (#11060) Inherited base-red at merge time (discriminated against the pure base tip, both reproduce WITHOUT this diff): getTokenLimit test + Vietnamese i18n key parity (new UI strings merged untranslated) + No new ESLint warnings gate. Merge integrity, Docs Gates, Vitest, Fast Production Build: green. --- changelog.d/fixes/11060-perplexity-filter.md | 1 + skills/omni-webhooks/SKILL.md | 4 +- .../models/discovery/providerModelsConfig.ts | 27 ++++++++ .../unit/perplexity-discovery-filter.test.ts | 63 +++++++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/11060-perplexity-filter.md create mode 100644 tests/unit/perplexity-discovery-filter.test.ts diff --git a/changelog.d/fixes/11060-perplexity-filter.md b/changelog.d/fixes/11060-perplexity-filter.md new file mode 100644 index 0000000000..c221d3ccab --- /dev/null +++ b/changelog.d/fixes/11060-perplexity-filter.md @@ -0,0 +1 @@ +- fix(providers): filter Perplexity model import to the Sonar family so Agent-API catalog ids stop surfacing as routable chat models (#11060) diff --git a/skills/omni-webhooks/SKILL.md b/skills/omni-webhooks/SKILL.md index 251b5f5817..c60df46eca 100644 --- a/skills/omni-webhooks/SKILL.md +++ b/skills/omni-webhooks/SKILL.md @@ -1,12 +1,12 @@ --- name: omni-webhooks -description: Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries. +description: Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries. --- ## Overview -Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries. +Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, request.failed, quota.exceeded, etc.) and manage delivery retries. ## Authentication diff --git a/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts b/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts index 62988798d7..0939b59b55 100644 --- a/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts +++ b/src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts @@ -87,6 +87,22 @@ export function parseAlibabaModelStudioModelsForConnection( export function parseQwenCloudTextModels(data: any): any[] { return parseCuratedDashscopeModels(data, QWEN_CLOUD_TEXT_MODELS, QWEN_CLOUD_TEXT_MODEL_IDS); } + +// Perplexity's /v1/models lists the Agent API catalog (vendor-prefixed ids like +// "anthropic/claude-fable-5"), but chat requests always go to the classic +// /chat/completions endpoint, which only accepts the Sonar family. Filter +// discovery to Sonar-family ids so agent-style ids never surface as routable +// chat models (#11060). Bounded pattern — no ReDoS-prone quantifiers. +export function parsePerplexitySonarModels(data: any): any[] { + const models = Array.isArray(data?.data) + ? data.data + : Array.isArray(data?.models) + ? data.models + : []; + return models.filter( + (model: any) => typeof model?.id === "string" && /^sonar(-|$)/.test(model.id) + ); +} type ProviderModelsHeaderContext = { authType?: string; providerSpecificData?: unknown; @@ -659,6 +675,17 @@ export const PROVIDER_MODELS_CONFIG: Record = headers: { Accept: "application/json" }, parseResponse: parseClinepassRecommendedModels, }, + // Perplexity's /v1/models lists the Agent API catalog (vendor-prefixed agent + // ids), but chat only accepts the Sonar family on /chat/completions. Import + // must keep Sonar-family ids only (#11060). + perplexity: { + url: "https://api.perplexity.ai/v1/models", + method: "GET", + headers: { "Content-Type": "application/json" }, + authHeader: "Authorization", + authPrefix: "Bearer ", + parseResponse: parsePerplexitySonarModels, + }, cohere: { url: "https://api.cohere.com/v2/models", method: "GET", diff --git a/tests/unit/perplexity-discovery-filter.test.ts b/tests/unit/perplexity-discovery-filter.test.ts new file mode 100644 index 0000000000..8533646746 --- /dev/null +++ b/tests/unit/perplexity-discovery-filter.test.ts @@ -0,0 +1,63 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { PROVIDER_MODELS_CONFIG } from "../../src/app/api/providers/[id]/models/discovery/providerModelsConfig.ts"; + +// Regression guard for #11060 — Perplexity's /v1/models endpoint lists the +// Agent API catalog (vendor-prefixed ids like "anthropic/claude-fable-5"), but +// chat requests always go to the classic /chat/completions endpoint, which only +// accepts the Sonar family. Without a PROVIDER_MODELS_CONFIG entry, generic +// model import pulled those agent-style ids into the connection's chat model +// list and every routed request failed with 400 "Invalid model". The discovery +// entry must exist and its parseResponse must keep only Sonar-family ids. + +test("perplexity has a discovery entry in PROVIDER_MODELS_CONFIG", () => { + const cfg = PROVIDER_MODELS_CONFIG.perplexity; + assert.ok(cfg, "expected a perplexity entry in PROVIDER_MODELS_CONFIG"); + assert.equal(cfg.method, "GET"); + assert.equal(cfg.url, "https://api.perplexity.ai/v1/models"); + assert.equal(typeof cfg.parseResponse, "function"); +}); + +test("perplexity parseResponse keeps only the Sonar family (#11060)", () => { + const cfg = PROVIDER_MODELS_CONFIG.perplexity; + const models = cfg.parseResponse({ + object: "list", + data: [ + { id: "anthropic/claude-fable-5", object: "model", owned_by: "anthropic" }, + { id: "sonar-pro", object: "model", owned_by: "perplexity" }, + { id: "sonar", object: "model", owned_by: "perplexity" }, + ], + }) as Array<{ id: string }>; + + assert.deepEqual( + models.map((model) => model.id), + ["sonar-pro", "sonar"] + ); +}); + +test("perplexity parseResponse keeps every Sonar variant and drops non-Sonar ids", () => { + const cfg = PROVIDER_MODELS_CONFIG.perplexity; + const models = cfg.parseResponse({ + data: [ + { id: "sonar-deep-research" }, + { id: "sonar-reasoning-pro" }, + { id: "sonar-pro" }, + { id: "sonar" }, + { id: "openai/gpt-5" }, + { id: "sonarish" }, + ], + }) as Array<{ id: string }>; + + assert.deepEqual( + models.map((model) => model.id), + ["sonar-deep-research", "sonar-reasoning-pro", "sonar-pro", "sonar"] + ); +}); + +test("perplexity parseResponse tolerates empty and malformed payloads", () => { + const cfg = PROVIDER_MODELS_CONFIG.perplexity; + assert.deepEqual(cfg.parseResponse({ data: [] }), []); + assert.deepEqual(cfg.parseResponse(undefined), []); + assert.deepEqual(cfg.parseResponse({}), []); +});