Skip to content

feat(tools): add EXA as web search provider#21628

Closed
mawazawa wants to merge 3 commits intoopenclaw:mainfrom
mawazawa:feature/exa-search-provider
Closed

feat(tools): add EXA as web search provider#21628
mawazawa wants to merge 3 commits intoopenclaw:mainfrom
mawazawa:feature/exa-search-provider

Conversation

@mawazawa
Copy link
Copy Markdown

@mawazawa mawazawa commented Feb 20, 2026

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

  • 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

Why EXA?

EXA is an AI-powered search engine designed specifically for LLMs. It provides:

  • Neural search (AI understanding of query intent)
  • Better results for complex/abstract queries
  • Competitive pricing vs Brave/Perplexity

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:

  • Missing EXA case in missingSearchKeyPayload function - users will see incorrect Brave error message when EXA API key is missing
  • Cache key generation doesn't handle EXA provider - will use wrong grok parameters for cache keys

Style concerns:

  • EXA API key resolution differs from other providers (inline vs dedicated resolver function)

Missing updates:

  • Documentation (docs/tools/web.md) doesn't mention EXA as a provider option
  • No test coverage added for EXA provider

Confidence Score: 2/5

  • This PR has critical logical errors that will cause incorrect behavior in production
  • Two critical bugs found: missing EXA error handling will show wrong error messages to users, and missing cache key logic will cause cache collisions with grok provider. These are functional regressions that must be fixed before merge.
  • Focus on src/agents/tools/web-search.ts lines 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!

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)
@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: S labels Feb 20, 2026
@mawazawa
Copy link
Copy Markdown
Author

@codex review

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +847 to +855
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 20, 2026

Additional Comments (2)

src/agents/tools/web-search.ts
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:

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",
  };
}
Prompt To Fix With AI
This 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.

src/agents/tools/web-search.ts
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.

Prompt To Fix With AI
This 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.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

: params.provider === "perplexity"
? `${params.provider}:${params.query}:${params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL}:${params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL}:${params.freshness || "default"}`
: `${params.provider}:${params.query}:${params.grokModel ?? DEFAULT_GROK_MODEL}:${String(params.grokInlineCitations ?? false)}`,

P1 Badge Include EXA count in cache key generation

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".

Comment on lines +738 to +742
results: results.map((r) => ({
title: r.title,
url: r.url,
snippet: r.text?.substring(0, 500),
})),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +258 to +259
if (raw === "exa") {
return "exa";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge 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 👍 / 👎.

EC2 Default User and others added 2 commits February 20, 2026 05:50
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>
@mawazawa
Copy link
Copy Markdown
Author

@codex address that feedback with precision and completeness and the kind of rigor deserving of a OpenClaw contributor status. No AI slop! Last chance

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +663 to +665
: params.provider === "exa"
? `${params.provider}:${params.query}`
: `${params.provider}:${params.query}:${params.grokModel ?? DEFAULT_GROK_MODEL}:${String(params.grokInlineCitations ?? false)}`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +748 to +752
results: results.map((r) => ({
title: r.title,
url: r.url,
snippet: r.text?.substring(0, 500),
})),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +618 to +619
const res = await fetch(EXA_API_ENDPOINT, {
method: "POST",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor configured EXA base URL

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 👍 / 👎.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 19, 2026
@vincentkoc
Copy link
Copy Markdown
Member

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.

@vincentkoc vincentkoc closed this Mar 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: S stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants