mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 06:52:07 +08:00
Compare commits
3 Commits
codex/fix-
...
fix/codex-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a91af22a5 | ||
|
|
e4bfcff5a8 | ||
|
|
c42dc2e8c2 |
@@ -11481,7 +11481,7 @@
|
||||
"filename": "src/agents/models-config.e2e-harness.ts",
|
||||
"hashed_secret": "7cf31e8b6cda49f70c31f1f25af05d46f924142d",
|
||||
"is_verified": false,
|
||||
"line_number": 131
|
||||
"line_number": 130
|
||||
}
|
||||
],
|
||||
"src/agents/models-config.fills-missing-provider-apikey-from-env-var.e2e.test.ts": [
|
||||
@@ -13034,5 +13034,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2026-03-08T13:52:40Z"
|
||||
"generated_at": "2026-03-08T05:05:36Z"
|
||||
}
|
||||
|
||||
@@ -113,7 +113,6 @@
|
||||
|
||||
**Full maintainer PR workflow (optional):** If you want the repo's end-to-end maintainer workflow (triage order, quality bar, rebase rules, commit/changelog conventions, co-contributor policy, and the `review-pr` > `prepare-pr` > `merge-pr` pipeline), see `.agents/skills/PR_WORKFLOW.md`. Maintainers may use other workflows; when a maintainer specifies a workflow, follow that. If no workflow is specified, default to PR_WORKFLOW.
|
||||
|
||||
- `/landpr` lives in the global Codex prompts (`~/.codex/prompts/landpr.md`); when landing or merging any PR, always follow that `/landpr` process.
|
||||
- Create commits with `scripts/committer "<msg>" <file...>`; avoid manual `git add`/`git commit` so staging stays scoped.
|
||||
- Follow concise, action-oriented commit messages (e.g., `CLI: add verbose flag to send`).
|
||||
- Group related changes; avoid bundling unrelated refactors.
|
||||
|
||||
@@ -7,7 +7,6 @@ Docs: https://docs.openclaw.ai
|
||||
### Changes
|
||||
|
||||
- TUI: infer the active agent from the current workspace when launched inside a configured agent workspace, while preserving explicit `agent:` session targets. (#39591) thanks @arceus77-7.
|
||||
- Tools/Brave web search: add opt-in `tools.web.search.brave.mode: "llm-context"` so `web_search` can call Brave's LLM Context endpoint and return extracted grounding snippets with source metadata, plus config/docs/test coverage. (#33383) Thanks @thirumaleshp.
|
||||
|
||||
### Fixes
|
||||
|
||||
@@ -15,9 +14,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Mattermost replies: keep `root_id` pinned to the existing thread root when an agent replies inside a thread, while still using reply-target threading for top-level posts. (#27744) thanks @hnykda.
|
||||
- Agents/failover: detect Amazon Bedrock `Too many tokens per day` quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window `too many tokens per request` errors out of the rate-limit lane. (#39377) Thanks @gambletan.
|
||||
- Android/Play distribution: remove self-update, background location, `screen.record`, and background mic capture from the Android app, narrow the foreground service to `dataSync` only, and clean up the legacy `location.enabledMode=always` preference migration. (#39660) Thanks @obviyus.
|
||||
- macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared `inout` visibility mutation from `OverlayPanelFactory.present`, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.
|
||||
- macOS Talk Mode: set the speech recognition request `taskHint` to `.dictation` for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.
|
||||
- macOS release packaging: default `scripts/package-mac-app.sh` to universal binaries for `BUILD_CONFIG=release`, and clarify that `scripts/package-mac-dist.sh` already produces the release zip + DMG. (#33891) Thanks @cgdusek.
|
||||
- Agents/openai-codex model resolution: fall through from inline `openai-codex` model entries without an `api` so GPT-5.4 keeps the codex transport and still preserves configured `baseUrl` and headers. (#39753) Thanks @justinhuangcode.
|
||||
|
||||
## 2026.3.7
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ enum HostEnvSecurityPolicy {
|
||||
"PS4",
|
||||
"GCONV_PATH",
|
||||
"IFS",
|
||||
"SSLKEYLOGFILE",
|
||||
"SSLKEYLOGFILE"
|
||||
]
|
||||
|
||||
static let blockedOverrideKeys: Set<String> = [
|
||||
@@ -50,17 +50,17 @@ enum HostEnvSecurityPolicy {
|
||||
"OPENSSL_ENGINES",
|
||||
"PYTHONSTARTUP",
|
||||
"WGETRC",
|
||||
"CURL_HOME",
|
||||
"CURL_HOME"
|
||||
]
|
||||
|
||||
static let blockedOverridePrefixes: [String] = [
|
||||
"GIT_CONFIG_",
|
||||
"NPM_CONFIG_",
|
||||
"NPM_CONFIG_"
|
||||
]
|
||||
|
||||
static let blockedPrefixes: [String] = [
|
||||
"DYLD_",
|
||||
"LD_",
|
||||
"BASH_FUNC_",
|
||||
"BASH_FUNC_"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -70,11 +70,6 @@ actor TalkModeRuntime {
|
||||
private let minSpeechRMS: Double = 1e-3
|
||||
private let speechBoostFactor: Double = 6.0
|
||||
|
||||
static func configureRecognitionRequest(_ request: SFSpeechAudioBufferRecognitionRequest) {
|
||||
request.shouldReportPartialResults = true
|
||||
request.taskHint = .dictation
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
func setEnabled(_ enabled: Bool) async {
|
||||
@@ -181,9 +176,9 @@ actor TalkModeRuntime {
|
||||
return
|
||||
}
|
||||
|
||||
let request = SFSpeechAudioBufferRecognitionRequest()
|
||||
Self.configureRecognitionRequest(request)
|
||||
self.recognitionRequest = request
|
||||
self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
|
||||
self.recognitionRequest?.shouldReportPartialResults = true
|
||||
guard let request = self.recognitionRequest else { return }
|
||||
|
||||
if self.audioEngine == nil {
|
||||
self.audioEngine = AVAudioEngine()
|
||||
|
||||
@@ -66,15 +66,6 @@ struct LowCoverageViewSmokeTests {
|
||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||
}
|
||||
|
||||
@Test func `talk overlay presents twice and dismisses`() async {
|
||||
let controller = TalkOverlayController()
|
||||
controller.present()
|
||||
controller.updateLevel(0.4)
|
||||
controller.present()
|
||||
controller.dismiss()
|
||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||
}
|
||||
|
||||
@Test func `visual effect view hosts in NS hosting view`() {
|
||||
let hosting = NSHostingView(rootView: VisualEffectView(material: .sidebar))
|
||||
_ = hosting.fittingSize
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import Speech
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct TalkModeRuntimeSpeechTests {
|
||||
@Test func `speech request uses dictation defaults`() {
|
||||
let request = SFSpeechAudioBufferRecognitionRequest()
|
||||
|
||||
TalkModeRuntime.configureRecognitionRequest(request)
|
||||
|
||||
#expect(request.shouldReportPartialResults)
|
||||
#expect(request.taskHint == .dictation)
|
||||
}
|
||||
}
|
||||
@@ -29,27 +29,23 @@ Notes:
|
||||
- `APP_BUILD` maps to `CFBundleVersion`/`sparkle:version`; keep it numeric + monotonic (no `-beta`), or Sparkle compares it as equal.
|
||||
- If `APP_BUILD` is omitted, `scripts/package-mac-app.sh` derives a Sparkle-safe default from `APP_VERSION` (`YYYYMMDDNN`: stable defaults to `90`, prereleases use a suffix-derived lane) and uses the higher of that value and git commit count.
|
||||
- You can still override `APP_BUILD` explicitly when release engineering needs a specific monotonic value.
|
||||
- For `BUILD_CONFIG=release`, `scripts/package-mac-app.sh` now defaults to universal (`arm64 x86_64`) automatically. You can still override with `BUILD_ARCHS=arm64` or `BUILD_ARCHS=x86_64`. For local/dev builds (`BUILD_CONFIG=debug`), it defaults to the current architecture (`$(uname -m)`).
|
||||
- Defaults to the current architecture (`$(uname -m)`). For release/universal builds, set `BUILD_ARCHS="arm64 x86_64"` (or `BUILD_ARCHS=all`).
|
||||
- Use `scripts/package-mac-dist.sh` for release artifacts (zip + DMG + notarization). Use `scripts/package-mac-app.sh` for local/dev packaging.
|
||||
|
||||
```bash
|
||||
# From repo root; set release IDs so Sparkle feed is enabled.
|
||||
# This command builds release artifacts without notarization.
|
||||
# APP_BUILD must be numeric + monotonic for Sparkle compare.
|
||||
# Default is auto-derived from APP_VERSION when omitted.
|
||||
SKIP_NOTARIZE=1 \
|
||||
BUNDLE_ID=ai.openclaw.mac \
|
||||
APP_VERSION=2026.3.8 \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-dist.sh
|
||||
scripts/package-mac-app.sh
|
||||
|
||||
# `package-mac-dist.sh` already creates the zip + DMG.
|
||||
# If you used `package-mac-app.sh` directly instead, create them manually:
|
||||
# If you want notarization/stapling in this step, use the NOTARIZE command below.
|
||||
# Zip for distribution (includes resource forks for Sparkle delta support)
|
||||
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.3.8.zip
|
||||
|
||||
# Optional: build a styled DMG for humans (drag to /Applications)
|
||||
# Optional: also build a styled DMG for humans (drag to /Applications)
|
||||
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.3.8.dmg
|
||||
|
||||
# Recommended: build + notarize/staple zip + DMG
|
||||
|
||||
@@ -111,29 +111,6 @@ Brave provides paid plans; check the Brave API portal for the current limits and
|
||||
}
|
||||
```
|
||||
|
||||
**Brave LLM Context mode:**
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
enabled: true,
|
||||
provider: "brave",
|
||||
apiKey: "YOUR_BRAVE_API_KEY", // optional if BRAVE_API_KEY is set // pragma: allowlist secret
|
||||
brave: {
|
||||
mode: "llm-context",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`llm-context` returns extracted page chunks for grounding instead of standard Brave snippets.
|
||||
In this mode, `country` and `language` / `search_lang` still work, but `ui_lang`,
|
||||
`freshness`, `date_after`, and `date_before` are rejected.
|
||||
|
||||
## Using Gemini (Google Search grounding)
|
||||
|
||||
Gemini models support built-in [Google Search grounding](https://ai.google.dev/gemini-api/docs/grounding),
|
||||
@@ -270,9 +247,6 @@ await web_search({
|
||||
});
|
||||
```
|
||||
|
||||
When Brave `llm-context` mode is enabled, `ui_lang`, `freshness`, `date_after`, and
|
||||
`date_before` are not supported. Use Brave `web` mode for those filters.
|
||||
|
||||
## web_fetch
|
||||
|
||||
Fetch a URL and extract readable content.
|
||||
|
||||
@@ -340,7 +340,6 @@
|
||||
"@grammyjs/runner": "^2.0.3",
|
||||
"@grammyjs/transformer-throttler": "^1.2.1",
|
||||
"@homebridge/ciao": "^1.3.5",
|
||||
"@larksuiteoapi/node-sdk": "^1.59.0",
|
||||
"@line/bot-sdk": "^10.6.0",
|
||||
"@lydell/node-pty": "1.2.0-beta.3",
|
||||
"@mariozechner/pi-agent-core": "0.55.3",
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -48,9 +48,6 @@ importers:
|
||||
'@homebridge/ciao':
|
||||
specifier: ^1.3.5
|
||||
version: 1.3.5
|
||||
'@larksuiteoapi/node-sdk':
|
||||
specifier: ^1.59.0
|
||||
version: 1.59.0
|
||||
'@line/bot-sdk':
|
||||
specifier: ^10.6.0
|
||||
version: 10.6.0
|
||||
|
||||
@@ -16,14 +16,7 @@ GIT_BUILD_NUMBER=$(cd "$ROOT_DIR" && git rev-list --count HEAD 2>/dev/null || ec
|
||||
APP_VERSION="${APP_VERSION:-$PKG_VERSION}"
|
||||
APP_BUILD="${APP_BUILD:-}"
|
||||
BUILD_CONFIG="${BUILD_CONFIG:-debug}"
|
||||
if [[ -n "${BUILD_ARCHS:-}" ]]; then
|
||||
BUILD_ARCHS_VALUE="${BUILD_ARCHS}"
|
||||
elif [[ "$BUILD_CONFIG" == "release" ]]; then
|
||||
# Release packaging should be universal unless explicitly overridden.
|
||||
BUILD_ARCHS_VALUE="all"
|
||||
else
|
||||
BUILD_ARCHS_VALUE="$(uname -m)"
|
||||
fi
|
||||
BUILD_ARCHS_VALUE="${BUILD_ARCHS:-$(uname -m)}"
|
||||
if [[ "${BUILD_ARCHS_VALUE}" == "all" ]]; then
|
||||
BUILD_ARCHS_VALUE="arm64 x86_64"
|
||||
fi
|
||||
|
||||
@@ -15,7 +15,6 @@ const {
|
||||
resolveKimiModel,
|
||||
resolveKimiBaseUrl,
|
||||
extractKimiCitations,
|
||||
resolveBraveMode,
|
||||
} = __testing;
|
||||
|
||||
const kimiApiKeyEnv = ["KIMI_API", "KEY"].join("_");
|
||||
@@ -277,25 +276,3 @@ describe("extractKimiCitations", () => {
|
||||
).toEqual(["https://example.com/a", "https://example.com/b", "https://example.com/c"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveBraveMode", () => {
|
||||
it("defaults to 'web' when no config is provided", () => {
|
||||
expect(resolveBraveMode({})).toBe("web");
|
||||
});
|
||||
|
||||
it("defaults to 'web' when mode is undefined", () => {
|
||||
expect(resolveBraveMode({ mode: undefined })).toBe("web");
|
||||
});
|
||||
|
||||
it("returns 'llm-context' when configured", () => {
|
||||
expect(resolveBraveMode({ mode: "llm-context" })).toBe("llm-context");
|
||||
});
|
||||
|
||||
it("returns 'web' when mode is explicitly 'web'", () => {
|
||||
expect(resolveBraveMode({ mode: "web" })).toBe("web");
|
||||
});
|
||||
|
||||
it("falls back to 'web' for unrecognized mode values", () => {
|
||||
expect(resolveBraveMode({ mode: "invalid" })).toBe("web");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,6 @@ const DEFAULT_SEARCH_COUNT = 5;
|
||||
const MAX_SEARCH_COUNT = 10;
|
||||
|
||||
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 PERPLEXITY_SEARCH_ENDPOINT = "https://api.perplexity.ai/search";
|
||||
|
||||
const XAI_API_ENDPOINT = "https://api.x.ai/v1/responses";
|
||||
@@ -248,17 +247,6 @@ type BraveSearchResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
type BraveLlmContextSnippet = { text: string };
|
||||
type BraveLlmContextResult = { url: string; title: string; snippets: BraveLlmContextSnippet[] };
|
||||
type BraveLlmContextResponse = {
|
||||
grounding: { generic?: BraveLlmContextResult[] };
|
||||
sources?: { url?: string; hostname?: string; date?: string }[];
|
||||
};
|
||||
|
||||
type BraveConfig = {
|
||||
mode?: string;
|
||||
};
|
||||
|
||||
type PerplexityConfig = {
|
||||
apiKey?: string;
|
||||
};
|
||||
@@ -562,21 +550,6 @@ function resolveSearchProvider(search?: WebSearchConfig): (typeof SEARCH_PROVIDE
|
||||
return "perplexity";
|
||||
}
|
||||
|
||||
function resolveBraveConfig(search?: WebSearchConfig): BraveConfig {
|
||||
if (!search || typeof search !== "object") {
|
||||
return {};
|
||||
}
|
||||
const brave = "brave" in search ? search.brave : undefined;
|
||||
if (!brave || typeof brave !== "object") {
|
||||
return {};
|
||||
}
|
||||
return brave as BraveConfig;
|
||||
}
|
||||
|
||||
function resolveBraveMode(brave: BraveConfig): "web" | "llm-context" {
|
||||
return brave.mode === "llm-context" ? "llm-context" : "web";
|
||||
}
|
||||
|
||||
function resolvePerplexityConfig(search?: WebSearchConfig): PerplexityConfig {
|
||||
if (!search || typeof search !== "object") {
|
||||
return {};
|
||||
@@ -1240,67 +1213,6 @@ async function runKimiSearch(params: {
|
||||
};
|
||||
}
|
||||
|
||||
async function runBraveLlmContextSearch(params: {
|
||||
query: string;
|
||||
apiKey: string;
|
||||
timeoutSeconds: number;
|
||||
country?: string;
|
||||
search_lang?: string;
|
||||
freshness?: string;
|
||||
}): Promise<{
|
||||
results: Array<{
|
||||
url: string;
|
||||
title: string;
|
||||
snippets: string[];
|
||||
siteName?: string;
|
||||
}>;
|
||||
sources?: BraveLlmContextResponse["sources"];
|
||||
}> {
|
||||
const url = new URL(BRAVE_LLM_CONTEXT_ENDPOINT);
|
||||
url.searchParams.set("q", params.query);
|
||||
if (params.country) {
|
||||
url.searchParams.set("country", params.country);
|
||||
}
|
||||
if (params.search_lang) {
|
||||
url.searchParams.set("search_lang", params.search_lang);
|
||||
}
|
||||
if (params.freshness) {
|
||||
url.searchParams.set("freshness", params.freshness);
|
||||
}
|
||||
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
url: url.toString(),
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
init: {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"X-Subscription-Token": params.apiKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (res) => {
|
||||
if (!res.ok) {
|
||||
const detailResult = await readResponseText(res, { maxBytes: 64_000 });
|
||||
const detail = detailResult.text;
|
||||
throw new Error(`Brave LLM Context API error (${res.status}): ${detail || res.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as BraveLlmContextResponse;
|
||||
const genericResults = Array.isArray(data.grounding?.generic) ? data.grounding.generic : [];
|
||||
const mapped = genericResults.map((entry) => ({
|
||||
url: entry.url ?? "",
|
||||
title: entry.title ?? "",
|
||||
snippets: (entry.snippets ?? []).map((s) => s.text ?? "").filter(Boolean),
|
||||
siteName: resolveSiteName(entry.url) || undefined,
|
||||
}));
|
||||
|
||||
return { results: mapped, sources: data.sources };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function runWebSearch(params: {
|
||||
query: string;
|
||||
count: number;
|
||||
@@ -1323,9 +1235,7 @@ async function runWebSearch(params: {
|
||||
geminiModel?: string;
|
||||
kimiBaseUrl?: string;
|
||||
kimiModel?: string;
|
||||
braveMode?: "web" | "llm-context";
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const effectiveBraveMode = params.braveMode ?? "web";
|
||||
const providerSpecificKey =
|
||||
params.provider === "grok"
|
||||
? `${params.grokModel ?? DEFAULT_GROK_MODEL}:${String(params.grokInlineCitations ?? false)}`
|
||||
@@ -1335,9 +1245,7 @@ async function runWebSearch(params: {
|
||||
? `${params.kimiBaseUrl ?? DEFAULT_KIMI_BASE_URL}:${params.kimiModel ?? DEFAULT_KIMI_MODEL}`
|
||||
: "";
|
||||
const cacheKey = normalizeCacheKey(
|
||||
params.provider === "brave" && effectiveBraveMode === "llm-context"
|
||||
? `${params.provider}:llm-context:${params.query}:${params.country || "default"}:${params.search_lang || params.language || "default"}:${params.freshness || "default"}`
|
||||
: `${params.provider}:${effectiveBraveMode}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || params.language || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}:${params.dateAfter || "default"}:${params.dateBefore || "default"}:${params.searchDomainFilter?.join(",") || "default"}:${params.maxTokens || "default"}:${params.maxTokensPerPage || "default"}:${providerSpecificKey}`,
|
||||
`${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || params.language || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}:${params.dateAfter || "default"}:${params.dateBefore || "default"}:${params.searchDomainFilter?.join(",") || "default"}:${params.maxTokens || "default"}:${params.maxTokensPerPage || "default"}:${providerSpecificKey}`,
|
||||
);
|
||||
const cached = readCache(SEARCH_CACHE, cacheKey);
|
||||
if (cached) {
|
||||
@@ -1464,42 +1372,6 @@ async function runWebSearch(params: {
|
||||
throw new Error("Unsupported web search provider.");
|
||||
}
|
||||
|
||||
if (effectiveBraveMode === "llm-context") {
|
||||
const { results: llmResults, sources } = await runBraveLlmContextSearch({
|
||||
query: params.query,
|
||||
apiKey: params.apiKey,
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
country: params.country,
|
||||
search_lang: params.search_lang,
|
||||
freshness: params.freshness,
|
||||
});
|
||||
|
||||
const mapped = llmResults.map((entry) => ({
|
||||
title: entry.title ? wrapWebContent(entry.title, "web_search") : "",
|
||||
url: entry.url,
|
||||
snippets: entry.snippets.map((s) => wrapWebContent(s, "web_search")),
|
||||
siteName: entry.siteName,
|
||||
}));
|
||||
|
||||
const payload = {
|
||||
query: params.query,
|
||||
provider: params.provider,
|
||||
mode: "llm-context" as const,
|
||||
count: mapped.length,
|
||||
tookMs: Date.now() - start,
|
||||
externalContent: {
|
||||
untrusted: true,
|
||||
source: "web_search",
|
||||
provider: params.provider,
|
||||
wrapped: true,
|
||||
},
|
||||
results: mapped,
|
||||
sources,
|
||||
};
|
||||
writeCache(SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs);
|
||||
return payload;
|
||||
}
|
||||
|
||||
const url = new URL(BRAVE_SEARCH_ENDPOINT);
|
||||
url.searchParams.set("q", params.query);
|
||||
url.searchParams.set("count", String(params.count));
|
||||
@@ -1593,8 +1465,6 @@ export function createWebSearchTool(options?: {
|
||||
const grokConfig = resolveGrokConfig(search);
|
||||
const geminiConfig = resolveGeminiConfig(search);
|
||||
const kimiConfig = resolveKimiConfig(search);
|
||||
const braveConfig = resolveBraveConfig(search);
|
||||
const braveMode = resolveBraveMode(braveConfig);
|
||||
|
||||
const description =
|
||||
provider === "perplexity"
|
||||
@@ -1605,9 +1475,7 @@ export function createWebSearchTool(options?: {
|
||||
? "Search the web using Kimi by Moonshot. Returns AI-synthesized answers with citations from native $web_search."
|
||||
: provider === "gemini"
|
||||
? "Search the web using Gemini with Google Search grounding. Returns AI-synthesized answers with citations from Google Search."
|
||||
: braveMode === "llm-context"
|
||||
? "Search the web using Brave Search LLM Context API. Returns pre-extracted page content (text chunks, tables, code blocks) optimized for LLM grounding."
|
||||
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.";
|
||||
: "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.";
|
||||
|
||||
return {
|
||||
label: "Web Search",
|
||||
@@ -1682,14 +1550,6 @@ export function createWebSearchTool(options?: {
|
||||
}
|
||||
const resolvedSearchLang = normalizedBraveLanguageParams.search_lang;
|
||||
const resolvedUiLang = normalizedBraveLanguageParams.ui_lang;
|
||||
if (resolvedUiLang && provider === "brave" && braveMode === "llm-context") {
|
||||
return jsonResult({
|
||||
error: "unsupported_ui_lang",
|
||||
message:
|
||||
"ui_lang is not supported by Brave llm-context mode. Remove ui_lang or use Brave web mode for locale-based UI hints.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
}
|
||||
const rawFreshness = readStringParam(params, "freshness");
|
||||
if (rawFreshness && provider !== "brave" && provider !== "perplexity") {
|
||||
return jsonResult({
|
||||
@@ -1698,14 +1558,6 @@ export function createWebSearchTool(options?: {
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
}
|
||||
if (rawFreshness && provider === "brave" && braveMode === "llm-context") {
|
||||
return jsonResult({
|
||||
error: "unsupported_freshness",
|
||||
message:
|
||||
"freshness filtering is not supported by Brave llm-context mode. Remove freshness or use Brave web mode.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
}
|
||||
const freshness = rawFreshness ? normalizeFreshness(rawFreshness, provider) : undefined;
|
||||
if (rawFreshness && !freshness) {
|
||||
return jsonResult({
|
||||
@@ -1731,14 +1583,6 @@ export function createWebSearchTool(options?: {
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
}
|
||||
if ((rawDateAfter || rawDateBefore) && provider === "brave" && braveMode === "llm-context") {
|
||||
return jsonResult({
|
||||
error: "unsupported_date_filter",
|
||||
message:
|
||||
"date_after/date_before filtering is not supported by Brave llm-context mode. Use Brave web mode for date filters.",
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
});
|
||||
}
|
||||
const dateAfter = rawDateAfter ? normalizeToIsoDate(rawDateAfter) : undefined;
|
||||
if (rawDateAfter && !dateAfter) {
|
||||
return jsonResult({
|
||||
@@ -1816,7 +1660,6 @@ export function createWebSearchTool(options?: {
|
||||
geminiModel: resolveGeminiModel(geminiConfig),
|
||||
kimiBaseUrl: resolveKimiBaseUrl(kimiConfig),
|
||||
kimiModel: resolveKimiModel(kimiConfig),
|
||||
braveMode,
|
||||
});
|
||||
return jsonResult(result);
|
||||
},
|
||||
@@ -1841,5 +1684,4 @@ export const __testing = {
|
||||
resolveKimiBaseUrl,
|
||||
extractKimiCitations,
|
||||
resolveRedirectUrl: resolveCitationRedirectUrl,
|
||||
resolveBraveMode,
|
||||
} as const;
|
||||
|
||||
@@ -31,23 +31,6 @@ function createPerplexitySearchTool(perplexityConfig?: { apiKey?: string }) {
|
||||
});
|
||||
}
|
||||
|
||||
function createBraveSearchTool(braveConfig?: { mode?: "web" | "llm-context" }) {
|
||||
return createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brave",
|
||||
apiKey: "brave-config-test", // pragma: allowlist secret
|
||||
...(braveConfig ? { brave: braveConfig } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
});
|
||||
}
|
||||
|
||||
function createKimiSearchTool(kimiConfig?: { apiKey?: string; baseUrl?: string; model?: string }) {
|
||||
return createWebSearchTool({
|
||||
config: {
|
||||
@@ -179,7 +162,7 @@ describe("web_search country and language parameters", () => {
|
||||
}>,
|
||||
) {
|
||||
const mockFetch = installMockFetch({ web: { results: [] } });
|
||||
const tool = createBraveSearchTool();
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
expect(tool).not.toBeNull();
|
||||
await tool?.execute?.("call-1", { query: "test", ...params });
|
||||
expect(mockFetch).toHaveBeenCalled();
|
||||
@@ -197,7 +180,7 @@ describe("web_search country and language parameters", () => {
|
||||
|
||||
it("should pass language parameter to Brave API as search_lang", async () => {
|
||||
const mockFetch = installMockFetch({ web: { results: [] } });
|
||||
const tool = createBraveSearchTool();
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
await tool?.execute?.("call-1", { query: "test", language: "de" });
|
||||
|
||||
const url = new URL(mockFetch.mock.calls[0][0] as string);
|
||||
@@ -221,7 +204,7 @@ describe("web_search country and language parameters", () => {
|
||||
|
||||
it("rejects unsupported Brave search_lang values before upstream request", async () => {
|
||||
const mockFetch = installMockFetch({ web: { results: [] } });
|
||||
const tool = createBraveSearchTool();
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
const result = await tool?.execute?.("call-1", { query: "test", search_lang: "xx" });
|
||||
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
@@ -528,27 +511,8 @@ describe("web_search external content wrapping", () => {
|
||||
return mock;
|
||||
}
|
||||
|
||||
function installBraveLlmContextFetch(
|
||||
result: Record<string, unknown>,
|
||||
mock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
grounding: {
|
||||
generic: [result],
|
||||
},
|
||||
sources: [{ url: "https://example.com/ctx", hostname: "example.com" }],
|
||||
}),
|
||||
} as Response),
|
||||
),
|
||||
) {
|
||||
global.fetch = withFetchPreconnect(mock);
|
||||
return mock;
|
||||
}
|
||||
|
||||
async function executeBraveSearch(query: string) {
|
||||
const tool = createBraveSearchTool();
|
||||
const tool = createWebSearchTool({ config: undefined, sandboxed: true });
|
||||
return tool?.execute?.("call-1", { query });
|
||||
}
|
||||
|
||||
@@ -581,154 +545,6 @@ describe("web_search external content wrapping", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses Brave llm-context endpoint when mode is configured", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const mockFetch = installBraveLlmContextFetch({
|
||||
title: "Context title",
|
||||
url: "https://example.com/ctx",
|
||||
snippets: [{ text: "Context chunk one" }, { text: "Context chunk two" }],
|
||||
});
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brave",
|
||||
brave: {
|
||||
mode: "llm-context",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
});
|
||||
const result = await tool?.execute?.("call-1", {
|
||||
query: "llm-context test",
|
||||
country: "DE",
|
||||
search_lang: "de",
|
||||
});
|
||||
|
||||
const requestUrl = new URL(mockFetch.mock.calls[0]?.[0] as string);
|
||||
expect(requestUrl.pathname).toBe("/res/v1/llm/context");
|
||||
expect(requestUrl.searchParams.get("q")).toBe("llm-context test");
|
||||
expect(requestUrl.searchParams.get("country")).toBe("DE");
|
||||
expect(requestUrl.searchParams.get("search_lang")).toBe("de");
|
||||
|
||||
const details = result?.details as {
|
||||
mode?: string;
|
||||
results?: Array<{
|
||||
title?: string;
|
||||
url?: string;
|
||||
snippets?: string[];
|
||||
siteName?: string;
|
||||
}>;
|
||||
sources?: Array<{ hostname?: string }>;
|
||||
};
|
||||
expect(details.mode).toBe("llm-context");
|
||||
expect(details.results?.[0]?.url).toBe("https://example.com/ctx");
|
||||
expect(details.results?.[0]?.title).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT");
|
||||
expect(details.results?.[0]?.snippets?.[0]).toContain("<<<EXTERNAL_UNTRUSTED_CONTENT");
|
||||
expect(details.results?.[0]?.snippets?.[0]).toContain("Context chunk one");
|
||||
expect(details.results?.[0]?.siteName).toBe("example.com");
|
||||
expect(details.sources?.[0]?.hostname).toBe("example.com");
|
||||
});
|
||||
|
||||
it("rejects freshness in Brave llm-context mode", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const mockFetch = installBraveLlmContextFetch({
|
||||
title: "unused",
|
||||
url: "https://example.com",
|
||||
snippets: ["unused"],
|
||||
});
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brave",
|
||||
brave: {
|
||||
mode: "llm-context",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
});
|
||||
const result = await tool?.execute?.("call-1", { query: "test", freshness: "week" });
|
||||
|
||||
expect(result?.details).toMatchObject({ error: "unsupported_freshness" });
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects date_after/date_before in Brave llm-context mode", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const mockFetch = installBraveLlmContextFetch({
|
||||
title: "unused",
|
||||
url: "https://example.com",
|
||||
snippets: ["unused"],
|
||||
});
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brave",
|
||||
brave: {
|
||||
mode: "llm-context",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
});
|
||||
const result = await tool?.execute?.("call-1", {
|
||||
query: "test",
|
||||
date_after: "2025-01-01",
|
||||
date_before: "2025-01-31",
|
||||
});
|
||||
|
||||
expect(result?.details).toMatchObject({ error: "unsupported_date_filter" });
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects ui_lang in Brave llm-context mode", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const mockFetch = installBraveLlmContextFetch({
|
||||
title: "unused",
|
||||
url: "https://example.com",
|
||||
snippets: ["unused"],
|
||||
});
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "brave",
|
||||
brave: {
|
||||
mode: "llm-context",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sandboxed: true,
|
||||
});
|
||||
const result = await tool?.execute?.("call-1", {
|
||||
query: "test",
|
||||
ui_lang: "de-DE",
|
||||
});
|
||||
|
||||
expect(result?.details).toMatchObject({ error: "unsupported_ui_lang" });
|
||||
expect(mockFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not wrap Brave result urls (raw for tool chaining)", async () => {
|
||||
vi.stubEnv("BRAVE_API_KEY", "test-key");
|
||||
const url = "https://example.com/some-page";
|
||||
|
||||
@@ -48,32 +48,6 @@ describe("web search provider config", () => {
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts brave llm-context mode config", () => {
|
||||
const res = validateConfigObject(
|
||||
buildWebSearchProviderConfig({
|
||||
provider: "brave",
|
||||
providerConfig: {
|
||||
mode: "llm-context",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects invalid brave mode config values", () => {
|
||||
const res = validateConfigObject(
|
||||
buildWebSearchProviderConfig({
|
||||
provider: "brave",
|
||||
providerConfig: {
|
||||
mode: "invalid-mode",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("web search provider auto-detection", () => {
|
||||
|
||||
@@ -666,8 +666,6 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Perplexity base URL override (default: https://openrouter.ai/api/v1 or https://api.perplexity.ai).",
|
||||
"tools.web.search.perplexity.model":
|
||||
'Perplexity model override (default: "perplexity/sonar-pro").',
|
||||
"tools.web.search.brave.mode":
|
||||
'Brave Search mode: "web" (URL results) or "llm-context" (pre-extracted page content for LLM grounding).',
|
||||
"tools.web.fetch.enabled": "Enable the web_fetch tool (lightweight HTTP fetch).",
|
||||
"tools.web.fetch.maxChars": "Max characters returned by web_fetch (truncated).",
|
||||
"tools.web.fetch.maxCharsCap":
|
||||
|
||||
@@ -224,7 +224,6 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
"tools.web.search.gemini.model": "Gemini Search Model",
|
||||
"tools.web.search.grok.apiKey": "Grok Search API Key", // pragma: allowlist secret
|
||||
"tools.web.search.grok.model": "Grok Search Model",
|
||||
"tools.web.search.brave.mode": "Brave Search Mode",
|
||||
"tools.web.search.kimi.apiKey": "Kimi Search API Key", // pragma: allowlist secret
|
||||
"tools.web.search.kimi.baseUrl": "Kimi Search Base URL",
|
||||
"tools.web.search.kimi.model": "Kimi Search Model",
|
||||
|
||||
@@ -485,11 +485,6 @@ export type ToolsConfig = {
|
||||
/** Model to use (defaults to "moonshot-v1-128k"). */
|
||||
model?: string;
|
||||
};
|
||||
/** Brave-specific configuration (used when provider="brave"). */
|
||||
brave?: {
|
||||
/** Brave Search mode: "web" (standard results) or "llm-context" (pre-extracted page content). Default: "web". */
|
||||
mode?: "web" | "llm-context";
|
||||
};
|
||||
};
|
||||
fetch?: {
|
||||
/** Enable web fetch tool (default: true). */
|
||||
|
||||
@@ -308,12 +308,6 @@ export const ToolsWebSearchSchema = z
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
brave: z
|
||||
.object({
|
||||
mode: z.union([z.literal("web"), z.literal("llm-context")]).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional();
|
||||
|
||||
Reference in New Issue
Block a user