feat(web_search): add optional model parameter for Perplexity provider
Allows runtime model override without config changes or gateway restart. - Add 'model' parameter to web_search tool schema - Override configured default when parameter is provided - Return error if used with non-Perplexity provider (Brave) Closes #2882
This commit is contained in:
parent
3fe4b2595a
commit
d03e156028
@ -2,8 +2,13 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import { __testing } from "./web-search.js";
|
import { __testing } from "./web-search.js";
|
||||||
|
|
||||||
const { inferPerplexityBaseUrlFromApiKey, resolvePerplexityBaseUrl, normalizeFreshness } =
|
const {
|
||||||
__testing;
|
inferPerplexityBaseUrlFromApiKey,
|
||||||
|
resolvePerplexityBaseUrl,
|
||||||
|
normalizeFreshness,
|
||||||
|
resolvePerplexityRequestParams,
|
||||||
|
buildWebSearchCacheKey,
|
||||||
|
} = __testing;
|
||||||
|
|
||||||
describe("web_search perplexity baseUrl defaults", () => {
|
describe("web_search perplexity baseUrl defaults", () => {
|
||||||
it("detects a Perplexity key prefix", () => {
|
it("detects a Perplexity key prefix", () => {
|
||||||
@ -69,3 +74,88 @@ describe("web_search freshness normalization", () => {
|
|||||||
expect(normalizeFreshness("2024-03-10to2024-03-01")).toBeUndefined();
|
expect(normalizeFreshness("2024-03-10to2024-03-01")).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("web_search model parameter", () => {
|
||||||
|
it("prefixes short model names for OpenRouter", () => {
|
||||||
|
const { effectivePerplexityModel: shortModel } = resolvePerplexityRequestParams({
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
model: "sonar",
|
||||||
|
});
|
||||||
|
expect(shortModel).toBe("perplexity/sonar");
|
||||||
|
|
||||||
|
const { effectivePerplexityModel: prefixedModel } = resolvePerplexityRequestParams({
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
model: "perplexity/sonar-pro",
|
||||||
|
});
|
||||||
|
expect(prefixedModel).toBe("perplexity/sonar-pro");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps short model names for direct Perplexity", () => {
|
||||||
|
const { effectivePerplexityModel } = resolvePerplexityRequestParams({
|
||||||
|
baseUrl: "https://api.perplexity.ai",
|
||||||
|
model: "sonar",
|
||||||
|
});
|
||||||
|
expect(effectivePerplexityModel).toBe("sonar");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("differentiates cache keys by model", () => {
|
||||||
|
const baseParams = {
|
||||||
|
provider: "perplexity" as const,
|
||||||
|
query: "model test",
|
||||||
|
count: 5,
|
||||||
|
perplexityBaseUrl: "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstParams = resolvePerplexityRequestParams({
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
model: "sonar",
|
||||||
|
});
|
||||||
|
const secondParams = resolvePerplexityRequestParams({
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
model: "sonar-pro",
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = buildWebSearchCacheKey({
|
||||||
|
...baseParams,
|
||||||
|
perplexityModel: firstParams.cachePerplexityModel,
|
||||||
|
perplexityBaseUrl: firstParams.cachePerplexityBaseUrl,
|
||||||
|
});
|
||||||
|
const second = buildWebSearchCacheKey({
|
||||||
|
...baseParams,
|
||||||
|
perplexityModel: secondParams.cachePerplexityModel,
|
||||||
|
perplexityBaseUrl: secondParams.cachePerplexityBaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(first).not.toBe(second);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("differentiates cache keys by baseUrl", () => {
|
||||||
|
const baseParams = {
|
||||||
|
provider: "perplexity" as const,
|
||||||
|
query: "base url test",
|
||||||
|
count: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
const openRouterParams = resolvePerplexityRequestParams({
|
||||||
|
baseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
model: "sonar",
|
||||||
|
});
|
||||||
|
const directParams = resolvePerplexityRequestParams({
|
||||||
|
baseUrl: "https://api.perplexity.ai",
|
||||||
|
model: "sonar",
|
||||||
|
});
|
||||||
|
|
||||||
|
const openRouter = buildWebSearchCacheKey({
|
||||||
|
...baseParams,
|
||||||
|
perplexityModel: openRouterParams.cachePerplexityModel,
|
||||||
|
perplexityBaseUrl: openRouterParams.cachePerplexityBaseUrl,
|
||||||
|
});
|
||||||
|
const direct = buildWebSearchCacheKey({
|
||||||
|
...baseParams,
|
||||||
|
perplexityModel: directParams.cachePerplexityModel,
|
||||||
|
perplexityBaseUrl: directParams.cachePerplexityBaseUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(openRouter).not.toBe(direct);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -63,6 +63,12 @@ const WebSearchSchema = Type.Object({
|
|||||||
"Filter results by discovery time (Brave only). Values: 'pd' (past 24h), 'pw' (past week), 'pm' (past month), 'py' (past year), or date range 'YYYY-MM-DDtoYYYY-MM-DD'.",
|
"Filter results by discovery time (Brave only). Values: 'pd' (past 24h), 'pw' (past week), 'pm' (past month), 'py' (past year), or date range 'YYYY-MM-DDtoYYYY-MM-DD'.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
model: Type.Optional(
|
||||||
|
Type.String({
|
||||||
|
description:
|
||||||
|
"Perplexity model to use (Perplexity provider only). Overrides the configured default. Examples: 'sonar', 'sonar-pro', 'sonar-reasoning'.",
|
||||||
|
}),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
type WebSearchConfig = NonNullable<MoltbotConfig["tools"]>["web"] extends infer Web
|
type WebSearchConfig = NonNullable<MoltbotConfig["tools"]>["web"] extends infer Web
|
||||||
@ -265,6 +271,62 @@ function resolveSiteName(url: string | undefined): string | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePerplexityRequestParams(params: { baseUrl?: string; model?: string }): {
|
||||||
|
effectivePerplexityBaseUrl: string;
|
||||||
|
effectivePerplexityModel: string;
|
||||||
|
cachePerplexityBaseUrl: string;
|
||||||
|
cachePerplexityModel: string;
|
||||||
|
} {
|
||||||
|
const normalizedPerplexityModel = typeof params.model === "string" ? params.model.trim() : "";
|
||||||
|
|
||||||
|
const normalizedPerplexityBaseUrlRaw =
|
||||||
|
typeof params.baseUrl === "string" ? params.baseUrl.trim() : "";
|
||||||
|
const normalizedPerplexityBaseUrl = normalizedPerplexityBaseUrlRaw.replace(/\/+$/, "");
|
||||||
|
const effectivePerplexityBaseUrl = normalizedPerplexityBaseUrl || DEFAULT_PERPLEXITY_BASE_URL;
|
||||||
|
const cachePerplexityBaseUrl =
|
||||||
|
!normalizedPerplexityBaseUrl || normalizedPerplexityBaseUrl === DEFAULT_PERPLEXITY_BASE_URL
|
||||||
|
? "default"
|
||||||
|
: normalizedPerplexityBaseUrl;
|
||||||
|
const isOpenRouterBaseUrl = effectivePerplexityBaseUrl === DEFAULT_PERPLEXITY_BASE_URL;
|
||||||
|
const resolvedPerplexityModel = normalizedPerplexityModel || DEFAULT_PERPLEXITY_MODEL;
|
||||||
|
const effectivePerplexityModel =
|
||||||
|
isOpenRouterBaseUrl &&
|
||||||
|
resolvedPerplexityModel &&
|
||||||
|
!resolvedPerplexityModel.startsWith("perplexity/")
|
||||||
|
? `perplexity/${resolvedPerplexityModel}`
|
||||||
|
: resolvedPerplexityModel;
|
||||||
|
const cachePerplexityModel =
|
||||||
|
!normalizedPerplexityModel || effectivePerplexityModel === DEFAULT_PERPLEXITY_MODEL
|
||||||
|
? "default"
|
||||||
|
: effectivePerplexityModel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
effectivePerplexityBaseUrl,
|
||||||
|
effectivePerplexityModel,
|
||||||
|
cachePerplexityBaseUrl,
|
||||||
|
cachePerplexityModel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWebSearchCacheKey(params: {
|
||||||
|
provider: (typeof SEARCH_PROVIDERS)[number];
|
||||||
|
query: string;
|
||||||
|
count: number;
|
||||||
|
country?: string;
|
||||||
|
search_lang?: string;
|
||||||
|
ui_lang?: string;
|
||||||
|
freshness?: string;
|
||||||
|
perplexityBaseUrl?: string;
|
||||||
|
perplexityModel?: string;
|
||||||
|
}): string {
|
||||||
|
const base = `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}`;
|
||||||
|
const suffix =
|
||||||
|
params.provider === "brave"
|
||||||
|
? `${params.freshness || "default"}`
|
||||||
|
: `${params.perplexityModel || "default"}:${params.perplexityBaseUrl || "default"}`;
|
||||||
|
return normalizeCacheKey(`${base}:${suffix}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function runPerplexitySearch(params: {
|
async function runPerplexitySearch(params: {
|
||||||
query: string;
|
query: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@ -320,11 +382,27 @@ async function runWebSearch(params: {
|
|||||||
perplexityBaseUrl?: string;
|
perplexityBaseUrl?: string;
|
||||||
perplexityModel?: string;
|
perplexityModel?: string;
|
||||||
}): Promise<Record<string, unknown>> {
|
}): Promise<Record<string, unknown>> {
|
||||||
const cacheKey = normalizeCacheKey(
|
const {
|
||||||
params.provider === "brave"
|
effectivePerplexityBaseUrl,
|
||||||
? `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}`
|
effectivePerplexityModel,
|
||||||
: `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}`,
|
cachePerplexityBaseUrl,
|
||||||
);
|
cachePerplexityModel,
|
||||||
|
} = resolvePerplexityRequestParams({
|
||||||
|
baseUrl: params.perplexityBaseUrl,
|
||||||
|
model: params.perplexityModel,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheKey = buildWebSearchCacheKey({
|
||||||
|
provider: params.provider,
|
||||||
|
query: params.query,
|
||||||
|
count: params.count,
|
||||||
|
country: params.country,
|
||||||
|
search_lang: params.search_lang,
|
||||||
|
ui_lang: params.ui_lang,
|
||||||
|
freshness: params.freshness,
|
||||||
|
perplexityBaseUrl: cachePerplexityBaseUrl,
|
||||||
|
perplexityModel: cachePerplexityModel,
|
||||||
|
});
|
||||||
const cached = readCache(SEARCH_CACHE, cacheKey);
|
const cached = readCache(SEARCH_CACHE, cacheKey);
|
||||||
if (cached) return { ...cached.value, cached: true };
|
if (cached) return { ...cached.value, cached: true };
|
||||||
|
|
||||||
@ -334,15 +412,15 @@ async function runWebSearch(params: {
|
|||||||
const { content, citations } = await runPerplexitySearch({
|
const { content, citations } = await runPerplexitySearch({
|
||||||
query: params.query,
|
query: params.query,
|
||||||
apiKey: params.apiKey,
|
apiKey: params.apiKey,
|
||||||
baseUrl: params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL,
|
baseUrl: effectivePerplexityBaseUrl,
|
||||||
model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL,
|
model: effectivePerplexityModel,
|
||||||
timeoutSeconds: params.timeoutSeconds,
|
timeoutSeconds: params.timeoutSeconds,
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
query: params.query,
|
query: params.query,
|
||||||
provider: params.provider,
|
provider: params.provider,
|
||||||
model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL,
|
model: effectivePerplexityModel,
|
||||||
tookMs: Date.now() - start,
|
tookMs: Date.now() - start,
|
||||||
content,
|
content,
|
||||||
citations,
|
citations,
|
||||||
@ -450,6 +528,14 @@ export function createWebSearchTool(options?: {
|
|||||||
docs: "https://docs.molt.bot/tools/web",
|
docs: "https://docs.molt.bot/tools/web",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const modelOverride = readStringParam(params, "model");
|
||||||
|
if (modelOverride && provider !== "perplexity") {
|
||||||
|
return jsonResult({
|
||||||
|
error: "unsupported_model_override",
|
||||||
|
message: "model parameter is only supported by the Perplexity web_search provider.",
|
||||||
|
docs: "https://docs.molt.bot/tools/web",
|
||||||
|
});
|
||||||
|
}
|
||||||
const freshness = rawFreshness ? normalizeFreshness(rawFreshness) : undefined;
|
const freshness = rawFreshness ? normalizeFreshness(rawFreshness) : undefined;
|
||||||
if (rawFreshness && !freshness) {
|
if (rawFreshness && !freshness) {
|
||||||
return jsonResult({
|
return jsonResult({
|
||||||
@ -475,7 +561,7 @@ export function createWebSearchTool(options?: {
|
|||||||
perplexityAuth?.source,
|
perplexityAuth?.source,
|
||||||
perplexityAuth?.apiKey,
|
perplexityAuth?.apiKey,
|
||||||
),
|
),
|
||||||
perplexityModel: resolvePerplexityModel(perplexityConfig),
|
perplexityModel: modelOverride || resolvePerplexityModel(perplexityConfig),
|
||||||
});
|
});
|
||||||
return jsonResult(result);
|
return jsonResult(result);
|
||||||
},
|
},
|
||||||
@ -486,4 +572,6 @@ export const __testing = {
|
|||||||
inferPerplexityBaseUrlFromApiKey,
|
inferPerplexityBaseUrlFromApiKey,
|
||||||
resolvePerplexityBaseUrl,
|
resolvePerplexityBaseUrl,
|
||||||
normalizeFreshness,
|
normalizeFreshness,
|
||||||
|
resolvePerplexityRequestParams,
|
||||||
|
buildWebSearchCacheKey,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user