mirror of https://github.com/openclaw/openclaw.git
feat(tools): add MiniMax as bundled web search provider
Add native MiniMax Search integration via their Coding Plan search API (POST /v1/coding_plan/search). This brings MiniMax in line with Brave, Kimi, Grok, Gemini, and other providers that already have bundled web search support. - Implement WebSearchProviderPlugin with caching, credential resolution, and trusted endpoint wrapping - Support both global (api.minimax.io) and CN (api.minimaxi.com) endpoints, inferred from explicit region config, model provider base URL, or minimax-portal OAuth base URL - Prefer MINIMAX_CODE_PLAN_KEY over MINIMAX_API_KEY in credential fallback, matching existing repo precedence - Accept SecretRef objects for webSearch.apiKey (type: [string, object]) - Register in bundled registry, provider-id compat map, and fast-path plugin id list with full alignment test coverage - Add unit tests for endpoint/region resolution and edge cases Closes #47927 Related #11399 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a722719720
commit
d204be80af
|
|
@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Plugins/install: add `openclaw plugins install --force` to overwrite existing plugin and hook-pack install targets without using the dangerous-code override flag. (#60544) Thanks @gumadeiras.
|
||||
- Providers/Anthropic: remove setup-token from new onboarding and auth-command setup paths, keep existing configured legacy token profiles runnable, and steer new Anthropic setup to Claude CLI or API keys.
|
||||
- Providers/OpenAI Codex: add forward-compat `openai-codex/gpt-5.4-mini` synthesis across provider runtime, model catalog, and model listing so Codex mini works before bundled Pi catalog updates land.
|
||||
- Tools/web_search: add a bundled MiniMax Search provider backed by the Coding Plan search API, with region reuse from `MINIMAX_API_HOST` and plugin-owned credential config. (#54648) Thanks @fengmk2.
|
||||
- Channels/context visibility: add configurable `contextVisibility` per channel (`all`, `allowlist`, `allowlist_quote`) so quoted, threaded, and fetched history context can be filtered by sender allowlists instead of always passing through as received.
|
||||
- Providers/request overrides: add shared model and media request transport overrides across OpenAI-, Anthropic-, Google-, and compatible provider paths, including headers, auth, proxy, and TLS controls. (#60200)
|
||||
- Matrix/exec approvals: add Matrix-native exec approval prompts with account-scoped approvers, channel-or-DM delivery, and room-thread aware resolution handling. (#58635) Thanks @gumadeiras.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ title: "MiniMax"
|
|||
|
||||
OpenClaw's MiniMax provider defaults to **MiniMax M2.7**.
|
||||
|
||||
MiniMax also provides:
|
||||
|
||||
- bundled speech synthesis via T2A v2
|
||||
- bundled `web_search` through the MiniMax Coding Plan search API
|
||||
|
||||
## Model lineup
|
||||
|
||||
- `MiniMax-M2.7`: default hosted reasoning model.
|
||||
|
|
@ -44,8 +49,24 @@ When onboarding or API-key setup writes explicit `models.providers.minimax`
|
|||
entries, OpenClaw materializes `MiniMax-M2.7` and
|
||||
`MiniMax-M2.7-highspeed` with `input: ["text", "image"]`.
|
||||
|
||||
The bundled MiniMax provider catalog itself currently advertises those chat
|
||||
refs as text-only metadata until explicit provider config is materialized.
|
||||
The bundled MiniMax provider catalog also advertises image input on those M2.7
|
||||
chat refs, so image-capable routing can use MiniMax without requiring explicit
|
||||
provider config first.
|
||||
|
||||
## Web search
|
||||
|
||||
The MiniMax plugin also registers `web_search` through the MiniMax Coding Plan
|
||||
search API.
|
||||
|
||||
- Provider id: `minimax`
|
||||
- Structured results: titles, URLs, snippets, related queries
|
||||
- Preferred env var: `MINIMAX_CODE_PLAN_KEY`
|
||||
- Accepted env alias: `MINIMAX_CODING_API_KEY`
|
||||
- Compatibility fallback: `MINIMAX_API_KEY` when it already points at a coding-plan token
|
||||
- Region reuse: `plugins.entries.minimax.config.webSearch.region`, then `MINIMAX_API_HOST`, then MiniMax provider base URLs
|
||||
|
||||
Config lives under `plugins.entries.minimax.config.webSearch.*`.
|
||||
See [MiniMax Search](/tools/minimax-search).
|
||||
|
||||
## Choose a setup
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ Scope intent:
|
|||
- `plugins.entries.moonshot.config.webSearch.apiKey`
|
||||
- `plugins.entries.perplexity.config.webSearch.apiKey`
|
||||
- `plugins.entries.firecrawl.config.webSearch.apiKey`
|
||||
- `plugins.entries.minimax.config.webSearch.apiKey`
|
||||
- `plugins.entries.tavily.config.webSearch.apiKey`
|
||||
- `tools.web.search.apiKey`
|
||||
- `gateway.auth.password`
|
||||
|
|
|
|||
|
|
@ -540,6 +540,13 @@
|
|||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.minimax.config.webSearch.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "plugins.entries.minimax.config.webSearch.apiKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "plugins.entries.moonshot.config.webSearch.apiKey",
|
||||
"configFile": "openclaw.json",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
---
|
||||
summary: "MiniMax Search via the Coding Plan search API"
|
||||
read_when:
|
||||
- You want to use MiniMax for web_search
|
||||
- You need a MiniMax Coding Plan key
|
||||
- You want MiniMax CN/global search host guidance
|
||||
title: "MiniMax Search"
|
||||
---
|
||||
|
||||
# MiniMax Search
|
||||
|
||||
OpenClaw supports MiniMax as a `web_search` provider through the MiniMax
|
||||
Coding Plan search API. It returns structured search results with titles, URLs,
|
||||
snippets, and related queries.
|
||||
|
||||
## Get a Coding Plan key
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a key">
|
||||
Create or copy a MiniMax Coding Plan key from
|
||||
[MiniMax Platform](https://platform.minimax.io/user-center/basic-information/interface-key).
|
||||
</Step>
|
||||
<Step title="Store the key">
|
||||
Set `MINIMAX_CODE_PLAN_KEY` in the Gateway environment, or configure via:
|
||||
|
||||
```bash
|
||||
openclaw configure --section web
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
OpenClaw also accepts `MINIMAX_CODING_API_KEY` as an env alias. `MINIMAX_API_KEY`
|
||||
is still read as a compatibility fallback when it already points at a coding-plan token.
|
||||
|
||||
## Config
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
minimax: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "sk-cp-...", // optional if MINIMAX_CODE_PLAN_KEY is set
|
||||
region: "global", // or "cn"
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "minimax",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Environment alternative:** set `MINIMAX_CODE_PLAN_KEY` in the Gateway environment.
|
||||
For a gateway install, put it in `~/.openclaw/.env`.
|
||||
|
||||
## Region selection
|
||||
|
||||
MiniMax Search uses these endpoints:
|
||||
|
||||
- Global: `https://api.minimax.io/v1/coding_plan/search`
|
||||
- CN: `https://api.minimaxi.com/v1/coding_plan/search`
|
||||
|
||||
If `plugins.entries.minimax.config.webSearch.region` is unset, OpenClaw resolves
|
||||
the region in this order:
|
||||
|
||||
1. `tools.web.search.minimax.region` / plugin-owned `webSearch.region`
|
||||
2. `MINIMAX_API_HOST`
|
||||
3. `models.providers.minimax.baseUrl`
|
||||
4. `models.providers.minimax-portal.baseUrl`
|
||||
|
||||
That means CN onboarding or `MINIMAX_API_HOST=https://api.minimaxi.com/...`
|
||||
automatically keeps MiniMax Search on the CN host too.
|
||||
|
||||
## Supported parameters
|
||||
|
||||
MiniMax Search supports:
|
||||
|
||||
- `query`
|
||||
- `count` (OpenClaw trims the returned result list to the requested count)
|
||||
|
||||
Provider-specific filters are not currently supported.
|
||||
|
||||
## Related
|
||||
|
||||
- [Web Search overview](/tools/web) -- all providers and auto-detection
|
||||
- [MiniMax](/providers/minimax) -- model, image, speech, and auth setup
|
||||
|
|
@ -80,6 +80,9 @@ local while `web_search` and `x_search` can use xAI Responses under the hood.
|
|||
<Card title="Kimi" icon="moon" href="/tools/kimi-search">
|
||||
AI-synthesized answers with citations via Moonshot web search.
|
||||
</Card>
|
||||
<Card title="MiniMax Search" icon="globe" href="/tools/minimax-search">
|
||||
Structured results via the MiniMax Coding Plan search API.
|
||||
</Card>
|
||||
<Card title="Ollama Web Search" icon="globe" href="/tools/ollama-search">
|
||||
Key-free search via your configured Ollama host. Requires `ollama signin`.
|
||||
</Card>
|
||||
|
|
@ -105,6 +108,7 @@ local while `web_search` and `x_search` can use xAI Responses under the hood.
|
|||
| [Gemini](/tools/gemini-search) | AI-synthesized + citations | -- | `GEMINI_API_KEY` |
|
||||
| [Grok](/tools/grok-search) | AI-synthesized + citations | -- | `XAI_API_KEY` |
|
||||
| [Kimi](/tools/kimi-search) | AI-synthesized + citations | -- | `KIMI_API_KEY` / `MOONSHOT_API_KEY` |
|
||||
| [MiniMax Search](/tools/minimax-search) | Structured snippets | Region (`global` / `cn`) | `MINIMAX_CODE_PLAN_KEY` / `MINIMAX_CODING_API_KEY` |
|
||||
| [Ollama Web Search](/tools/ollama-search) | Structured snippets | -- | None by default; `ollama signin` required, can reuse Ollama provider bearer auth |
|
||||
| [Perplexity](/tools/perplexity-search) | Structured snippets | Country, language, time, domains, content limits | `PERPLEXITY_API_KEY` / `OPENROUTER_API_KEY` |
|
||||
| [SearXNG](/tools/searxng-search) | Structured snippets | Categories, language | None (self-hosted) |
|
||||
|
|
@ -158,19 +162,20 @@ first one that is ready:
|
|||
API-backed providers first:
|
||||
|
||||
1. **Brave** -- `BRAVE_API_KEY` or `plugins.entries.brave.config.webSearch.apiKey` (order 10)
|
||||
2. **Gemini** -- `GEMINI_API_KEY` or `plugins.entries.google.config.webSearch.apiKey` (order 20)
|
||||
3. **Grok** -- `XAI_API_KEY` or `plugins.entries.xai.config.webSearch.apiKey` (order 30)
|
||||
4. **Kimi** -- `KIMI_API_KEY` / `MOONSHOT_API_KEY` or `plugins.entries.moonshot.config.webSearch.apiKey` (order 40)
|
||||
5. **Perplexity** -- `PERPLEXITY_API_KEY` / `OPENROUTER_API_KEY` or `plugins.entries.perplexity.config.webSearch.apiKey` (order 50)
|
||||
6. **Firecrawl** -- `FIRECRAWL_API_KEY` or `plugins.entries.firecrawl.config.webSearch.apiKey` (order 60)
|
||||
7. **Exa** -- `EXA_API_KEY` or `plugins.entries.exa.config.webSearch.apiKey` (order 65)
|
||||
8. **Tavily** -- `TAVILY_API_KEY` or `plugins.entries.tavily.config.webSearch.apiKey` (order 70)
|
||||
2. **MiniMax Search** -- `MINIMAX_CODE_PLAN_KEY` / `MINIMAX_CODING_API_KEY` or `plugins.entries.minimax.config.webSearch.apiKey` (order 15)
|
||||
3. **Gemini** -- `GEMINI_API_KEY` or `plugins.entries.google.config.webSearch.apiKey` (order 20)
|
||||
4. **Grok** -- `XAI_API_KEY` or `plugins.entries.xai.config.webSearch.apiKey` (order 30)
|
||||
5. **Kimi** -- `KIMI_API_KEY` / `MOONSHOT_API_KEY` or `plugins.entries.moonshot.config.webSearch.apiKey` (order 40)
|
||||
6. **Perplexity** -- `PERPLEXITY_API_KEY` / `OPENROUTER_API_KEY` or `plugins.entries.perplexity.config.webSearch.apiKey` (order 50)
|
||||
7. **Firecrawl** -- `FIRECRAWL_API_KEY` or `plugins.entries.firecrawl.config.webSearch.apiKey` (order 60)
|
||||
8. **Exa** -- `EXA_API_KEY` or `plugins.entries.exa.config.webSearch.apiKey` (order 65)
|
||||
9. **Tavily** -- `TAVILY_API_KEY` or `plugins.entries.tavily.config.webSearch.apiKey` (order 70)
|
||||
|
||||
Key-free fallbacks after that:
|
||||
|
||||
9. **DuckDuckGo** -- key-free HTML fallback with no account or API key (order 100)
|
||||
10. **Ollama Web Search** -- key-free fallback via your configured Ollama host; requires Ollama to be reachable and signed in with `ollama signin` and can reuse Ollama provider bearer auth if the host needs it (order 110)
|
||||
11. **SearXNG** -- `SEARXNG_BASE_URL` or `plugins.entries.searxng.config.webSearch.baseUrl` (order 200)
|
||||
10. **DuckDuckGo** -- key-free HTML fallback with no account or API key (order 100)
|
||||
11. **Ollama Web Search** -- key-free fallback via your configured Ollama host; requires Ollama to be reachable and signed in with `ollama signin` and can reuse Ollama provider bearer auth if the host needs it (order 110)
|
||||
12. **SearXNG** -- `SEARXNG_BASE_URL` or `plugins.entries.searxng.config.webSearch.baseUrl` (order 200)
|
||||
|
||||
If no provider is detected, it falls back to Brave (you will get a missing-key
|
||||
error prompting you to configure one).
|
||||
|
|
|
|||
|
|
@ -129,4 +129,25 @@ describe("minimax provider hooks", () => {
|
|||
expect(resolvedApiModelId).toBe("MiniMax-M2.7-highspeed");
|
||||
expect(resolvedPortalModelId).toBe("MiniMax-M2.7-highspeed");
|
||||
});
|
||||
|
||||
it("registers the bundled MiniMax web search provider", () => {
|
||||
const webSearchProviders: unknown[] = [];
|
||||
|
||||
minimaxPlugin.register({
|
||||
registerProvider() {},
|
||||
registerMediaUnderstandingProvider() {},
|
||||
registerImageGenerationProvider() {},
|
||||
registerSpeechProvider() {},
|
||||
registerWebSearchProvider(provider: unknown) {
|
||||
webSearchProviders.push(provider);
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(webSearchProviders).toHaveLength(1);
|
||||
expect(webSearchProviders[0]).toMatchObject({
|
||||
id: "minimax",
|
||||
label: "MiniMax Search",
|
||||
envVars: ["MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import type { MiniMaxRegion } from "./oauth.js";
|
|||
import { applyMinimaxApiConfig, applyMinimaxApiConfigCn } from "./onboard.js";
|
||||
import { buildMinimaxPortalProvider, buildMinimaxProvider } from "./provider-catalog.js";
|
||||
import { buildMinimaxSpeechProvider } from "./speech-provider.js";
|
||||
import { createMiniMaxWebSearchProvider } from "./src/minimax-web-search-provider.js";
|
||||
|
||||
const API_PROVIDER_ID = "minimax";
|
||||
const PORTAL_PROVIDER_ID = "minimax-portal";
|
||||
|
|
@ -237,7 +238,11 @@ export default definePluginEntry({
|
|||
},
|
||||
resolveUsageAuth: async (ctx) => {
|
||||
const apiKey = ctx.resolveApiKeyFromConfigAndStore({
|
||||
envDirect: [ctx.env.MINIMAX_CODE_PLAN_KEY, ctx.env.MINIMAX_API_KEY],
|
||||
envDirect: [
|
||||
ctx.env.MINIMAX_CODE_PLAN_KEY,
|
||||
ctx.env.MINIMAX_CODING_API_KEY,
|
||||
ctx.env.MINIMAX_API_KEY,
|
||||
],
|
||||
});
|
||||
return apiKey ? { token: apiKey } : null;
|
||||
},
|
||||
|
|
@ -303,5 +308,6 @@ export default definePluginEntry({
|
|||
api.registerImageGenerationProvider(buildMinimaxImageGenerationProvider());
|
||||
api.registerImageGenerationProvider(buildMinimaxPortalImageGenerationProvider());
|
||||
api.registerSpeechProvider(buildMinimaxSpeechProvider());
|
||||
api.registerWebSearchProvider(createMiniMaxWebSearchProvider());
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,11 +63,38 @@
|
|||
"contracts": {
|
||||
"speechProviders": ["minimax"],
|
||||
"mediaUnderstandingProviders": ["minimax", "minimax-portal"],
|
||||
"imageGenerationProviders": ["minimax", "minimax-portal"]
|
||||
"imageGenerationProviders": ["minimax", "minimax-portal"],
|
||||
"webSearchProviders": ["minimax"]
|
||||
},
|
||||
"uiHints": {
|
||||
"webSearch.apiKey": {
|
||||
"label": "MiniMax Coding Plan key",
|
||||
"help": "MiniMax Coding Plan key (fallback: MINIMAX_CODE_PLAN_KEY, MINIMAX_CODING_API_KEY, or MINIMAX_API_KEY if it already points at a coding-plan token).",
|
||||
"sensitive": true,
|
||||
"placeholder": "sk-cp-..."
|
||||
},
|
||||
"webSearch.region": {
|
||||
"label": "MiniMax Search Region",
|
||||
"help": "Search endpoint region override. Leave unset to reuse your configured MiniMax host or MINIMAX_API_HOST."
|
||||
}
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
"properties": {
|
||||
"webSearch": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"apiKey": {
|
||||
"type": ["string", "object"]
|
||||
},
|
||||
"region": {
|
||||
"type": "string",
|
||||
"enum": ["global", "cn"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { __testing } from "./minimax-web-search-provider.js";
|
||||
|
||||
const {
|
||||
MINIMAX_SEARCH_ENDPOINT_GLOBAL,
|
||||
MINIMAX_SEARCH_ENDPOINT_CN,
|
||||
resolveMiniMaxApiKey,
|
||||
resolveMiniMaxEndpoint,
|
||||
resolveMiniMaxRegion,
|
||||
} = __testing;
|
||||
|
||||
describe("minimax web search provider", () => {
|
||||
const originalApiHost = process.env.MINIMAX_API_HOST;
|
||||
const originalCodePlanKey = process.env.MINIMAX_CODE_PLAN_KEY;
|
||||
const originalCodingApiKey = process.env.MINIMAX_CODING_API_KEY;
|
||||
const originalApiKey = process.env.MINIMAX_API_KEY;
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.MINIMAX_API_HOST;
|
||||
delete process.env.MINIMAX_CODE_PLAN_KEY;
|
||||
delete process.env.MINIMAX_CODING_API_KEY;
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.MINIMAX_API_HOST = originalApiHost;
|
||||
process.env.MINIMAX_CODE_PLAN_KEY = originalCodePlanKey;
|
||||
process.env.MINIMAX_CODING_API_KEY = originalCodingApiKey;
|
||||
process.env.MINIMAX_API_KEY = originalApiKey;
|
||||
});
|
||||
|
||||
describe("resolveMiniMaxRegion", () => {
|
||||
it("returns global by default", () => {
|
||||
expect(resolveMiniMaxRegion()).toBe("global");
|
||||
expect(resolveMiniMaxRegion({})).toBe("global");
|
||||
});
|
||||
|
||||
it("returns cn when explicit region is cn", () => {
|
||||
expect(resolveMiniMaxRegion({ minimax: { region: "cn" } })).toBe("cn");
|
||||
});
|
||||
|
||||
it("returns global when explicit region is not cn", () => {
|
||||
expect(resolveMiniMaxRegion({ minimax: { region: "global" } })).toBe("global");
|
||||
expect(resolveMiniMaxRegion({ minimax: { region: "us" } })).toBe("global");
|
||||
});
|
||||
|
||||
it("infers cn from MINIMAX_API_HOST", () => {
|
||||
process.env.MINIMAX_API_HOST = "https://api.minimaxi.com/anthropic";
|
||||
expect(resolveMiniMaxRegion()).toBe("cn");
|
||||
});
|
||||
|
||||
it("infers cn from model provider base URL", () => {
|
||||
const cnConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
minimax: { baseUrl: "https://api.minimaxi.com/anthropic" },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(resolveMiniMaxRegion({}, cnConfig)).toBe("cn");
|
||||
});
|
||||
|
||||
it("infers cn from minimax-portal base URL (OAuth CN path)", () => {
|
||||
const cnPortalConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
"minimax-portal": { baseUrl: "https://api.minimaxi.com/anthropic" },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(resolveMiniMaxRegion({}, cnPortalConfig)).toBe("cn");
|
||||
});
|
||||
|
||||
it("returns global when model provider base URL is global", () => {
|
||||
const globalConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
minimax: { baseUrl: "https://api.minimax.io/anthropic" },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(resolveMiniMaxRegion({}, globalConfig)).toBe("global");
|
||||
});
|
||||
|
||||
it("explicit search config region takes priority over base URL", () => {
|
||||
const cnConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
minimax: { baseUrl: "https://api.minimaxi.com/anthropic" },
|
||||
},
|
||||
},
|
||||
};
|
||||
// Explicit global region overrides CN base URL
|
||||
expect(resolveMiniMaxRegion({ minimax: { region: "global" } }, cnConfig)).toBe("global");
|
||||
});
|
||||
|
||||
it("handles non-object minimax search config gracefully", () => {
|
||||
expect(resolveMiniMaxRegion({ minimax: "invalid" })).toBe("global");
|
||||
expect(resolveMiniMaxRegion({ minimax: null })).toBe("global");
|
||||
expect(resolveMiniMaxRegion({ minimax: [1, 2] })).toBe("global");
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMiniMaxEndpoint", () => {
|
||||
it("returns global endpoint by default", () => {
|
||||
expect(resolveMiniMaxEndpoint()).toBe(MINIMAX_SEARCH_ENDPOINT_GLOBAL);
|
||||
});
|
||||
|
||||
it("returns CN endpoint when region is cn", () => {
|
||||
expect(resolveMiniMaxEndpoint({ minimax: { region: "cn" } })).toBe(
|
||||
MINIMAX_SEARCH_ENDPOINT_CN,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns CN endpoint when inferred from model provider base URL", () => {
|
||||
const cnConfig = {
|
||||
models: {
|
||||
providers: {
|
||||
minimax: { baseUrl: "https://api.minimaxi.com/anthropic" },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(resolveMiniMaxEndpoint({}, cnConfig)).toBe(MINIMAX_SEARCH_ENDPOINT_CN);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveMiniMaxApiKey", () => {
|
||||
it("prefers configured apiKey over env vars", () => {
|
||||
process.env.MINIMAX_CODE_PLAN_KEY = "env-key";
|
||||
expect(resolveMiniMaxApiKey({ apiKey: "configured-key" })).toBe("configured-key");
|
||||
});
|
||||
|
||||
it("accepts MINIMAX_CODING_API_KEY as a coding-plan alias", () => {
|
||||
process.env.MINIMAX_CODING_API_KEY = "coding-key";
|
||||
expect(resolveMiniMaxApiKey()).toBe("coding-key");
|
||||
});
|
||||
|
||||
it("falls back to MINIMAX_API_KEY last", () => {
|
||||
process.env.MINIMAX_API_KEY = "plain-key";
|
||||
expect(resolveMiniMaxApiKey()).toBe("plain-key");
|
||||
});
|
||||
});
|
||||
|
||||
describe("endpoint constants", () => {
|
||||
it("uses correct global endpoint", () => {
|
||||
expect(MINIMAX_SEARCH_ENDPOINT_GLOBAL).toBe("https://api.minimax.io/v1/coding_plan/search");
|
||||
});
|
||||
|
||||
it("uses correct CN endpoint", () => {
|
||||
expect(MINIMAX_SEARCH_ENDPOINT_CN).toBe("https://api.minimaxi.com/v1/coding_plan/search");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
import { Type } from "@sinclair/typebox";
|
||||
import {
|
||||
DEFAULT_SEARCH_COUNT,
|
||||
MAX_SEARCH_COUNT,
|
||||
buildSearchCacheKey,
|
||||
formatCliCommand,
|
||||
mergeScopedSearchConfig,
|
||||
readCachedSearchPayload,
|
||||
readConfiguredSecretString,
|
||||
readNumberParam,
|
||||
readProviderEnvValue,
|
||||
readStringParam,
|
||||
resolveProviderWebSearchPluginConfig,
|
||||
resolveSearchCacheTtlMs,
|
||||
resolveSearchCount,
|
||||
resolveSearchTimeoutSeconds,
|
||||
resolveSiteName,
|
||||
setProviderWebSearchPluginConfigValue,
|
||||
setTopLevelCredentialValue,
|
||||
withTrustedWebSearchEndpoint,
|
||||
wrapWebContent,
|
||||
writeCachedSearchPayload,
|
||||
type SearchConfigRecord,
|
||||
type WebSearchProviderPlugin,
|
||||
type WebSearchProviderToolDefinition,
|
||||
} from "openclaw/plugin-sdk/provider-web-search";
|
||||
|
||||
const MINIMAX_SEARCH_ENDPOINT_GLOBAL = "https://api.minimax.io/v1/coding_plan/search";
|
||||
const MINIMAX_SEARCH_ENDPOINT_CN = "https://api.minimaxi.com/v1/coding_plan/search";
|
||||
const MINIMAX_CODING_PLAN_ENV_VARS = [
|
||||
"MINIMAX_CODE_PLAN_KEY",
|
||||
"MINIMAX_CODING_API_KEY",
|
||||
] as const;
|
||||
|
||||
type MiniMaxSearchResult = {
|
||||
title?: string;
|
||||
link?: string;
|
||||
snippet?: string;
|
||||
date?: string;
|
||||
};
|
||||
|
||||
type MiniMaxRelatedSearch = {
|
||||
query?: string;
|
||||
};
|
||||
|
||||
type MiniMaxSearchResponse = {
|
||||
organic?: MiniMaxSearchResult[];
|
||||
related_searches?: MiniMaxRelatedSearch[];
|
||||
base_resp?: {
|
||||
status_code?: number;
|
||||
status_msg?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function resolveMiniMaxApiKey(searchConfig?: SearchConfigRecord): string | undefined {
|
||||
return (
|
||||
readConfiguredSecretString(searchConfig?.apiKey, "tools.web.search.apiKey") ??
|
||||
readProviderEnvValue([...MINIMAX_CODING_PLAN_ENV_VARS, "MINIMAX_API_KEY"])
|
||||
);
|
||||
}
|
||||
|
||||
function isMiniMaxCnHost(value: string | undefined): boolean {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return new URL(trimmed).hostname.endsWith("minimaxi.com");
|
||||
} catch {
|
||||
return trimmed.includes("minimaxi.com");
|
||||
}
|
||||
}
|
||||
|
||||
function resolveMiniMaxRegion(
|
||||
searchConfig?: SearchConfigRecord,
|
||||
config?: Record<string, unknown>,
|
||||
): "cn" | "global" {
|
||||
// 1. Explicit region in search config takes priority
|
||||
const minimax =
|
||||
typeof searchConfig?.minimax === "object" &&
|
||||
searchConfig.minimax !== null &&
|
||||
!Array.isArray(searchConfig.minimax)
|
||||
? (searchConfig.minimax as Record<string, unknown>)
|
||||
: undefined;
|
||||
if (typeof minimax?.region === "string" && minimax.region.trim()) {
|
||||
return minimax.region === "cn" ? "cn" : "global";
|
||||
}
|
||||
|
||||
// 2. Infer from the shared MiniMax host override.
|
||||
if (isMiniMaxCnHost(process.env.MINIMAX_API_HOST)) {
|
||||
return "cn";
|
||||
}
|
||||
|
||||
// 3. Infer from model provider base URL (set by CN onboarding)
|
||||
const models = config?.models as Record<string, unknown> | undefined;
|
||||
const providers = models?.providers as Record<string, unknown> | undefined;
|
||||
const minimaxProvider = providers?.minimax as Record<string, unknown> | undefined;
|
||||
const portalProvider = providers?.["minimax-portal"] as Record<string, unknown> | undefined;
|
||||
const baseUrl = typeof minimaxProvider?.baseUrl === "string" ? minimaxProvider.baseUrl : "";
|
||||
const portalBaseUrl = typeof portalProvider?.baseUrl === "string" ? portalProvider.baseUrl : "";
|
||||
if (isMiniMaxCnHost(baseUrl) || isMiniMaxCnHost(portalBaseUrl)) {
|
||||
return "cn";
|
||||
}
|
||||
|
||||
return "global";
|
||||
}
|
||||
|
||||
function resolveMiniMaxEndpoint(
|
||||
searchConfig?: SearchConfigRecord,
|
||||
config?: Record<string, unknown>,
|
||||
): string {
|
||||
return resolveMiniMaxRegion(searchConfig, config) === "cn"
|
||||
? MINIMAX_SEARCH_ENDPOINT_CN
|
||||
: MINIMAX_SEARCH_ENDPOINT_GLOBAL;
|
||||
}
|
||||
|
||||
async function runMiniMaxSearch(params: {
|
||||
query: string;
|
||||
count: number;
|
||||
apiKey: string;
|
||||
endpoint: string;
|
||||
timeoutSeconds: number;
|
||||
}): Promise<{
|
||||
results: Array<Record<string, unknown>>;
|
||||
relatedSearches?: string[];
|
||||
}> {
|
||||
return withTrustedWebSearchEndpoint(
|
||||
{
|
||||
url: params.endpoint,
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${params.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify({ q: params.query }),
|
||||
},
|
||||
},
|
||||
async (res) => {
|
||||
if (!res.ok) {
|
||||
const detail = await res.text();
|
||||
throw new Error(`MiniMax Search API error (${res.status}): ${detail || res.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as MiniMaxSearchResponse;
|
||||
|
||||
if (data.base_resp?.status_code && data.base_resp.status_code !== 0) {
|
||||
throw new Error(
|
||||
`MiniMax Search API error (${data.base_resp.status_code}): ${data.base_resp.status_msg || "unknown error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
const organic = Array.isArray(data.organic) ? data.organic : [];
|
||||
const results = organic.slice(0, params.count).map((entry) => {
|
||||
const title = entry.title ?? "";
|
||||
const url = entry.link ?? "";
|
||||
const snippet = entry.snippet ?? "";
|
||||
return {
|
||||
title: title ? wrapWebContent(title, "web_search") : "",
|
||||
url,
|
||||
description: snippet ? wrapWebContent(snippet, "web_search") : "",
|
||||
published: entry.date || undefined,
|
||||
siteName: resolveSiteName(url) || undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const relatedSearches = Array.isArray(data.related_searches)
|
||||
? data.related_searches
|
||||
.map((r) => r.query)
|
||||
.filter((q): q is string => typeof q === "string" && q.length > 0)
|
||||
.map((q) => wrapWebContent(q, "web_search"))
|
||||
: undefined;
|
||||
|
||||
return { results, relatedSearches };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const MiniMaxSearchSchema = Type.Object({
|
||||
query: Type.String({ description: "Search query string." }),
|
||||
count: Type.Optional(
|
||||
Type.Number({
|
||||
description: "Number of results to return (1-10).",
|
||||
minimum: 1,
|
||||
maximum: MAX_SEARCH_COUNT,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
function missingMiniMaxKeyPayload() {
|
||||
return {
|
||||
error: "missing_minimax_api_key",
|
||||
message: `web_search (minimax) needs a MiniMax Coding Plan key. Run \`${formatCliCommand("openclaw configure --section web")}\` to store it, or set MINIMAX_CODE_PLAN_KEY, MINIMAX_CODING_API_KEY, or MINIMAX_API_KEY in the Gateway environment.`,
|
||||
docs: "https://docs.openclaw.ai/tools/web",
|
||||
};
|
||||
}
|
||||
|
||||
function createMiniMaxToolDefinition(
|
||||
searchConfig?: SearchConfigRecord,
|
||||
config?: Record<string, unknown>,
|
||||
): WebSearchProviderToolDefinition {
|
||||
return {
|
||||
description:
|
||||
"Search the web using MiniMax Search API. Returns titles, URLs, snippets, and related search suggestions.",
|
||||
parameters: MiniMaxSearchSchema,
|
||||
execute: async (args) => {
|
||||
const apiKey = resolveMiniMaxApiKey(searchConfig);
|
||||
if (!apiKey) {
|
||||
return missingMiniMaxKeyPayload();
|
||||
}
|
||||
|
||||
const params = args as Record<string, unknown>;
|
||||
const query = readStringParam(params, "query", { required: true });
|
||||
const count =
|
||||
readNumberParam(params, "count", { integer: true }) ??
|
||||
searchConfig?.maxResults ??
|
||||
undefined;
|
||||
|
||||
const resolvedCount = resolveSearchCount(count, DEFAULT_SEARCH_COUNT);
|
||||
const endpoint = resolveMiniMaxEndpoint(searchConfig, config);
|
||||
|
||||
const cacheKey = buildSearchCacheKey(["minimax", endpoint, query, resolvedCount]);
|
||||
const cached = readCachedSearchPayload(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
const timeoutSeconds = resolveSearchTimeoutSeconds(searchConfig);
|
||||
const cacheTtlMs = resolveSearchCacheTtlMs(searchConfig);
|
||||
|
||||
const { results, relatedSearches } = await runMiniMaxSearch({
|
||||
query,
|
||||
count: resolvedCount,
|
||||
apiKey,
|
||||
endpoint,
|
||||
timeoutSeconds,
|
||||
});
|
||||
|
||||
const payload: Record<string, unknown> = {
|
||||
query,
|
||||
provider: "minimax",
|
||||
count: results.length,
|
||||
tookMs: Date.now() - start,
|
||||
externalContent: {
|
||||
untrusted: true,
|
||||
source: "web_search",
|
||||
provider: "minimax",
|
||||
wrapped: true,
|
||||
},
|
||||
results,
|
||||
};
|
||||
|
||||
if (relatedSearches && relatedSearches.length > 0) {
|
||||
payload.relatedSearches = relatedSearches;
|
||||
}
|
||||
|
||||
writeCachedSearchPayload(cacheKey, payload, cacheTtlMs);
|
||||
return payload;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
MINIMAX_SEARCH_ENDPOINT_GLOBAL,
|
||||
MINIMAX_SEARCH_ENDPOINT_CN,
|
||||
resolveMiniMaxApiKey,
|
||||
resolveMiniMaxEndpoint,
|
||||
resolveMiniMaxRegion,
|
||||
} as const;
|
||||
|
||||
export function createMiniMaxWebSearchProvider(): WebSearchProviderPlugin {
|
||||
return {
|
||||
id: "minimax",
|
||||
label: "MiniMax Search",
|
||||
hint: "Structured results via MiniMax Coding Plan search API",
|
||||
credentialLabel: "MiniMax Coding Plan key",
|
||||
envVars: [...MINIMAX_CODING_PLAN_ENV_VARS],
|
||||
placeholder: "sk-cp-...",
|
||||
signupUrl: "https://platform.minimax.io/user-center/basic-information/interface-key",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/minimax-search",
|
||||
autoDetectOrder: 15,
|
||||
credentialPath: "plugins.entries.minimax.config.webSearch.apiKey",
|
||||
inactiveSecretPaths: ["plugins.entries.minimax.config.webSearch.apiKey"],
|
||||
getCredentialValue: (searchConfig) => searchConfig?.apiKey,
|
||||
setCredentialValue: setTopLevelCredentialValue,
|
||||
getConfiguredCredentialValue: (config) =>
|
||||
resolveProviderWebSearchPluginConfig(config, "minimax")?.apiKey,
|
||||
setConfiguredCredentialValue: (configTarget, value) => {
|
||||
setProviderWebSearchPluginConfigValue(configTarget, "minimax", "apiKey", value);
|
||||
},
|
||||
createTool: (ctx) =>
|
||||
createMiniMaxToolDefinition(
|
||||
mergeScopedSearchConfig(
|
||||
ctx.searchConfig as SearchConfigRecord | undefined,
|
||||
"minimax",
|
||||
resolveProviderWebSearchPluginConfig(ctx.config, "minimax"),
|
||||
{ mirrorApiKeyToTopLevel: true },
|
||||
) as SearchConfigRecord | undefined,
|
||||
ctx.config as Record<string, unknown> | undefined,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
@ -5,6 +5,13 @@ vi.mock("../runtime.js", () => ({
|
|||
defaultRuntime: { log: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
vi.mock("../plugin-sdk/telegram-command-config.js", () => ({
|
||||
TELEGRAM_COMMAND_NAME_PATTERN: /^[a-z0-9_]+$/,
|
||||
normalizeTelegramCommandName: (value: string) => value.trim().toLowerCase(),
|
||||
normalizeTelegramCommandDescription: (value: string) => value.trim(),
|
||||
resolveTelegramCustomCommands: () => ({ commands: [], issues: [] }),
|
||||
}));
|
||||
|
||||
const getScopedWebSearchCredential = (key: string) => (search?: Record<string, unknown>) =>
|
||||
(search?.[key] as { apiKey?: unknown } | undefined)?.apiKey;
|
||||
const getConfiguredPluginWebSearchConfig =
|
||||
|
|
@ -59,6 +66,13 @@ const mockWebSearchProviders = [
|
|||
getCredentialValue: getScopedWebSearchCredential("kimi"),
|
||||
getConfiguredCredentialValue: getConfiguredPluginWebSearchCredential("moonshot"),
|
||||
},
|
||||
{
|
||||
id: "minimax",
|
||||
envVars: ["MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY"],
|
||||
credentialPath: "plugins.entries.minimax.config.webSearch.apiKey",
|
||||
getCredentialValue: getScopedWebSearchCredential("minimax"),
|
||||
getConfiguredCredentialValue: getConfiguredPluginWebSearchCredential("minimax"),
|
||||
},
|
||||
{
|
||||
id: "perplexity",
|
||||
envVars: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
|
||||
|
|
@ -91,24 +105,8 @@ vi.mock("../plugins/web-search-providers.js", () => {
|
|||
};
|
||||
});
|
||||
|
||||
const secretInputSchema = {
|
||||
oneOf: [
|
||||
{ type: "string" },
|
||||
{
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
source: { type: "string" },
|
||||
provider: { type: "string" },
|
||||
id: { type: "string" },
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function buildWebSearchPluginSchema() {
|
||||
return {
|
||||
vi.mock("../plugins/manifest-registry.js", () => {
|
||||
const buildSchema = () => ({
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
|
|
@ -116,34 +114,69 @@ function buildWebSearchPluginSchema() {
|
|||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
apiKey: secretInputSchema,
|
||||
baseUrl: secretInputSchema,
|
||||
apiKey: {
|
||||
oneOf: [
|
||||
{ type: "string" },
|
||||
{
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
source: { type: "string" },
|
||||
provider: { type: "string" },
|
||||
id: { type: "string" },
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
},
|
||||
],
|
||||
},
|
||||
baseUrl: {
|
||||
oneOf: [
|
||||
{ type: "string" },
|
||||
{
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
source: { type: "string" },
|
||||
provider: { type: "string" },
|
||||
id: { type: "string" },
|
||||
},
|
||||
required: ["source", "provider", "id"],
|
||||
},
|
||||
],
|
||||
},
|
||||
model: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
vi.mock("../plugins/manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: () => ({
|
||||
plugins: [
|
||||
{
|
||||
id: "brave",
|
||||
origin: "bundled",
|
||||
channels: [],
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
rootDir: "/tmp/plugins/brave",
|
||||
source: "test",
|
||||
manifestPath: "/tmp/plugins/brave/openclaw.plugin.json",
|
||||
schemaCacheKey: "test:brave",
|
||||
configSchema: buildWebSearchPluginSchema(),
|
||||
},
|
||||
...["firecrawl", "google", "moonshot", "perplexity", "searxng", "tavily", "xai"].map(
|
||||
(id) => ({
|
||||
return {
|
||||
loadPluginManifestRegistry: () => ({
|
||||
plugins: [
|
||||
{
|
||||
id: "brave",
|
||||
origin: "bundled",
|
||||
channels: [],
|
||||
providers: [],
|
||||
cliBackends: [],
|
||||
skills: [],
|
||||
hooks: [],
|
||||
rootDir: "/tmp/plugins/brave",
|
||||
source: "test",
|
||||
manifestPath: "/tmp/plugins/brave/openclaw.plugin.json",
|
||||
schemaCacheKey: "test:brave",
|
||||
configSchema: buildSchema(),
|
||||
},
|
||||
...[
|
||||
"firecrawl",
|
||||
"google",
|
||||
"minimax",
|
||||
"moonshot",
|
||||
"perplexity",
|
||||
"searxng",
|
||||
"tavily",
|
||||
"xai",
|
||||
].map((id) => ({
|
||||
id,
|
||||
origin: "bundled",
|
||||
channels: [],
|
||||
|
|
@ -155,13 +188,13 @@ vi.mock("../plugins/manifest-registry.js", () => ({
|
|||
source: "test",
|
||||
manifestPath: `/tmp/plugins/${id}/openclaw.plugin.json`,
|
||||
schemaCacheKey: `test:${id}`,
|
||||
configSchema: buildWebSearchPluginSchema(),
|
||||
}),
|
||||
),
|
||||
],
|
||||
diagnostics: [],
|
||||
}),
|
||||
}));
|
||||
configSchema: buildSchema(),
|
||||
})),
|
||||
],
|
||||
diagnostics: [],
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
let validateConfigObjectWithPlugins: typeof import("./config.js").validateConfigObjectWithPlugins;
|
||||
let resolveSearchProvider: typeof import("../agents/tools/web-search.js").__testing.resolveSearchProvider;
|
||||
|
|
@ -278,6 +311,24 @@ describe("web search provider config", () => {
|
|||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts minimax provider config on the plugin-owned path", () => {
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
buildWebSearchProviderConfig({
|
||||
enabled: true,
|
||||
provider: "minimax",
|
||||
providerConfig: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "MINIMAX_CODE_PLAN_KEY",
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(res.ok).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts searxng provider config on the plugin-owned path", () => {
|
||||
const res = validateConfigObjectWithPlugins(
|
||||
buildWebSearchProviderConfig({
|
||||
|
|
@ -363,6 +414,9 @@ describe("web search provider auto-detection", () => {
|
|||
delete process.env.FIRECRAWL_API_KEY;
|
||||
delete process.env.GEMINI_API_KEY;
|
||||
delete process.env.KIMI_API_KEY;
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
delete process.env.MINIMAX_CODE_PLAN_KEY;
|
||||
delete process.env.MINIMAX_CODING_API_KEY;
|
||||
delete process.env.MOONSHOT_API_KEY;
|
||||
delete process.env.PERPLEXITY_API_KEY;
|
||||
delete process.env.OPENROUTER_API_KEY;
|
||||
|
|
@ -412,6 +466,11 @@ describe("web search provider auto-detection", () => {
|
|||
expect(resolveSearchProvider({})).toBe("kimi");
|
||||
});
|
||||
|
||||
it("auto-detects minimax when only MINIMAX_CODE_PLAN_KEY is set", () => {
|
||||
process.env.MINIMAX_CODE_PLAN_KEY = "sk-cp-test";
|
||||
expect(resolveSearchProvider({})).toBe("minimax");
|
||||
});
|
||||
|
||||
it("auto-detects perplexity when only PERPLEXITY_API_KEY is set", () => {
|
||||
process.env.PERPLEXITY_API_KEY = "test-perplexity-key"; // pragma: allowlist secret
|
||||
expect(resolveSearchProvider({})).toBe("perplexity");
|
||||
|
|
|
|||
|
|
@ -67,6 +67,13 @@ describe("bundled web search helpers", () => {
|
|||
});
|
||||
vi.mocked(loadBundledCapabilityRuntimeRegistry).mockReturnValue({
|
||||
webSearchProviders: [
|
||||
{
|
||||
pluginId: "minimax",
|
||||
provider: createMockedBundledWebSearchProvider({
|
||||
pluginId: "minimax",
|
||||
providerId: "minimax",
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "xai",
|
||||
provider: createMockedBundledWebSearchProvider({
|
||||
|
|
@ -116,15 +123,18 @@ describe("bundled web search helpers", () => {
|
|||
|
||||
it("maps bundled provider ids back to their owning plugins", () => {
|
||||
expect(resolveBundledWebSearchPluginId(" gemini ")).toBe("google");
|
||||
expect(resolveBundledWebSearchPluginId(" minimax ")).toBe("minimax");
|
||||
expect(resolveBundledWebSearchPluginId("missing")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("loads bundled provider entries through the capability runtime registry once", () => {
|
||||
expect(listBundledWebSearchProviders()).toEqual([
|
||||
expect.objectContaining({ pluginId: "minimax", id: "minimax" }),
|
||||
expect.objectContaining({ pluginId: "xai", id: "grok" }),
|
||||
expect.objectContaining({ pluginId: "google", id: "gemini" }),
|
||||
]);
|
||||
expect(listBundledWebSearchProviders()).toEqual([
|
||||
expect.objectContaining({ pluginId: "minimax", id: "minimax" }),
|
||||
expect.objectContaining({ pluginId: "xai", id: "grok" }),
|
||||
expect.objectContaining({ pluginId: "google", id: "gemini" }),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import { describeBundledWebSearchFastPathContract } from "../../../test/helpers/plugins/bundled-web-search-fast-path-contract.js";
|
||||
|
||||
describeBundledWebSearchFastPathContract("minimax");
|
||||
|
|
@ -37,7 +37,7 @@ describe("provider env vars", () => {
|
|||
]),
|
||||
);
|
||||
expect(listKnownProviderAuthEnvVarNames()).toEqual(
|
||||
expect.arrayContaining(["MINIMAX_CODE_PLAN_KEY"]),
|
||||
expect.arrayContaining(["MINIMAX_CODE_PLAN_KEY", "MINIMAX_CODING_API_KEY"]),
|
||||
);
|
||||
expect(listKnownSecretEnvVarNames()).not.toContain("OPENCLAW_API_KEY");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@ export function getProviderEnvVars(providerId: string): string[] {
|
|||
return Array.isArray(envVars) ? [...envVars] : [];
|
||||
}
|
||||
|
||||
const EXTRA_PROVIDER_AUTH_ENV_VARS = ["MINIMAX_CODE_PLAN_KEY"] as const;
|
||||
const EXTRA_PROVIDER_AUTH_ENV_VARS = [
|
||||
"MINIMAX_CODE_PLAN_KEY",
|
||||
"MINIMAX_CODING_API_KEY",
|
||||
] as const;
|
||||
|
||||
const KNOWN_SECRET_ENV_VARS = [
|
||||
...new Set(Object.values(PROVIDER_ENV_VARS).flatMap((keys) => keys)),
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ vi.mock("../plugins/web-fetch-providers.runtime.js", () => ({
|
|||
}));
|
||||
|
||||
function createTestProvider(params: {
|
||||
id: "brave" | "gemini" | "grok" | "kimi" | "perplexity" | "firecrawl" | "tavily";
|
||||
id: "brave" | "gemini" | "grok" | "kimi" | "minimax" | "perplexity" | "firecrawl" | "tavily";
|
||||
pluginId: string;
|
||||
order: number;
|
||||
}): PluginWebSearchProviderEntry {
|
||||
|
|
@ -100,6 +100,7 @@ function buildTestWebSearchProviders(): PluginWebSearchProviderEntry[] {
|
|||
createTestProvider({ id: "gemini", pluginId: "google", order: 20 }),
|
||||
createTestProvider({ id: "grok", pluginId: "xai", order: 30 }),
|
||||
createTestProvider({ id: "kimi", pluginId: "moonshot", order: 40 }),
|
||||
createTestProvider({ id: "minimax", pluginId: "minimax", order: 15 }),
|
||||
createTestProvider({ id: "perplexity", pluginId: "perplexity", order: 50 }),
|
||||
createTestProvider({ id: "firecrawl", pluginId: "firecrawl", order: 60 }),
|
||||
createTestProvider({ id: "tavily", pluginId: "tavily", order: 70 }),
|
||||
|
|
@ -260,6 +261,9 @@ function buildConfigForOpenClawTarget(entry: SecretRegistryEntry, envId: string)
|
|||
if (entry.id === "plugins.entries.firecrawl.config.webSearch.apiKey") {
|
||||
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "firecrawl");
|
||||
}
|
||||
if (entry.id === "plugins.entries.minimax.config.webSearch.apiKey") {
|
||||
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "minimax");
|
||||
}
|
||||
if (entry.id === "plugins.entries.tavily.config.webSearch.apiKey") {
|
||||
setPathCreateStrict(config, ["tools", "web", "search", "provider"], "tavily");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -422,6 +422,17 @@ const CORE_SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
|
|||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
{
|
||||
id: "plugins.entries.minimax.config.webSearch.apiKey",
|
||||
targetType: "plugins.entries.minimax.config.webSearch.apiKey",
|
||||
configFile: "openclaw.json",
|
||||
pathPattern: "plugins.entries.minimax.config.webSearch.apiKey",
|
||||
secretShape: SECRET_INPUT_SHAPE,
|
||||
expectedResolvedValue: "string",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
];
|
||||
|
||||
const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
|
||||
|
|
|
|||
Loading…
Reference in New Issue