mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-25 23:47:20 +00:00
refactor web search provider execution out of core
This commit is contained in:
File diff suppressed because it is too large
Load Diff
213
src/agents/tools/web-search-provider-common.ts
Normal file
213
src/agents/tools/web-search-provider-common.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { normalizeResolvedSecretInputString } from "../../config/types.secrets.js";
|
||||
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
||||
import { withTrustedWebToolsEndpoint } from "./web-guarded-fetch.js";
|
||||
import {
|
||||
CacheEntry,
|
||||
DEFAULT_CACHE_TTL_MINUTES,
|
||||
DEFAULT_TIMEOUT_SECONDS,
|
||||
normalizeCacheKey,
|
||||
readCache,
|
||||
readResponseText,
|
||||
resolveCacheTtlMs,
|
||||
resolveTimeoutSeconds,
|
||||
writeCache,
|
||||
} from "./web-shared.js";
|
||||
|
||||
export type SearchConfigRecord = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
|
||||
? Web extends { search?: infer Search }
|
||||
? Search extends Record<string, unknown>
|
||||
? Search
|
||||
: Record<string, unknown>
|
||||
: Record<string, unknown>
|
||||
: Record<string, unknown>;
|
||||
|
||||
export const DEFAULT_SEARCH_COUNT = 5;
|
||||
export const MAX_SEARCH_COUNT = 10;
|
||||
|
||||
const SEARCH_CACHE_KEY = Symbol.for("openclaw.web-search.cache");
|
||||
|
||||
function getSharedSearchCache(): Map<string, CacheEntry<Record<string, unknown>>> {
|
||||
const root = globalThis as Record<PropertyKey, unknown>;
|
||||
const existing = root[SEARCH_CACHE_KEY];
|
||||
if (existing instanceof Map) {
|
||||
return existing as Map<string, CacheEntry<Record<string, unknown>>>;
|
||||
}
|
||||
const next = new Map<string, CacheEntry<Record<string, unknown>>>();
|
||||
root[SEARCH_CACHE_KEY] = next;
|
||||
return next;
|
||||
}
|
||||
|
||||
export const SEARCH_CACHE = getSharedSearchCache();
|
||||
|
||||
export function resolveSearchTimeoutSeconds(searchConfig?: SearchConfigRecord): number {
|
||||
return resolveTimeoutSeconds(searchConfig?.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS);
|
||||
}
|
||||
|
||||
export function resolveSearchCacheTtlMs(searchConfig?: SearchConfigRecord): number {
|
||||
return resolveCacheTtlMs(searchConfig?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES);
|
||||
}
|
||||
|
||||
export function resolveSearchCount(value: unknown, fallback: number): number {
|
||||
const parsed = typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
||||
const clamped = Math.max(1, Math.min(MAX_SEARCH_COUNT, Math.floor(parsed)));
|
||||
return clamped;
|
||||
}
|
||||
|
||||
export function readConfiguredSecretString(value: unknown, path: string): string | undefined {
|
||||
return normalizeSecretInput(normalizeResolvedSecretInputString({ value, path })) || undefined;
|
||||
}
|
||||
|
||||
export function readProviderEnvValue(envVars: string[]): string | undefined {
|
||||
for (const envVar of envVars) {
|
||||
const value = normalizeSecretInput(process.env[envVar]);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function withTrustedWebSearchEndpoint<T>(
|
||||
params: {
|
||||
url: string;
|
||||
timeoutSeconds: number;
|
||||
init: RequestInit;
|
||||
},
|
||||
run: (response: Response) => Promise<T>,
|
||||
): Promise<T> {
|
||||
return withTrustedWebToolsEndpoint(
|
||||
{
|
||||
url: params.url,
|
||||
init: params.init,
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
},
|
||||
async ({ response }) => run(response),
|
||||
);
|
||||
}
|
||||
|
||||
export async function throwWebSearchApiError(res: Response, providerLabel: string): Promise<never> {
|
||||
const detailResult = await readResponseText(res, { maxBytes: 64_000 });
|
||||
const detail = detailResult.text;
|
||||
throw new Error(`${providerLabel} API error (${res.status}): ${detail || res.statusText}`);
|
||||
}
|
||||
|
||||
export function resolveSiteName(url: string | undefined): string | undefined {
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const BRAVE_FRESHNESS_SHORTCUTS = new Set(["pd", "pw", "pm", "py"]);
|
||||
const BRAVE_FRESHNESS_RANGE = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/;
|
||||
const PERPLEXITY_RECENCY_VALUES = new Set(["day", "week", "month", "year"]);
|
||||
|
||||
export const FRESHNESS_TO_RECENCY: Record<string, string> = {
|
||||
pd: "day",
|
||||
pw: "week",
|
||||
pm: "month",
|
||||
py: "year",
|
||||
};
|
||||
export const RECENCY_TO_FRESHNESS: Record<string, string> = {
|
||||
day: "pd",
|
||||
week: "pw",
|
||||
month: "pm",
|
||||
year: "py",
|
||||
};
|
||||
|
||||
const ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||
const PERPLEXITY_DATE_PATTERN = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/;
|
||||
|
||||
function isValidIsoDate(value: string): boolean {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||
return false;
|
||||
}
|
||||
const [year, month, day] = value.split("-").map((part) => Number.parseInt(part, 10));
|
||||
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const date = new Date(Date.UTC(year, month - 1, day));
|
||||
return (
|
||||
date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
|
||||
);
|
||||
}
|
||||
|
||||
export function isoToPerplexityDate(iso: string): string | undefined {
|
||||
const match = iso.match(ISO_DATE_PATTERN);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const [, year, month, day] = match;
|
||||
return `${parseInt(month, 10)}/${parseInt(day, 10)}/${year}`;
|
||||
}
|
||||
|
||||
export function normalizeToIsoDate(value: string): string | undefined {
|
||||
const trimmed = value.trim();
|
||||
if (ISO_DATE_PATTERN.test(trimmed)) {
|
||||
return isValidIsoDate(trimmed) ? trimmed : undefined;
|
||||
}
|
||||
const match = trimmed.match(PERPLEXITY_DATE_PATTERN);
|
||||
if (match) {
|
||||
const [, month, day, year] = match;
|
||||
const iso = `${year}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
|
||||
return isValidIsoDate(iso) ? iso : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function normalizeFreshness(
|
||||
value: string | undefined,
|
||||
provider: "brave" | "perplexity",
|
||||
): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (BRAVE_FRESHNESS_SHORTCUTS.has(lower)) {
|
||||
return provider === "brave" ? lower : FRESHNESS_TO_RECENCY[lower];
|
||||
}
|
||||
if (PERPLEXITY_RECENCY_VALUES.has(lower)) {
|
||||
return provider === "perplexity" ? lower : RECENCY_TO_FRESHNESS[lower];
|
||||
}
|
||||
if (provider === "brave") {
|
||||
const match = trimmed.match(BRAVE_FRESHNESS_RANGE);
|
||||
if (match) {
|
||||
const [, start, end] = match;
|
||||
if (isValidIsoDate(start) && isValidIsoDate(end) && start <= end) {
|
||||
return `${start}to${end}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function readCachedSearchPayload(cacheKey: string): Record<string, unknown> | undefined {
|
||||
const cached = readCache(SEARCH_CACHE, cacheKey);
|
||||
return cached ? { ...cached.value, cached: true } : undefined;
|
||||
}
|
||||
|
||||
export function buildSearchCacheKey(parts: Array<string | number | boolean | undefined>): string {
|
||||
return normalizeCacheKey(
|
||||
parts.map((part) => (part === undefined ? "default" : String(part))).join(":"),
|
||||
);
|
||||
}
|
||||
|
||||
export function writeCachedSearchPayload(
|
||||
cacheKey: string,
|
||||
payload: Record<string, unknown>,
|
||||
ttlMs: number,
|
||||
): void {
|
||||
writeCache(SEARCH_CACHE, cacheKey, payload, ttlMs);
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import type { WebSearchProviderPlugin } from "../../plugins/types.js";
|
||||
import { createWebSearchTool as createLegacyWebSearchTool } from "./web-search-core.js";
|
||||
|
||||
type ConfiguredWebSearchProvider = NonNullable<
|
||||
NonNullable<NonNullable<OpenClawConfig["tools"]>["web"]>["search"]
|
||||
>["provider"];
|
||||
|
||||
export type WebSearchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
|
||||
? Web extends { search?: infer Search }
|
||||
? Search
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
function cloneWithDescriptors<T extends object>(value: T | undefined): T {
|
||||
const next = Object.create(Object.getPrototypeOf(value ?? {})) as T;
|
||||
if (value) {
|
||||
@@ -14,7 +18,7 @@ function cloneWithDescriptors<T extends object>(value: T | undefined): T {
|
||||
return next;
|
||||
}
|
||||
|
||||
function withForcedProvider(
|
||||
export function withForcedProvider(
|
||||
config: OpenClawConfig | undefined,
|
||||
provider: ConfiguredWebSearchProvider,
|
||||
): OpenClawConfig {
|
||||
@@ -31,33 +35,6 @@ function withForcedProvider(
|
||||
return next;
|
||||
}
|
||||
|
||||
export function createPluginBackedWebSearchProvider(
|
||||
provider: Omit<WebSearchProviderPlugin, "createTool"> & {
|
||||
id: ConfiguredWebSearchProvider;
|
||||
},
|
||||
): WebSearchProviderPlugin {
|
||||
return {
|
||||
...provider,
|
||||
createTool: (ctx) => {
|
||||
const tool = createLegacyWebSearchTool({
|
||||
config: withForcedProvider(ctx.config, provider.id),
|
||||
runtimeWebSearch: ctx.runtimeMetadata,
|
||||
});
|
||||
if (!tool) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
description: tool.description,
|
||||
parameters: tool.parameters as Record<string, unknown>,
|
||||
execute: async (args) => {
|
||||
const result = await tool.execute(`web-search:${provider.id}`, args);
|
||||
return (result.details ?? {}) as Record<string, unknown>;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getTopLevelCredentialValue(searchConfig?: Record<string, unknown>): unknown {
|
||||
return searchConfig?.apiKey;
|
||||
}
|
||||
@@ -92,3 +69,24 @@ export function setScopedCredentialValue(
|
||||
}
|
||||
(scoped as Record<string, unknown>).apiKey = value;
|
||||
}
|
||||
|
||||
export function resolveSearchConfig(cfg?: OpenClawConfig): WebSearchConfig {
|
||||
const search = cfg?.tools?.web?.search;
|
||||
if (!search || typeof search !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
return search as WebSearchConfig;
|
||||
}
|
||||
|
||||
export function resolveSearchEnabled(params: {
|
||||
search?: WebSearchConfig;
|
||||
sandboxed?: boolean;
|
||||
}): boolean {
|
||||
if (typeof params.search?.enabled === "boolean") {
|
||||
return params.search.enabled;
|
||||
}
|
||||
if (params.sandboxed) {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { __testing as braveTesting } from "../../../extensions/brave/src/brave-web-search-provider.js";
|
||||
import { __testing as moonshotTesting } from "../../../extensions/moonshot/src/kimi-web-search-provider.js";
|
||||
import { __testing as perplexityTesting } from "../../../extensions/perplexity/web-search-provider.js";
|
||||
import { __testing as xaiTesting } from "../../../extensions/xai/src/grok-web-search-provider.js";
|
||||
import { withEnv } from "../../test-utils/env.js";
|
||||
import { __testing } from "./web-search.js";
|
||||
|
||||
const {
|
||||
inferPerplexityBaseUrlFromApiKey,
|
||||
resolvePerplexityBaseUrl,
|
||||
@@ -10,21 +12,19 @@ const {
|
||||
isDirectPerplexityBaseUrl,
|
||||
resolvePerplexityRequestModel,
|
||||
resolvePerplexityApiKey,
|
||||
normalizeBraveLanguageParams,
|
||||
normalizeFreshness,
|
||||
normalizeToIsoDate,
|
||||
isoToPerplexityDate,
|
||||
resolveGrokApiKey,
|
||||
resolveGrokModel,
|
||||
resolveGrokInlineCitations,
|
||||
extractGrokContent,
|
||||
resolveKimiApiKey,
|
||||
resolveKimiModel,
|
||||
resolveKimiBaseUrl,
|
||||
extractKimiCitations,
|
||||
} = perplexityTesting;
|
||||
const {
|
||||
normalizeBraveLanguageParams,
|
||||
normalizeFreshness,
|
||||
resolveBraveMode,
|
||||
mapBraveLlmContextResults,
|
||||
} = __testing;
|
||||
} = braveTesting;
|
||||
const { resolveGrokApiKey, resolveGrokModel, resolveGrokInlineCitations, extractGrokContent } =
|
||||
xaiTesting;
|
||||
const { resolveKimiApiKey, resolveKimiModel, resolveKimiBaseUrl, extractKimiCitations } =
|
||||
moonshotTesting;
|
||||
|
||||
const kimiApiKeyEnv = ["KIMI_API", "KEY"].join("_");
|
||||
const moonshotApiKeyEnv = ["MOONSHOT_API", "KEY"].join("_");
|
||||
|
||||
@@ -1,29 +1,123 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { normalizeResolvedSecretInputString } from "../../config/types.secrets.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { resolvePluginWebSearchProviders } from "../../plugins/web-search-providers.js";
|
||||
import type { PluginWebSearchProviderEntry } from "../../plugins/types.js";
|
||||
import type { RuntimeWebSearchMetadata } from "../../secrets/runtime-web-tools.types.js";
|
||||
import { __testing as runtimeTesting } from "../../web-search/runtime.js";
|
||||
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { jsonResult } from "./common.js";
|
||||
import { SEARCH_CACHE } from "./web-search-provider-common.js";
|
||||
import {
|
||||
__testing as coreTesting,
|
||||
createWebSearchTool as createWebSearchToolCore,
|
||||
} from "./web-search-core.js";
|
||||
resolveSearchConfig,
|
||||
resolveSearchEnabled,
|
||||
type WebSearchConfig,
|
||||
} from "./web-search-provider-config.js";
|
||||
|
||||
function readProviderEnvValue(envVars: string[]): string | undefined {
|
||||
for (const envVar of envVars) {
|
||||
const value = normalizeSecretInput(process.env[envVar]);
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function hasProviderCredential(
|
||||
provider: PluginWebSearchProviderEntry,
|
||||
search: WebSearchConfig | undefined,
|
||||
): boolean {
|
||||
const rawValue = provider.getCredentialValue(search as Record<string, unknown> | undefined);
|
||||
const fromConfig = normalizeSecretInput(
|
||||
normalizeResolvedSecretInputString({
|
||||
value: rawValue,
|
||||
path: provider.credentialPath,
|
||||
}),
|
||||
);
|
||||
return Boolean(fromConfig || readProviderEnvValue(provider.envVars));
|
||||
}
|
||||
|
||||
function resolveSearchProvider(search?: WebSearchConfig): string {
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
bundledAllowlistCompat: true,
|
||||
});
|
||||
const raw =
|
||||
search && "provider" in search && typeof search.provider === "string"
|
||||
? search.provider.trim().toLowerCase()
|
||||
: "";
|
||||
|
||||
if (raw) {
|
||||
const explicit = providers.find((provider) => provider.id === raw);
|
||||
if (explicit) {
|
||||
return explicit.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!raw) {
|
||||
for (const provider of providers) {
|
||||
if (!hasProviderCredential(provider, search)) {
|
||||
continue;
|
||||
}
|
||||
logVerbose(
|
||||
`web_search: no provider configured, auto-detected "${provider.id}" from available API keys`,
|
||||
);
|
||||
return provider.id;
|
||||
}
|
||||
}
|
||||
|
||||
return providers[0]?.id ?? "";
|
||||
}
|
||||
|
||||
export function createWebSearchTool(options?: {
|
||||
config?: OpenClawConfig;
|
||||
sandboxed?: boolean;
|
||||
runtimeWebSearch?: RuntimeWebSearchMetadata;
|
||||
}): AnyAgentTool | null {
|
||||
return createWebSearchToolCore(options);
|
||||
const search = resolveSearchConfig(options?.config);
|
||||
if (!resolveSearchEnabled({ search, sandboxed: options?.sandboxed })) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config: options?.config,
|
||||
bundledAllowlistCompat: true,
|
||||
});
|
||||
if (providers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const providerId =
|
||||
options?.runtimeWebSearch?.selectedProvider ??
|
||||
options?.runtimeWebSearch?.providerConfigured ??
|
||||
resolveSearchProvider(search);
|
||||
const provider =
|
||||
providers.find((entry) => entry.id === providerId) ??
|
||||
providers.find((entry) => entry.id === resolveSearchProvider(search)) ??
|
||||
providers[0];
|
||||
if (!provider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const definition = provider.createTool({
|
||||
config: options?.config,
|
||||
searchConfig: search as Record<string, unknown> | undefined,
|
||||
runtimeMetadata: options?.runtimeWebSearch,
|
||||
});
|
||||
if (!definition) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
label: "Web Search",
|
||||
name: "web_search",
|
||||
description: definition.description,
|
||||
parameters: definition.parameters,
|
||||
execute: async (_toolCallId, args) => jsonResult(await definition.execute(args)),
|
||||
};
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
...coreTesting,
|
||||
resolveSearchProvider: (
|
||||
search?: OpenClawConfig["tools"] extends infer Tools
|
||||
? Tools extends { web?: infer Web }
|
||||
? Web extends { search?: infer Search }
|
||||
? Search
|
||||
: undefined
|
||||
: undefined
|
||||
: undefined,
|
||||
) => runtimeTesting.resolveWebSearchProviderId({ search }),
|
||||
SEARCH_CACHE,
|
||||
resolveSearchProvider,
|
||||
};
|
||||
|
||||
@@ -571,7 +571,9 @@ describe("web_search perplexity OpenRouter compatibility", () => {
|
||||
});
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
expect(result?.details).toMatchObject({ error: "unsupported_domain_filter" });
|
||||
expect((result?.details as { error?: string } | undefined)?.error).toMatch(
|
||||
/^unsupported_(domain_filter|structured_filter)$/,
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps Search API schema params visible before runtime auth routing", () => {
|
||||
|
||||
@@ -6,27 +6,13 @@ import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
} from "../config/types.secrets.js";
|
||||
import { enablePluginInConfig } from "../plugins/enable.js";
|
||||
import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
|
||||
import { resolvePluginWebSearchProviders } from "../plugins/web-search-providers.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import type { SecretInputMode } from "./onboard-types.js";
|
||||
|
||||
export type SearchProvider = NonNullable<
|
||||
NonNullable<NonNullable<NonNullable<OpenClawConfig["tools"]>["web"]>["search"]>["provider"]
|
||||
>;
|
||||
|
||||
const SEARCH_PROVIDER_IDS = ["brave", "firecrawl", "gemini", "grok", "kimi", "perplexity"] as const;
|
||||
|
||||
function isSearchProvider(value: string): value is SearchProvider {
|
||||
return (SEARCH_PROVIDER_IDS as readonly string[]).includes(value);
|
||||
}
|
||||
|
||||
function hasSearchProviderId<T extends { id: string }>(
|
||||
provider: T,
|
||||
): provider is T & { id: SearchProvider } {
|
||||
return isSearchProvider(provider.id);
|
||||
}
|
||||
export type SearchProvider = string;
|
||||
|
||||
type SearchProviderEntry = {
|
||||
value: SearchProvider;
|
||||
@@ -35,21 +21,23 @@ type SearchProviderEntry = {
|
||||
envKeys: string[];
|
||||
placeholder: string;
|
||||
signupUrl: string;
|
||||
credentialPath: string;
|
||||
applySelectionConfig?: PluginWebSearchProviderEntry["applySelectionConfig"];
|
||||
};
|
||||
|
||||
export const SEARCH_PROVIDER_OPTIONS: readonly SearchProviderEntry[] =
|
||||
resolvePluginWebSearchProviders({
|
||||
bundledAllowlistCompat: true,
|
||||
})
|
||||
.filter(hasSearchProviderId)
|
||||
.map((provider) => ({
|
||||
value: provider.id,
|
||||
label: provider.label,
|
||||
hint: provider.hint,
|
||||
envKeys: provider.envVars,
|
||||
placeholder: provider.placeholder,
|
||||
signupUrl: provider.signupUrl,
|
||||
}));
|
||||
}).map((provider) => ({
|
||||
value: provider.id,
|
||||
label: provider.label,
|
||||
hint: provider.hint,
|
||||
envKeys: provider.envVars,
|
||||
placeholder: provider.placeholder,
|
||||
signupUrl: provider.signupUrl,
|
||||
credentialPath: provider.credentialPath,
|
||||
applySelectionConfig: provider.applySelectionConfig,
|
||||
}));
|
||||
|
||||
export function hasKeyInEnv(entry: SearchProviderEntry): boolean {
|
||||
return entry.envKeys.some((k) => Boolean(process.env[k]?.trim()));
|
||||
@@ -83,7 +71,7 @@ function buildSearchEnvRef(provider: SearchProvider): SecretRef {
|
||||
const envVar = entry?.envKeys.find((k) => Boolean(process.env[k]?.trim())) ?? entry?.envKeys[0];
|
||||
if (!envVar) {
|
||||
throw new Error(
|
||||
`No env var mapping for search provider "${provider}" in secret-input-mode=ref.`,
|
||||
`No env var mapping for search provider "${provider}" at ${entry?.credentialPath ?? "unknown path"} in secret-input-mode=ref.`,
|
||||
);
|
||||
}
|
||||
return { source: "env", provider: DEFAULT_SECRET_PROVIDER_ALIAS, id: envVar };
|
||||
@@ -107,29 +95,30 @@ export function applySearchKey(
|
||||
provider: SearchProvider,
|
||||
key: SecretInput,
|
||||
): OpenClawConfig {
|
||||
const search = { ...config.tools?.web?.search, provider, enabled: true };
|
||||
const entry = resolvePluginWebSearchProviders({
|
||||
const providerEntry = resolvePluginWebSearchProviders({
|
||||
config,
|
||||
bundledAllowlistCompat: true,
|
||||
}).find((candidate) => candidate.id === provider);
|
||||
if (entry) {
|
||||
entry.setCredentialValue(search as Record<string, unknown>, key);
|
||||
const search = { ...config.tools?.web?.search, provider, enabled: true };
|
||||
if (providerEntry) {
|
||||
providerEntry.setCredentialValue(search as Record<string, unknown>, key);
|
||||
}
|
||||
const next = {
|
||||
const nextBase = {
|
||||
...config,
|
||||
tools: {
|
||||
...config.tools,
|
||||
web: { ...config.tools?.web, search },
|
||||
},
|
||||
};
|
||||
if (provider !== "firecrawl") {
|
||||
return next;
|
||||
}
|
||||
return enablePluginInConfig(next, "firecrawl").config;
|
||||
return providerEntry?.applySelectionConfig?.(nextBase) ?? nextBase;
|
||||
}
|
||||
|
||||
function applyProviderOnly(config: OpenClawConfig, provider: SearchProvider): OpenClawConfig {
|
||||
const next = {
|
||||
const providerEntry = resolvePluginWebSearchProviders({
|
||||
config,
|
||||
bundledAllowlistCompat: true,
|
||||
}).find((candidate) => candidate.id === provider);
|
||||
const nextBase = {
|
||||
...config,
|
||||
tools: {
|
||||
...config.tools,
|
||||
@@ -143,10 +132,7 @@ function applyProviderOnly(config: OpenClawConfig, provider: SearchProvider): Op
|
||||
},
|
||||
},
|
||||
};
|
||||
if (provider !== "firecrawl") {
|
||||
return next;
|
||||
}
|
||||
return enablePluginInConfig(next, "firecrawl").config;
|
||||
return providerEntry?.applySelectionConfig?.(nextBase) ?? nextBase;
|
||||
}
|
||||
|
||||
function preserveDisabledState(original: OpenClawConfig, result: OpenClawConfig): OpenClawConfig {
|
||||
@@ -203,7 +189,7 @@ export async function setupSearch(
|
||||
return SEARCH_PROVIDER_OPTIONS[0].value;
|
||||
})();
|
||||
|
||||
type PickerValue = SearchProvider | "__skip__";
|
||||
type PickerValue = string;
|
||||
const choice = await prompter.select<PickerValue>({
|
||||
message: "Search provider",
|
||||
options: [
|
||||
|
||||
@@ -14,31 +14,37 @@ vi.mock("../plugins/web-search-providers.js", () => {
|
||||
{
|
||||
id: "brave",
|
||||
envVars: ["BRAVE_API_KEY"],
|
||||
credentialPath: "tools.web.search.apiKey",
|
||||
getCredentialValue: (search?: Record<string, unknown>) => search?.apiKey,
|
||||
},
|
||||
{
|
||||
id: "firecrawl",
|
||||
envVars: ["FIRECRAWL_API_KEY"],
|
||||
credentialPath: "tools.web.search.firecrawl.apiKey",
|
||||
getCredentialValue: getScoped("firecrawl"),
|
||||
},
|
||||
{
|
||||
id: "gemini",
|
||||
envVars: ["GEMINI_API_KEY"],
|
||||
credentialPath: "tools.web.search.gemini.apiKey",
|
||||
getCredentialValue: getScoped("gemini"),
|
||||
},
|
||||
{
|
||||
id: "grok",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
credentialPath: "tools.web.search.grok.apiKey",
|
||||
getCredentialValue: getScoped("grok"),
|
||||
},
|
||||
{
|
||||
id: "kimi",
|
||||
envVars: ["KIMI_API_KEY", "MOONSHOT_API_KEY"],
|
||||
credentialPath: "tools.web.search.kimi.apiKey",
|
||||
getCredentialValue: getScoped("kimi"),
|
||||
},
|
||||
{
|
||||
id: "perplexity",
|
||||
envVars: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
|
||||
credentialPath: "tools.web.search.perplexity.apiKey",
|
||||
getCredentialValue: getScoped("perplexity"),
|
||||
},
|
||||
],
|
||||
|
||||
@@ -866,6 +866,19 @@ export type WebSearchProviderContext = {
|
||||
runtimeMetadata?: RuntimeWebSearchMetadata;
|
||||
};
|
||||
|
||||
export type WebSearchCredentialResolutionSource = "config" | "secretRef" | "env" | "missing";
|
||||
|
||||
export type WebSearchRuntimeMetadataContext = {
|
||||
config?: OpenClawConfig;
|
||||
searchConfig?: Record<string, unknown>;
|
||||
runtimeMetadata?: RuntimeWebSearchMetadata;
|
||||
resolvedCredential?: {
|
||||
value?: string;
|
||||
source: WebSearchCredentialResolutionSource;
|
||||
fallbackEnvVar?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type WebSearchProviderPlugin = {
|
||||
id: WebSearchProviderId;
|
||||
label: string;
|
||||
@@ -875,8 +888,14 @@ export type WebSearchProviderPlugin = {
|
||||
signupUrl: string;
|
||||
docsUrl?: string;
|
||||
autoDetectOrder?: number;
|
||||
credentialPath: string;
|
||||
inactiveSecretPaths?: string[];
|
||||
getCredentialValue: (searchConfig?: Record<string, unknown>) => unknown;
|
||||
setCredentialValue: (searchConfigTarget: Record<string, unknown>, value: unknown) => void;
|
||||
applySelectionConfig?: (config: OpenClawConfig) => OpenClawConfig;
|
||||
resolveRuntimeMetadata?: (
|
||||
ctx: WebSearchRuntimeMetadataContext,
|
||||
) => Partial<RuntimeWebSearchMetadata> | Promise<Partial<RuntimeWebSearchMetadata>>;
|
||||
createTool: (ctx: WebSearchProviderContext) => WebSearchProviderToolDefinition | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,20 @@ describe("resolvePluginWebSearchProviders", () => {
|
||||
"perplexity:perplexity",
|
||||
"firecrawl:firecrawl",
|
||||
]);
|
||||
expect(providers.map((provider) => provider.credentialPath)).toEqual([
|
||||
"tools.web.search.apiKey",
|
||||
"tools.web.search.gemini.apiKey",
|
||||
"tools.web.search.grok.apiKey",
|
||||
"tools.web.search.kimi.apiKey",
|
||||
"tools.web.search.perplexity.apiKey",
|
||||
"tools.web.search.firecrawl.apiKey",
|
||||
]);
|
||||
expect(providers.find((provider) => provider.id === "firecrawl")?.applySelectionConfig).toEqual(
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(
|
||||
providers.find((provider) => provider.id === "perplexity")?.resolveRuntimeMetadata,
|
||||
).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it("can augment restrictive allowlists for bundled compatibility", () => {
|
||||
@@ -95,6 +109,7 @@ describe("resolvePluginWebSearchProviders", () => {
|
||||
placeholder: "custom-...",
|
||||
signupUrl: "https://example.com/signup",
|
||||
autoDetectOrder: 1,
|
||||
credentialPath: "tools.web.search.custom.apiKey",
|
||||
getCredentialValue: () => "configured",
|
||||
setCredentialValue: () => {},
|
||||
createTool: () => ({
|
||||
|
||||
@@ -1,37 +1,108 @@
|
||||
import bravePlugin from "../../extensions/brave/index.js";
|
||||
import firecrawlPlugin from "../../extensions/firecrawl/index.js";
|
||||
import googlePlugin from "../../extensions/google/index.js";
|
||||
import moonshotPlugin from "../../extensions/moonshot/index.js";
|
||||
import perplexityPlugin from "../../extensions/perplexity/index.js";
|
||||
import xaiPlugin from "../../extensions/xai/index.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import {
|
||||
withBundledPluginAllowlistCompat,
|
||||
withBundledPluginEnablementCompat,
|
||||
} from "./bundled-compat.js";
|
||||
import { capturePluginRegistration } from "./captured-registration.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import type { PluginWebSearchProviderRegistration } from "./registry.js";
|
||||
import {
|
||||
pluginRegistrationContractRegistry,
|
||||
webSearchProviderContractRegistry,
|
||||
} from "./contracts/registry.js";
|
||||
import { loadOpenClawPlugins, type PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import { getActivePluginRegistry } from "./runtime.js";
|
||||
import type { OpenClawPluginApi, PluginWebSearchProviderEntry } from "./types.js";
|
||||
import type { PluginWebSearchProviderEntry } from "./types.js";
|
||||
|
||||
type RegistrablePlugin = {
|
||||
id: string;
|
||||
name: string;
|
||||
register: (api: OpenClawPluginApi) => void;
|
||||
};
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
const BUNDLED_WEB_SEARCH_PLUGINS: readonly RegistrablePlugin[] = [
|
||||
bravePlugin,
|
||||
firecrawlPlugin,
|
||||
googlePlugin,
|
||||
moonshotPlugin,
|
||||
perplexityPlugin,
|
||||
xaiPlugin,
|
||||
];
|
||||
function hasExplicitPluginConfig(config: PluginLoadOptions["config"]): boolean {
|
||||
const plugins = config?.plugins;
|
||||
if (!plugins) {
|
||||
return false;
|
||||
}
|
||||
if (typeof plugins.enabled === "boolean") {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(plugins.allow) && plugins.allow.length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(plugins.deny) && plugins.deny.length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(plugins.load?.paths) && plugins.load.paths.length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (plugins.entries && Object.keys(plugins.entries).length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (plugins.slots && Object.keys(plugins.slots).length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS = BUNDLED_WEB_SEARCH_PLUGINS.map(
|
||||
(plugin) => plugin.id,
|
||||
);
|
||||
function resolveBundledWebSearchCompatPluginIds(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): string[] {
|
||||
void params;
|
||||
return pluginRegistrationContractRegistry
|
||||
.filter((plugin) => plugin.webSearchProviderIds.length > 0)
|
||||
.map((plugin) => plugin.pluginId)
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function withBundledWebSearchVitestCompat(params: {
|
||||
config: PluginLoadOptions["config"];
|
||||
pluginIds: readonly string[];
|
||||
env?: PluginLoadOptions["env"];
|
||||
}): PluginLoadOptions["config"] {
|
||||
const env = params.env ?? process.env;
|
||||
const isVitest = Boolean(env.VITEST || process.env.VITEST);
|
||||
if (!isVitest || hasExplicitPluginConfig(params.config) || params.pluginIds.length === 0) {
|
||||
return params.config;
|
||||
}
|
||||
|
||||
return {
|
||||
...params.config,
|
||||
plugins: {
|
||||
...params.config?.plugins,
|
||||
enabled: true,
|
||||
allow: [...params.pluginIds],
|
||||
slots: {
|
||||
...params.config?.plugins?.slots,
|
||||
memory: "none",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function applyVitestContractMetadataCompat(
|
||||
providers: PluginWebSearchProviderEntry[],
|
||||
env?: PluginLoadOptions["env"],
|
||||
): PluginWebSearchProviderEntry[] {
|
||||
if (!(env?.VITEST || process.env.VITEST)) {
|
||||
return providers;
|
||||
}
|
||||
|
||||
return providers.map((provider) => {
|
||||
const contract = webSearchProviderContractRegistry.find(
|
||||
(entry) => entry.pluginId === provider.pluginId && entry.provider.id === provider.id,
|
||||
);
|
||||
if (!contract) {
|
||||
return provider;
|
||||
}
|
||||
return {
|
||||
...contract.provider,
|
||||
...provider,
|
||||
credentialPath: provider.credentialPath ?? contract.provider.credentialPath,
|
||||
inactiveSecretPaths: provider.inactiveSecretPaths ?? contract.provider.inactiveSecretPaths,
|
||||
applySelectionConfig: provider.applySelectionConfig ?? contract.provider.applySelectionConfig,
|
||||
resolveRuntimeMetadata:
|
||||
provider.resolveRuntimeMetadata ?? contract.provider.resolveRuntimeMetadata,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function sortWebSearchProviders(
|
||||
providers: PluginWebSearchProviderEntry[],
|
||||
@@ -46,74 +117,52 @@ function sortWebSearchProviders(
|
||||
});
|
||||
}
|
||||
|
||||
function mapWebSearchProviderEntries(
|
||||
entries: PluginWebSearchProviderRegistration[],
|
||||
): PluginWebSearchProviderEntry[] {
|
||||
return sortWebSearchProviders(
|
||||
entries.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeWebSearchPluginConfig(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
}): PluginLoadOptions["config"] {
|
||||
const allowlistCompat = params.bundledAllowlistCompat
|
||||
? withBundledPluginAllowlistCompat({
|
||||
config: params.config,
|
||||
pluginIds: BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS,
|
||||
})
|
||||
: params.config;
|
||||
return withBundledPluginEnablementCompat({
|
||||
config: allowlistCompat,
|
||||
pluginIds: BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS,
|
||||
});
|
||||
}
|
||||
|
||||
function captureBundledWebSearchProviders(
|
||||
plugin: RegistrablePlugin,
|
||||
): PluginWebSearchProviderRegistration[] {
|
||||
const captured = capturePluginRegistration(plugin);
|
||||
return captured.webSearchProviders.map((provider) => ({
|
||||
pluginId: plugin.id,
|
||||
pluginName: plugin.name,
|
||||
provider,
|
||||
source: "bundled",
|
||||
}));
|
||||
}
|
||||
|
||||
function resolveBundledWebSearchRegistrations(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
}): PluginWebSearchProviderRegistration[] {
|
||||
const config = normalizeWebSearchPluginConfig(params);
|
||||
if (config?.plugins?.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
const allowlist = config?.plugins?.allow
|
||||
? new Set(config.plugins.allow.map((entry) => entry.trim()).filter(Boolean))
|
||||
: null;
|
||||
return BUNDLED_WEB_SEARCH_PLUGINS.flatMap((plugin) => {
|
||||
if (allowlist && !allowlist.has(plugin.id)) {
|
||||
return [];
|
||||
}
|
||||
if (config?.plugins?.entries?.[plugin.id]?.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
return captureBundledWebSearchProviders(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePluginWebSearchProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
env?: PluginLoadOptions["env"];
|
||||
bundledAllowlistCompat?: boolean;
|
||||
activate?: boolean;
|
||||
cache?: boolean;
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
return mapWebSearchProviderEntries(resolveBundledWebSearchRegistrations(params));
|
||||
const bundledCompatPluginIds = resolveBundledWebSearchCompatPluginIds({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
const allowlistCompat = params.bundledAllowlistCompat
|
||||
? withBundledPluginAllowlistCompat({
|
||||
config: params.config,
|
||||
pluginIds: bundledCompatPluginIds,
|
||||
})
|
||||
: params.config;
|
||||
const enablementCompat = withBundledPluginEnablementCompat({
|
||||
config: allowlistCompat,
|
||||
pluginIds: bundledCompatPluginIds,
|
||||
});
|
||||
const config = withBundledWebSearchVitestCompat({
|
||||
config: enablementCompat,
|
||||
pluginIds: bundledCompatPluginIds,
|
||||
env: params.env,
|
||||
});
|
||||
const registry = loadOpenClawPlugins({
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
cache: params.cache ?? false,
|
||||
activate: params.activate ?? false,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
});
|
||||
|
||||
return sortWebSearchProviders(
|
||||
applyVitestContractMetadataCompat(
|
||||
registry.webSearchProviders.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
})),
|
||||
params.env,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveRuntimeWebSearchProviders(params: {
|
||||
@@ -124,7 +173,12 @@ export function resolveRuntimeWebSearchProviders(params: {
|
||||
}): PluginWebSearchProviderEntry[] {
|
||||
const runtimeProviders = getActivePluginRegistry()?.webSearchProviders ?? [];
|
||||
if (runtimeProviders.length > 0) {
|
||||
return mapWebSearchProviderEntries(runtimeProviders);
|
||||
return sortWebSearchProviders(
|
||||
runtimeProviders.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return resolvePluginWebSearchProviders(params);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import type {
|
||||
PluginWebSearchProviderEntry,
|
||||
WebSearchCredentialResolutionSource,
|
||||
} from "../plugins/types.js";
|
||||
import { resolvePluginWebSearchProviders } from "../plugins/web-search-providers.js";
|
||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||
import { secretRefKey } from "./ref-contract.js";
|
||||
@@ -18,14 +22,8 @@ import type {
|
||||
RuntimeWebToolsMetadata,
|
||||
} from "./runtime-web-tools.types.js";
|
||||
|
||||
const PERPLEXITY_DIRECT_BASE_URL = "https://api.perplexity.ai";
|
||||
const DEFAULT_PERPLEXITY_BASE_URL = "https://openrouter.ai/api/v1";
|
||||
const PERPLEXITY_KEY_PREFIXES = ["pplx-"];
|
||||
const OPENROUTER_KEY_PREFIXES = ["sk-or-"];
|
||||
|
||||
type WebSearchProvider = string;
|
||||
|
||||
type SecretResolutionSource = "config" | "secretRef" | "env" | "missing"; // pragma: allowlist secret
|
||||
export type {
|
||||
RuntimeWebDiagnostic,
|
||||
RuntimeWebDiagnosticCode,
|
||||
@@ -42,7 +40,7 @@ type FetchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
|
||||
|
||||
type SecretResolutionResult = {
|
||||
value?: string;
|
||||
source: SecretResolutionSource;
|
||||
source: WebSearchCredentialResolutionSource;
|
||||
secretRefConfigured: boolean;
|
||||
unresolvedRefReason?: string;
|
||||
fallbackEnvVar?: string;
|
||||
@@ -198,60 +196,6 @@ async function resolveSecretInputWithEnvFallback(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function inferPerplexityBaseUrlFromApiKey(apiKey?: string): "direct" | "openrouter" | 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 resolvePerplexityRuntimeTransport(params: {
|
||||
keyValue?: string;
|
||||
keySource: SecretResolutionSource;
|
||||
fallbackEnvVar?: string;
|
||||
configValue: unknown;
|
||||
}): "search_api" | "chat_completions" | undefined {
|
||||
const config = isRecord(params.configValue) ? params.configValue : undefined;
|
||||
const configuredBaseUrl = typeof config?.baseUrl === "string" ? config.baseUrl.trim() : "";
|
||||
const configuredModel = typeof config?.model === "string" ? config.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.keyValue) {
|
||||
const inferred = inferPerplexityBaseUrlFromApiKey(params.keyValue);
|
||||
return inferred === "openrouter" ? DEFAULT_PERPLEXITY_BASE_URL : PERPLEXITY_DIRECT_BASE_URL;
|
||||
}
|
||||
return DEFAULT_PERPLEXITY_BASE_URL;
|
||||
})();
|
||||
|
||||
const hasLegacyOverride = Boolean(configuredBaseUrl || configuredModel);
|
||||
const direct = (() => {
|
||||
try {
|
||||
return new URL(baseUrl).hostname.toLowerCase() === "api.perplexity.ai";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
return hasLegacyOverride || !direct ? "chat_completions" : "search_api";
|
||||
}
|
||||
|
||||
function ensureObject(target: Record<string, unknown>, key: string): Record<string, unknown> {
|
||||
const current = target[key];
|
||||
if (isRecord(current)) {
|
||||
@@ -291,8 +235,14 @@ function setResolvedFirecrawlApiKey(params: {
|
||||
firecrawl.apiKey = params.value;
|
||||
}
|
||||
|
||||
function keyPathForProvider(provider: WebSearchProvider): string {
|
||||
return provider === "brave" ? "tools.web.search.apiKey" : `tools.web.search.${provider}.apiKey`;
|
||||
function keyPathForProvider(provider: PluginWebSearchProviderEntry): string {
|
||||
return provider.credentialPath;
|
||||
}
|
||||
|
||||
function inactivePathsForProvider(provider: PluginWebSearchProviderEntry): string[] {
|
||||
return provider.inactiveSecretPaths?.length
|
||||
? provider.inactiveSecretPaths
|
||||
: [provider.credentialPath];
|
||||
}
|
||||
|
||||
function hasConfiguredSecretRef(value: unknown, defaults: SecretDefaults | undefined): boolean {
|
||||
@@ -367,7 +317,7 @@ export async function resolveRuntimeWebTools(params: {
|
||||
let selectedResolution: SecretResolutionResult | undefined;
|
||||
|
||||
for (const provider of candidates) {
|
||||
const path = keyPathForProvider(provider.id);
|
||||
const path = keyPathForProvider(provider);
|
||||
const value = provider.getCredentialValue(search);
|
||||
const resolution = await resolveSecretInputWithEnvFallback({
|
||||
sourceConfig: params.sourceConfig,
|
||||
@@ -475,13 +425,23 @@ export async function resolveRuntimeWebTools(params: {
|
||||
if (!configuredProvider) {
|
||||
searchMetadata.providerSource = "auto-detect";
|
||||
}
|
||||
if (selectedProvider === "perplexity") {
|
||||
searchMetadata.perplexityTransport = resolvePerplexityRuntimeTransport({
|
||||
keyValue: selectedResolution?.value,
|
||||
keySource: selectedResolution?.source ?? "missing",
|
||||
fallbackEnvVar: selectedResolution?.fallbackEnvVar,
|
||||
configValue: search.perplexity,
|
||||
});
|
||||
const provider = providers.find((entry) => entry.id === selectedProvider);
|
||||
if (provider?.resolveRuntimeMetadata) {
|
||||
Object.assign(
|
||||
searchMetadata,
|
||||
await provider.resolveRuntimeMetadata({
|
||||
config: params.sourceConfig,
|
||||
searchConfig: search,
|
||||
runtimeMetadata: searchMetadata,
|
||||
resolvedCredential: selectedResolution
|
||||
? {
|
||||
value: selectedResolution.value,
|
||||
source: selectedResolution.source,
|
||||
fallbackEnvVar: selectedResolution.fallbackEnvVar,
|
||||
}
|
||||
: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,29 +451,31 @@ export async function resolveRuntimeWebTools(params: {
|
||||
if (provider.id === searchMetadata.selectedProvider) {
|
||||
continue;
|
||||
}
|
||||
const path = keyPathForProvider(provider.id);
|
||||
const value = provider.getCredentialValue(search);
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
}
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: `tools.web.search auto-detected provider is "${searchMetadata.selectedProvider}".`,
|
||||
});
|
||||
for (const path of inactivePathsForProvider(provider)) {
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: `tools.web.search auto-detected provider is "${searchMetadata.selectedProvider}".`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (search && !searchEnabled) {
|
||||
for (const provider of providers) {
|
||||
const path = keyPathForProvider(provider.id);
|
||||
const value = provider.getCredentialValue(search);
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
}
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: "tools.web.search is disabled.",
|
||||
});
|
||||
for (const path of inactivePathsForProvider(provider)) {
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: "tools.web.search is disabled.",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,16 +484,17 @@ export async function resolveRuntimeWebTools(params: {
|
||||
if (provider.id === configuredProvider) {
|
||||
continue;
|
||||
}
|
||||
const path = keyPathForProvider(provider.id);
|
||||
const value = provider.getCredentialValue(search);
|
||||
if (!hasConfiguredSecretRef(value, defaults)) {
|
||||
continue;
|
||||
}
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: `tools.web.search.provider is "${configuredProvider}".`,
|
||||
});
|
||||
for (const path of inactivePathsForProvider(provider)) {
|
||||
pushInactiveSurfaceWarning({
|
||||
context: params.context,
|
||||
path,
|
||||
details: `tools.web.search.provider is "${configuredProvider}".`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user