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:
parent
ae0bbe32fc
commit
2eebeccb93
9 changed files with 266 additions and 21 deletions
|
|
@ -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) |
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue