feat(search): add MiniMax web search provider

New search backend alongside tavily/brave/serper/exa/ollama. API key
resolution: MINIMAX_CODE_PLAN_KEY → MINIMAX_CODING_API_KEY →
MINIMAX_API_KEY (fallback order matches MiniMax's documented aliases).

Wired through every existing seam:
- type union: SearchProvider = 'tavily' | 'minimax' | 'brave' | 'ollama'
- VALID_PREFERENCES set + selection logic in provider.ts
- native-search routing (Anthropic native web_search delegates correctly)
- /search-provider CLI command (tab completion, select UI, parser)
- tool-search.ts: search execution path
- tool-llm-context.ts: prefetch / context-builder path
- preferences-types + preferences-validation
- configuration.md user docs
- extension-manifest description

Tests not added in this commit — the bunker reference tests don't match
our preferences/provider export shape (we have serper/exa/combosearch
that bunker doesn't). Tests for getMiniMaxSearchApiKey priority order,
resolveSearchProvider returning "minimax", /search-provider minimax CLI
behavior, no-key error messages, and executeMiniMaxSearch request shape
are TODO.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-04-29 13:55:04 +02:00
parent ae0bbe32fc
commit 2eebeccb93
9 changed files with 266 additions and 21 deletions

View file

@ -65,6 +65,7 @@ This opens an interactive wizard showing which keys are configured and which are
| Tool | Environment Variable | Purpose | Get a key |
|------|---------------------|---------|-----------|
| Tavily Search | `TAVILY_API_KEY` | Web search for non-Anthropic models | [tavily.com/app/api-keys](https://tavily.com/app/api-keys) |
| MiniMax Search | `MINIMAX_API_KEY` (`MINIMAX_CODE_PLAN_KEY` and `MINIMAX_CODING_API_KEY` also accepted) | Web search for non-Anthropic models | MiniMax key |
| Brave Search | `BRAVE_API_KEY` | Web search for non-Anthropic models | [brave.com/search/api](https://brave.com/search/api) |
| Context7 Docs | `CONTEXT7_API_KEY` | Library documentation lookup | [context7.com/dashboard](https://context7.com/dashboard) |

View file

@ -1,9 +1,9 @@
/**
* /search-provider slash command.
*
* Lets users switch between tavily, brave, serper, exa, ollama, combosearch, and auto search backends.
* Lets users switch between tavily, minimax, brave, serper, exa, ollama, combosearch, and auto search backends.
* Supports direct arg (`/search-provider tavily`) or interactive select UI.
* Tab completion provides the three valid options with key status.
* Tab completion provides the valid options with key status.
*
* All provider logic lives in provider.ts (S01) this is pure UI wiring.
*/
@ -13,6 +13,7 @@ import type { AutocompleteItem } from "@singularity-forge/pi-tui";
import {
getBraveApiKey,
getExaApiKey,
getMiniMaxSearchApiKey,
getOllamaApiKey,
getSearchProviderPreference,
getSerperApiKey,
@ -24,6 +25,7 @@ import {
const VALID_PREFERENCES: SearchProviderPreference[] = [
"tavily",
"minimax",
"brave",
"serper",
"exa",
@ -33,9 +35,10 @@ const VALID_PREFERENCES: SearchProviderPreference[] = [
];
function keyStatus(
provider: "tavily" | "brave" | "serper" | "exa" | "ollama",
provider: "tavily" | "minimax" | "brave" | "serper" | "exa" | "ollama",
): string {
if (provider === "tavily") return getTavilyApiKey() ? "✓" : "✗";
if (provider === "minimax") return getMiniMaxSearchApiKey() ? "✓" : "✗";
if (provider === "serper") return getSerperApiKey() ? "✓" : "✗";
if (provider === "exa") return getExaApiKey() ? "✓" : "✗";
if (provider === "ollama") return getOllamaApiKey() ? "✓" : "✗";
@ -45,6 +48,7 @@ function keyStatus(
function comboStatus(): string {
const available = [
getTavilyApiKey() ? "tavily" : null,
getMiniMaxSearchApiKey() ? "minimax" : null,
getBraveApiKey() ? "brave" : null,
getSerperApiKey() ? "serper" : null,
getExaApiKey() ? "exa" : null,
@ -58,6 +62,7 @@ function comboStatus(): string {
function buildSelectOptions(): string[] {
return [
`tavily (key: ${keyStatus("tavily")})`,
`minimax (key: ${keyStatus("minimax")})`,
`brave (key: ${keyStatus("brave")})`,
`serper (key: ${keyStatus("serper")})`,
`exa (key: ${keyStatus("exa")})`,
@ -69,6 +74,7 @@ function buildSelectOptions(): string[] {
function parseSelectChoice(choice: string): SearchProviderPreference {
if (choice.startsWith("tavily")) return "tavily";
if (choice.startsWith("minimax")) return "minimax";
if (choice.startsWith("brave")) return "brave";
if (choice.startsWith("serper")) return "serper";
if (choice.startsWith("exa")) return "exa";
@ -80,14 +86,14 @@ function parseSelectChoice(choice: string): SearchProviderPreference {
export function registerSearchProviderCommand(pi: ExtensionAPI): void {
pi.registerCommand("search-provider", {
description:
"Switch search provider (tavily, brave, serper, exa, ollama, combosearch, auto)",
"Switch search provider (tavily, minimax, brave, serper, exa, ollama, combosearch, auto)",
getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
const trimmed = prefix.trim().toLowerCase();
return VALID_PREFERENCES.filter((p) => p.startsWith(trimmed)).map((p) => {
let description: string;
if (p === "auto") {
description = `Auto-select (tavily: ${keyStatus("tavily")}, brave: ${keyStatus("brave")}, serper: ${keyStatus("serper")}, exa: ${keyStatus("exa")}, ollama: ${keyStatus("ollama")})`;
description = `Auto-select (tavily: ${keyStatus("tavily")}, minimax: ${keyStatus("minimax")}, brave: ${keyStatus("brave")}, serper: ${keyStatus("serper")}, exa: ${keyStatus("exa")}, ollama: ${keyStatus("ollama")})`;
} else if (p === "combosearch") {
description = `fan-out aggregator (${comboStatus()})`;
} else {

View file

@ -2,7 +2,7 @@
"id": "search-the-web",
"name": "Web Search",
"version": "1.0.0",
"description": "Web search via Brave and page extraction via Jina Reader",
"description": "Web search via Tavily, MiniMax, Serper, Exa, Ollama, or Brave plus page extraction",
"tier": "bundled",
"requires": { "platform": ">=2.29.0" },
"provides": {

View file

@ -37,6 +37,7 @@ export function preferBraveSearch(): boolean {
if (
prefsPref === "brave" ||
prefsPref === "tavily" ||
prefsPref === "minimax" ||
prefsPref === "serper" ||
prefsPref === "exa" ||
prefsPref === "ollama" ||
@ -116,6 +117,9 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): {
const hasSearchKey = !!(
process.env.BRAVE_API_KEY ||
process.env.TAVILY_API_KEY ||
process.env.MINIMAX_CODE_PLAN_KEY ||
process.env.MINIMAX_CODING_API_KEY ||
process.env.MINIMAX_API_KEY ||
process.env.SERPER_API_KEY ||
process.env.EXA_API_KEY ||
process.env.OLLAMA_API_KEY
@ -156,7 +160,7 @@ export function registerNativeSearchHooks(pi: NativeSearchPI): {
ctx.ui.notify("Brave search active (PREFER_BRAVE_SEARCH)", "info");
} else if (!isAnthropicProvider && !hasSearchKey) {
ctx.ui.notify(
"Web search: Set BRAVE_API_KEY, TAVILY_API_KEY, SERPER_API_KEY, EXA_API_KEY, or OLLAMA_API_KEY, or use an Anthropic model for built-in search",
"Web search: Set BRAVE_API_KEY, TAVILY_API_KEY, MINIMAX_CODE_PLAN_KEY, SERPER_API_KEY, EXA_API_KEY, or OLLAMA_API_KEY, or use an Anthropic model for built-in search",
"warning",
);
}

View file

@ -5,7 +5,7 @@
* Reads API keys from process.env at call time (not module load time) so
* hot-reloaded keys work. Preference is stored in auth.json under the
* synthetic provider key `search_provider` as
* { type: "api_key", key: "tavily" | "brave" | "serper" | "exa" | "ollama" | "combosearch" | "auto" }.
* { type: "api_key", key: "tavily" | "minimax" | "brave" | "serper" | "exa" | "ollama" | "combosearch" | "auto" }.
*
* @see S01-RESEARCH.md for the storage decision rationale (D002).
*/
@ -23,6 +23,7 @@ const authFilePath = join(sfHome, "agent", "auth.json");
export type SearchProvider =
| "tavily"
| "minimax"
| "brave"
| "serper"
| "exa"
@ -32,6 +33,7 @@ export type SearchProviderPreference = SearchProvider | "auto";
const VALID_PREFERENCES = new Set<string>([
"tavily",
"minimax",
"brave",
"serper",
"exa",
@ -65,6 +67,16 @@ export function getOllamaApiKey(): string {
return process.env.OLLAMA_API_KEY || "";
}
/** Returns the MiniMax Coding Plan search key, accepting documented aliases. */
export function getMiniMaxSearchApiKey(): string {
return (
process.env.MINIMAX_CODE_PLAN_KEY ||
process.env.MINIMAX_CODING_API_KEY ||
process.env.MINIMAX_API_KEY ||
""
);
}
/** Returns the Serper API key from the environment, or empty string if not set. */
export function getSerperApiKey(): string {
return process.env.SERPER_API_KEY || "";
@ -129,17 +141,25 @@ export function resolveSearchProvider(
overridePreference?: string,
): SearchProvider | null {
const tavilyKey = getTavilyApiKey();
const minimaxKey = getMiniMaxSearchApiKey();
const braveKey = getBraveApiKey();
const serperKey = getSerperApiKey();
const exaKey = getExaApiKey();
const ollamaKey = getOllamaApiKey();
const hasTavily = tavilyKey.length > 0;
const hasMiniMax = minimaxKey.length > 0;
const hasBrave = braveKey.length > 0;
const hasSerper = serperKey.length > 0;
const hasExa = exaKey.length > 0;
const hasOllama = ollamaKey.length > 0;
const hasAny = hasTavily || hasBrave || hasSerper || hasExa || hasOllama;
const hasAny =
hasTavily ||
hasMiniMax ||
hasBrave ||
hasSerper ||
hasExa ||
hasOllama;
// Determine effective preference
let pref: SearchProviderPreference;
@ -163,6 +183,7 @@ export function resolveSearchProvider(
// Resolve based on preference
if (pref === "auto") {
if (hasTavily) return "tavily";
if (hasMiniMax) return "minimax";
if (hasBrave) return "brave";
if (hasSerper) return "serper";
if (hasExa) return "exa";
@ -175,6 +196,17 @@ export function resolveSearchProvider(
}
if (pref === "tavily") {
if (hasTavily) return "tavily";
if (hasMiniMax) return "minimax";
if (hasBrave) return "brave";
if (hasSerper) return "serper";
if (hasExa) return "exa";
if (hasOllama) return "ollama";
return null;
}
if (pref === "minimax") {
if (hasMiniMax) return "minimax";
if (hasTavily) return "tavily";
if (hasBrave) return "brave";
if (hasSerper) return "serper";
@ -186,6 +218,7 @@ export function resolveSearchProvider(
if (pref === "brave") {
if (hasBrave) return "brave";
if (hasTavily) return "tavily";
if (hasMiniMax) return "minimax";
if (hasSerper) return "serper";
if (hasExa) return "exa";
if (hasOllama) return "ollama";
@ -195,6 +228,7 @@ export function resolveSearchProvider(
if (pref === "serper") {
if (hasSerper) return "serper";
if (hasTavily) return "tavily";
if (hasMiniMax) return "minimax";
if (hasBrave) return "brave";
if (hasExa) return "exa";
if (hasOllama) return "ollama";
@ -205,6 +239,7 @@ export function resolveSearchProvider(
if (hasExa) return "exa";
if (hasSerper) return "serper";
if (hasTavily) return "tavily";
if (hasMiniMax) return "minimax";
if (hasBrave) return "brave";
if (hasOllama) return "ollama";
return null;
@ -213,6 +248,7 @@ export function resolveSearchProvider(
if (pref === "ollama") {
if (hasOllama) return "ollama";
if (hasTavily) return "tavily";
if (hasMiniMax) return "minimax";
if (hasBrave) return "brave";
if (hasSerper) return "serper";
if (hasExa) return "exa";

View file

@ -7,9 +7,11 @@
*
* Supports multiple backends:
* - Tavily: POST-based, client-side token budgeting via budgetContent()
* - MiniMax: POST-based search snippets with client-side token budgeting
* - Brave: GET-based LLM Context API with server-side budgeting
* - Serper: search API + Jina Reader extraction
* - Exa: search API with built-in extracted contents
* - Ollama: POST-based web search with client-side token budgeting
*
* Provider is selected by resolveSearchProvider() same as tool-search.ts.
*
@ -43,6 +45,7 @@ import {
braveHeaders,
getBraveApiKey,
getExaApiKey,
getMiniMaxSearchApiKey,
getOllamaApiKey,
getSerperApiKey,
getTavilyApiKey,
@ -105,7 +108,14 @@ interface LLMContextDetails {
errorKind?: string;
error?: string;
retryAfterMs?: number;
provider?: "tavily" | "brave" | "serper" | "exa" | "ollama" | "combosearch";
provider?:
| "tavily"
| "minimax"
| "brave"
| "serper"
| "exa"
| "ollama"
| "combosearch";
}
interface SerperOrganicResult {
@ -130,6 +140,21 @@ interface ExaLLMContextResponse {
results?: ExaLLMContextResult[];
}
interface MiniMaxSearchResult {
title?: string;
link?: string;
snippet?: string;
date?: string;
}
interface MiniMaxSearchResponse {
organic?: MiniMaxSearchResult[];
base_resp?: {
status_code?: number;
status_msg?: string;
};
}
// =============================================================================
// Cache
// =============================================================================
@ -362,6 +387,63 @@ async function executeOllamaLLMContext(
return { cached, latencyMs: timed.latencyMs, rateLimit: timed.rateLimit };
}
/**
* Execute a search_and_read query against the MiniMax Coding Plan search API.
*
* MiniMax currently returns search snippets rather than full fetched pages, so
* this path exposes those snippets through the same LLM context formatter.
*/
async function executeMiniMaxLLMContext(
params: { query: string; maxTokens: number; threshold: string },
signal?: AbortSignal,
): Promise<{
cached: CachedLLMContext;
latencyMs: number;
rateLimit?: RateLimitInfo;
}> {
const scoreThreshold = THRESHOLD_TO_SCORE[params.threshold] ?? 0.5;
const timed = await fetchWithRetryTimed(
"https://api.minimax.io/v1/coding_plan/search",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getMiniMaxSearchApiKey()}`,
"MM-API-Source": "SF",
},
body: JSON.stringify({ q: params.query }),
signal,
},
2,
);
const data: MiniMaxSearchResponse = await timed.response.json();
if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
throw new Error(
`MiniMax search failed: ${data.base_resp.status_msg ?? data.base_resp.status_code}`,
);
}
const tavilyLikeResults: TavilyResult[] = (data.organic || [])
.filter((r) => typeof r.link === "string" && r.link.length > 0)
.map((r) => ({
title: r.title || "(untitled)",
url: r.link as string,
content: r.snippet || "",
published_date: r.date,
score: 1.0,
}));
const cached = budgetContent(
tavilyLikeResults,
params.maxTokens,
scoreThreshold,
);
return { cached, latencyMs: timed.latencyMs, rateLimit: timed.rateLimit };
}
async function executeBraveLLMContext(
params: {
query: string;
@ -634,10 +716,13 @@ async function executeExaLLMContext(
}
function availableComboProviders(): Array<
"tavily" | "brave" | "serper" | "exa" | "ollama"
"tavily" | "minimax" | "brave" | "serper" | "exa" | "ollama"
> {
const providers: Array<"tavily" | "brave" | "serper" | "exa" | "ollama"> = [];
const providers: Array<
"tavily" | "minimax" | "brave" | "serper" | "exa" | "ollama"
> = [];
if (getTavilyApiKey()) providers.push("tavily");
if (getMiniMaxSearchApiKey()) providers.push("minimax");
if (getBraveApiKey()) providers.push("brave");
if (getSerperApiKey()) providers.push("serper");
if (getExaApiKey()) providers.push("exa");
@ -695,6 +780,16 @@ async function executeComboLLMContext(
if (provider === "tavily") {
return executeTavilyLLMContext(params, signal);
}
if (provider === "minimax") {
return executeMiniMaxLLMContext(
{
query: params.query,
maxTokens: params.maxTokens,
threshold: params.threshold,
},
signal,
);
}
if (provider === "ollama") {
return executeOllamaLLMContext(
{
@ -837,7 +932,7 @@ export function registerLLMContextTool(pi: ExtensionAPI) {
content: [
{
type: "text",
text: "search_and_read unavailable: No search API key is set. Use secure_env_collect to set TAVILY_API_KEY, BRAVE_API_KEY, SERPER_API_KEY, EXA_API_KEY, or OLLAMA_API_KEY.",
text: "search_and_read unavailable: No search API key is set. Use secure_env_collect to set TAVILY_API_KEY, MINIMAX_CODE_PLAN_KEY, BRAVE_API_KEY, SERPER_API_KEY, EXA_API_KEY, or OLLAMA_API_KEY.",
},
],
isError: true,
@ -944,6 +1039,14 @@ export function registerLLMContextTool(pi: ExtensionAPI) {
result = ollamaResult.cached;
latencyMs = ollamaResult.latencyMs;
rateLimit = ollamaResult.rateLimit;
} else if (provider === "minimax") {
const minimaxResult = await executeMiniMaxLLMContext(
{ query: params.query, maxTokens, threshold },
signal,
);
result = minimaxResult.cached;
latencyMs = minimaxResult.latencyMs;
rateLimit = minimaxResult.rateLimit;
} else if (provider === "serper") {
const serperResult = await executeSerperLLMContext(
{ query: params.query, maxTokens, maxUrls, threshold, count },

View file

@ -1,5 +1,5 @@
/**
* search-the-web tool Rich web search with full Brave API support.
* search-the-web tool Rich web search with Tavily, MiniMax, Ollama, Serper, Exa, and legacy Brave support.
*
* v3 improvements:
* - Structured error taxonomy (auth_error, rate_limited, network_error, etc.)
@ -37,6 +37,7 @@ import {
braveHeaders,
getBraveApiKey,
getExaApiKey,
getMiniMaxSearchApiKey,
getOllamaApiKey,
getSerperApiKey,
getTavilyApiKey,
@ -124,7 +125,14 @@ interface SearchDetails {
errorKind?: string;
error?: string;
retryAfterMs?: number;
provider?: "tavily" | "brave" | "serper" | "exa" | "ollama" | "combosearch";
provider?:
| "tavily"
| "minimax"
| "brave"
| "serper"
| "exa"
| "ollama"
| "combosearch";
}
// =============================================================================
@ -356,6 +364,22 @@ interface ExaSearchResponse {
results?: ExaSearchResult[];
}
interface MiniMaxSearchResult {
title?: string;
link?: string;
snippet?: string;
date?: string;
}
interface MiniMaxSearchResponse {
organic?: MiniMaxSearchResult[];
related_searches?: Array<{ query?: string }>;
base_resp?: {
status_code?: number;
status_msg?: string;
};
}
/**
* Execute a search against the Ollama web_search API.
* Returns a CachedSearchResult with normalized, deduplicated results.
@ -401,6 +425,60 @@ async function executeOllamaSearch(
};
}
/**
* Execute a search against the MiniMax Coding Plan search API.
*/
async function executeMiniMaxSearch(
params: { query: string },
signal?: AbortSignal,
): Promise<{
results: CachedSearchResult;
latencyMs: number;
rateLimit?: RateLimitInfo;
}> {
const timed = await fetchWithRetryTimed(
"https://api.minimax.io/v1/coding_plan/search",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getMiniMaxSearchApiKey()}`,
"MM-API-Source": "SF",
},
body: JSON.stringify({ q: params.query }),
signal,
},
2,
);
const data: MiniMaxSearchResponse = await timed.response.json();
if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
throw new Error(
`MiniMax search failed: ${data.base_resp.status_msg ?? data.base_resp.status_code}`,
);
}
const normalized: SearchResultFormatted[] = (data.organic || [])
.filter((r) => typeof r.link === "string" && r.link.length > 0)
.map((r) => ({
title: r.title || "(untitled)",
url: r.link as string,
description: r.snippet || "",
age: r.date || undefined,
}));
const deduplicated = deduplicateResults(normalized);
return {
results: {
results: deduplicated,
queryCorrected: false,
moreResultsAvailable: (data.related_searches?.length ?? 0) > 0,
},
latencyMs: timed.latencyMs,
rateLimit: timed.rateLimit,
};
}
async function executeSerperSearch(
params: { query: string; domain?: string; count: number },
signal?: AbortSignal,
@ -599,10 +677,13 @@ async function executeBraveSearch(
}
function availableComboProviders(): Array<
"tavily" | "brave" | "serper" | "exa" | "ollama"
"tavily" | "minimax" | "brave" | "serper" | "exa" | "ollama"
> {
const providers: Array<"tavily" | "brave" | "serper" | "exa" | "ollama"> = [];
const providers: Array<
"tavily" | "minimax" | "brave" | "serper" | "exa" | "ollama"
> = [];
if (getTavilyApiKey()) providers.push("tavily");
if (getMiniMaxSearchApiKey()) providers.push("minimax");
if (getBraveApiKey()) providers.push("brave");
if (getSerperApiKey()) providers.push("serper");
if (getExaApiKey()) providers.push("exa");
@ -637,6 +718,9 @@ async function executeComboSearch(
signal,
);
}
if (provider === "minimax") {
return executeMiniMaxSearch({ query: params.query }, signal);
}
if (provider === "ollama") {
return executeOllamaSearch(
{ query: params.query, count: Math.max(10, params.count) },
@ -749,7 +833,8 @@ export function registerSearchTool(pi: ExtensionAPI) {
name: "search-the-web",
label: "Web Search",
description:
"Search the web using Brave Search API. Returns top results with titles, URLs, descriptions, " +
"Search the web using Tavily, MiniMax, Ollama, Serper, Exa, or an existing Brave Search API key. " +
"Returns top results with titles, URLs, descriptions, " +
"extra contextual snippets, result ages, and optional AI summary. " +
"Supports freshness filtering, domain filtering, and auto-detects recency-sensitive queries.",
promptSnippet: "Search the web for information",
@ -810,7 +895,7 @@ export function registerSearchTool(pi: ExtensionAPI) {
content: [
{
type: "text",
text: "Web search unavailable: No search API key is set. Use secure_env_collect to set TAVILY_API_KEY, BRAVE_API_KEY, SERPER_API_KEY, EXA_API_KEY, or OLLAMA_API_KEY.",
text: "Web search unavailable: No search API key is set. Use secure_env_collect to set TAVILY_API_KEY, MINIMAX_CODE_PLAN_KEY, BRAVE_API_KEY, SERPER_API_KEY, EXA_API_KEY, or OLLAMA_API_KEY.",
},
],
isError: true,
@ -1013,6 +1098,14 @@ export function registerSearchTool(pi: ExtensionAPI) {
searchResult = ollamaResult.results;
latencyMs = ollamaResult.latencyMs;
rateLimit = ollamaResult.rateLimit;
} else if (provider === "minimax") {
const minimaxResult = await executeMiniMaxSearch(
{ query: params.query },
signal,
);
searchResult = minimaxResult.results;
latencyMs = minimaxResult.latencyMs;
rateLimit = minimaxResult.rateLimit;
} else if (provider === "serper") {
const serperResult = await executeSerperSearch(
{ query: params.query, domain: params.domain, count: 10 },

View file

@ -389,10 +389,11 @@ export interface SFPreferences {
verification_commands?: string[];
verification_auto_fix?: boolean;
verification_max_retries?: number;
/** Search provider preference. "brave"/"tavily"/"serper"/"exa"/"ollama"/"combosearch" force that backend and disable native Anthropic search. "native" forces native only. "auto" = current default behavior. */
/** Search provider preference. "brave"/"tavily"/"minimax"/"serper"/"exa"/"ollama"/"combosearch" force that backend and disable native Anthropic search. "native" forces native only. "auto" = current default behavior. */
search_provider?:
| "brave"
| "tavily"
| "minimax"
| "serper"
| "exa"
| "ollama"

View file

@ -467,6 +467,7 @@ export function validatePreferences(preferences: SFPreferences): {
const validSearchProviders = new Set([
"brave",
"tavily",
"minimax",
"serper",
"exa",
"ollama",
@ -482,7 +483,7 @@ export function validatePreferences(preferences: SFPreferences): {
preferences.search_provider as SFPreferences["search_provider"];
} else {
errors.push(
`search_provider must be one of: brave, tavily, serper, exa, ollama, combosearch, native, auto`,
`search_provider must be one of: brave, tavily, minimax, serper, exa, ollama, combosearch, native, auto`,
);
}
}