fix(providers): perplexity import filters to Sonar-family models (#11060)

This commit is contained in:
Xiangzhe
2026-08-22 13:13:32 -03:00
parent 6cd4d38e21
commit b4d7433253
3 changed files with 91 additions and 0 deletions

View File

@@ -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)

View File

@@ -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<string, ProviderModelsConfigEntry> =
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",

View File

@@ -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({}), []);
});