mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 04:40:43 +00:00
feat(brave): add http diagnostics flag
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Crestodian/CLI: exit non-zero when interactive Crestodian is invoked without a TTY, so scripts and CI no longer treat the setup error as success. Fixes #73646 and supersedes #73928 and #74059. Thanks @bittoby, @luyao618, and @Linux2010.
|
||||
- Cron: keep implicit/default isolated cron announce deliveries out of the main session awareness queue, so isolated jobs do not accumulate in the main conversation. Fixes #61426. Thanks @Lihannon.
|
||||
- Subagents: avoid duplicate parent-visible replies when a parent uses `sessions_send` on its own persistent native subagent session, while preserving announce delivery for async sends. Fixes #73550. Thanks @sylviazhang2006-design.
|
||||
- Web search/Brave: add opt-in `brave.http` diagnostics for Brave request URLs/query params, response status/timing, and cache hit/miss/write events without logging API keys or response bodies. Fixes #55196. Thanks @mecampbellsoup.
|
||||
- Agents/sandbox: preserve existing workspace file modes when sandbox edits atomically replace files, so 0644 files do not collapse to 0600 after Write/Edit/apply_patch. Fixes #44077. Thanks @patosullivan.
|
||||
- Agents/models: keep legacy CLI runtime model refs such as `claude-cli/*` in the configured allowlist after canonical runtime migration, so cron `payload.model` overrides keep working. Fixes #75753. Thanks @RyanSandoval.
|
||||
- Codex/app-server: restart the shared Codex app-server client once when it closes during startup thread resume, preserving the existing thread binding instead of retrying `thread/start` on a closed client. Thanks @vincentkoc.
|
||||
|
||||
@@ -31,7 +31,7 @@ Multiple flags:
|
||||
```json
|
||||
{
|
||||
"diagnostics": {
|
||||
"flags": ["telegram.http", "gateway.*"]
|
||||
"flags": ["telegram.http", "brave.http", "gateway.*"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -111,6 +111,12 @@ Filter for Telegram HTTP diagnostics:
|
||||
rg "telegram http error" /tmp/openclaw/openclaw-*.log
|
||||
```
|
||||
|
||||
Filter for Brave Search HTTP diagnostics:
|
||||
|
||||
```bash
|
||||
rg "brave http" /tmp/openclaw/openclaw-*.log
|
||||
```
|
||||
|
||||
Or tail while reproducing:
|
||||
|
||||
```bash
|
||||
@@ -122,6 +128,7 @@ For remote gateways, you can also use `openclaw logs --follow` (see [/cli/logs](
|
||||
## Notes
|
||||
|
||||
- If `logging.level` is set higher than `warn`, these logs may be suppressed. Default `info` is fine.
|
||||
- `brave.http` logs Brave Search request URLs/query params, response status/timing, and cache hit/miss/write events. It does not log API keys or response bodies, but search queries can be sensitive.
|
||||
- Flags are safe to leave enabled; they only affect log volume for the specific subsystem.
|
||||
- Use [/logging](/logging) to change log destinations, levels, and redaction.
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ await web_search({
|
||||
- `llm-context` mode supports `freshness` and bounded `date_after` + `date_before` ranges. It does not support `ui_lang`; `date_before` without `date_after` is rejected because Brave requires custom freshness ranges to include both start and end dates.
|
||||
- `ui_lang` must include a region subtag like `en-US`.
|
||||
- Results are cached for 15 minutes by default (configurable via `cacheTtlMinutes`).
|
||||
- Enable the `brave.http` diagnostics flag to log Brave request URLs/query params, response status/timing, and search-cache hit/miss/write events while troubleshooting. The flag never logs the API key or response bodies, but search queries can be sensitive.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
wrapWebContent,
|
||||
writeCachedSearchPayload,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import {
|
||||
type BraveLlmContextResponse,
|
||||
mapBraveLlmContextResults,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
|
||||
const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
|
||||
const BRAVE_LLM_CONTEXT_ENDPOINT = "https://api.search.brave.com/res/v1/llm/context";
|
||||
const braveHttpLogger = createSubsystemLogger("brave/http");
|
||||
|
||||
type BraveSearchResult = {
|
||||
title?: string;
|
||||
@@ -43,6 +45,33 @@ type BraveSearchResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
type BraveHttpDiagnostics = {
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
function logBraveHttp(
|
||||
diagnostics: BraveHttpDiagnostics | undefined,
|
||||
event: string,
|
||||
meta?: Record<string, unknown>,
|
||||
): void {
|
||||
if (!diagnostics?.enabled) {
|
||||
return;
|
||||
}
|
||||
braveHttpLogger.info(`brave http ${event}`, meta);
|
||||
}
|
||||
|
||||
function describeBraveRequestUrl(url: URL): {
|
||||
url: string;
|
||||
query: string;
|
||||
params: Record<string, string>;
|
||||
} {
|
||||
return {
|
||||
url: url.toString(),
|
||||
query: url.searchParams.get("q") ?? "",
|
||||
params: Object.fromEntries(url.searchParams.entries()),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBraveApiKey(searchConfig?: SearchConfigRecord): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ??
|
||||
@@ -62,6 +91,7 @@ async function runBraveLlmContextSearch(params: {
|
||||
query: string;
|
||||
apiKey: string;
|
||||
timeoutSeconds: number;
|
||||
diagnostics?: BraveHttpDiagnostics;
|
||||
country?: string;
|
||||
search_lang?: string;
|
||||
freshness?: string;
|
||||
@@ -95,6 +125,11 @@ async function runBraveLlmContextSearch(params: {
|
||||
);
|
||||
}
|
||||
|
||||
logBraveHttp(params.diagnostics, "request", {
|
||||
mode: "llm-context",
|
||||
...describeBraveRequestUrl(url),
|
||||
});
|
||||
const startedAt = Date.now();
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
url: url.toString(),
|
||||
@@ -108,6 +143,12 @@ async function runBraveLlmContextSearch(params: {
|
||||
},
|
||||
},
|
||||
async (response) => {
|
||||
logBraveHttp(params.diagnostics, "response", {
|
||||
mode: "llm-context",
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(
|
||||
@@ -126,6 +167,7 @@ async function runBraveWebSearch(params: {
|
||||
count: number;
|
||||
apiKey: string;
|
||||
timeoutSeconds: number;
|
||||
diagnostics?: BraveHttpDiagnostics;
|
||||
country?: string;
|
||||
search_lang?: string;
|
||||
ui_lang?: string;
|
||||
@@ -158,6 +200,11 @@ async function runBraveWebSearch(params: {
|
||||
url.searchParams.set("freshness", `1970-01-01to${params.dateBefore}`);
|
||||
}
|
||||
|
||||
logBraveHttp(params.diagnostics, "request", {
|
||||
mode: "web",
|
||||
...describeBraveRequestUrl(url),
|
||||
});
|
||||
const startedAt = Date.now();
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
url: url.toString(),
|
||||
@@ -171,6 +218,12 @@ async function runBraveWebSearch(params: {
|
||||
},
|
||||
},
|
||||
async (response) => {
|
||||
logBraveHttp(params.diagnostics, "response", {
|
||||
mode: "web",
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
durationMs: Date.now() - startedAt,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
throw new Error(
|
||||
@@ -199,6 +252,9 @@ async function runBraveWebSearch(params: {
|
||||
export async function executeBraveSearch(
|
||||
args: Record<string, unknown>,
|
||||
searchConfig?: SearchConfigRecord,
|
||||
options?: {
|
||||
diagnosticsEnabled?: boolean;
|
||||
},
|
||||
): Promise<Record<string, unknown>> {
|
||||
const apiKey = resolveBraveApiKey(searchConfig);
|
||||
if (!apiKey) {
|
||||
@@ -322,10 +378,13 @@ export async function executeBraveSearch(
|
||||
dateBefore,
|
||||
],
|
||||
);
|
||||
const diagnostics: BraveHttpDiagnostics = { enabled: options?.diagnosticsEnabled === true };
|
||||
const cached = readCachedSearchPayload(cacheKey);
|
||||
if (cached) {
|
||||
logBraveHttp(diagnostics, "cache hit", { mode: braveMode, query, cacheKey });
|
||||
return cached;
|
||||
}
|
||||
logBraveHttp(diagnostics, "cache miss", { mode: braveMode, query, cacheKey });
|
||||
|
||||
const start = Date.now();
|
||||
const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);
|
||||
@@ -336,6 +395,7 @@ export async function executeBraveSearch(
|
||||
query,
|
||||
apiKey,
|
||||
timeoutSeconds,
|
||||
diagnostics,
|
||||
country: country ?? undefined,
|
||||
search_lang: normalizedLanguage.search_lang,
|
||||
freshness,
|
||||
@@ -363,6 +423,13 @@ export async function executeBraveSearch(
|
||||
sources,
|
||||
};
|
||||
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
||||
logBraveHttp(diagnostics, "cache write", {
|
||||
mode: "llm-context",
|
||||
query,
|
||||
cacheKey,
|
||||
ttlMs: cacheTtlMs,
|
||||
count: results.length,
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -371,6 +438,7 @@ export async function executeBraveSearch(
|
||||
count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
|
||||
apiKey,
|
||||
timeoutSeconds,
|
||||
diagnostics,
|
||||
country: country ?? undefined,
|
||||
search_lang: normalizedLanguage.search_lang,
|
||||
ui_lang: normalizedLanguage.ui_lang,
|
||||
@@ -392,5 +460,12 @@ export async function executeBraveSearch(
|
||||
results,
|
||||
};
|
||||
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
||||
logBraveHttp(diagnostics, "cache write", {
|
||||
mode: "web",
|
||||
query,
|
||||
cacheKey,
|
||||
ttlMs: cacheTtlMs,
|
||||
count: results.length,
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,32 @@ import { __testing } from "../test-api.js";
|
||||
import { createBraveWebSearchProvider as createBraveWebSearchContractProvider } from "../web-search-contract-api.js";
|
||||
import { createBraveWebSearchProvider } from "./brave-web-search-provider.js";
|
||||
|
||||
const loggerInfoMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
createSubsystemLogger: () => ({
|
||||
info: loggerInfoMock,
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
raw: vi.fn(),
|
||||
isEnabled: () => true,
|
||||
child: () => ({
|
||||
info: loggerInfoMock,
|
||||
debug: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
fatal: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
raw: vi.fn(),
|
||||
isEnabled: () => true,
|
||||
child: vi.fn(),
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
|
||||
const braveManifest = JSON.parse(
|
||||
fs.readFileSync(new URL("../openclaw.plugin.json", import.meta.url), "utf-8"),
|
||||
) as {
|
||||
@@ -46,6 +72,7 @@ describe("brave web search provider", () => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
loggerInfoMock.mockClear();
|
||||
global.fetch = priorFetch;
|
||||
});
|
||||
|
||||
@@ -422,4 +449,77 @@ describe("brave web search provider", () => {
|
||||
const requestUrl = new URL(String(mockFetch.mock.calls[0]?.[0]));
|
||||
expect(requestUrl.searchParams.get("country")).toBe("ALL");
|
||||
});
|
||||
|
||||
it("emits brave.http diagnostics for requests, responses, and cache events", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "");
|
||||
const mockFetch = vi.fn(async (_input?: unknown, _init?: unknown) => {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
web: {
|
||||
results: [
|
||||
{
|
||||
title: "Diagnostics",
|
||||
url: "https://example.com/diagnostics",
|
||||
description: "debug details",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
} as Response;
|
||||
});
|
||||
global.fetch = mockFetch as typeof global.fetch;
|
||||
|
||||
const provider = createBraveWebSearchProvider();
|
||||
const tool = provider.createTool({
|
||||
config: { diagnostics: { flags: ["brave.http"] } },
|
||||
searchConfig: {
|
||||
apiKey: "brave-test-key",
|
||||
brave: { mode: "web" },
|
||||
},
|
||||
});
|
||||
if (!tool) {
|
||||
throw new Error("Expected tool definition");
|
||||
}
|
||||
|
||||
await tool.execute({ query: "unique brave diagnostics query", count: 1 });
|
||||
await tool.execute({ query: "unique brave diagnostics query", count: 1 });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
const messages = loggerInfoMock.mock.calls.map((call) => call[0]);
|
||||
expect(messages).toEqual(
|
||||
expect.arrayContaining([
|
||||
"brave http cache miss",
|
||||
"brave http request",
|
||||
"brave http response",
|
||||
"brave http cache write",
|
||||
"brave http cache hit",
|
||||
]),
|
||||
);
|
||||
expect(loggerInfoMock.mock.calls).toEqual(
|
||||
expect.arrayContaining([
|
||||
[
|
||||
"brave http request",
|
||||
expect.objectContaining({
|
||||
mode: "web",
|
||||
query: "unique brave diagnostics query",
|
||||
params: expect.objectContaining({ q: "unique brave diagnostics query", count: "1" }),
|
||||
url: expect.stringContaining("api.search.brave.com/res/v1/web/search"),
|
||||
}),
|
||||
],
|
||||
[
|
||||
"brave http response",
|
||||
expect.objectContaining({
|
||||
mode: "web",
|
||||
status: 200,
|
||||
ok: true,
|
||||
durationMs: expect.any(Number),
|
||||
}),
|
||||
],
|
||||
]),
|
||||
);
|
||||
expect(JSON.stringify(loggerInfoMock.mock.calls)).not.toContain("brave-test-key");
|
||||
expect(JSON.stringify(loggerInfoMock.mock.calls)).not.toContain("X-Subscription-Token");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isDiagnosticFlagEnabled } from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import type {
|
||||
SearchConfigRecord,
|
||||
WebSearchProviderPlugin,
|
||||
@@ -111,8 +112,10 @@ function resolveBraveMode(searchConfig?: Record<string, unknown>): "web" | "llm-
|
||||
|
||||
function createBraveToolDefinition(
|
||||
searchConfig?: SearchConfigRecord,
|
||||
config?: Parameters<typeof isDiagnosticFlagEnabled>[1],
|
||||
): WebSearchProviderToolDefinition {
|
||||
const braveMode = resolveBraveMode(searchConfig);
|
||||
const diagnosticsEnabled = isDiagnosticFlagEnabled("brave.http", config);
|
||||
|
||||
return {
|
||||
description:
|
||||
@@ -122,7 +125,7 @@ function createBraveToolDefinition(
|
||||
parameters: BraveSearchSchema,
|
||||
execute: async (args) => {
|
||||
const { executeBraveSearch } = await loadBraveWebSearchRuntime();
|
||||
return await executeBraveSearch(args, searchConfig);
|
||||
return await executeBraveSearch(args, searchConfig, { diagnosticsEnabled });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -153,6 +156,7 @@ export function createBraveWebSearchProvider(): WebSearchProviderPlugin {
|
||||
resolveProviderWebSearchPluginConfig(ctx.config, "brave"),
|
||||
{ mirrorApiKeyToTopLevel: true },
|
||||
),
|
||||
ctx.config,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user