feat(tools): add EXA as web search provider#21628
feat(tools): add EXA as web search provider#21628mawazawa wants to merge 3 commits intoopenclaw:mainfrom
Conversation
Add EXA AI as a new search provider for web_search tool. EXA provides neural search optimized for LLMs with high-quality, context-aware results. Changes: - Add EXA to SEARCH_PROVIDERS array - Add ExaConfig type and resolveExaConfig function - Add runExaSearch function for API calls - Update provider type unions in types.tools.ts - Add EXA config section to zod schema - Update schema help text AI-assisted implementation (Claude Code)
|
@codex review |
| const exaAuth = provider === "exa" ? exaConfig.apiKey : undefined; | ||
| const apiKey = | ||
| provider === "perplexity" | ||
| ? perplexityAuth?.apiKey | ||
| : provider === "grok" | ||
| ? resolveGrokApiKey(grokConfig) | ||
| : resolveSearchApiKey(search); | ||
| : provider === "exa" | ||
| ? exaAuth || process.env.EXA_API_KEY | ||
| : resolveSearchApiKey(search); |
There was a problem hiding this comment.
inconsistent EXA API key resolution pattern
EXA uses a different pattern (exaConfig.apiKey || process.env.EXA_API_KEY) compared to other providers which have dedicated resolver functions. This bypasses the normalizeSecretInput utility used by Perplexity and Grok.
Consider adding a resolveExaApiKey function similar to resolveGrokApiKey (line 394) for consistency and to ensure proper secret normalization.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/tools/web-search.ts
Line: 847-855
Comment:
inconsistent EXA API key resolution pattern
EXA uses a different pattern (`exaConfig.apiKey || process.env.EXA_API_KEY`) compared to other providers which have dedicated resolver functions. This bypasses the `normalizeSecretInput` utility used by Perplexity and Grok.
Consider adding a `resolveExaApiKey` function similar to `resolveGrokApiKey` (line 394) for consistency and to ensure proper secret normalization.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.
Additional Comments (2)
The function handles Add an EXA case before the default return: Prompt To Fix With AIThis is a comment left during a code review.
Path: src/agents/tools/web-search.ts
Line: 220-242
Comment:
missing EXA error message in `missingSearchKeyPayload`
The function handles `perplexity` and `grok` but doesn't have a case for `exa`. When an EXA API key is missing, users will see the default Brave error message instead of helpful EXA-specific instructions.
Add an EXA case before the default return:
```suggestion
function missingSearchKeyPayload(provider: (typeof SEARCH_PROVIDERS)[number]) {
if (provider === "perplexity") {
return {
error: "missing_perplexity_api_key",
message:
"web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure tools.web.search.perplexity.apiKey.",
docs: "https://docs.openclaw.ai/tools/web",
};
}
if (provider === "grok") {
return {
error: "missing_xai_api_key",
message:
"web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure tools.web.search.grok.apiKey.",
docs: "https://docs.openclaw.ai/tools/web",
};
}
if (provider === "exa") {
return {
error: "missing_exa_api_key",
message:
"web_search (exa) needs an EXA API key. Set EXA_API_KEY in the Gateway environment, or configure tools.web.search.exa.apiKey.",
docs: "https://docs.openclaw.ai/tools/web",
};
}
return {
error: "missing_brave_api_key",
message: `web_search needs a Brave Search API key. Run \`${formatCliCommand("openclaw configure --section web")}\` to store it, or set BRAVE_API_KEY in the Gateway environment.`,
docs: "https://docs.openclaw.ai/tools/web",
};
}
```
How can I resolve this? If you propose a fix, please make it concise.
The cache key generation doesn't handle the Prompt To Fix With AIThis is a comment left during a code review.
Path: src/agents/tools/web-search.ts
Line: 650-656
Comment:
cache key excludes EXA parameters
The cache key generation doesn't handle the `exa` provider. If EXA is selected, the code will fall through to the grok case and create an incorrect cache key using grok model parameters that don't exist for EXA.
How can I resolve this? If you propose a fix, please make it concise. |
There was a problem hiding this comment.
💡 Codex Review
openclaw/src/agents/tools/web-search.ts
Lines 653 to 655 in 24607d7
The EXA provider currently falls into the Grok cache-key branch, which does not include count; this means two EXA searches with the same query but different requested result counts will collide and return stale/cropped cached data from the first call. This is a functional regression for EXA because runExaSearch actually uses count to control result size.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| results: results.map((r) => ({ | ||
| title: r.title, | ||
| url: r.url, | ||
| snippet: r.text?.substring(0, 500), | ||
| })), |
There was a problem hiding this comment.
Wrap EXA result text before declaring wrapped output
This EXA payload marks externalContent.wrapped as true but forwards title/snippet directly from the upstream API without wrapWebContent, unlike the existing Brave/Grok paths. In environments relying on wrapped web content to mitigate prompt-injection from untrusted pages, EXA results can bypass that guard while being labeled as safe.
Useful? React with 👍 / 👎.
| count: number; | ||
| timeoutSeconds: number; | ||
| }): Promise<ExaSearchResult[]> { | ||
| const res = await fetch(EXA_API_ENDPOINT, { |
There was a problem hiding this comment.
Respect configured EXA base URL at request time
The commit introduces tools.web.search.exa.baseUrl in config types/schema, but EXA requests are still hardcoded to https://api.exa.ai/search. Any deployment that sets a custom EXA endpoint (proxy, regional gateway, or mock server) will be silently ignored, making the new config field ineffective.
Useful? React with 👍 / 👎.
| if (raw === "exa") { | ||
| return "exa"; |
There was a problem hiding this comment.
Add EXA branch to missing API key payload
After adding exa as a valid provider, missing-key handling still falls through to the Brave error payload. When EXA is selected without credentials, users are told to set a Brave key instead of EXA_API_KEY/tools.web.search.exa.apiKey, which sends them to the wrong remediation path.
Useful? React with 👍 / 👎.
Add missing case for EXA API key error handling in missingSearchKeyPayload function to provide helpful error messages when EXA API key is missing. Co-authored-by: Eli Rook <[email protected]>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
@codex address that feedback with precision and completeness and the kind of rigor deserving of a OpenClaw contributor status. No AI slop! Last chance |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9e92e0c31a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| : params.provider === "exa" | ||
| ? `${params.provider}:${params.query}` | ||
| : `${params.provider}:${params.query}:${params.grokModel ?? DEFAULT_GROK_MODEL}:${String(params.grokInlineCitations ?? false)}`, |
There was a problem hiding this comment.
Include EXA count in cache key
runWebSearch caches EXA responses using only provider and query, so a request like count=1 can poison the cache for a later count=10 call on the same query, returning too few results until TTL expiry. The Brave path already keys on count, and EXA should do the same to avoid deterministic stale-result mismatches for users who vary result size.
Useful? React with 👍 / 👎.
| results: results.map((r) => ({ | ||
| title: r.title, | ||
| url: r.url, | ||
| snippet: r.text?.substring(0, 500), | ||
| })), |
There was a problem hiding this comment.
Wrap EXA result text as untrusted web content
The EXA branch marks payloads as externalContent.wrapped: true, but title and snippet are returned raw instead of going through wrapWebContent like other web_search providers. When EXA returns prompt-injection text, that content reaches the model without the expected untrusted-content boundary markers, weakening the same safety contract enforced for Brave/Perplexity/Grok outputs.
Useful? React with 👍 / 👎.
| const res = await fetch(EXA_API_ENDPOINT, { | ||
| method: "POST", |
There was a problem hiding this comment.
The config schema and types add tools.web.search.exa.baseUrl, but runExaSearch always calls the hardcoded EXA_API_ENDPOINT and no caller passes a configurable base URL. This makes the new config option non-functional (e.g., custom gateway/proxy/test endpoints are ignored) and will confuse operators because accepted config has no runtime effect.
Useful? React with 👍 / 👎.
|
This pull request has been automatically marked as stale due to inactivity. |
|
Thanks for taking this on. I'm closing this as a duplicate of #52617. This was part of the older inline-provider approach before the web-search work moved to bundled plugins, and the merged PR now handles the same Exa provider outcome in the current architecture. If you think I missed unique scope here, tell me and I can reopen review right away. |
Summary
Add EXA AI as a new search provider for the web_search tool. EXA provides neural search optimized for LLMs with high-quality, context-aware results.
Changes
Why EXA?
EXA is an AI-powered search engine designed specifically for LLMs. It provides:
Testing
Tested the build locally with
pnpm build- passes successfully.AI-assisted
This PR was AI-assisted (built by Eli Rook, an autonomous AI).
Closes: [feature request]
Greptile Summary
This PR adds EXA AI as a fourth web search provider alongside Brave, Perplexity, and Grok. The implementation follows the existing pattern for other providers by adding EXA to the provider array, defining config types, implementing the API call function, and integrating it into the main search flow.
Critical issues found:
missingSearchKeyPayloadfunction - users will see incorrect Brave error message when EXA API key is missingStyle concerns:
Missing updates:
docs/tools/web.md) doesn't mention EXA as a provider optionConfidence Score: 2/5
src/agents/tools/web-search.tslines 220-242 (error messages) and 650-656 (cache keys)Last reviewed commit: 24607d7
(2/5) Greptile learns from your feedback when you react with thumbs up/down!