Merge a0c35a20fd into bc432d8435
This commit is contained in:
commit
490125f045
@ -86,6 +86,42 @@ current limits and pricing.
|
||||
environment. For a gateway install, put it in `~/.openclaw/.env` (or your
|
||||
service environment). See [Env vars](/help/faq#how-does-openclaw-load-environment-variables).
|
||||
|
||||
### Custom Brave Search endpoint
|
||||
|
||||
If you're running a self-hosted Brave Search instance or using a proxy, set `tools.web.search.baseUrl` to your custom endpoint:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
baseUrl: "https://your-brave-instance.example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The path `/res/v1/web/search` will be automatically appended to the base URL.
|
||||
|
||||
### Auth header style
|
||||
|
||||
Brave's official API uses `X-Subscription-Token` for authentication. Some proxies expect `Authorization: Bearer` instead. Set `authStyle` to switch:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
baseUrl: "https://your-proxy.example.com",
|
||||
authStyle: "bearer" // Uses "Authorization: Bearer <key>"
|
||||
// authStyle: "x-subscription-token" // Default: uses "X-Subscription-Token: <key>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Using Perplexity (direct or via OpenRouter)
|
||||
|
||||
Perplexity Sonar models have built-in web search capabilities and return AI-synthesized
|
||||
@ -158,6 +194,8 @@ Search the web using your configured provider.
|
||||
search: {
|
||||
enabled: true,
|
||||
apiKey: "BRAVE_API_KEY_HERE", // optional if BRAVE_API_KEY is set
|
||||
baseUrl: "https://api.search.brave.com", // optional: custom Brave Search base URL
|
||||
authStyle: "x-subscription-token", // optional: "x-subscription-token" (default) or "bearer" (for proxies)
|
||||
maxResults: 5,
|
||||
timeoutSeconds: 30,
|
||||
cacheTtlMinutes: 15
|
||||
|
||||
@ -2,8 +2,13 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { __testing } from "./web-search.js";
|
||||
|
||||
const { inferPerplexityBaseUrlFromApiKey, resolvePerplexityBaseUrl, normalizeFreshness } =
|
||||
__testing;
|
||||
const {
|
||||
inferPerplexityBaseUrlFromApiKey,
|
||||
resolvePerplexityBaseUrl,
|
||||
resolveBraveBaseUrl,
|
||||
resolveBraveAuthStyle,
|
||||
normalizeFreshness,
|
||||
} = __testing;
|
||||
|
||||
describe("web_search perplexity baseUrl defaults", () => {
|
||||
it("detects a Perplexity key prefix", () => {
|
||||
@ -53,6 +58,65 @@ describe("web_search perplexity baseUrl defaults", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("web_search brave baseUrl defaults", () => {
|
||||
it("returns default Brave URL when no config is provided", () => {
|
||||
expect(resolveBraveBaseUrl(undefined)).toBe("https://api.search.brave.com");
|
||||
});
|
||||
|
||||
it("returns default Brave URL when config is empty", () => {
|
||||
expect(resolveBraveBaseUrl({})).toBe("https://api.search.brave.com");
|
||||
});
|
||||
|
||||
it("uses custom baseUrl from config", () => {
|
||||
expect(resolveBraveBaseUrl({ baseUrl: "https://custom-brave.example.com" })).toBe(
|
||||
"https://custom-brave.example.com",
|
||||
);
|
||||
});
|
||||
|
||||
it("trims whitespace from custom baseUrl", () => {
|
||||
expect(resolveBraveBaseUrl({ baseUrl: " https://custom-brave.example.com " })).toBe(
|
||||
"https://custom-brave.example.com",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns default when baseUrl is empty string", () => {
|
||||
expect(resolveBraveBaseUrl({ baseUrl: "" })).toBe("https://api.search.brave.com");
|
||||
});
|
||||
|
||||
it("returns default when baseUrl is only whitespace", () => {
|
||||
expect(resolveBraveBaseUrl({ baseUrl: " " })).toBe("https://api.search.brave.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("web_search brave authStyle defaults", () => {
|
||||
it("returns x-subscription-token when no config is provided", () => {
|
||||
expect(resolveBraveAuthStyle(undefined)).toBe("x-subscription-token");
|
||||
});
|
||||
|
||||
it("returns x-subscription-token when config is empty", () => {
|
||||
expect(resolveBraveAuthStyle({})).toBe("x-subscription-token");
|
||||
});
|
||||
|
||||
it("returns bearer when authStyle is bearer", () => {
|
||||
expect(resolveBraveAuthStyle({ authStyle: "bearer" })).toBe("bearer");
|
||||
});
|
||||
|
||||
it("returns x-subscription-token when authStyle is x-subscription-token", () => {
|
||||
expect(resolveBraveAuthStyle({ authStyle: "x-subscription-token" })).toBe(
|
||||
"x-subscription-token",
|
||||
);
|
||||
});
|
||||
|
||||
it("is case-insensitive", () => {
|
||||
expect(resolveBraveAuthStyle({ authStyle: "BEARER" } as never)).toBe("bearer");
|
||||
expect(resolveBraveAuthStyle({ authStyle: "Bearer" } as never)).toBe("bearer");
|
||||
});
|
||||
|
||||
it("returns default for unknown values", () => {
|
||||
expect(resolveBraveAuthStyle({ authStyle: "unknown" } as never)).toBe("x-subscription-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("web_search freshness normalization", () => {
|
||||
it("accepts Brave shortcut values", () => {
|
||||
expect(normalizeFreshness("pd")).toBe("pd");
|
||||
|
||||
@ -18,10 +18,12 @@ import {
|
||||
} from "./web-shared.js";
|
||||
|
||||
const SEARCH_PROVIDERS = ["brave", "perplexity"] as const;
|
||||
const BRAVE_AUTH_STYLES = ["x-subscription-token", "bearer"] as const;
|
||||
const DEFAULT_SEARCH_COUNT = 5;
|
||||
const MAX_SEARCH_COUNT = 10;
|
||||
|
||||
const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
|
||||
const DEFAULT_BRAVE_BASE_URL = "https://api.search.brave.com";
|
||||
const BRAVE_SEARCH_PATH = "/res/v1/web/search";
|
||||
const DEFAULT_PERPLEXITY_BASE_URL = "https://openrouter.ai/api/v1";
|
||||
const PERPLEXITY_DIRECT_BASE_URL = "https://api.perplexity.ai";
|
||||
const DEFAULT_PERPLEXITY_MODEL = "perplexity/sonar-pro";
|
||||
@ -122,6 +124,23 @@ function resolveSearchApiKey(search?: WebSearchConfig): string | undefined {
|
||||
return fromConfig || fromEnv || undefined;
|
||||
}
|
||||
|
||||
function resolveBraveBaseUrl(search?: WebSearchConfig): string {
|
||||
const fromConfig =
|
||||
search && "baseUrl" in search && typeof search.baseUrl === "string"
|
||||
? search.baseUrl.trim()
|
||||
: "";
|
||||
return fromConfig || DEFAULT_BRAVE_BASE_URL;
|
||||
}
|
||||
|
||||
function resolveBraveAuthStyle(search?: WebSearchConfig): (typeof BRAVE_AUTH_STYLES)[number] {
|
||||
const raw =
|
||||
search && "authStyle" in search && typeof search.authStyle === "string"
|
||||
? search.authStyle.trim().toLowerCase()
|
||||
: "";
|
||||
if (raw === "bearer") return "bearer";
|
||||
return "x-subscription-token";
|
||||
}
|
||||
|
||||
function missingSearchKeyPayload(provider: (typeof SEARCH_PROVIDERS)[number]) {
|
||||
if (provider === "perplexity") {
|
||||
return {
|
||||
@ -317,6 +336,8 @@ async function runWebSearch(params: {
|
||||
search_lang?: string;
|
||||
ui_lang?: string;
|
||||
freshness?: string;
|
||||
braveBaseUrl?: string;
|
||||
braveAuthStyle?: (typeof BRAVE_AUTH_STYLES)[number];
|
||||
perplexityBaseUrl?: string;
|
||||
perplexityModel?: string;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
@ -355,7 +376,8 @@ async function runWebSearch(params: {
|
||||
throw new Error("Unsupported web search provider.");
|
||||
}
|
||||
|
||||
const url = new URL(BRAVE_SEARCH_ENDPOINT);
|
||||
const baseUrl = params.braveBaseUrl ?? DEFAULT_BRAVE_BASE_URL;
|
||||
const url = new URL(BRAVE_SEARCH_PATH, baseUrl);
|
||||
url.searchParams.set("q", params.query);
|
||||
url.searchParams.set("count", String(params.count));
|
||||
if (params.country) {
|
||||
@ -371,11 +393,16 @@ async function runWebSearch(params: {
|
||||
url.searchParams.set("freshness", params.freshness);
|
||||
}
|
||||
|
||||
// Build auth header based on configured style (default: X-Subscription-Token for Brave)
|
||||
const authStyle = params.braveAuthStyle ?? "x-subscription-token";
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"X-Subscription-Token": params.apiKey,
|
||||
...(authStyle === "bearer"
|
||||
? { Authorization: `Bearer ${params.apiKey}` }
|
||||
: { "X-Subscription-Token": params.apiKey }),
|
||||
},
|
||||
signal: withTimeout(undefined, params.timeoutSeconds * 1000),
|
||||
});
|
||||
@ -470,6 +497,8 @@ export function createWebSearchTool(options?: {
|
||||
search_lang,
|
||||
ui_lang,
|
||||
freshness,
|
||||
braveBaseUrl: resolveBraveBaseUrl(search),
|
||||
braveAuthStyle: resolveBraveAuthStyle(search),
|
||||
perplexityBaseUrl: resolvePerplexityBaseUrl(
|
||||
perplexityConfig,
|
||||
perplexityAuth?.source,
|
||||
@ -485,5 +514,7 @@ export function createWebSearchTool(options?: {
|
||||
export const __testing = {
|
||||
inferPerplexityBaseUrlFromApiKey,
|
||||
resolvePerplexityBaseUrl,
|
||||
resolveBraveBaseUrl,
|
||||
resolveBraveAuthStyle,
|
||||
normalizeFreshness,
|
||||
} as const;
|
||||
|
||||
@ -437,6 +437,10 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"tools.web.search.enabled": "Enable the web_search tool (requires a provider API key).",
|
||||
"tools.web.search.provider": 'Search provider ("brave" or "perplexity").',
|
||||
"tools.web.search.apiKey": "Brave Search API key (fallback: BRAVE_API_KEY env var).",
|
||||
"tools.web.search.baseUrl":
|
||||
"Brave Search base URL override (default: https://api.search.brave.com).",
|
||||
"tools.web.search.authStyle":
|
||||
'Auth header style: "x-subscription-token" (Brave default) or "bearer" (for select proxies).',
|
||||
"tools.web.search.maxResults": "Default number of results to return (1-10).",
|
||||
"tools.web.search.timeoutSeconds": "Timeout in seconds for web_search requests.",
|
||||
"tools.web.search.cacheTtlMinutes": "Cache TTL in minutes for web_search results.",
|
||||
|
||||
@ -340,6 +340,10 @@ export type ToolsConfig = {
|
||||
provider?: "brave" | "perplexity";
|
||||
/** Brave Search API key (optional; defaults to BRAVE_API_KEY env var). */
|
||||
apiKey?: string;
|
||||
/** Brave Search base URL (optional; defaults to https://api.search.brave.com). */
|
||||
baseUrl?: string;
|
||||
/** Auth header style: "x-subscription-token" (Brave default) or "bearer" (for proxies). */
|
||||
authStyle?: "x-subscription-token" | "bearer";
|
||||
/** Default search results count (1-10). */
|
||||
maxResults?: number;
|
||||
/** Timeout in seconds for search requests. */
|
||||
|
||||
@ -167,6 +167,8 @@ export const ToolsWebSearchSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
provider: z.union([z.literal("brave"), z.literal("perplexity")]).optional(),
|
||||
apiKey: z.string().optional(),
|
||||
baseUrl: z.string().optional(),
|
||||
authStyle: z.union([z.literal("x-subscription-token"), z.literal("bearer")]).optional(),
|
||||
maxResults: z.number().int().positive().optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
cacheTtlMinutes: z.number().nonnegative().optional(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user