2026-03-17 22:21:44 -05:00
import { Type } from "@sinclair/typebox" ;
import {
readNumberParam ,
readStringArrayParam ,
readStringParam ,
2026-03-18 05:26:19 +00:00
} from "openclaw/plugin-sdk/provider-web-search" ;
import {
2026-03-17 22:21:44 -05:00
buildSearchCacheKey ,
DEFAULT_SEARCH_COUNT ,
MAX_SEARCH_COUNT ,
isoToPerplexityDate ,
normalizeFreshness ,
normalizeToIsoDate ,
readCachedSearchPayload ,
readConfiguredSecretString ,
readProviderEnvValue ,
2026-03-18 07:39:49 +00:00
resolveProviderWebSearchPluginConfig ,
2026-03-17 22:21:44 -05:00
resolveSearchCacheTtlMs ,
resolveSearchCount ,
resolveSearchTimeoutSeconds ,
resolveSiteName ,
2026-03-18 00:24:30 -05:00
setProviderWebSearchPluginConfigValue ,
2026-03-17 22:21:44 -05:00
throwWebSearchApiError ,
2026-03-18 00:24:30 -05:00
type SearchConfigRecord ,
type WebSearchCredentialResolutionSource ,
type WebSearchProviderPlugin ,
type WebSearchProviderToolDefinition ,
2026-03-17 22:21:44 -05:00
withTrustedWebSearchEndpoint ,
2026-03-18 00:24:30 -05:00
wrapWebContent ,
2026-03-17 22:21:44 -05:00
writeCachedSearchPayload ,
2026-03-18 00:24:30 -05:00
} from "openclaw/plugin-sdk/provider-web-search" ;
2026-03-17 22:21:44 -05:00
const DEFAULT_PERPLEXITY_BASE_URL = "https://openrouter.ai/api/v1" ;
const PERPLEXITY_DIRECT_BASE_URL = "https://api.perplexity.ai" ;
const PERPLEXITY_SEARCH_ENDPOINT = "https://api.perplexity.ai/search" ;
const DEFAULT_PERPLEXITY_MODEL = "perplexity/sonar-pro" ;
const PERPLEXITY_KEY_PREFIXES = [ "pplx-" ] ;
const OPENROUTER_KEY_PREFIXES = [ "sk-or-" ] ;
type PerplexityConfig = {
apiKey? : string ;
baseUrl? : string ;
model? : string ;
} ;
type PerplexityTransport = "search_api" | "chat_completions" ;
type PerplexityBaseUrlHint = "direct" | "openrouter" ;
type PerplexitySearchResponse = {
choices? : Array < {
message ? : {
content? : string ;
annotations? : Array < {
type ? : string ;
url? : string ;
url_citation ? : {
url? : string ;
} ;
} > ;
} ;
} > ;
citations? : string [ ] ;
} ;
type PerplexitySearchApiResponse = {
results? : Array < {
title? : string ;
url? : string ;
snippet? : string ;
date? : string ;
} > ;
} ;
2026-03-18 05:26:19 +00:00
function resolvePerplexityConfig ( searchConfig? : SearchConfigRecord ) : PerplexityConfig {
const perplexity = searchConfig ? . perplexity ;
2026-03-17 22:21:44 -05:00
return perplexity && typeof perplexity === "object" && ! Array . isArray ( perplexity )
? ( perplexity as PerplexityConfig )
: { } ;
}
function inferPerplexityBaseUrlFromApiKey ( apiKey? : string ) : PerplexityBaseUrlHint | undefined {
if ( ! apiKey ) {
return undefined ;
}
const normalized = apiKey . toLowerCase ( ) ;
if ( PERPLEXITY_KEY_PREFIXES . some ( ( prefix ) = > normalized . startsWith ( prefix ) ) ) {
return "direct" ;
}
if ( OPENROUTER_KEY_PREFIXES . some ( ( prefix ) = > normalized . startsWith ( prefix ) ) ) {
return "openrouter" ;
}
return undefined ;
}
function resolvePerplexityApiKey ( perplexity? : PerplexityConfig ) : {
apiKey? : string ;
source : "config" | "perplexity_env" | "openrouter_env" | "none" ;
} {
const fromConfig = readConfiguredSecretString (
perplexity ? . apiKey ,
2026-03-18 05:26:19 +00:00
"tools.web.search.perplexity.apiKey" ,
2026-03-17 22:21:44 -05:00
) ;
if ( fromConfig ) {
return { apiKey : fromConfig , source : "config" } ;
}
const fromPerplexityEnv = readProviderEnvValue ( [ "PERPLEXITY_API_KEY" ] ) ;
if ( fromPerplexityEnv ) {
return { apiKey : fromPerplexityEnv , source : "perplexity_env" } ;
}
const fromOpenRouterEnv = readProviderEnvValue ( [ "OPENROUTER_API_KEY" ] ) ;
if ( fromOpenRouterEnv ) {
return { apiKey : fromOpenRouterEnv , source : "openrouter_env" } ;
}
return { apiKey : undefined , source : "none" } ;
}
function resolvePerplexityBaseUrl (
perplexity? : PerplexityConfig ,
authSource : "config" | "perplexity_env" | "openrouter_env" | "none" = "none" ,
configuredKey? : string ,
) : string {
const fromConfig = typeof perplexity ? . baseUrl === "string" ? perplexity . baseUrl . trim ( ) : "" ;
if ( fromConfig ) {
return fromConfig ;
}
if ( authSource === "perplexity_env" ) {
return PERPLEXITY_DIRECT_BASE_URL ;
}
if ( authSource === "openrouter_env" ) {
return DEFAULT_PERPLEXITY_BASE_URL ;
}
if ( authSource === "config" ) {
return inferPerplexityBaseUrlFromApiKey ( configuredKey ) === "openrouter"
? DEFAULT_PERPLEXITY_BASE_URL
: PERPLEXITY_DIRECT_BASE_URL ;
}
return DEFAULT_PERPLEXITY_BASE_URL ;
}
function resolvePerplexityModel ( perplexity? : PerplexityConfig ) : string {
const model = typeof perplexity ? . model === "string" ? perplexity . model . trim ( ) : "" ;
return model || DEFAULT_PERPLEXITY_MODEL ;
}
function isDirectPerplexityBaseUrl ( baseUrl : string ) : boolean {
try {
return new URL ( baseUrl . trim ( ) ) . hostname . toLowerCase ( ) === "api.perplexity.ai" ;
} catch {
return false ;
}
}
function resolvePerplexityRequestModel ( baseUrl : string , model : string ) : string {
if ( ! isDirectPerplexityBaseUrl ( baseUrl ) ) {
return model ;
}
return model . startsWith ( "perplexity/" ) ? model . slice ( "perplexity/" . length ) : model ;
}
function resolvePerplexityTransport ( perplexity? : PerplexityConfig ) : {
apiKey? : string ;
source : "config" | "perplexity_env" | "openrouter_env" | "none" ;
baseUrl : string ;
model : string ;
transport : PerplexityTransport ;
} {
const auth = resolvePerplexityApiKey ( perplexity ) ;
const baseUrl = resolvePerplexityBaseUrl ( perplexity , auth . source , auth . apiKey ) ;
const model = resolvePerplexityModel ( perplexity ) ;
const hasLegacyOverride = Boolean (
( perplexity ? . baseUrl && perplexity . baseUrl . trim ( ) ) ||
( perplexity ? . model && perplexity . model . trim ( ) ) ,
) ;
return {
. . . auth ,
baseUrl ,
model ,
transport :
hasLegacyOverride || ! isDirectPerplexityBaseUrl ( baseUrl ) ? "chat_completions" : "search_api" ,
} ;
}
function extractPerplexityCitations ( data : PerplexitySearchResponse ) : string [ ] {
const topLevel = ( data . citations ? ? [ ] ) . filter (
( url ) : url is string = > typeof url === "string" && Boolean ( url . trim ( ) ) ,
) ;
if ( topLevel . length > 0 ) {
return [ . . . new Set ( topLevel ) ] ;
}
const citations : string [ ] = [ ] ;
for ( const choice of data . choices ? ? [ ] ) {
for ( const annotation of choice . message ? . annotations ? ? [ ] ) {
if ( annotation . type !== "url_citation" ) {
continue ;
}
const url =
typeof annotation . url_citation ? . url === "string"
? annotation . url_citation . url
: typeof annotation . url === "string"
? annotation . url
: undefined ;
if ( url ? . trim ( ) ) {
citations . push ( url . trim ( ) ) ;
}
}
}
return [ . . . new Set ( citations ) ] ;
}
async function runPerplexitySearchApi ( params : {
query : string ;
apiKey : string ;
count : number ;
timeoutSeconds : number ;
country? : string ;
searchDomainFilter? : string [ ] ;
searchRecencyFilter? : string ;
searchLanguageFilter? : string [ ] ;
searchAfterDate? : string ;
searchBeforeDate? : string ;
maxTokens? : number ;
maxTokensPerPage? : number ;
} ) : Promise < Array < Record < string , unknown > > > {
const body : Record < string , unknown > = {
query : params.query ,
max_results : params.count ,
} ;
if ( params . country ) body . country = params . country ;
if ( params . searchDomainFilter ? . length ) body . search_domain_filter = params . searchDomainFilter ;
if ( params . searchRecencyFilter ) body . search_recency_filter = params . searchRecencyFilter ;
if ( params . searchLanguageFilter ? . length )
body . search_language_filter = params . searchLanguageFilter ;
if ( params . searchAfterDate ) body . search_after_date = params . searchAfterDate ;
if ( params . searchBeforeDate ) body . search_before_date = params . searchBeforeDate ;
if ( params . maxTokens !== undefined ) body . max_tokens = params . maxTokens ;
if ( params . maxTokensPerPage !== undefined ) body . max_tokens_per_page = params . maxTokensPerPage ;
return withTrustedWebSearchEndpoint (
{
url : PERPLEXITY_SEARCH_ENDPOINT ,
timeoutSeconds : params.timeoutSeconds ,
init : {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
Accept : "application/json" ,
Authorization : ` Bearer ${ params . apiKey } ` ,
"HTTP-Referer" : "https://openclaw.ai" ,
"X-Title" : "OpenClaw Web Search" ,
} ,
body : JSON.stringify ( body ) ,
} ,
} ,
async ( res ) = > {
if ( ! res . ok ) {
return await throwWebSearchApiError ( res , "Perplexity Search" ) ;
}
const data = ( await res . json ( ) ) as PerplexitySearchApiResponse ;
return ( data . results ? ? [ ] ) . map ( ( entry ) = > ( {
title : entry.title ? wrapWebContent ( entry . title , "web_search" ) : "" ,
url : entry.url ? ? "" ,
description : entry.snippet ? wrapWebContent ( entry . snippet , "web_search" ) : "" ,
published : entry.date ? ? undefined ,
siteName : resolveSiteName ( entry . url ) || undefined ,
} ) ) ;
} ,
) ;
}
async function runPerplexitySearch ( params : {
query : string ;
apiKey : string ;
baseUrl : string ;
model : string ;
timeoutSeconds : number ;
freshness? : string ;
} ) : Promise < { content : string ; citations : string [ ] } > {
const endpoint = ` ${ params . baseUrl . trim ( ) . replace ( /\/$/ , "" ) } /chat/completions ` ;
const body : Record < string , unknown > = {
model : resolvePerplexityRequestModel ( params . baseUrl , params . model ) ,
messages : [ { role : "user" , content : params.query } ] ,
} ;
if ( params . freshness ) {
body . search_recency_filter = params . freshness ;
}
return withTrustedWebSearchEndpoint (
{
url : endpoint ,
timeoutSeconds : params.timeoutSeconds ,
init : {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
Authorization : ` Bearer ${ params . apiKey } ` ,
"HTTP-Referer" : "https://openclaw.ai" ,
"X-Title" : "OpenClaw Web Search" ,
} ,
body : JSON.stringify ( body ) ,
} ,
} ,
async ( res ) = > {
if ( ! res . ok ) {
return await throwWebSearchApiError ( res , "Perplexity" ) ;
}
const data = ( await res . json ( ) ) as PerplexitySearchResponse ;
return {
content : data.choices?. [ 0 ] ? . message ? . content ? ? "No response" ,
citations : extractPerplexityCitations ( data ) ,
} ;
} ,
) ;
}
function resolveRuntimeTransport ( params : {
searchConfig? : Record < string , unknown > ;
resolvedKey? : string ;
keySource : WebSearchCredentialResolutionSource ;
fallbackEnvVar? : string ;
} ) : PerplexityTransport | undefined {
2026-03-18 05:26:19 +00:00
const perplexity = params . searchConfig ? . perplexity ;
const scoped =
perplexity && typeof perplexity === "object" && ! Array . isArray ( perplexity )
? ( perplexity as { baseUrl? : string ; model? : string } )
: undefined ;
2026-03-17 22:21:44 -05:00
const configuredBaseUrl = typeof scoped ? . baseUrl === "string" ? scoped . baseUrl . trim ( ) : "" ;
const configuredModel = typeof scoped ? . model === "string" ? scoped . model . trim ( ) : "" ;
const baseUrl = ( ( ) = > {
if ( configuredBaseUrl ) {
return configuredBaseUrl ;
}
if ( params . keySource === "env" ) {
if ( params . fallbackEnvVar === "PERPLEXITY_API_KEY" ) return PERPLEXITY_DIRECT_BASE_URL ;
if ( params . fallbackEnvVar === "OPENROUTER_API_KEY" ) return DEFAULT_PERPLEXITY_BASE_URL ;
}
if ( ( params . keySource === "config" || params . keySource === "secretRef" ) && params . resolvedKey ) {
return inferPerplexityBaseUrlFromApiKey ( params . resolvedKey ) === "openrouter"
? DEFAULT_PERPLEXITY_BASE_URL
: PERPLEXITY_DIRECT_BASE_URL ;
}
return DEFAULT_PERPLEXITY_BASE_URL ;
} ) ( ) ;
return configuredBaseUrl || configuredModel || ! isDirectPerplexityBaseUrl ( baseUrl )
? "chat_completions"
: "search_api" ;
}
function createPerplexitySchema ( transport? : PerplexityTransport ) {
const querySchema = {
query : Type.String ( { description : "Search query string." } ) ,
count : Type.Optional (
Type . Number ( {
description : "Number of results to return (1-10)." ,
minimum : 1 ,
maximum : MAX_SEARCH_COUNT ,
} ) ,
) ,
freshness : Type.Optional (
Type . String ( { description : "Filter by time: 'day' (24h), 'week', 'month', or 'year'." } ) ,
) ,
} ;
if ( transport === "chat_completions" ) {
return Type . Object ( querySchema ) ;
}
return Type . Object ( {
. . . querySchema ,
country : Type.Optional (
Type . String ( { description : "Native Perplexity Search API only. 2-letter country code." } ) ,
) ,
language : Type.Optional (
Type . String ( { description : "Native Perplexity Search API only. ISO 639-1 language code." } ) ,
) ,
date_after : Type.Optional (
Type . String ( {
description :
"Native Perplexity Search API only. Only results published after this date (YYYY-MM-DD)." ,
} ) ,
) ,
date_before : Type.Optional (
Type . String ( {
description :
"Native Perplexity Search API only. Only results published before this date (YYYY-MM-DD)." ,
} ) ,
) ,
domain_filter : Type.Optional (
Type . Array ( Type . String ( ) , {
description : "Native Perplexity Search API only. Domain filter (max 20)." ,
} ) ,
) ,
max_tokens : Type.Optional (
Type . Number ( {
description : "Native Perplexity Search API only. Total content budget across all results." ,
minimum : 1 ,
maximum : 1000000 ,
} ) ,
) ,
max_tokens_per_page : Type.Optional (
Type . Number ( {
description : "Native Perplexity Search API only. Max tokens extracted per page." ,
minimum : 1 ,
} ) ,
) ,
} ) ;
}
function createPerplexityToolDefinition (
searchConfig? : SearchConfigRecord ,
runtimeTransport? : PerplexityTransport ,
) : WebSearchProviderToolDefinition {
2026-03-18 05:26:19 +00:00
const perplexityConfig = resolvePerplexityConfig ( searchConfig ) ;
2026-03-17 22:21:44 -05:00
const schemaTransport =
runtimeTransport ? ?
( perplexityConfig . baseUrl || perplexityConfig . model ? "chat_completions" : undefined ) ;
return {
description :
schemaTransport === "chat_completions"
? "Search the web using Perplexity Sonar via Perplexity/OpenRouter chat completions. Returns AI-synthesized answers with citations from web-grounded search."
: "Search the web using Perplexity. Runtime routing decides between native Search API and Sonar chat-completions compatibility. Structured filters are available on the native Search API path." ,
parameters : createPerplexitySchema ( schemaTransport ) ,
execute : async ( args ) = > {
const runtime = resolvePerplexityTransport ( perplexityConfig ) ;
if ( ! runtime . apiKey ) {
return {
error : "missing_perplexity_api_key" ,
message :
2026-03-18 05:26:19 +00:00
"web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure tools.web.search.perplexity.apiKey." ,
2026-03-17 22:21:44 -05:00
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
const params = args as Record < string , unknown > ;
const query = readStringParam ( params , "query" , { required : true } ) ;
const count =
readNumberParam ( params , "count" , { integer : true } ) ? ?
searchConfig ? . maxResults ? ?
undefined ;
const rawFreshness = readStringParam ( params , "freshness" ) ;
const freshness = rawFreshness ? normalizeFreshness ( rawFreshness , "perplexity" ) : undefined ;
if ( rawFreshness && ! freshness ) {
return {
error : "invalid_freshness" ,
message : "freshness must be day, week, month, or year." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
const structured = runtime . transport === "search_api" ;
const country = readStringParam ( params , "country" ) ;
const language = readStringParam ( params , "language" ) ;
const rawDateAfter = readStringParam ( params , "date_after" ) ;
const rawDateBefore = readStringParam ( params , "date_before" ) ;
const domainFilter = readStringArrayParam ( params , "domain_filter" ) ;
const maxTokens = readNumberParam ( params , "max_tokens" , { integer : true } ) ;
const maxTokensPerPage = readNumberParam ( params , "max_tokens_per_page" , { integer : true } ) ;
if ( ! structured ) {
if ( country ) {
return {
error : "unsupported_country" ,
message :
"country filtering is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
if ( language ) {
return {
error : "unsupported_language" ,
message :
"language filtering is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
if ( rawDateAfter || rawDateBefore ) {
return {
error : "unsupported_date_filter" ,
message :
"date_after/date_before are only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable them." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
if ( domainFilter ? . length ) {
return {
error : "unsupported_domain_filter" ,
message :
"domain_filter is only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable it." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
if ( maxTokens !== undefined || maxTokensPerPage !== undefined ) {
return {
error : "unsupported_content_budget" ,
message :
"max_tokens and max_tokens_per_page are only supported by the native Perplexity Search API path. Remove Perplexity baseUrl/model overrides or use a direct PERPLEXITY_API_KEY to enable them." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
}
if ( language && ! /^[a-z]{2}$/i . test ( language ) ) {
return {
error : "invalid_language" ,
message : "language must be a 2-letter ISO 639-1 code like 'en', 'de', or 'fr'." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
if ( rawFreshness && ( rawDateAfter || rawDateBefore ) ) {
return {
error : "conflicting_time_filters" ,
message :
"freshness and date_after/date_before cannot be used together. Use either freshness (day/week/month/year) or a date range (date_after/date_before), not both." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
const dateAfter = rawDateAfter ? normalizeToIsoDate ( rawDateAfter ) : undefined ;
const dateBefore = rawDateBefore ? normalizeToIsoDate ( rawDateBefore ) : undefined ;
if ( rawDateAfter && ! dateAfter ) {
return {
error : "invalid_date" ,
message : "date_after must be YYYY-MM-DD format." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
if ( rawDateBefore && ! dateBefore ) {
return {
error : "invalid_date" ,
message : "date_before must be YYYY-MM-DD format." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
if ( dateAfter && dateBefore && dateAfter > dateBefore ) {
return {
error : "invalid_date_range" ,
message : "date_after must be before date_before." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
if ( domainFilter ? . length ) {
const hasDeny = domainFilter . some ( ( entry ) = > entry . startsWith ( "-" ) ) ;
const hasAllow = domainFilter . some ( ( entry ) = > ! entry . startsWith ( "-" ) ) ;
if ( hasDeny && hasAllow ) {
return {
error : "invalid_domain_filter" ,
message :
"domain_filter cannot mix allowlist and denylist entries. Use either all positive entries (allowlist) or all entries prefixed with '-' (denylist)." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
if ( domainFilter . length > 20 ) {
return {
error : "invalid_domain_filter" ,
message : "domain_filter supports a maximum of 20 domains." ,
docs : "https://docs.openclaw.ai/tools/web" ,
} ;
}
}
const cacheKey = buildSearchCacheKey ( [
"perplexity" ,
runtime . transport ,
runtime . baseUrl ,
runtime . model ,
query ,
resolveSearchCount ( count , DEFAULT_SEARCH_COUNT ) ,
country ,
language ,
freshness ,
dateAfter ,
dateBefore ,
domainFilter ? . join ( "," ) ,
maxTokens ,
maxTokensPerPage ,
] ) ;
const cached = readCachedSearchPayload ( cacheKey ) ;
if ( cached ) {
return cached ;
}
const start = Date . now ( ) ;
const timeoutSeconds = resolveSearchTimeoutSeconds ( searchConfig ) ;
const payload =
runtime . transport === "chat_completions"
? {
query ,
provider : "perplexity" ,
model : runtime.model ,
tookMs : Date.now ( ) - start ,
externalContent : {
untrusted : true ,
source : "web_search" ,
provider : "perplexity" ,
wrapped : true ,
} ,
. . . ( await ( async ( ) = > {
const result = await runPerplexitySearch ( {
query ,
apiKey : runtime.apiKey ! ,
baseUrl : runtime.baseUrl ,
model : runtime.model ,
timeoutSeconds ,
freshness ,
} ) ;
return {
content : wrapWebContent ( result . content , "web_search" ) ,
citations : result.citations ,
} ;
} ) ( ) ) ,
}
: {
query ,
provider : "perplexity" ,
count : 0 ,
tookMs : Date.now ( ) - start ,
externalContent : {
untrusted : true ,
source : "web_search" ,
provider : "perplexity" ,
wrapped : true ,
} ,
results : await runPerplexitySearchApi ( {
query ,
apiKey : runtime.apiKey ! ,
count : resolveSearchCount ( count , DEFAULT_SEARCH_COUNT ) ,
timeoutSeconds ,
country : country ? ? undefined ,
searchDomainFilter : domainFilter ,
searchRecencyFilter : freshness ,
searchLanguageFilter : language ? [ language ] : undefined ,
searchAfterDate : dateAfter ? isoToPerplexityDate ( dateAfter ) : undefined ,
searchBeforeDate : dateBefore ? isoToPerplexityDate ( dateBefore ) : undefined ,
maxTokens : maxTokens ? ? undefined ,
maxTokensPerPage : maxTokensPerPage ? ? undefined ,
} ) ,
} ;
if ( Array . isArray ( ( payload as { results? : unknown [ ] } ) . results ) ) {
( payload as { count : number } ) . count = ( payload as { results : unknown [ ] } ) . results . length ;
( payload as { tookMs : number } ) . tookMs = Date . now ( ) - start ;
} else {
( payload as { tookMs : number } ) . tookMs = Date . now ( ) - start ;
}
writeCachedSearchPayload ( cacheKey , payload , resolveSearchCacheTtlMs ( searchConfig ) ) ;
return payload ;
} ,
} ;
}
export function createPerplexityWebSearchProvider ( ) : WebSearchProviderPlugin {
return {
id : "perplexity" ,
label : "Perplexity Search" ,
hint : "Structured results · domain/country/language/time filters" ,
envVars : [ "PERPLEXITY_API_KEY" , "OPENROUTER_API_KEY" ] ,
placeholder : "pplx-..." ,
signupUrl : "https://www.perplexity.ai/settings/api" ,
docsUrl : "https://docs.openclaw.ai/perplexity" ,
autoDetectOrder : 50 ,
2026-03-17 23:29:52 -05:00
credentialPath : "plugins.entries.perplexity.config.webSearch.apiKey" ,
inactiveSecretPaths : [ "plugins.entries.perplexity.config.webSearch.apiKey" ] ,
2026-03-17 22:21:44 -05:00
getCredentialValue : ( searchConfig ) = > {
const perplexity = searchConfig ? . perplexity ;
return perplexity && typeof perplexity === "object" && ! Array . isArray ( perplexity )
? ( perplexity as Record < string , unknown > ) . apiKey
: undefined ;
} ,
setCredentialValue : ( searchConfigTarget , value ) = > {
const scoped = searchConfigTarget . perplexity ;
if ( ! scoped || typeof scoped !== "object" || Array . isArray ( scoped ) ) {
searchConfigTarget . perplexity = { apiKey : value } ;
return ;
}
( scoped as Record < string , unknown > ) . apiKey = value ;
} ,
2026-03-17 23:29:52 -05:00
getConfiguredCredentialValue : ( config ) = >
resolveProviderWebSearchPluginConfig ( config , "perplexity" ) ? . apiKey ,
setConfiguredCredentialValue : ( configTarget , value ) = > {
setProviderWebSearchPluginConfigValue ( configTarget , "perplexity" , "apiKey" , value ) ;
} ,
2026-03-17 22:21:44 -05:00
resolveRuntimeMetadata : ( ctx ) = > ( {
perplexityTransport : resolveRuntimeTransport ( {
2026-03-18 05:26:19 +00:00
searchConfig : {
. . . ( ctx . searchConfig as SearchConfigRecord | undefined ) ,
perplexity : {
. . . ( ( ctx . searchConfig as SearchConfigRecord | undefined ) ? . perplexity as
| Record < string , unknown >
| undefined ) ,
. . . ( resolveProviderWebSearchPluginConfig ( ctx . config , "perplexity" ) as
| Record < string , unknown >
| undefined ) ,
} ,
} ,
2026-03-17 22:21:44 -05:00
resolvedKey : ctx.resolvedCredential?.value ,
keySource : ctx.resolvedCredential?.source ? ? "missing" ,
fallbackEnvVar : ctx.resolvedCredential?.fallbackEnvVar ,
} ) ,
} ) ,
2026-03-18 07:39:49 +00:00
createTool : ( ctx ) = >
createPerplexityToolDefinition (
( ( ) = > {
const searchConfig = ctx . searchConfig as SearchConfigRecord | undefined ;
const pluginConfig = resolveProviderWebSearchPluginConfig ( ctx . config , "perplexity" ) ;
if ( ! pluginConfig ) {
return searchConfig ;
}
return {
. . . ( searchConfig ? ? { } ) ,
perplexity : {
. . . resolvePerplexityConfig ( searchConfig ) ,
. . . pluginConfig ,
} ,
} as SearchConfigRecord ;
} ) ( ) ,
2026-03-17 22:21:44 -05:00
ctx . runtimeMetadata ? . perplexityTransport as PerplexityTransport | undefined ,
2026-03-18 07:39:49 +00:00
) ,
2026-03-17 22:21:44 -05:00
} ;
}
export const __testing = {
inferPerplexityBaseUrlFromApiKey ,
resolvePerplexityBaseUrl ,
resolvePerplexityModel ,
resolvePerplexityTransport ,
isDirectPerplexityBaseUrl ,
resolvePerplexityRequestModel ,
resolvePerplexityApiKey ,
normalizeToIsoDate ,
isoToPerplexityDate ,
} as const ;