From 21728777dfa06ced1d74b96cca78cd2a0aa8c87c Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Thu, 18 Jun 2026 18:44:57 +0800 Subject: [PATCH] feat(plugins): externalize official provider batch --- docs/concepts/model-providers.md | 65 +- docs/gateway/config-agents.md | 2 +- docs/gateway/config-tools.md | 2 +- docs/help/faq.md | 2 +- docs/nodes/audio.md | 2 +- docs/plugins/plugin-inventory.md | 76 +- docs/plugins/reference/arcee.md | 2 +- docs/plugins/reference/cerebras.md | 2 +- docs/plugins/reference/chutes.md | 2 +- .../reference/cloudflare-ai-gateway.md | 2 +- docs/plugins/reference/deepinfra.md | 2 +- docs/plugins/reference/deepseek.md | 2 +- docs/plugins/reference/exa.md | 2 +- docs/plugins/reference/firecrawl.md | 2 +- docs/plugins/reference/gradium.md | 2 +- docs/plugins/reference/groq.md | 2 +- docs/plugins/reference/inworld.md | 2 +- docs/plugins/reference/kilocode.md | 2 +- docs/plugins/reference/kimi.md | 2 +- docs/plugins/reference/perplexity.md | 2 +- docs/plugins/reference/qianfan.md | 2 +- docs/plugins/reference/qwen.md | 2 +- docs/plugins/reference/stepfun.md | 2 +- docs/providers/arcee.md | 11 +- docs/providers/cerebras.md | 17 +- docs/providers/chutes.md | 25 +- docs/providers/cloudflare-ai-gateway.md | 9 + docs/providers/deepinfra.md | 11 +- docs/providers/deepseek.md | 11 +- docs/providers/gradium.md | 11 +- docs/providers/groq.md | 31 +- docs/providers/inworld.md | 13 +- docs/providers/kilocode.md | 13 +- docs/providers/moonshot.md | 6 + docs/providers/perplexity-provider.md | 9 + docs/providers/qianfan.md | 13 +- docs/providers/qwen-oauth.md | 4 +- docs/providers/qwen.md | 33 +- docs/providers/stepfun.md | 13 +- docs/tools/exa-search.md | 9 + docs/tools/firecrawl.md | 13 +- docs/tools/parallel-search.md | 11 +- docs/tools/perplexity-search.md | 9 + docs/tools/video-generation.md | 2 +- docs/tools/web-fetch.md | 7 +- docs/tools/web.md | 7 +- extensions/arcee/README.md | 12 + extensions/arcee/npm-shrinkwrap.json | 12 + extensions/arcee/package.json | 26 +- extensions/cerebras/README.md | 12 + extensions/cerebras/npm-shrinkwrap.json | 12 + extensions/cerebras/package.json | 26 +- extensions/chutes/README.md | 12 + extensions/chutes/npm-shrinkwrap.json | 12 + extensions/chutes/package.json | 26 +- extensions/cloudflare-ai-gateway/README.md | 12 + .../cloudflare-ai-gateway/npm-shrinkwrap.json | 12 + extensions/cloudflare-ai-gateway/package.json | 26 +- extensions/deepinfra/README.md | 12 + extensions/deepinfra/npm-shrinkwrap.json | 12 + extensions/deepinfra/package.json | 26 +- extensions/deepseek/README.md | 12 + extensions/deepseek/npm-shrinkwrap.json | 12 + extensions/deepseek/package.json | 26 +- extensions/exa/README.md | 12 + extensions/exa/npm-shrinkwrap.json | 12 + extensions/exa/package.json | 26 +- extensions/firecrawl/README.md | 12 + extensions/firecrawl/npm-shrinkwrap.json | 21 + extensions/firecrawl/package.json | 26 +- extensions/gradium/README.md | 12 + extensions/gradium/npm-shrinkwrap.json | 12 + extensions/gradium/package.json | 26 +- extensions/groq/README.md | 12 + extensions/groq/index.test.ts | 13 +- extensions/groq/index.ts | 25 +- extensions/groq/npm-shrinkwrap.json | 12 + extensions/groq/package.json | 26 +- extensions/inworld/README.md | 12 + extensions/inworld/npm-shrinkwrap.json | 12 + extensions/inworld/package.json | 26 +- extensions/kilocode/README.md | 12 + extensions/kilocode/npm-shrinkwrap.json | 12 + extensions/kilocode/package.json | 26 +- extensions/kimi-coding/README.md | 12 + extensions/kimi-coding/npm-shrinkwrap.json | 12 + extensions/kimi-coding/package.json | 26 +- extensions/parallel/README.md | 12 + extensions/parallel/npm-shrinkwrap.json | 12 + extensions/parallel/package.json | 26 +- extensions/perplexity/README.md | 12 + extensions/perplexity/npm-shrinkwrap.json | 12 + extensions/perplexity/package.json | 26 +- extensions/qianfan/README.md | 12 + extensions/qianfan/npm-shrinkwrap.json | 12 + extensions/qianfan/package.json | 26 +- extensions/qwen/README.md | 12 + extensions/qwen/npm-shrinkwrap.json | 12 + extensions/qwen/package.json | 26 +- extensions/stepfun/README.md | 12 + extensions/stepfun/npm-shrinkwrap.json | 12 + extensions/stepfun/package.json | 26 +- package.json | 18 + .../lib/official-external-plugin-catalog.json | 237 +++++ .../official-external-provider-catalog.json | 866 +++++++++++++++++- src/cli/program/register.onboard.test.ts | 2 +- src/cli/program/register.onboard.ts | 4 +- src/commands/auth-choice.apply.ts | 14 +- src/commands/auth-choice.test.ts | 35 + src/commands/doctor/repair-sequencing.test.ts | 52 ++ src/commands/doctor/repair-sequencing.ts | 18 +- .../configured-provider-plugin-installs.ts | 119 +++ .../missing-configured-plugin-install.test.ts | 485 +++++++++- .../missing-configured-plugin-install.ts | 102 ++- ...release-configured-plugin-installs.test.ts | 149 +++ .../release-configured-plugin-installs.ts | 131 +-- .../local/auth-choice-inference.test.ts | 10 +- .../local/auth-choice-inference.ts | 4 +- .../auth-choice.plugin-providers.test.ts | 96 ++ .../local/auth-choice.plugin-providers.ts | 69 +- src/config/plugin-auto-enable.core.test.ts | 25 + src/config/plugin-auto-enable.shared.ts | 47 +- src/config/plugin-auto-enable.test-helpers.ts | 7 +- src/config/plugin-auto-enable.types.ts | 9 + src/plugins/channel-plugin-ids.test.ts | 18 + .../official-external-plugin-catalog.test.ts | 137 +++ .../official-external-plugin-catalog.ts | 126 ++- src/plugins/provider-auth-choices.test.ts | 75 ++ src/plugins/provider-auth-choices.ts | 47 + src/plugins/provider-install-catalog.ts | 3 + src/plugins/web-fetch-providers.runtime.ts | 3 + ...web-provider-resolution-candidates.test.ts | 37 + src/plugins/web-provider-resolution-shared.ts | 9 +- .../web-provider-runtime-shared.test.ts | 2 + src/plugins/web-provider-runtime-shared.ts | 7 +- .../web-search-install-catalog.test.ts | 46 + src/plugins/web-search-install-catalog.ts | 27 +- src/secrets/runtime-web-tools.test.ts | 35 +- src/secrets/runtime-web-tools.ts | 4 +- src/web-fetch/runtime.test.ts | 7 +- src/web-fetch/runtime.ts | 2 +- 141 files changed, 4135 insertions(+), 367 deletions(-) create mode 100644 extensions/arcee/README.md create mode 100644 extensions/arcee/npm-shrinkwrap.json create mode 100644 extensions/cerebras/README.md create mode 100644 extensions/cerebras/npm-shrinkwrap.json create mode 100644 extensions/chutes/README.md create mode 100644 extensions/chutes/npm-shrinkwrap.json create mode 100644 extensions/cloudflare-ai-gateway/README.md create mode 100644 extensions/cloudflare-ai-gateway/npm-shrinkwrap.json create mode 100644 extensions/deepinfra/README.md create mode 100644 extensions/deepinfra/npm-shrinkwrap.json create mode 100644 extensions/deepseek/README.md create mode 100644 extensions/deepseek/npm-shrinkwrap.json create mode 100644 extensions/exa/README.md create mode 100644 extensions/exa/npm-shrinkwrap.json create mode 100644 extensions/firecrawl/README.md create mode 100644 extensions/firecrawl/npm-shrinkwrap.json create mode 100644 extensions/gradium/README.md create mode 100644 extensions/gradium/npm-shrinkwrap.json create mode 100644 extensions/groq/README.md create mode 100644 extensions/groq/npm-shrinkwrap.json create mode 100644 extensions/inworld/README.md create mode 100644 extensions/inworld/npm-shrinkwrap.json create mode 100644 extensions/kilocode/README.md create mode 100644 extensions/kilocode/npm-shrinkwrap.json create mode 100644 extensions/kimi-coding/README.md create mode 100644 extensions/kimi-coding/npm-shrinkwrap.json create mode 100644 extensions/parallel/README.md create mode 100644 extensions/parallel/npm-shrinkwrap.json create mode 100644 extensions/perplexity/README.md create mode 100644 extensions/perplexity/npm-shrinkwrap.json create mode 100644 extensions/qianfan/README.md create mode 100644 extensions/qianfan/npm-shrinkwrap.json create mode 100644 extensions/qwen/README.md create mode 100644 extensions/qwen/npm-shrinkwrap.json create mode 100644 extensions/stepfun/README.md create mode 100644 extensions/stepfun/npm-shrinkwrap.json create mode 100644 src/commands/doctor/shared/configured-provider-plugin-installs.ts create mode 100644 src/plugins/web-search-install-catalog.test.ts diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 1bdfb5f915e..4d0b84afe0d 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -278,50 +278,28 @@ messages and normalizes `stats.cached` into `cacheRead`; legacy - Example models: `vercel-ai-gateway/anthropic/claude-opus-4.6`, `vercel-ai-gateway/moonshotai/kimi-k2.6` - CLI: `openclaw onboard --auth-choice ai-gateway-api-key` -### Kilo Gateway - -- Provider: `kilocode` -- Auth: `KILOCODE_API_KEY` -- Example model: `kilocode/kilo/auto` -- CLI: `openclaw onboard --auth-choice kilocode-api-key` -- Base URL: `https://api.kilo.ai/api/gateway/` -- Static fallback catalog ships `kilocode/kilo/auto`; live `https://api.kilo.ai/api/gateway/models` discovery can expand the runtime catalog further. -- Exact upstream routing behind `kilocode/kilo/auto` is owned by Kilo Gateway, not hard-coded in OpenClaw. - -See [/providers/kilocode](/providers/kilocode) for setup details. - ### Other bundled provider plugins -| Provider | Id | Auth env | Example model | -| --------------------------------------- | -------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- | -| BytePlus | `byteplus` / `byteplus-plan` | `BYTEPLUS_API_KEY` | `byteplus-plan/ark-code-latest` | -| Cerebras | `cerebras` | `CEREBRAS_API_KEY` | `cerebras/zai-glm-4.7` | -| Cohere | `cohere` | `COHERE_API_KEY` | `cohere/command-a-03-2025` | -| Cloudflare AI Gateway | `cloudflare-ai-gateway` | `CLOUDFLARE_AI_GATEWAY_API_KEY` | - | -| DeepInfra | `deepinfra` | `DEEPINFRA_API_KEY` | `deepinfra/deepseek-ai/DeepSeek-V4-Flash` | -| DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` | `deepseek/deepseek-v4-flash` | -| GitHub Copilot | `github-copilot` | `COPILOT_GITHUB_TOKEN` / `GH_TOKEN` / `GITHUB_TOKEN` | - | -| Groq | `groq` | `GROQ_API_KEY` | - | -| Hugging Face Inference | `huggingface` | `HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN` | `huggingface/deepseek-ai/DeepSeek-R1` | -| Kilo Gateway | `kilocode` | `KILOCODE_API_KEY` | `kilocode/kilo/auto` | -| Kimi Coding | `kimi` | `KIMI_API_KEY` or `KIMICODE_API_KEY` | `kimi/kimi-for-coding` | -| MiniMax | `minimax` / `minimax-portal` | `MINIMAX_API_KEY` / `MINIMAX_OAUTH_TOKEN` | `minimax/MiniMax-M3` | -| Mistral | `mistral` | `MISTRAL_API_KEY` | `mistral/mistral-large-latest` | -| Moonshot | `moonshot` | `MOONSHOT_API_KEY` | `moonshot/kimi-k2.6` | -| NVIDIA | `nvidia` | `NVIDIA_API_KEY` | `nvidia/nvidia/nemotron-3-ultra-550b-a55b` | -| NovitaAI | `novita` | `NOVITA_API_KEY` | `novita/deepseek/deepseek-v3-0324` | -| [Ollama Cloud](/providers/ollama-cloud) | `ollama-cloud` | `OLLAMA_API_KEY` | `ollama-cloud/kimi-k2.6` | -| OpenRouter | `openrouter` | OpenRouter OAuth or `OPENROUTER_API_KEY` | `openrouter/auto` | -| Qianfan | `qianfan` | `QIANFAN_API_KEY` | `qianfan/deepseek-v3.2` | -| Qwen Cloud | `qwen` | `QWEN_API_KEY` / `MODELSTUDIO_API_KEY` / `DASHSCOPE_API_KEY` | `qwen/qwen3.5-plus` | -| [Qwen OAuth](/providers/qwen-oauth) | `qwen-oauth` | `QWEN_API_KEY` | `qwen-oauth/qwen3.5-plus` | -| StepFun | `stepfun` / `stepfun-plan` | `STEPFUN_API_KEY` | `stepfun/step-3.5-flash` | -| Together | `together` | `TOGETHER_API_KEY` | `together/meta-llama/Llama-3.3-70B-Instruct-Turbo` | -| Venice | `venice` | `VENICE_API_KEY` | - | -| Vercel AI Gateway | `vercel-ai-gateway` | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway/anthropic/claude-opus-4.6` | -| Volcano Engine (Doubao) | `volcengine` / `volcengine-plan` | `VOLCANO_ENGINE_API_KEY` | `volcengine-plan/ark-code-latest` | -| xAI | `xai` | SuperGrok/X Premium OAuth or `XAI_API_KEY` | `xai/grok-4.3` | -| Xiaomi | `xiaomi` / `xiaomi-token-plan` | `XIAOMI_API_KEY` / `XIAOMI_TOKEN_PLAN_API_KEY` | `xiaomi/mimo-v2-flash` / `xiaomi-token-plan/mimo-v2.5-pro` | +| Provider | Id | Auth env | Example model | +| --------------------------------------- | -------------------------------- | ---------------------------------------------------- | ---------------------------------------------------------- | +| BytePlus | `byteplus` / `byteplus-plan` | `BYTEPLUS_API_KEY` | `byteplus-plan/ark-code-latest` | +| Cohere | `cohere` | `COHERE_API_KEY` | `cohere/command-a-03-2025` | +| GitHub Copilot | `github-copilot` | `COPILOT_GITHUB_TOKEN` / `GH_TOKEN` / `GITHUB_TOKEN` | - | +| Hugging Face Inference | `huggingface` | `HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN` | `huggingface/deepseek-ai/DeepSeek-R1` | +| MiniMax | `minimax` / `minimax-portal` | `MINIMAX_API_KEY` / `MINIMAX_OAUTH_TOKEN` | `minimax/MiniMax-M3` | +| Mistral | `mistral` | `MISTRAL_API_KEY` | `mistral/mistral-large-latest` | +| Moonshot | `moonshot` | `MOONSHOT_API_KEY` | `moonshot/kimi-k2.6` | +| NVIDIA | `nvidia` | `NVIDIA_API_KEY` | `nvidia/nvidia/nemotron-3-ultra-550b-a55b` | +| NovitaAI | `novita` | `NOVITA_API_KEY` | `novita/deepseek/deepseek-v3-0324` | +| [Ollama Cloud](/providers/ollama-cloud) | `ollama-cloud` | `OLLAMA_API_KEY` | `ollama-cloud/kimi-k2.6` | +| OpenRouter | `openrouter` | OpenRouter OAuth or `OPENROUTER_API_KEY` | `openrouter/auto` | +| [Qwen OAuth](/providers/qwen-oauth) | `qwen-oauth` | `QWEN_API_KEY` | `qwen-oauth/qwen3.5-plus` | +| Together | `together` | `TOGETHER_API_KEY` | `together/meta-llama/Llama-3.3-70B-Instruct-Turbo` | +| Venice | `venice` | `VENICE_API_KEY` | - | +| Vercel AI Gateway | `vercel-ai-gateway` | `AI_GATEWAY_API_KEY` | `vercel-ai-gateway/anthropic/claude-opus-4.6` | +| Volcano Engine (Doubao) | `volcengine` / `volcengine-plan` | `VOLCANO_ENGINE_API_KEY` | `volcengine-plan/ark-code-latest` | +| xAI | `xai` | SuperGrok/X Premium OAuth or `XAI_API_KEY` | `xai/grok-4.3` | +| Xiaomi | `xiaomi` / `xiaomi-token-plan` | `XIAOMI_API_KEY` / `XIAOMI_TOKEN_PLAN_API_KEY` | `xiaomi/mimo-v2-flash` / `xiaomi-token-plan/mimo-v2.5-pro` | #### Quirks worth knowing @@ -341,9 +319,6 @@ See [/providers/kilocode](/providers/kilocode) for setup details. Uses the xAI Responses path. The recommended path is SuperGrok/X Premium OAuth; API keys still work via `XAI_API_KEY` or plugin config, and Grok `web_search` reuses the same auth profile before API-key fallback. `grok-4.3` is the bundled default chat model, and `grok-build-0.1` is selectable for build/coding-focused work. `/fast` or `params.fastMode: true` rewrites `grok-3`, `grok-3-mini`, `grok-4`, and `grok-4-0709` to their `*-fast` variants. `tool_stream` defaults on; disable via `agents.defaults.models["xai/"].params.tool_stream=false`. - - Ships as the bundled `cerebras` provider plugin. GLM uses `zai-glm-4.7`; OpenAI-compatible base URL is `https://api.cerebras.ai/v1`. - ## Providers via `models.providers` (custom/base URL) diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index d4496f9f034..cd5ddc7be56 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -438,7 +438,7 @@ Time format in system prompt. Default: `auto` (OS preference). - Typical values: `qwen/wan2.6-t2v`, `qwen/wan2.6-i2v`, `qwen/wan2.6-r2v`, `qwen/wan2.6-r2v-flash`, or `qwen/wan2.7-r2v`. - If omitted, `video_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered video-generation providers in provider-id order. - If you select a provider/model directly, configure the matching provider auth/API key too. - - The bundled Qwen video-generation provider supports up to 1 output video, 1 input image, 4 input videos, 10 seconds duration, and provider-level `size`, `aspectRatio`, `resolution`, `audio`, and `watermark` options. + - The official Qwen video-generation plugin supports up to 1 output video, 1 input image, 4 input videos, 10 seconds duration, and provider-level `size`, `aspectRatio`, `resolution`, `audio`, and `watermark` options. - `pdfModel`: accepts either a string (`"provider/model"`) or an object (`{ primary, fallbacks }`). - Used by the `pdf` tool for model routing. - If omitted, the PDF tool falls back to `imageModel`, then to the resolved session/default model. diff --git a/docs/gateway/config-tools.md b/docs/gateway/config-tools.md index bf6c282fad9..ac567de58db 100644 --- a/docs/gateway/config-tools.md +++ b/docs/gateway/config-tools.md @@ -590,7 +590,7 @@ Interactive custom-provider onboarding infers image input for common vision mode - The bundled `cerebras` provider plugin can configure this via `openclaw onboard --auth-choice cerebras-api-key`. Use explicit provider config only when overriding defaults. + The official external `cerebras` provider plugin can configure this via `openclaw onboard --auth-choice cerebras-api-key`. Use explicit provider config only when overriding defaults. ```json5 { diff --git a/docs/help/faq.md b/docs/help/faq.md index f4817a823c0..1c761fc8250 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -857,7 +857,7 @@ lives on the [First-run FAQ](/help/faq-first-run). - If you use allowlists, add `web_search`/`web_fetch`/`x_search` or `group:web`. - `web_fetch` is enabled by default (unless explicitly disabled). - - If `tools.web.fetch.provider` is omitted, OpenClaw auto-detects the first ready fetch fallback provider from configured credentials. Today the bundled provider is Firecrawl. + - If `tools.web.fetch.provider` is omitted, OpenClaw auto-detects the first ready fetch fallback provider from available credentials. The official Firecrawl plugin provides that fallback. - Daemons read env vars from `~/.openclaw/.env` (or the service environment). Docs: [Web tools](/tools/web). diff --git a/docs/nodes/audio.md b/docs/nodes/audio.md index fde1ff90338..4152c16da25 100644 --- a/docs/nodes/audio.md +++ b/docs/nodes/audio.md @@ -28,7 +28,7 @@ OpenClaw auto-detects in this order and stops at the first working option: - `whisper` (Python CLI; downloads models automatically) 3. **Provider auth** - Configured `models.providers.*` entries that support audio are tried first - - Bundled fallback order: OpenAI → Groq → xAI → Deepgram → Google → SenseAudio → ElevenLabs → Mistral + - Provider fallback order: OpenAI → Groq → xAI → Deepgram → Google → SenseAudio → ElevenLabs → Mistral As of 2026-05-22, Gemini CLI auto-detect is no longer supported for media understanding. Google is transitioning Gemini CLI users to Antigravity CLI; audio should use local or provider transcription, while image/video CLI fallback should move to Antigravity CLI (`agy`). diff --git a/docs/plugins/plugin-inventory.md b/docs/plugins/plugin-inventory.md index ae956860658..a80e272fa6c 100644 --- a/docs/plugins/plugin-inventory.md +++ b/docs/plugins/plugin-inventory.md @@ -51,7 +51,7 @@ Each entry lists the package, distribution route, and description. ## Core npm package -90 plugins +72 plugins - **[admin-http-rpc](/plugins/reference/admin-http-rpc)** (`@openclaw/admin-http-rpc`) - included in OpenClaw. OpenClaw admin HTTP RPC endpoint. @@ -59,8 +59,6 @@ Each entry lists the package, distribution route, and description. - **[anthropic](/plugins/reference/anthropic)** (`@openclaw/anthropic-provider`) - included in OpenClaw. Adds Anthropic model provider support to OpenClaw. -- **[arcee](/plugins/reference/arcee)** (`@openclaw/arcee-provider`) - included in OpenClaw. Adds Arcee model provider support to OpenClaw. - - **[azure-speech](/plugins/reference/azure-speech)** (`@openclaw/azure-speech`) - included in OpenClaw. Azure AI Speech text-to-speech (MP3, native Ogg/Opus voice notes, PCM telephony). - **[bonjour](/plugins/reference/bonjour)** (`@openclaw/bonjour`) - included in OpenClaw. Advertise the local OpenClaw gateway over Bonjour/mDNS. @@ -71,14 +69,8 @@ Each entry lists the package, distribution route, and description. - **[canvas](/plugins/reference/canvas)** (`@openclaw/canvas-plugin`) - included in OpenClaw. Experimental Canvas control and A2UI rendering surfaces for paired nodes. -- **[cerebras](/plugins/reference/cerebras)** (`@openclaw/cerebras-provider`) - included in OpenClaw. Adds Cerebras model provider support to OpenClaw. - -- **[chutes](/plugins/reference/chutes)** (`@openclaw/chutes-provider`) - included in OpenClaw. Adds Chutes model provider support to OpenClaw. - - **[clickclack](/plugins/reference/clickclack)** (`@openclaw/clickclack`) - included in OpenClaw. Adds the Clickclack channel surface for sending and receiving OpenClaw messages. -- **[cloudflare-ai-gateway](/plugins/reference/cloudflare-ai-gateway)** (`@openclaw/cloudflare-ai-gateway-provider`) - included in OpenClaw. Adds Cloudflare AI Gateway model provider support to OpenClaw. - - **[codex-supervisor](/plugins/reference/codex-supervisor)** (`@openclaw/codex-supervisor`) - included in OpenClaw. Supervise Codex app-server sessions from OpenClaw. - **[cohere](/plugins/reference/cohere)** (`@openclaw/cohere-provider`) - included in OpenClaw; npm; ClawHub: `clawhub:@openclaw/cohere-provider`. OpenClaw Cohere provider plugin. @@ -89,46 +81,28 @@ Each entry lists the package, distribution route, and description. - **[deepgram](/plugins/reference/deepgram)** (`@openclaw/deepgram-provider`) - included in OpenClaw. Adds media understanding provider support. Adds realtime transcription provider support. -- **[deepinfra](/plugins/reference/deepinfra)** (`@openclaw/deepinfra-provider`) - included in OpenClaw. Adds DeepInfra model provider support to OpenClaw. - -- **[deepseek](/plugins/reference/deepseek)** (`@openclaw/deepseek-provider`) - included in OpenClaw. Adds DeepSeek model provider support to OpenClaw. - - **[document-extract](/plugins/reference/document-extract)** (`@openclaw/document-extract-plugin`) - included in OpenClaw. Extract text and fallback page images from local document attachments. - **[duckduckgo](/plugins/reference/duckduckgo)** (`@openclaw/duckduckgo-plugin`) - included in OpenClaw. Adds web search provider support. - **[elevenlabs](/plugins/reference/elevenlabs)** (`@openclaw/elevenlabs-speech`) - included in OpenClaw. Adds media understanding provider support. Adds realtime transcription provider support. Adds text-to-speech provider support. -- **[exa](/plugins/reference/exa)** (`@openclaw/exa-plugin`) - included in OpenClaw. Adds web search provider support. - - **[fal](/plugins/reference/fal)** (`@openclaw/fal-provider`) - included in OpenClaw. Adds fal model provider support to OpenClaw. - **[file-transfer](/plugins/reference/file-transfer)** (`@openclaw/file-transfer`) - included in OpenClaw. Fetch, list, and write files on paired nodes via dedicated node commands. Bypasses bash stdout truncation by using base64 over node.invoke for binaries up to 16 MB. -- **[firecrawl](/plugins/reference/firecrawl)** (`@openclaw/firecrawl-plugin`) - included in OpenClaw. Adds agent-callable tools. Adds web fetch provider support. Adds web search provider support. - - **[fireworks](/plugins/reference/fireworks)** (`@openclaw/fireworks-provider`) - included in OpenClaw. Adds Fireworks model provider support to OpenClaw. - **[github-copilot](/plugins/reference/github-copilot)** (`@openclaw/github-copilot-provider`) - included in OpenClaw. Adds GitHub Copilot model provider support to OpenClaw. - **[google](/plugins/reference/google)** (`@openclaw/google-plugin`) - included in OpenClaw. Adds Google, Google Gemini CLI, Google Vertex model provider support to OpenClaw. -- **[gradium](/plugins/reference/gradium)** (`@openclaw/gradium-speech`) - included in OpenClaw. Adds text-to-speech provider support. - -- **[groq](/plugins/reference/groq)** (`@openclaw/groq-provider`) - included in OpenClaw. Adds Groq model provider support to OpenClaw. - - **[huggingface](/plugins/reference/huggingface)** (`@openclaw/huggingface-provider`) - included in OpenClaw. Adds Hugging Face model provider support to OpenClaw. - **[imessage](/plugins/reference/imessage)** (`@openclaw/imessage`) - included in OpenClaw. Adds the iMessage channel surface for sending and receiving OpenClaw messages. -- **[inworld](/plugins/reference/inworld)** (`@openclaw/inworld-speech`) - included in OpenClaw. Inworld streaming text-to-speech (MP3, OGG_OPUS, PCM telephony). - - **[irc](/plugins/reference/irc)** (`@openclaw/irc`) - included in OpenClaw. Adds the IRC channel surface for sending and receiving OpenClaw messages. -- **[kilocode](/plugins/reference/kilocode)** (`@openclaw/kilocode-provider`) - included in OpenClaw. Adds Kilocode model provider support to OpenClaw. - -- **[kimi](/plugins/reference/kimi)** (`@openclaw/kimi-provider`) - included in OpenClaw. Adds Kimi, Kimi Coding model provider support to OpenClaw. - - **[litellm](/plugins/reference/litellm)** (`@openclaw/litellm-provider`) - included in OpenClaw. Adds LiteLLM model provider support to OpenClaw. - **[llm-task](/plugins/reference/llm-task)** (`@openclaw/llm-task`) - included in OpenClaw. Generic JSON-only LLM tool for structured tasks callable from workflows. @@ -173,16 +147,8 @@ Each entry lists the package, distribution route, and description. - **[openrouter](/plugins/reference/openrouter)** (`@openclaw/openrouter-provider`) - included in OpenClaw. Adds OpenRouter model provider support to OpenClaw. -- **[parallel](/tools/parallel-search)** (`@openclaw/parallel-plugin`) - included in OpenClaw. Adds web search provider support. - -- **[perplexity](/plugins/reference/perplexity)** (`@openclaw/perplexity-plugin`) - included in OpenClaw. Adds web search provider support. - - **[policy](/plugins/reference/policy)** (`@openclaw/policy`) - included in OpenClaw. Adds policy-backed doctor checks for workspace conformance. -- **[qianfan](/plugins/reference/qianfan)** (`@openclaw/qianfan-provider`) - included in OpenClaw. Adds Qianfan model provider support to OpenClaw. - -- **[qwen](/plugins/reference/qwen)** (`@openclaw/qwen-provider`) - included in OpenClaw. Adds Qwen, Qwen Cloud, Model Studio, DashScope, Qwen Oauth, Qwen Portal, Qwen CLI model provider support to OpenClaw. - - **[runway](/plugins/reference/runway)** (`@openclaw/runway-provider`) - included in OpenClaw. Adds video generation provider support. - **[searxng](/plugins/reference/searxng)** (`@openclaw/searxng-plugin`) - included in OpenClaw. Adds web search provider support. @@ -195,8 +161,6 @@ Each entry lists the package, distribution route, and description. - **[sms](/plugins/reference/sms)** (`@openclaw/sms`) - included in OpenClaw. Twilio SMS channel plugin for OpenClaw text messages. -- **[stepfun](/plugins/reference/stepfun)** (`@openclaw/stepfun-provider`) - included in OpenClaw. Adds StepFun, StepFun Plan model provider support to OpenClaw. - - **[synthetic](/plugins/reference/synthetic)** (`@openclaw/synthetic-provider`) - included in OpenClaw. Adds Synthetic model provider support to OpenClaw. - **[tavily](/plugins/reference/tavily)** (`@openclaw/tavily-plugin`) - included in OpenClaw. Adds agent-callable tools. Adds web search provider support. @@ -235,7 +199,7 @@ Each entry lists the package, distribution route, and description. ## Official external packages -36 plugins +54 plugins - **[acpx](/plugins/reference/acpx)** (`@openclaw/acpx`) - npm; ClawHub. OpenClaw ACP runtime backend with plugin-owned session and transport management. @@ -245,12 +209,24 @@ Each entry lists the package, distribution route, and description. - **[anthropic-vertex](/plugins/reference/anthropic-vertex)** (`@openclaw/anthropic-vertex-provider`) - npm; ClawHub. OpenClaw Anthropic Vertex provider plugin for Claude models on Google Vertex AI. +- **[arcee](/plugins/reference/arcee)** (`@openclaw/arcee-provider`) - npm; ClawHub: `clawhub:@openclaw/arcee-provider`. Adds Arcee model provider support to OpenClaw. + - **[brave](/plugins/reference/brave)** (`@openclaw/brave-plugin`) - npm; ClawHub. OpenClaw Brave Search provider plugin for web search. +- **[cerebras](/plugins/reference/cerebras)** (`@openclaw/cerebras-provider`) - npm; ClawHub: `clawhub:@openclaw/cerebras-provider`. Adds Cerebras model provider support to OpenClaw. + +- **[chutes](/plugins/reference/chutes)** (`@openclaw/chutes-provider`) - npm; ClawHub: `clawhub:@openclaw/chutes-provider`. Adds Chutes model provider support to OpenClaw. + +- **[cloudflare-ai-gateway](/plugins/reference/cloudflare-ai-gateway)** (`@openclaw/cloudflare-ai-gateway-provider`) - npm; ClawHub: `clawhub:@openclaw/cloudflare-ai-gateway-provider`. Adds Cloudflare AI Gateway model provider support to OpenClaw. + - **[codex](/plugins/reference/codex)** (`@openclaw/codex`) - npm; ClawHub. OpenClaw Codex app-server harness and model provider plugin with a Codex-managed GPT catalog. - **[copilot](/plugins/reference/copilot)** (`@openclaw/copilot`) - npm; ClawHub: `clawhub:@openclaw/copilot`. Registers the GitHub Copilot agent runtime. +- **[deepinfra](/plugins/reference/deepinfra)** (`@openclaw/deepinfra-provider`) - npm; ClawHub: `clawhub:@openclaw/deepinfra-provider`. Adds DeepInfra model provider support to OpenClaw. + +- **[deepseek](/plugins/reference/deepseek)** (`@openclaw/deepseek-provider`) - npm; ClawHub: `clawhub:@openclaw/deepseek-provider`. Adds DeepSeek model provider support to OpenClaw. + - **[diagnostics-otel](/plugins/reference/diagnostics-otel)** (`@openclaw/diagnostics-otel`) - npm; ClawHub: `clawhub:@openclaw/diagnostics-otel`. OpenClaw diagnostics OpenTelemetry exporter for metrics and traces. - **[diagnostics-prometheus](/plugins/reference/diagnostics-prometheus)** (`@openclaw/diagnostics-prometheus`) - npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus`. OpenClaw diagnostics Prometheus exporter for runtime metrics. @@ -261,14 +237,28 @@ Each entry lists the package, distribution route, and description. - **[discord](/plugins/reference/discord)** (`@openclaw/discord`) - npm; ClawHub. OpenClaw Discord channel plugin for channels, DMs, commands, and app events. +- **[exa](/plugins/reference/exa)** (`@openclaw/exa-plugin`) - npm; ClawHub: `clawhub:@openclaw/exa-plugin`. Adds web search provider support. + - **[feishu](/plugins/reference/feishu)** (`@openclaw/feishu`) - npm; ClawHub. OpenClaw Feishu/Lark channel plugin for chats and workplace tools (community maintained by @m1heng). +- **[firecrawl](/plugins/reference/firecrawl)** (`@openclaw/firecrawl-plugin`) - npm; ClawHub: `clawhub:@openclaw/firecrawl-plugin`. Adds agent-callable tools. Adds web fetch provider support. Adds web search provider support. + - **[gmi](/plugins/reference/gmi)** (`@openclaw/gmi-provider`) - npm; ClawHub: `clawhub:@openclaw/gmi-provider`. OpenClaw GMI Cloud provider plugin. - **[google-meet](/plugins/reference/google-meet)** (`@openclaw/google-meet`) - npm; ClawHub. OpenClaw Google Meet participant plugin for joining calls through Chrome or Twilio transports. - **[googlechat](/plugins/reference/googlechat)** (`@openclaw/googlechat`) - npm; ClawHub. OpenClaw Google Chat channel plugin for spaces and direct messages. +- **[gradium](/plugins/reference/gradium)** (`@openclaw/gradium-speech`) - npm; ClawHub: `clawhub:@openclaw/gradium-speech`. Adds text-to-speech provider support. + +- **[groq](/plugins/reference/groq)** (`@openclaw/groq-provider`) - npm; ClawHub: `clawhub:@openclaw/groq-provider`. Adds Groq model provider support to OpenClaw. + +- **[inworld](/plugins/reference/inworld)** (`@openclaw/inworld-speech`) - npm; ClawHub: `clawhub:@openclaw/inworld-speech`. Inworld streaming text-to-speech (MP3, OGG_OPUS, PCM telephony). + +- **[kilocode](/plugins/reference/kilocode)** (`@openclaw/kilocode-provider`) - npm; ClawHub: `clawhub:@openclaw/kilocode-provider`. Adds Kilocode model provider support to OpenClaw. + +- **[kimi](/plugins/reference/kimi)** (`@openclaw/kimi-provider`) - npm; ClawHub: `clawhub:@openclaw/kimi-provider`. Adds Kimi, Kimi Coding model provider support to OpenClaw. + - **[line](/plugins/reference/line)** (`@openclaw/line`) - npm; ClawHub. OpenClaw LINE channel plugin for LINE Bot API chats. - **[llama-cpp](/plugins/reference/llama-cpp)** (`@openclaw/llama-cpp-provider`) - npm; ClawHub. Local GGUF embeddings through node-llama-cpp. @@ -287,12 +277,22 @@ Each entry lists the package, distribution route, and description. - **[openshell](/plugins/reference/openshell)** (`@openclaw/openshell-sandbox`) - npm; ClawHub. OpenClaw sandbox backend for the NVIDIA OpenShell CLI with mirrored local workspaces and SSH command execution. +- **[parallel](/tools/parallel-search)** (`@openclaw/parallel-plugin`) - npm; ClawHub: `clawhub:@openclaw/parallel-plugin`. Adds web search provider support. + +- **[perplexity](/plugins/reference/perplexity)** (`@openclaw/perplexity-plugin`) - npm; ClawHub: `clawhub:@openclaw/perplexity-plugin`. Adds web search provider support. + - **[pixverse](/plugins/reference/pixverse)** (`@openclaw/pixverse-provider`) - npm; ClawHub: `clawhub:@openclaw/pixverse-provider`. OpenClaw PixVerse video generation provider plugin. +- **[qianfan](/plugins/reference/qianfan)** (`@openclaw/qianfan-provider`) - npm; ClawHub: `clawhub:@openclaw/qianfan-provider`. Adds Qianfan model provider support to OpenClaw. + - **[qqbot](/plugins/reference/qqbot)** (`@openclaw/qqbot`) - npm; ClawHub. OpenClaw QQ Bot channel plugin for group and direct-message workflows. +- **[qwen](/plugins/reference/qwen)** (`@openclaw/qwen-provider`) - npm; ClawHub: `clawhub:@openclaw/qwen-provider`. Adds Qwen, Qwen Cloud, Model Studio, DashScope, Qwen Oauth, Qwen Portal, Qwen CLI model provider support to OpenClaw. + - **[slack](/plugins/reference/slack)** (`@openclaw/slack`) - npm; ClawHub. OpenClaw Slack channel plugin for channels, DMs, commands, and app events. +- **[stepfun](/plugins/reference/stepfun)** (`@openclaw/stepfun-provider`) - npm; ClawHub: `clawhub:@openclaw/stepfun-provider`. Adds StepFun, StepFun Plan model provider support to OpenClaw. + - **[synology-chat](/plugins/reference/synology-chat)** (`@openclaw/synology-chat`) - npm; ClawHub. Synology Chat channel plugin for OpenClaw channels and direct messages. - **[tlon](/plugins/reference/tlon)** (`@openclaw/tlon`) - npm; ClawHub. OpenClaw Tlon/Urbit channel plugin for chat workflows. diff --git a/docs/plugins/reference/arcee.md b/docs/plugins/reference/arcee.md index d44dda878f0..46093977211 100644 --- a/docs/plugins/reference/arcee.md +++ b/docs/plugins/reference/arcee.md @@ -12,7 +12,7 @@ Adds Arcee model provider support to OpenClaw. ## Distribution - Package: `@openclaw/arcee-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/arcee-provider` ## Surface diff --git a/docs/plugins/reference/cerebras.md b/docs/plugins/reference/cerebras.md index 60670914772..1db2f11e9f9 100644 --- a/docs/plugins/reference/cerebras.md +++ b/docs/plugins/reference/cerebras.md @@ -12,7 +12,7 @@ Adds Cerebras model provider support to OpenClaw. ## Distribution - Package: `@openclaw/cerebras-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/cerebras-provider` ## Surface diff --git a/docs/plugins/reference/chutes.md b/docs/plugins/reference/chutes.md index 6116125182e..495538545c1 100644 --- a/docs/plugins/reference/chutes.md +++ b/docs/plugins/reference/chutes.md @@ -12,7 +12,7 @@ Adds Chutes model provider support to OpenClaw. ## Distribution - Package: `@openclaw/chutes-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/chutes-provider` ## Surface diff --git a/docs/plugins/reference/cloudflare-ai-gateway.md b/docs/plugins/reference/cloudflare-ai-gateway.md index 435f0f4051b..a0879363688 100644 --- a/docs/plugins/reference/cloudflare-ai-gateway.md +++ b/docs/plugins/reference/cloudflare-ai-gateway.md @@ -12,7 +12,7 @@ Adds Cloudflare AI Gateway model provider support to OpenClaw. ## Distribution - Package: `@openclaw/cloudflare-ai-gateway-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/cloudflare-ai-gateway-provider` ## Surface diff --git a/docs/plugins/reference/deepinfra.md b/docs/plugins/reference/deepinfra.md index 7fbc3afd496..db170be8a41 100644 --- a/docs/plugins/reference/deepinfra.md +++ b/docs/plugins/reference/deepinfra.md @@ -12,7 +12,7 @@ Adds DeepInfra model provider support to OpenClaw. ## Distribution - Package: `@openclaw/deepinfra-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/deepinfra-provider` ## Surface diff --git a/docs/plugins/reference/deepseek.md b/docs/plugins/reference/deepseek.md index 286a5b0424e..7488c851f22 100644 --- a/docs/plugins/reference/deepseek.md +++ b/docs/plugins/reference/deepseek.md @@ -12,7 +12,7 @@ Adds DeepSeek model provider support to OpenClaw. ## Distribution - Package: `@openclaw/deepseek-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/deepseek-provider` ## Surface diff --git a/docs/plugins/reference/exa.md b/docs/plugins/reference/exa.md index 9125f4d955d..564c57c3e6d 100644 --- a/docs/plugins/reference/exa.md +++ b/docs/plugins/reference/exa.md @@ -12,7 +12,7 @@ Adds web search provider support. ## Distribution - Package: `@openclaw/exa-plugin` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/exa-plugin` ## Surface diff --git a/docs/plugins/reference/firecrawl.md b/docs/plugins/reference/firecrawl.md index ecd3ef26774..9d289e33c1a 100644 --- a/docs/plugins/reference/firecrawl.md +++ b/docs/plugins/reference/firecrawl.md @@ -12,7 +12,7 @@ Adds agent-callable tools. Adds web fetch provider support. Adds web search prov ## Distribution - Package: `@openclaw/firecrawl-plugin` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/firecrawl-plugin` ## Surface diff --git a/docs/plugins/reference/gradium.md b/docs/plugins/reference/gradium.md index 71521d56e21..be9795726d4 100644 --- a/docs/plugins/reference/gradium.md +++ b/docs/plugins/reference/gradium.md @@ -12,7 +12,7 @@ Adds text-to-speech provider support. ## Distribution - Package: `@openclaw/gradium-speech` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/gradium-speech` ## Surface diff --git a/docs/plugins/reference/groq.md b/docs/plugins/reference/groq.md index fe102d650e1..fbfa8c84d63 100644 --- a/docs/plugins/reference/groq.md +++ b/docs/plugins/reference/groq.md @@ -12,7 +12,7 @@ Adds Groq model provider support to OpenClaw. ## Distribution - Package: `@openclaw/groq-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/groq-provider` ## Surface diff --git a/docs/plugins/reference/inworld.md b/docs/plugins/reference/inworld.md index 2956b72a7b8..13a32d304b3 100644 --- a/docs/plugins/reference/inworld.md +++ b/docs/plugins/reference/inworld.md @@ -12,7 +12,7 @@ Inworld streaming text-to-speech (MP3, OGG_OPUS, PCM telephony). ## Distribution - Package: `@openclaw/inworld-speech` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/inworld-speech` ## Surface diff --git a/docs/plugins/reference/kilocode.md b/docs/plugins/reference/kilocode.md index f0f89be9b12..f88c36fbca2 100644 --- a/docs/plugins/reference/kilocode.md +++ b/docs/plugins/reference/kilocode.md @@ -12,7 +12,7 @@ Adds Kilocode model provider support to OpenClaw. ## Distribution - Package: `@openclaw/kilocode-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/kilocode-provider` ## Surface diff --git a/docs/plugins/reference/kimi.md b/docs/plugins/reference/kimi.md index 60944c43eb4..63d324da4ee 100644 --- a/docs/plugins/reference/kimi.md +++ b/docs/plugins/reference/kimi.md @@ -12,7 +12,7 @@ Adds Kimi, Kimi Coding model provider support to OpenClaw. ## Distribution - Package: `@openclaw/kimi-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/kimi-provider` ## Surface diff --git a/docs/plugins/reference/perplexity.md b/docs/plugins/reference/perplexity.md index 7197ed3d4ba..74fb899ea4a 100644 --- a/docs/plugins/reference/perplexity.md +++ b/docs/plugins/reference/perplexity.md @@ -12,7 +12,7 @@ Adds web search provider support. ## Distribution - Package: `@openclaw/perplexity-plugin` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/perplexity-plugin` ## Surface diff --git a/docs/plugins/reference/qianfan.md b/docs/plugins/reference/qianfan.md index 58e81688c8b..6036c48240d 100644 --- a/docs/plugins/reference/qianfan.md +++ b/docs/plugins/reference/qianfan.md @@ -12,7 +12,7 @@ Adds Qianfan model provider support to OpenClaw. ## Distribution - Package: `@openclaw/qianfan-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/qianfan-provider` ## Surface diff --git a/docs/plugins/reference/qwen.md b/docs/plugins/reference/qwen.md index 67e8edbdf6b..0db5f898902 100644 --- a/docs/plugins/reference/qwen.md +++ b/docs/plugins/reference/qwen.md @@ -12,7 +12,7 @@ Adds Qwen, Qwen Cloud, Model Studio, DashScope, Qwen Oauth, Qwen Portal, Qwen CL ## Distribution - Package: `@openclaw/qwen-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/qwen-provider` ## Surface diff --git a/docs/plugins/reference/stepfun.md b/docs/plugins/reference/stepfun.md index 6ba03f483a4..661e8500ee4 100644 --- a/docs/plugins/reference/stepfun.md +++ b/docs/plugins/reference/stepfun.md @@ -12,7 +12,7 @@ Adds StepFun, StepFun Plan model provider support to OpenClaw. ## Distribution - Package: `@openclaw/stepfun-provider` -- Install route: included in OpenClaw +- Install route: npm; ClawHub: `clawhub:@openclaw/stepfun-provider` ## Surface diff --git a/docs/providers/arcee.md b/docs/providers/arcee.md index 673c55a017b..963f9bc6af5 100644 --- a/docs/providers/arcee.md +++ b/docs/providers/arcee.md @@ -17,6 +17,15 @@ Arcee AI models can be accessed directly via the Arcee platform or through [Open | API | OpenAI-compatible | | Base URL | `https://api.arcee.ai/api/v1` (direct) or `https://openrouter.ai/api/v1` (OpenRouter) | +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/arcee-provider +openclaw gateway restart +``` + ## Getting started @@ -96,7 +105,7 @@ Arcee AI models can be accessed directly via the Arcee platform or through [Open ## Built-in catalog -OpenClaw currently ships this bundled Arcee catalog: +OpenClaw currently ships this Arcee static catalog: | Model ref | Name | Input | Context | Cost (in/out per 1M) | Notes | | ------------------------------ | ---------------------- | ----- | ------- | -------------------- | ----------------------------------------- | diff --git a/docs/providers/cerebras.md b/docs/providers/cerebras.md index 2031f374b56..1212e8e2328 100644 --- a/docs/providers/cerebras.md +++ b/docs/providers/cerebras.md @@ -6,12 +6,12 @@ read_when: - You need the Cerebras API key env var or CLI auth choice --- -[Cerebras](https://www.cerebras.ai) provides high-speed OpenAI-compatible inference on custom inference hardware. OpenClaw includes a bundled Cerebras provider plugin with a static four-model catalog. +[Cerebras](https://www.cerebras.ai) provides high-speed OpenAI-compatible inference on custom inference hardware. The Cerebras provider plugin includes a static four-model catalog. | Property | Value | | --------------- | ---------------------------------------- | | Provider id | `cerebras` | -| Plugin | bundled, `enabledByDefault: true` | +| Plugin | official external package | | Auth env var | `CEREBRAS_API_KEY` | | Onboarding flag | `--auth-choice cerebras-api-key` | | Direct CLI flag | `--cerebras-api-key ` | @@ -19,6 +19,15 @@ read_when: | Base URL | `https://api.cerebras.ai/v1` | | Default model | `cerebras/zai-glm-4.7` | +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/cerebras-provider +openclaw gateway restart +``` + ## Getting started @@ -50,7 +59,7 @@ export CEREBRAS_API_KEY=csk-... openclaw models list --provider cerebras ``` - The list should include all four bundled models. If `CEREBRAS_API_KEY` is unresolved, `openclaw models status --json` reports the missing credential under `auth.unusableProfiles`. + The list should include all four static models. If `CEREBRAS_API_KEY` is unresolved, `openclaw models status --json` reports the missing credential under `auth.unusableProfiles`. @@ -81,7 +90,7 @@ OpenClaw ships a static Cerebras catalog that mirrors the public OpenAI-compatib ## Manual config -The bundled plugin usually means you only need the API key. Use explicit `models.providers.cerebras` config when you want to override model metadata or run in `mode: "merge"` against the static catalog: +The plugin usually means you only need the API key. Use explicit `models.providers.cerebras` config when you want to override model metadata or run in `mode: "merge"` against the static catalog: ```json5 { diff --git a/docs/providers/chutes.md b/docs/providers/chutes.md index f1dc343b536..1b6fc46d0fe 100644 --- a/docs/providers/chutes.md +++ b/docs/providers/chutes.md @@ -9,7 +9,7 @@ read_when: [Chutes](https://chutes.ai) exposes open-source model catalogs through an OpenAI-compatible API. OpenClaw supports both browser OAuth and direct API-key -auth for the bundled `chutes` provider. +auth for the `chutes` provider. | Property | Value | | -------- | ---------------------------- | @@ -18,6 +18,15 @@ auth for the bundled `chutes` provider. | Base URL | `https://llm.chutes.ai/v1` | | Auth | OAuth or API key (see below) | +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/chutes-provider +openclaw gateway restart +``` + ## Getting started @@ -33,7 +42,7 @@ auth for the bundled `chutes` provider. After onboarding, the default model is set to - `chutes/zai-org/GLM-4.7-TEE` and the bundled Chutes catalog is + `chutes/zai-org/GLM-4.7-TEE` and the Chutes static catalog is registered. @@ -51,7 +60,7 @@ auth for the bundled `chutes` provider. After onboarding, the default model is set to - `chutes/zai-org/GLM-4.7-TEE` and the bundled Chutes catalog is + `chutes/zai-org/GLM-4.7-TEE` and the Chutes static catalog is registered. @@ -59,7 +68,7 @@ auth for the bundled `chutes` provider. -Both auth paths register the bundled Chutes catalog and set the default model to +Both auth paths register the Chutes static catalog and set the default model to `chutes/zai-org/GLM-4.7-TEE`. Runtime environment variables: `CHUTES_API_KEY`, `CHUTES_OAUTH_TOKEN`. @@ -68,11 +77,11 @@ Both auth paths register the bundled Chutes catalog and set the default model to When Chutes auth is available, OpenClaw queries the Chutes catalog with that credential and uses the discovered models. If discovery fails, OpenClaw falls -back to a bundled static catalog so onboarding and startup still work. +back to a static catalog so onboarding and startup still work. ## Default aliases -OpenClaw registers three convenience aliases for the bundled Chutes catalog: +OpenClaw registers three convenience aliases for the Chutes static catalog: | Alias | Target model | | --------------- | ----------------------------------------------------- | @@ -82,7 +91,7 @@ OpenClaw registers three convenience aliases for the bundled Chutes catalog: ## Built-in starter catalog -The bundled fallback catalog includes current Chutes refs: +The static fallback catalog includes current Chutes refs: | Model ref | | ----------------------------------------------------- | @@ -130,7 +139,7 @@ The bundled fallback catalog includes current Chutes refs: - API-key and OAuth discovery both use the same `chutes` provider id. - Chutes models are registered as `chutes/`. - - If discovery fails at startup, the bundled static catalog is used automatically. + - If discovery fails at startup, the static catalog is used automatically. diff --git a/docs/providers/cloudflare-ai-gateway.md b/docs/providers/cloudflare-ai-gateway.md index df8bca88ef5..27266fedd05 100644 --- a/docs/providers/cloudflare-ai-gateway.md +++ b/docs/providers/cloudflare-ai-gateway.md @@ -24,6 +24,15 @@ assistant prefill turns before sending the payload through Cloudflare AI Gateway Anthropic rejects response prefilling with extended thinking, while ordinary non-thinking prefill remains available. +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/cloudflare-ai-gateway-provider +openclaw gateway restart +``` + ## Getting started diff --git a/docs/providers/deepinfra.md b/docs/providers/deepinfra.md index 6f600287e12..376646afe49 100644 --- a/docs/providers/deepinfra.md +++ b/docs/providers/deepinfra.md @@ -9,6 +9,15 @@ title: "DeepInfra" DeepInfra provides a **unified API** that routes requests to the most popular open source and frontier models behind a single endpoint and API key. It is OpenAI-compatible, so most OpenAI SDKs work by switching the base URL. +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/deepinfra-provider +openclaw gateway restart +``` + ## Getting an API key 1. Go to [https://deepinfra.com/](https://deepinfra.com/) @@ -42,7 +51,7 @@ export DEEPINFRA_API_KEY="" # pragma: allowlist secret ## Supported OpenClaw surfaces -The bundled plugin registers all DeepInfra surfaces that match current +The plugin registers all DeepInfra surfaces that match current OpenClaw provider contracts. Chat, image generation, and video generation refresh their model catalogues live from `/v1/openai/models?sort_by=openclaw&filter=with_meta` when `DEEPINFRA_API_KEY` is configured; the other surfaces use the curated diff --git a/docs/providers/deepseek.md b/docs/providers/deepseek.md index 72f51073bf2..c43540a3e99 100644 --- a/docs/providers/deepseek.md +++ b/docs/providers/deepseek.md @@ -15,6 +15,15 @@ read_when: | API | OpenAI-compatible | | Base URL | `https://api.deepseek.com` | +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/deepseek-provider +openclaw gateway restart +``` + ## Getting started @@ -34,7 +43,7 @@ read_when: openclaw models list --provider deepseek ``` - To inspect the bundled static catalog without requiring a running Gateway, + To inspect the plugin's static catalog without requiring a running Gateway, use: ```bash diff --git a/docs/providers/gradium.md b/docs/providers/gradium.md index 65fdd8a5a7b..1bf90bf4d3b 100644 --- a/docs/providers/gradium.md +++ b/docs/providers/gradium.md @@ -6,7 +6,7 @@ read_when: title: "Gradium" --- -[Gradium](https://gradium.ai) is a bundled text-to-speech provider for OpenClaw. The plugin can render normal audio replies (WAV), voice-note-compatible Opus output, and 8 kHz u-law audio for telephony surfaces. +[Gradium](https://gradium.ai) is a text-to-speech provider for OpenClaw. The plugin can render normal audio replies (WAV), voice-note-compatible Opus output, and 8 kHz u-law audio for telephony surfaces. | Property | Value | | ------------- | ------------------------------------ | @@ -15,6 +15,15 @@ title: "Gradium" | Base URL | `https://api.gradium.ai` (default) | | Default voice | `Emma` (`YTpq7expH9539ERJ`) | +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/gradium-speech +openclaw gateway restart +``` + ## Setup Create a Gradium API key, then expose it to OpenClaw with either an env var or the config key. diff --git a/docs/providers/groq.md b/docs/providers/groq.md index 99dbd8287a5..b8092e20a11 100644 --- a/docs/providers/groq.md +++ b/docs/providers/groq.md @@ -7,19 +7,27 @@ read_when: - You are configuring Whisper audio transcription on Groq --- -[Groq](https://groq.com) provides ultra-fast inference on open-weight models (Llama, Gemma, Kimi, Qwen, GPT OSS, and more) using custom LPU hardware. OpenClaw includes a bundled Groq plugin that registers both an OpenAI-compatible chat provider and an audio media-understanding provider. +[Groq](https://groq.com) provides ultra-fast inference on open-weight models (Llama, Gemma, Kimi, Qwen, GPT OSS, and more) using custom LPU hardware. The Groq plugin registers both an OpenAI-compatible chat provider and an audio media-understanding provider. | Property | Value | | ---------------------- | ---------------------------------------- | | Provider id | `groq` | -| Plugin | bundled, `enabledByDefault: true` | +| Plugin | official external package | | Auth env var | `GROQ_API_KEY` | -| Onboarding flag | `--auth-choice groq-api-key` | | API | OpenAI-compatible (`openai-completions`) | | Base URL | `https://api.groq.com/openai/v1` | | Audio transcription | `whisper-large-v3-turbo` (default) | | Suggested chat default | `groq/llama-3.3-70b-versatile` | +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/groq-provider +openclaw gateway restart +``` + ## Getting started @@ -27,18 +35,9 @@ read_when: Create an API key at [console.groq.com/keys](https://console.groq.com/keys). - - -```bash Onboarding -openclaw onboard --auth-choice groq-api-key -``` - -```bash Env only + ```bash export GROQ_API_KEY=gsk_... ``` - - - ```json5 @@ -73,7 +72,7 @@ export GROQ_API_KEY=gsk_... ## Built-in catalog -OpenClaw ships a manifest-backed Groq catalog with both reasoning and non-reasoning entries. Run `openclaw models list --provider groq` to see the bundled rows for your installed version, or check [console.groq.com/docs/models](https://console.groq.com/docs/models) for Groq's authoritative list. +OpenClaw ships a manifest-backed Groq catalog with both reasoning and non-reasoning entries. Run `openclaw models list --provider groq` to see the static rows for your installed version, or check [console.groq.com/docs/models](https://console.groq.com/docs/models) for Groq's authoritative list. | Model ref | Name | Reasoning | Input | Context | | ------------------------------------------------ | ----------------------- | --------- | ------------ | ------- | @@ -103,7 +102,7 @@ See [Thinking modes](/tools/thinking) for the shared `/think` levels and how Ope ## Audio transcription -Groq's bundled plugin also registers an **audio media-understanding provider** so voice messages can be transcribed through the shared `tools.media.audio` surface. +Groq's plugin also registers an **audio media-understanding provider** so voice messages can be transcribed through the shared `tools.media.audio` surface. | Property | Value | | ------------------ | ----------------------------------------- | @@ -138,7 +137,7 @@ To make Groq the default audio backend: - OpenClaw accepts any Groq model id at runtime. Use the exact id shown by Groq and prefix it with `groq/`. The bundled catalog covers the common cases; uncatalogued ids fall through to the default OpenAI-compatible template. + OpenClaw accepts any Groq model id at runtime. Use the exact id shown by Groq and prefix it with `groq/`. The static catalog covers the common cases; uncatalogued ids fall through to the default OpenAI-compatible template. ```json5 { diff --git a/docs/providers/inworld.md b/docs/providers/inworld.md index f1183f74412..32e7f4c1e16 100644 --- a/docs/providers/inworld.md +++ b/docs/providers/inworld.md @@ -17,7 +17,7 @@ the standard reply-audio pipeline. | Property | Value | | ------------- | --------------------------------------------------------------- | | Provider id | `inworld` | -| Plugin | bundled, `enabledByDefault: true` | +| Plugin | official external package | | Contract | `speechProviders` (TTS only) | | Auth env var | `INWORLD_API_KEY` (HTTP Basic, Base64 dashboard credential) | | Base URL | `https://api.inworld.ai` | @@ -27,6 +27,15 @@ the standard reply-audio pipeline. | Website | [inworld.ai](https://inworld.ai) | | Docs | [docs.inworld.ai/tts/tts](https://docs.inworld.ai/tts/tts) | +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/inworld-speech +openclaw gateway restart +``` + ## Getting started @@ -112,7 +121,7 @@ the standard reply-audio pipeline. Full config reference including `messages.tts` settings. - All bundled OpenClaw providers. + All supported OpenClaw providers. Common issues and debugging steps. diff --git a/docs/providers/kilocode.md b/docs/providers/kilocode.md index f62225610c2..eb07cde9d12 100644 --- a/docs/providers/kilocode.md +++ b/docs/providers/kilocode.md @@ -16,6 +16,15 @@ endpoint and API key. It is OpenAI-compatible, so most OpenAI SDKs work by switc | API | OpenAI-compatible | | Base URL | `https://api.kilo.ai/api/gateway/` | +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/kilocode-provider +openclaw gateway restart +``` + ## Getting started @@ -70,7 +79,7 @@ Any model available on the gateway can be used with the `kilocode/` prefix: At startup, OpenClaw queries `GET https://api.kilo.ai/api/gateway/models` and merges -discovered models ahead of the static fallback catalog. The bundled fallback always +discovered models ahead of the static fallback catalog. The static fallback always includes `kilocode/kilo/auto` (`Kilo Auto`) with `input: ["text", "image"]`, `reasoning: true`, `contextWindow: 1000000`, and `maxTokens: 128000`. @@ -113,7 +122,7 @@ includes `kilocode/kilo/auto` (`Kilo Auto`) with `input: ["text", "image"]`, - - If model discovery fails at startup, OpenClaw falls back to the bundled static catalog containing `kilocode/kilo/auto`. + - If model discovery fails at startup, OpenClaw falls back to the static catalog containing `kilocode/kilo/auto`. - Confirm your API key is valid and that your Kilo account has the desired models enabled. - When the Gateway runs as a daemon, ensure `KILOCODE_API_KEY` is available to that process (for example in `~/.openclaw/.env` or via `env.shellEnv`). diff --git a/docs/providers/moonshot.md b/docs/providers/moonshot.md index c17d94219cb..4d843bc0282 100644 --- a/docs/providers/moonshot.md +++ b/docs/providers/moonshot.md @@ -200,6 +200,12 @@ Choose your provider and follow the setup steps. + Install the official plugin, then restart Gateway: + + ```bash + openclaw plugins install @openclaw/kimi-provider + openclaw gateway restart + ``` **Best for:** code-focused tasks via the Kimi Coding endpoint. diff --git a/docs/providers/perplexity-provider.md b/docs/providers/perplexity-provider.md index 50975a97e99..3a196139c93 100644 --- a/docs/providers/perplexity-provider.md +++ b/docs/providers/perplexity-provider.md @@ -19,6 +19,15 @@ This page is the Perplexity **provider** setup. For the Perplexity **tool** (how | Auth | `PERPLEXITY_API_KEY` (direct) or `OPENROUTER_API_KEY` (via OpenRouter) | | Config path | `plugins.entries.perplexity.config.webSearch.apiKey` | +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/perplexity-plugin +openclaw gateway restart +``` + ## Getting started diff --git a/docs/providers/qianfan.md b/docs/providers/qianfan.md index bba2f0ba44c..5da626b1fb4 100644 --- a/docs/providers/qianfan.md +++ b/docs/providers/qianfan.md @@ -16,6 +16,15 @@ endpoint and API key. It is OpenAI-compatible, so most OpenAI SDKs work by switc | API | OpenAI-compatible | | Base URL | `https://qianfan.baidubce.com/v2` | +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/qianfan-provider +openclaw gateway restart +``` + ## Getting started @@ -45,7 +54,7 @@ endpoint and API key. It is OpenAI-compatible, so most OpenAI SDKs work by switc | `qianfan/ernie-5.0-thinking-preview` | text, image | 119,000 | 64,000 | Yes | Multimodal | -The default bundled model ref is `qianfan/deepseek-v3.2`. You only need to override `models.providers.qianfan` when you need a custom base URL or model metadata. +The default model ref is `qianfan/deepseek-v3.2`. You only need to override `models.providers.qianfan` when you need a custom base URL or model metadata. ## Config example @@ -98,7 +107,7 @@ The default bundled model ref is `qianfan/deepseek-v3.2`. You only need to overr - The bundled catalog currently includes `deepseek-v3.2` and `ernie-5.0-thinking-preview`. Add or override `models.providers.qianfan` only when you need a custom base URL or model metadata. + The static catalog currently includes `deepseek-v3.2` and `ernie-5.0-thinking-preview`. Add or override `models.providers.qianfan` only when you need a custom base URL or model metadata. Model refs use the `qianfan/` prefix (for example `qianfan/deepseek-v3.2`). diff --git a/docs/providers/qwen-oauth.md b/docs/providers/qwen-oauth.md index 84f486ccf8e..683afc90b64 100644 --- a/docs/providers/qwen-oauth.md +++ b/docs/providers/qwen-oauth.md @@ -64,11 +64,11 @@ provider instead. - You need to test compatibility with the Qwen Portal endpoint specifically. Choose [Qwen](/providers/qwen) for new setup, broader endpoint choices, Standard -ModelStudio, Coding Plan, and the full bundled Qwen catalog. +ModelStudio, Coding Plan, and the full Qwen plugin catalog. ## Models -The bundled catalog seeds the Qwen Portal default: +The Qwen plugin catalog seeds the Qwen Portal default: - `qwen-oauth/qwen3.5-plus` diff --git a/docs/providers/qwen.md b/docs/providers/qwen.md index 30c74f09ee8..e3b131c57ee 100644 --- a/docs/providers/qwen.md +++ b/docs/providers/qwen.md @@ -1,13 +1,13 @@ --- -summary: "Use Qwen Cloud via OpenClaw's bundled qwen provider" +summary: "Use Qwen Cloud through its OpenClaw plugin" read_when: - You want to use Qwen with OpenClaw - You previously used Qwen OAuth title: "Qwen" --- -OpenClaw now treats Qwen as a first-class bundled provider with canonical id -`qwen`. The bundled provider targets the Qwen Cloud / Alibaba DashScope and +OpenClaw now treats Qwen as a first-class provider plugin with canonical id +`qwen`. The provider plugin targets the Qwen Cloud / Alibaba DashScope and Coding Plan endpoints, keeps legacy `modelstudio` ids working as a compatibility alias, and also exposes the Qwen Portal token flow as provider `qwen-oauth`. @@ -22,6 +22,15 @@ If you want `qwen3.6-plus`, prefer the **Standard (pay-as-you-go)** endpoint. Coding Plan support can lag behind the public catalog. +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/qwen-provider +openclaw gateway restart +``` + ## Getting started Choose your plan type and follow the setup steps. @@ -185,7 +194,7 @@ You can override with a custom `baseUrl` in config. ## Built-in catalog -OpenClaw currently ships this bundled Qwen catalog. The configured catalog is +OpenClaw currently ships this Qwen static catalog. The configured catalog is endpoint-aware: Coding Plan configs omit models that are only known to work on the Standard endpoint. @@ -204,12 +213,12 @@ the Standard endpoint. Availability can still vary by endpoint and billing plan even when a model is -present in the bundled catalog. +present in the static catalog. ## Thinking Controls -For reasoning-enabled Qwen Cloud models, the bundled provider maps OpenClaw +For reasoning-enabled Qwen Cloud models, the provider maps OpenClaw thinking levels to DashScope's top-level `enable_thinking` request flag. Disabled thinking sends `enable_thinking: false`; other thinking levels send `enable_thinking: true`. @@ -242,7 +251,7 @@ See [Video Generation](/tools/video-generation) for shared tool parameters, prov - The bundled Qwen plugin registers media understanding for images and video + The Qwen plugin registers media understanding for images and video on the **Standard** DashScope endpoints (not the Coding Plan endpoints). | Property | Value | @@ -267,7 +276,7 @@ See [Video Generation](/tools/video-generation) for shared tool parameters, prov `qwen3.6-plus`, switch to Standard (pay-as-you-go) instead of the Coding Plan endpoint/key pair. - OpenClaw's bundled Qwen catalog does not advertise `qwen3.6-plus` on Coding + OpenClaw's Qwen static catalog does not advertise `qwen3.6-plus` on Coding Plan endpoints, but explicitly configured `qwen/qwen3.6-plus` entries under `models.providers.qwen.models` are honored on Coding Plan baseUrls so you can opt that model in if Aliyun enables it on your subscription. The @@ -279,13 +288,13 @@ See [Video Generation](/tools/video-generation) for shared tool parameters, prov The `qwen` plugin is being positioned as the vendor home for the full Qwen Cloud surface, not just coding/text models. - - **Text/chat models:** bundled now + - **Text/chat models:** available through the plugin - **Tool calling, structured output, thinking:** inherited from the OpenAI-compatible transport - **Image generation:** planned at the provider-plugin layer - - **Image/video understanding:** bundled now on the Standard endpoint + - **Image/video understanding:** available through the plugin on the Standard endpoint - **Speech/audio:** planned at the provider-plugin layer - **Memory embeddings/reranking:** planned through the embedding adapter surface - - **Video generation:** bundled now through the shared video-generation capability + - **Video generation:** available through the plugin through the shared video-generation capability @@ -300,7 +309,7 @@ See [Video Generation](/tools/video-generation) for shared tool parameters, prov Coding Plan or Standard Qwen hosts still keeps video generation on the correct regional DashScope video endpoint. - Current bundled Qwen video-generation limits: + Current Qwen video-generation limits: - Up to **1** output video per request - Up to **1** input image diff --git a/docs/providers/stepfun.md b/docs/providers/stepfun.md index 913e936ddba..e39f35bff1e 100644 --- a/docs/providers/stepfun.md +++ b/docs/providers/stepfun.md @@ -6,7 +6,7 @@ read_when: title: "StepFun" --- -OpenClaw includes a bundled StepFun provider plugin with two provider ids: +The StepFun provider plugin supports two provider ids: - `stepfun` for the standard endpoint - `stepfun-plan` for the Step Plan endpoint @@ -15,6 +15,15 @@ OpenClaw includes a bundled StepFun provider plugin with two provider ids: Standard and Step Plan are **separate providers** with different endpoints and model ref prefixes (`stepfun/...` vs `stepfun-plan/...`). Use a China key with the `.com` endpoints and a global key with the `.ai` endpoints. +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/stepfun-provider +openclaw gateway restart +``` + ## Region and endpoint overview | Endpoint | China (`.com`) | Global (`.ai`) | @@ -199,7 +208,7 @@ Choose your provider surface and follow the setup steps. - - The provider is bundled with OpenClaw, so there is no separate plugin install step. + - The provider is an official external package; install it before setup. - `step-3.5-flash-2603` is currently exposed only on `stepfun-plan`. - A single auth flow writes region-matched profiles for both `stepfun` and `stepfun-plan`, so both surfaces can be discovered together. - Use `openclaw models list` and `openclaw models set ` to inspect or switch models. diff --git a/docs/tools/exa-search.md b/docs/tools/exa-search.md index 8a101cc5cf8..cfe4dbc4a57 100644 --- a/docs/tools/exa-search.md +++ b/docs/tools/exa-search.md @@ -11,6 +11,15 @@ OpenClaw supports [Exa AI](https://exa.ai/) as a `web_search` provider. Exa offers neural, keyword, and hybrid search modes with built-in content extraction (highlights, text, summaries). +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/exa-plugin +openclaw gateway restart +``` + ## Get an API key diff --git a/docs/tools/firecrawl.md b/docs/tools/firecrawl.md index 44896881c31..9c4d0e88b52 100644 --- a/docs/tools/firecrawl.md +++ b/docs/tools/firecrawl.md @@ -18,6 +18,15 @@ OpenClaw can use **Firecrawl** in three ways: It is a hosted extraction/search service that supports bot circumvention and caching, which helps with JS-heavy sites or pages that block plain HTTP fetches. +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/firecrawl-plugin +openclaw gateway restart +``` + ## Keyless web_fetch and API keys The explicitly selected hosted Firecrawl `web_fetch` fallback supports starter @@ -54,7 +63,7 @@ or configure it when you need higher limits. Firecrawl `web_search` and Notes: -- Choosing Firecrawl in onboarding or `openclaw configure --section web` enables the bundled Firecrawl plugin automatically. +- Choosing Firecrawl in onboarding or `openclaw configure --section web` enables the installed Firecrawl plugin automatically. - `web_search` with Firecrawl supports `query` and `count`. - For Firecrawl-specific controls like `sources`, `categories`, or result scraping, use `firecrawl_search`. - `baseUrl` defaults to hosted Firecrawl at `https://api.firecrawl.dev`. Self-hosted overrides are allowed only for private/internal endpoints; HTTP is accepted only for those private targets. @@ -157,7 +166,7 @@ than basic-only scraping. The selection knob is `tools.web.fetch.provider`. If you omit it, OpenClaw auto-detects the first ready web-fetch provider from available credentials. -Today the bundled provider is Firecrawl. +The official Firecrawl plugin provides that fallback. ## Related diff --git a/docs/tools/parallel-search.md b/docs/tools/parallel-search.md index c114bdc5723..dc219606f7e 100644 --- a/docs/tools/parallel-search.md +++ b/docs/tools/parallel-search.md @@ -7,7 +7,7 @@ read_when: title: "Parallel search" --- -OpenClaw bundles two [Parallel](https://parallel.ai/) `web_search` providers: +The Parallel plugin provides two [Parallel](https://parallel.ai/) `web_search` providers: - **Parallel Search (Free)** (`parallel-free`) -- Parallel's free [Search MCP](https://docs.parallel.ai/integrations/mcp/search-mcp). Requires no @@ -27,6 +27,15 @@ explicitly. through Parallel. +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/parallel-plugin +openclaw gateway restart +``` + ## API key (paid provider) `parallel-free` requires no API key, but it still must be selected as the diff --git a/docs/tools/perplexity-search.md b/docs/tools/perplexity-search.md index 9920d2267d3..29a17822f68 100644 --- a/docs/tools/perplexity-search.md +++ b/docs/tools/perplexity-search.md @@ -12,6 +12,15 @@ It returns structured results with `title`, `url`, and `snippet` fields. For compatibility, OpenClaw also supports legacy Perplexity Sonar/OpenRouter setups. If you use `OPENROUTER_API_KEY`, an `sk-or-...` key in `plugins.entries.perplexity.config.webSearch.apiKey`, or set `plugins.entries.perplexity.config.webSearch.baseUrl` / `model`, the provider switches to the chat-completions path and returns AI-synthesized answers with citations instead of structured Search API results. +## Install plugin + +Install the official plugin, then restart Gateway: + +```bash +openclaw plugins install @openclaw/perplexity-plugin +openclaw gateway restart +``` + ## Getting a Perplexity API key 1. Create a Perplexity account at [perplexity.ai/settings/api](https://www.perplexity.ai/settings/api) diff --git a/docs/tools/video-generation.md b/docs/tools/video-generation.md index 5cf8627bf4f..b45913a39c0 100644 --- a/docs/tools/video-generation.md +++ b/docs/tools/video-generation.md @@ -144,7 +144,7 @@ the shared live sweep: | Alibaba | ✓ | ✓ | ✓ | `generate`, `imageToVideo`; `videoToVideo` skipped because this provider needs remote `http(s)` video URLs | | BytePlus | ✓ | ✓ | - | `generate`, `imageToVideo` | | ComfyUI | ✓ | ✓ | - | Not in the shared sweep; workflow-specific coverage lives with Comfy tests | -| DeepInfra | ✓ | - | - | `generate`; native DeepInfra video schemas are text-to-video in the bundled contract | +| DeepInfra | ✓ | - | - | `generate`; native DeepInfra video schemas are text-to-video in the plugin contract | | fal | ✓ | ✓ | ✓ | `generate`, `imageToVideo`; `videoToVideo` only when using Seedance reference-to-video | | Google | ✓ | ✓ | ✓ | `generate`, `imageToVideo`; shared `videoToVideo` skipped because the current buffer-backed Gemini/Veo sweep does not accept that input | | MiniMax | ✓ | ✓ | - | `generate`, `imageToVideo` | diff --git a/docs/tools/web-fetch.md b/docs/tools/web-fetch.md index 13a7d12f206..59130381641 100644 --- a/docs/tools/web-fetch.md +++ b/docs/tools/web-fetch.md @@ -153,8 +153,11 @@ Current runtime behavior: - If `provider` is omitted, OpenClaw auto-detects the first ready web-fetch provider from configured credentials. Non-sandboxed `web_fetch` can use installed plugins that declare `contracts.webFetchProviders` and register a - matching provider at runtime. Today the bundled provider is Firecrawl. -- Sandboxed `web_fetch` calls stay limited to bundled providers. + matching provider at runtime. The official Firecrawl plugin provides this + fallback. +- Sandboxed `web_fetch` calls allow bundled providers plus installed providers + whose official npm or ClawHub provenance is verified. Today that permits the + official Firecrawl plugin; third-party external fetch plugins stay excluded. - If Readability is disabled, `web_fetch` skips straight to the selected provider fallback. If no provider is available, it fails closed. diff --git a/docs/tools/web.md b/docs/tools/web.md index c28bb38da70..f54180e8dd8 100644 --- a/docs/tools/web.md +++ b/docs/tools/web.md @@ -260,7 +260,7 @@ to route them through the managed path. All provider key fields support SecretRef objects. Plugin-scoped SecretRefs under `plugins.entries..config.webSearch.apiKey` are resolved for the - bundled API-backed web search providers, including Brave, Exa, Firecrawl, + installed API-backed web search providers, including Brave, Exa, Firecrawl, Gemini, Grok, Kimi, MiniMax, Parallel, Perplexity, and Tavily, whether the provider is picked explicitly via `tools.web.search.provider` or selected through auto-detect. In auto-detect mode, OpenClaw resolves only the @@ -309,8 +309,9 @@ plugin or run `openclaw doctor --fix` to clean up the stale config. - or omit that field and let OpenClaw auto-detect the first ready web-fetch provider from configured credentials - non-sandboxed `web_fetch` can use installed plugin providers that declare - `contracts.webFetchProviders`; sandboxed fetches stay bundled-only -- today the bundled web-fetch provider is Firecrawl, configured under + `contracts.webFetchProviders`; sandboxed fetches allow bundled providers and + verified official plugin installs, but exclude third-party external plugins +- the official Firecrawl plugin provides web-fetch fallback, configured under `plugins.entries.firecrawl.config.webFetch.*` When you choose **Kimi** during `openclaw onboard` or diff --git a/extensions/arcee/README.md b/extensions/arcee/README.md new file mode 100644 index 00000000000..20488012ece --- /dev/null +++ b/extensions/arcee/README.md @@ -0,0 +1,12 @@ +# OpenClaw Arcee AI Provider + +Official OpenClaw provider plugin for Arcee AI. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/arcee-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/arcee/npm-shrinkwrap.json b/extensions/arcee/npm-shrinkwrap.json new file mode 100644 index 00000000000..b531279f756 --- /dev/null +++ b/extensions/arcee/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/arcee-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/arcee-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/arcee/package.json b/extensions/arcee/package.json index ab6e91f0ed3..bee06433637 100644 --- a/extensions/arcee/package.json +++ b/extensions/arcee/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/arcee-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Arcee provider plugin", + "description": "OpenClaw Arcee provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/arcee-provider", + "npmSpec": "@openclaw/arcee-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/cerebras/README.md b/extensions/cerebras/README.md new file mode 100644 index 00000000000..3d62a3db0ff --- /dev/null +++ b/extensions/cerebras/README.md @@ -0,0 +1,12 @@ +# OpenClaw Cerebras Provider + +Official OpenClaw provider plugin for Cerebras. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/cerebras-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/cerebras/npm-shrinkwrap.json b/extensions/cerebras/npm-shrinkwrap.json new file mode 100644 index 00000000000..2bb34e772c4 --- /dev/null +++ b/extensions/cerebras/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/cerebras-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/cerebras-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/cerebras/package.json b/extensions/cerebras/package.json index baa6144d5da..87a1b01d6e5 100644 --- a/extensions/cerebras/package.json +++ b/extensions/cerebras/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/cerebras-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Cerebras provider plugin", + "description": "OpenClaw Cerebras provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/cerebras-provider", + "npmSpec": "@openclaw/cerebras-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/chutes/README.md b/extensions/chutes/README.md new file mode 100644 index 00000000000..4c670d4bf0d --- /dev/null +++ b/extensions/chutes/README.md @@ -0,0 +1,12 @@ +# OpenClaw Chutes Provider + +Official OpenClaw provider plugin for Chutes. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/chutes-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/chutes/npm-shrinkwrap.json b/extensions/chutes/npm-shrinkwrap.json new file mode 100644 index 00000000000..485e3228003 --- /dev/null +++ b/extensions/chutes/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/chutes-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/chutes-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/chutes/package.json b/extensions/chutes/package.json index 9101db988e1..665bbe270d4 100644 --- a/extensions/chutes/package.json +++ b/extensions/chutes/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/chutes-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Chutes.ai provider plugin", + "description": "OpenClaw Chutes.ai provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/chutes-provider", + "npmSpec": "@openclaw/chutes-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/cloudflare-ai-gateway/README.md b/extensions/cloudflare-ai-gateway/README.md new file mode 100644 index 00000000000..91114608ae7 --- /dev/null +++ b/extensions/cloudflare-ai-gateway/README.md @@ -0,0 +1,12 @@ +# OpenClaw Cloudflare AI Gateway Provider + +Official OpenClaw provider plugin for Cloudflare AI Gateway. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/cloudflare-ai-gateway-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/cloudflare-ai-gateway/npm-shrinkwrap.json b/extensions/cloudflare-ai-gateway/npm-shrinkwrap.json new file mode 100644 index 00000000000..e584812be0a --- /dev/null +++ b/extensions/cloudflare-ai-gateway/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/cloudflare-ai-gateway-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/cloudflare-ai-gateway-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/cloudflare-ai-gateway/package.json b/extensions/cloudflare-ai-gateway/package.json index 14182032e44..7c8097e130b 100644 --- a/extensions/cloudflare-ai-gateway/package.json +++ b/extensions/cloudflare-ai-gateway/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/cloudflare-ai-gateway-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Cloudflare AI Gateway provider plugin", + "description": "OpenClaw Cloudflare AI Gateway provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/cloudflare-ai-gateway-provider", + "npmSpec": "@openclaw/cloudflare-ai-gateway-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/deepinfra/README.md b/extensions/deepinfra/README.md new file mode 100644 index 00000000000..6558794cf30 --- /dev/null +++ b/extensions/deepinfra/README.md @@ -0,0 +1,12 @@ +# OpenClaw DeepInfra Provider + +Official OpenClaw provider plugin for DeepInfra. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/deepinfra-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/deepinfra/npm-shrinkwrap.json b/extensions/deepinfra/npm-shrinkwrap.json new file mode 100644 index 00000000000..d70110cd591 --- /dev/null +++ b/extensions/deepinfra/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/deepinfra-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/deepinfra-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/deepinfra/package.json b/extensions/deepinfra/package.json index 2e3bc7defa7..2163fec661b 100644 --- a/extensions/deepinfra/package.json +++ b/extensions/deepinfra/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/deepinfra-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw DeepInfra provider plugin", + "description": "OpenClaw DeepInfra provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/deepinfra-provider", + "npmSpec": "@openclaw/deepinfra-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/deepseek/README.md b/extensions/deepseek/README.md new file mode 100644 index 00000000000..3aa324f9cba --- /dev/null +++ b/extensions/deepseek/README.md @@ -0,0 +1,12 @@ +# OpenClaw DeepSeek Provider + +Official OpenClaw provider plugin for DeepSeek. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/deepseek-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/deepseek/npm-shrinkwrap.json b/extensions/deepseek/npm-shrinkwrap.json new file mode 100644 index 00000000000..11b14dc9b87 --- /dev/null +++ b/extensions/deepseek/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/deepseek-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/deepseek-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/deepseek/package.json b/extensions/deepseek/package.json index cb398ddeb3f..ed7ae5358e1 100644 --- a/extensions/deepseek/package.json +++ b/extensions/deepseek/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/deepseek-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw DeepSeek provider plugin", + "description": "OpenClaw DeepSeek provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/deepseek-provider", + "npmSpec": "@openclaw/deepseek-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/exa/README.md b/extensions/exa/README.md new file mode 100644 index 00000000000..a7a55b09a57 --- /dev/null +++ b/extensions/exa/README.md @@ -0,0 +1,12 @@ +# OpenClaw Exa Plugin + +Official OpenClaw plugin for Exa. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/exa-plugin +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/exa/npm-shrinkwrap.json b/extensions/exa/npm-shrinkwrap.json new file mode 100644 index 00000000000..043c861ad4a --- /dev/null +++ b/extensions/exa/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/exa-plugin", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/exa-plugin", + "version": "2026.6.8" + } + } +} diff --git a/extensions/exa/package.json b/extensions/exa/package.json index 860ee8d7171..6ec949f5639 100644 --- a/extensions/exa/package.json +++ b/extensions/exa/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/exa-plugin", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Exa plugin", + "description": "OpenClaw Exa plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/exa-plugin", + "npmSpec": "@openclaw/exa-plugin", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/firecrawl/README.md b/extensions/firecrawl/README.md new file mode 100644 index 00000000000..bcf4489cea3 --- /dev/null +++ b/extensions/firecrawl/README.md @@ -0,0 +1,12 @@ +# OpenClaw Firecrawl Plugin + +Official OpenClaw plugin for Firecrawl. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/firecrawl-plugin +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/firecrawl/npm-shrinkwrap.json b/extensions/firecrawl/npm-shrinkwrap.json new file mode 100644 index 00000000000..1897804771f --- /dev/null +++ b/extensions/firecrawl/npm-shrinkwrap.json @@ -0,0 +1,21 @@ +{ + "name": "@openclaw/firecrawl-plugin", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/firecrawl-plugin", + "version": "2026.6.8", + "dependencies": { + "typebox": "1.1.39" + } + }, + "node_modules/typebox": { + "version": "1.1.39", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.39.tgz", + "integrity": "sha512-vj0afVtOfLQvv0GR0VxVagYxsXN64btL7Z9XoaG0ZggH3mruMMkOO6hXdgMsjCY3shZgEvooAWVeznQVs5c43w==", + "license": "MIT" + } + } +} diff --git a/extensions/firecrawl/package.json b/extensions/firecrawl/package.json index 696d2063eff..00c8a291ceb 100644 --- a/extensions/firecrawl/package.json +++ b/extensions/firecrawl/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/firecrawl-plugin", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Firecrawl plugin", + "description": "OpenClaw Firecrawl plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "dependencies": { "typebox": "1.1.39" @@ -13,6 +16,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/firecrawl-plugin", + "npmSpec": "@openclaw/firecrawl-plugin", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/gradium/README.md b/extensions/gradium/README.md new file mode 100644 index 00000000000..18a774bf70a --- /dev/null +++ b/extensions/gradium/README.md @@ -0,0 +1,12 @@ +# OpenClaw Gradium Plugin + +Official OpenClaw plugin for Gradium. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/gradium-speech +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/gradium/npm-shrinkwrap.json b/extensions/gradium/npm-shrinkwrap.json new file mode 100644 index 00000000000..cf37db272cb --- /dev/null +++ b/extensions/gradium/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/gradium-speech", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/gradium-speech", + "version": "2026.6.8" + } + } +} diff --git a/extensions/gradium/package.json b/extensions/gradium/package.json index 44e5f2dd3bb..73d62c0f0db 100644 --- a/extensions/gradium/package.json +++ b/extensions/gradium/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/gradium-speech", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Gradium speech plugin", + "description": "OpenClaw Gradium speech plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/gradium-speech", + "npmSpec": "@openclaw/gradium-speech", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/groq/README.md b/extensions/groq/README.md new file mode 100644 index 00000000000..532d4358a2d --- /dev/null +++ b/extensions/groq/README.md @@ -0,0 +1,12 @@ +# OpenClaw Groq Provider + +Official OpenClaw provider plugin for Groq. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/groq-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/groq/index.test.ts b/extensions/groq/index.test.ts index 201afcf8732..062449ef063 100644 --- a/extensions/groq/index.test.ts +++ b/extensions/groq/index.test.ts @@ -36,13 +36,22 @@ describe("groq provider compat", () => { if (!provider) { throw new Error("Expected Groq provider"); } - expect(provider).toEqual({ - auth: [], + expect(provider).toMatchObject({ docsPath: "/providers/groq", envVars: ["GROQ_API_KEY"], id: "groq", label: "Groq", }); + expect(provider.auth).toHaveLength(1); + expect(provider.auth[0]).toMatchObject({ + id: "api-key", + kind: "api_key", + label: "Groq API key", + wizard: { + choiceId: "groq-api-key", + groupId: "groq", + }, + }); expect(captured.mediaUnderstandingProviders).toHaveLength(1); const [mediaProvider] = captured.mediaUnderstandingProviders; if (!mediaProvider) { diff --git a/extensions/groq/index.ts b/extensions/groq/index.ts index b9cf2f928e3..f63c84bbaf8 100644 --- a/extensions/groq/index.ts +++ b/extensions/groq/index.ts @@ -1,7 +1,10 @@ // Groq plugin entrypoint registers its OpenClaw integration. import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import { groqMediaUnderstandingProvider } from "./media-understanding-provider.js"; +const GROQ_DEFAULT_MODEL_REF = "groq/llama-3.3-70b-versatile"; + export default definePluginEntry({ id: "groq", name: "Groq Provider", @@ -12,7 +15,27 @@ export default definePluginEntry({ label: "Groq", docsPath: "/providers/groq", envVars: ["GROQ_API_KEY"], - auth: [], + auth: [ + createProviderApiKeyAuthMethod({ + providerId: "groq", + methodId: "api-key", + label: "Groq API key", + hint: "Fast OpenAI-compatible inference", + optionKey: "groqApiKey", + flagName: "--groq-api-key", + envVar: "GROQ_API_KEY", + promptMessage: "Enter Groq API key", + defaultModel: GROQ_DEFAULT_MODEL_REF, + wizard: { + choiceId: "groq-api-key", + choiceLabel: "Groq API key", + choiceHint: "Fast OpenAI-compatible inference", + groupId: "groq", + groupLabel: "Groq", + groupHint: "Fast OpenAI-compatible inference", + }, + }), + ], }); api.registerMediaUnderstandingProvider(groqMediaUnderstandingProvider); }, diff --git a/extensions/groq/npm-shrinkwrap.json b/extensions/groq/npm-shrinkwrap.json new file mode 100644 index 00000000000..6e2a4041a64 --- /dev/null +++ b/extensions/groq/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/groq-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/groq-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/groq/package.json b/extensions/groq/package.json index 2b33e902aaf..4889bd4d1cd 100644 --- a/extensions/groq/package.json +++ b/extensions/groq/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/groq-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Groq media-understanding provider", + "description": "OpenClaw Groq media-understanding provider.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/groq-provider", + "npmSpec": "@openclaw/groq-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/inworld/README.md b/extensions/inworld/README.md new file mode 100644 index 00000000000..15edd341de4 --- /dev/null +++ b/extensions/inworld/README.md @@ -0,0 +1,12 @@ +# OpenClaw Inworld Plugin + +Official OpenClaw plugin for Inworld. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/inworld-speech +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/inworld/npm-shrinkwrap.json b/extensions/inworld/npm-shrinkwrap.json new file mode 100644 index 00000000000..df9647f48d9 --- /dev/null +++ b/extensions/inworld/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/inworld-speech", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/inworld-speech", + "version": "2026.6.8" + } + } +} diff --git a/extensions/inworld/package.json b/extensions/inworld/package.json index 22347cf2fd2..46f86d7d68f 100644 --- a/extensions/inworld/package.json +++ b/extensions/inworld/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/inworld-speech", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Inworld speech plugin", + "description": "OpenClaw Inworld speech plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/inworld-speech", + "npmSpec": "@openclaw/inworld-speech", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/kilocode/README.md b/extensions/kilocode/README.md new file mode 100644 index 00000000000..a54b02c9013 --- /dev/null +++ b/extensions/kilocode/README.md @@ -0,0 +1,12 @@ +# OpenClaw Kilo Gateway Provider + +Official OpenClaw provider plugin for Kilo Gateway. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/kilocode-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/kilocode/npm-shrinkwrap.json b/extensions/kilocode/npm-shrinkwrap.json new file mode 100644 index 00000000000..3bc0a0dcc85 --- /dev/null +++ b/extensions/kilocode/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/kilocode-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/kilocode-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/kilocode/package.json b/extensions/kilocode/package.json index f5e31485a95..80f0ef9916a 100644 --- a/extensions/kilocode/package.json +++ b/extensions/kilocode/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/kilocode-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Kilo Gateway provider plugin", + "description": "OpenClaw Kilo Gateway provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/kilocode-provider", + "npmSpec": "@openclaw/kilocode-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/kimi-coding/README.md b/extensions/kimi-coding/README.md new file mode 100644 index 00000000000..fa75caf1e89 --- /dev/null +++ b/extensions/kimi-coding/README.md @@ -0,0 +1,12 @@ +# OpenClaw Kimi Coding Provider + +Official OpenClaw provider plugin for Kimi Coding. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/kimi-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/kimi-coding/npm-shrinkwrap.json b/extensions/kimi-coding/npm-shrinkwrap.json new file mode 100644 index 00000000000..b7d358d4c36 --- /dev/null +++ b/extensions/kimi-coding/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/kimi-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/kimi-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/kimi-coding/package.json b/extensions/kimi-coding/package.json index c52ce9cba59..41d2cf19606 100644 --- a/extensions/kimi-coding/package.json +++ b/extensions/kimi-coding/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/kimi-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Kimi provider plugin", + "description": "OpenClaw Kimi provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/kimi-provider", + "npmSpec": "@openclaw/kimi-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/parallel/README.md b/extensions/parallel/README.md new file mode 100644 index 00000000000..0b718066e33 --- /dev/null +++ b/extensions/parallel/README.md @@ -0,0 +1,12 @@ +# OpenClaw Parallel Plugin + +Official OpenClaw plugin for Parallel. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/parallel-plugin +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/parallel/npm-shrinkwrap.json b/extensions/parallel/npm-shrinkwrap.json new file mode 100644 index 00000000000..4da26cdd13a --- /dev/null +++ b/extensions/parallel/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/parallel-plugin", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/parallel-plugin", + "version": "2026.6.8" + } + } +} diff --git a/extensions/parallel/package.json b/extensions/parallel/package.json index 99a7d492dae..5e52a674909 100644 --- a/extensions/parallel/package.json +++ b/extensions/parallel/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/parallel-plugin", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Parallel web search plugin", + "description": "OpenClaw Parallel web search plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/parallel-plugin", + "npmSpec": "@openclaw/parallel-plugin", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/perplexity/README.md b/extensions/perplexity/README.md new file mode 100644 index 00000000000..8d25dfdf8fa --- /dev/null +++ b/extensions/perplexity/README.md @@ -0,0 +1,12 @@ +# OpenClaw Perplexity Plugin + +Official OpenClaw plugin for Perplexity. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/perplexity-plugin +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/perplexity/npm-shrinkwrap.json b/extensions/perplexity/npm-shrinkwrap.json new file mode 100644 index 00000000000..c1871b7de11 --- /dev/null +++ b/extensions/perplexity/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/perplexity-plugin", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/perplexity-plugin", + "version": "2026.6.8" + } + } +} diff --git a/extensions/perplexity/package.json b/extensions/perplexity/package.json index 367aea5955f..1a5578c545c 100644 --- a/extensions/perplexity/package.json +++ b/extensions/perplexity/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/perplexity-plugin", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Perplexity plugin", + "description": "OpenClaw Perplexity plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/perplexity-plugin", + "npmSpec": "@openclaw/perplexity-plugin", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/qianfan/README.md b/extensions/qianfan/README.md new file mode 100644 index 00000000000..64483d79333 --- /dev/null +++ b/extensions/qianfan/README.md @@ -0,0 +1,12 @@ +# OpenClaw Qianfan Provider + +Official OpenClaw provider plugin for Qianfan. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/qianfan-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/qianfan/npm-shrinkwrap.json b/extensions/qianfan/npm-shrinkwrap.json new file mode 100644 index 00000000000..9cd7f3360c1 --- /dev/null +++ b/extensions/qianfan/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/qianfan-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/qianfan-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/qianfan/package.json b/extensions/qianfan/package.json index 8322baee28d..ee18299d3f1 100644 --- a/extensions/qianfan/package.json +++ b/extensions/qianfan/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/qianfan-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Qianfan provider plugin", + "description": "OpenClaw Qianfan provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/qianfan-provider", + "npmSpec": "@openclaw/qianfan-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/qwen/README.md b/extensions/qwen/README.md new file mode 100644 index 00000000000..75d5db6a7de --- /dev/null +++ b/extensions/qwen/README.md @@ -0,0 +1,12 @@ +# OpenClaw Qwen Cloud Provider + +Official OpenClaw provider plugin for Qwen Cloud. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/qwen-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/qwen/npm-shrinkwrap.json b/extensions/qwen/npm-shrinkwrap.json new file mode 100644 index 00000000000..3251fe6e1b2 --- /dev/null +++ b/extensions/qwen/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/qwen-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/qwen-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/qwen/package.json b/extensions/qwen/package.json index f476859d32d..b2f1550dbed 100644 --- a/extensions/qwen/package.json +++ b/extensions/qwen/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/qwen-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw Qwen Cloud provider plugin", + "description": "OpenClaw Qwen Cloud provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/qwen-provider", + "npmSpec": "@openclaw/qwen-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/extensions/stepfun/README.md b/extensions/stepfun/README.md new file mode 100644 index 00000000000..08f960201db --- /dev/null +++ b/extensions/stepfun/README.md @@ -0,0 +1,12 @@ +# OpenClaw StepFun Provider + +Official OpenClaw provider plugin for StepFun. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/stepfun-provider +openclaw gateway restart +``` + +See for setup and configuration. diff --git a/extensions/stepfun/npm-shrinkwrap.json b/extensions/stepfun/npm-shrinkwrap.json new file mode 100644 index 00000000000..443acb31c58 --- /dev/null +++ b/extensions/stepfun/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/stepfun-provider", + "version": "2026.6.8", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/stepfun-provider", + "version": "2026.6.8" + } + } +} diff --git a/extensions/stepfun/package.json b/extensions/stepfun/package.json index bab868a89a5..fefb90d923a 100644 --- a/extensions/stepfun/package.json +++ b/extensions/stepfun/package.json @@ -1,8 +1,11 @@ { "name": "@openclaw/stepfun-provider", "version": "2026.6.8", - "private": true, - "description": "OpenClaw StepFun provider plugin", + "description": "OpenClaw StepFun provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, "type": "module", "devDependencies": { "@openclaw/plugin-sdk": "workspace:*" @@ -10,6 +13,23 @@ "openclaw": { "extensions": [ "./index.ts" - ] + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/stepfun-provider", + "npmSpec": "@openclaw/stepfun-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.8" + }, + "build": { + "openclawVersion": "2026.6.8", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } } } diff --git a/package.json b/package.json index d90b90562ff..e86c171afbc 100644 --- a/package.json +++ b/package.json @@ -79,18 +79,31 @@ "!dist/extensions/anthropic-vertex/**", "!dist/extensions/node_modules/**", "!dist/extensions/*/node_modules/**", + "!dist/extensions/arcee/**", "!dist/extensions/brave/**", + "!dist/extensions/cerebras/**", + "!dist/extensions/chutes/**", + "!dist/extensions/cloudflare-ai-gateway/**", "!dist/extensions/codex/**", "!dist/extensions/copilot/**", + "!dist/extensions/deepinfra/**", + "!dist/extensions/deepseek/**", "!dist/extensions/diagnostics-otel/**", "!dist/extensions/diagnostics-prometheus/**", "!dist/extensions/diffs/**", "!dist/extensions/diffs-language-pack/**", "!dist/extensions/discord/**", + "!dist/extensions/exa/**", "!dist/extensions/feishu/**", + "!dist/extensions/firecrawl/**", "!dist/extensions/google-meet/**", "!dist/extensions/googlechat/**", "!dist/extensions/gmi/**", + "!dist/extensions/gradium/**", + "!dist/extensions/groq/**", + "!dist/extensions/inworld/**", + "!dist/extensions/kilocode/**", + "!dist/extensions/kimi-coding/**", "!dist/extensions/line/**", "!dist/extensions/llama-cpp/**", "!dist/extensions/lobster/**", @@ -99,13 +112,18 @@ "!dist/extensions/msteams/**", "!dist/extensions/nextcloud-talk/**", "!dist/extensions/nostr/**", + "!dist/extensions/parallel/**", + "!dist/extensions/perplexity/**", + "!dist/extensions/qianfan/**", "!dist/extensions/qqbot/**", "!dist/extensions/pixverse/**", "!dist/extensions/qa-channel/**", "!dist/extensions/qa-lab/**", "!dist/extensions/qa-matrix/**", "!dist/extensions/openshell/**", + "!dist/extensions/qwen/**", "!dist/extensions/slack/**", + "!dist/extensions/stepfun/**", "!dist/extensions/synology-chat/**", "!dist/extensions/tokenjuice/**", "!dist/extensions/tlon/**", diff --git a/scripts/lib/official-external-plugin-catalog.json b/scripts/lib/official-external-plugin-catalog.json index 162e2149fc5..b2fe1001277 100644 --- a/scripts/lib/official-external-plugin-catalog.json +++ b/scripts/lib/official-external-plugin-catalog.json @@ -139,6 +139,97 @@ } } }, + { + "name": "@openclaw/exa-plugin", + "description": "OpenClaw Exa plugin.", + "source": "official", + "kind": "plugin", + "openclaw": { + "plugin": { + "id": "exa", + "label": "Exa" + }, + "contracts": { + "webSearchProviders": [ + "exa" + ] + }, + "webSearchProviders": [ + { + "id": "exa", + "label": "Exa Search", + "hint": "Neural + keyword search with date filters and content extraction", + "onboardingScopes": [ + "text-inference" + ], + "credentialLabel": "Exa API key", + "envVars": [ + "EXA_API_KEY" + ], + "placeholder": "exa-...", + "signupUrl": "https://exa.ai/", + "docsUrl": "https://docs.openclaw.ai/tools/web", + "credentialPath": "plugins.entries.exa.config.webSearch.apiKey", + "autoDetectOrder": 65 + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/exa-plugin", + "npmSpec": "@openclaw/exa-plugin", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/firecrawl-plugin", + "description": "OpenClaw Firecrawl plugin.", + "source": "official", + "kind": "plugin", + "openclaw": { + "plugin": { + "id": "firecrawl", + "label": "Firecrawl" + }, + "contracts": { + "webFetchProviders": [ + "firecrawl" + ], + "webSearchProviders": [ + "firecrawl" + ], + "tools": [ + "firecrawl_search", + "firecrawl_scrape" + ] + }, + "webSearchProviders": [ + { + "id": "firecrawl", + "label": "Firecrawl Search", + "hint": "Structured results with optional result scraping", + "onboardingScopes": [ + "text-inference" + ], + "credentialLabel": "Firecrawl API key", + "envVars": [ + "FIRECRAWL_API_KEY" + ], + "placeholder": "fc-...", + "signupUrl": "https://www.firecrawl.dev/", + "docsUrl": "https://docs.openclaw.ai/tools/firecrawl", + "credentialPath": "plugins.entries.firecrawl.config.webSearch.apiKey", + "autoDetectOrder": 60 + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/firecrawl-plugin", + "npmSpec": "@openclaw/firecrawl-plugin", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, { "name": "@openclaw/google-meet", "description": "OpenClaw Google Meet participant plugin", @@ -156,6 +247,52 @@ } } }, + { + "name": "@openclaw/gradium-speech", + "description": "OpenClaw Gradium speech plugin.", + "source": "official", + "kind": "plugin", + "openclaw": { + "plugin": { + "id": "gradium", + "label": "Gradium" + }, + "contracts": { + "speechProviders": [ + "gradium" + ] + }, + "install": { + "clawhubSpec": "clawhub:@openclaw/gradium-speech", + "npmSpec": "@openclaw/gradium-speech", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/inworld-speech", + "description": "OpenClaw Inworld speech plugin.", + "source": "official", + "kind": "plugin", + "openclaw": { + "plugin": { + "id": "inworld", + "label": "Inworld" + }, + "contracts": { + "speechProviders": [ + "inworld" + ] + }, + "install": { + "clawhubSpec": "clawhub:@openclaw/inworld-speech", + "npmSpec": "@openclaw/inworld-speech", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, { "name": "@openclaw/lobster", "description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)", @@ -227,6 +364,106 @@ } } }, + { + "name": "@openclaw/parallel-plugin", + "description": "OpenClaw Parallel web search plugin.", + "source": "official", + "kind": "plugin", + "openclaw": { + "plugin": { + "id": "parallel", + "label": "Parallel" + }, + "contracts": { + "webSearchProviders": [ + "parallel", + "parallel-free" + ] + }, + "webSearchProviders": [ + { + "id": "parallel", + "label": "Parallel Search", + "hint": "LLM-optimized dense excerpts from web sources", + "onboardingScopes": [ + "text-inference" + ], + "credentialLabel": "Parallel API key", + "envVars": [ + "PARALLEL_API_KEY" + ], + "placeholder": "par-...", + "signupUrl": "https://platform.parallel.ai", + "docsUrl": "https://docs.openclaw.ai/tools/parallel-search", + "credentialPath": "plugins.entries.parallel.config.webSearch.apiKey", + "autoDetectOrder": 75 + }, + { + "id": "parallel-free", + "label": "Parallel Search (Free)", + "hint": "Free web search via Parallel's hosted Search MCP — no API key required", + "onboardingScopes": [ + "text-inference" + ], + "requiresCredential": false, + "envVars": [], + "placeholder": "(no key needed)", + "signupUrl": "https://parallel.ai", + "docsUrl": "https://docs.openclaw.ai/tools/parallel-search", + "credentialPath": "" + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/parallel-plugin", + "npmSpec": "@openclaw/parallel-plugin", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/perplexity-plugin", + "description": "OpenClaw Perplexity plugin.", + "source": "official", + "kind": "plugin", + "openclaw": { + "plugin": { + "id": "perplexity", + "label": "Perplexity" + }, + "contracts": { + "webSearchProviders": [ + "perplexity" + ] + }, + "webSearchProviders": [ + { + "id": "perplexity", + "label": "Perplexity Search", + "hint": "Requires Perplexity API key or OpenRouter API key · structured results", + "onboardingScopes": [ + "text-inference" + ], + "credentialLabel": "Perplexity API key", + "envVars": [ + "PERPLEXITY_API_KEY", + "OPENROUTER_API_KEY" + ], + "placeholder": "pplx-...", + "signupUrl": "https://www.perplexity.ai/settings/api", + "docsUrl": "https://docs.openclaw.ai/perplexity", + "credentialPath": "plugins.entries.perplexity.config.webSearch.apiKey", + "autoDetectOrder": 50 + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/perplexity-plugin", + "npmSpec": "@openclaw/perplexity-plugin", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, { "name": "@openclaw/pixverse-provider", "description": "OpenClaw PixVerse video generation provider plugin", diff --git a/scripts/lib/official-external-provider-catalog.json b/scripts/lib/official-external-provider-catalog.json index 6c6c435e04b..47bc23d4c98 100644 --- a/scripts/lib/official-external-provider-catalog.json +++ b/scripts/lib/official-external-provider-catalog.json @@ -75,6 +75,280 @@ } } }, + { + "name": "@openclaw/arcee-provider", + "description": "OpenClaw Arcee provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "arcee", + "label": "Arcee AI" + }, + "providers": [ + { + "id": "arcee", + "name": "Arcee AI", + "docs": "/providers/arcee", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "ARCEEAI_API_KEY" + ], + "authChoices": [ + { + "method": "arcee-platform", + "choiceId": "arceeai-api-key", + "choiceLabel": "Arcee AI API key", + "choiceHint": "Direct (chat.arcee.ai)", + "groupId": "arcee", + "groupLabel": "Arcee AI", + "groupHint": "Direct API or OpenRouter", + "optionKey": "arceeaiApiKey", + "cliFlag": "--arceeai-api-key", + "cliOption": "--arceeai-api-key ", + "cliDescription": "Arcee AI API key", + "onboardingScopes": [ + "text-inference" + ] + }, + { + "method": "openrouter", + "choiceId": "arceeai-openrouter", + "choiceLabel": "OpenRouter API key", + "choiceHint": "Via OpenRouter (openrouter.ai)", + "groupId": "arcee", + "groupLabel": "Arcee AI", + "groupHint": "Direct API or OpenRouter", + "optionKey": "openrouterApiKey", + "cliFlag": "--openrouter-api-key", + "cliOption": "--openrouter-api-key ", + "cliDescription": "OpenRouter API key for Arcee AI models", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/arcee-provider", + "npmSpec": "@openclaw/arcee-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/cerebras-provider", + "description": "OpenClaw Cerebras provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "cerebras", + "label": "Cerebras" + }, + "providers": [ + { + "id": "cerebras", + "name": "Cerebras", + "docs": "/providers/cerebras", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "CEREBRAS_API_KEY" + ], + "authChoices": [ + { + "method": "api-key", + "choiceId": "cerebras-api-key", + "choiceLabel": "Cerebras API key", + "groupId": "cerebras", + "groupLabel": "Cerebras", + "groupHint": "Fast OpenAI-compatible inference", + "optionKey": "cerebrasApiKey", + "cliFlag": "--cerebras-api-key", + "cliOption": "--cerebras-api-key ", + "cliDescription": "Cerebras API key", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/cerebras-provider", + "npmSpec": "@openclaw/cerebras-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/chutes-provider", + "description": "OpenClaw Chutes.ai provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "chutes", + "label": "Chutes" + }, + "providers": [ + { + "id": "chutes", + "name": "Chutes", + "docs": "/providers/chutes", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "CHUTES_API_KEY", + "CHUTES_OAUTH_TOKEN" + ], + "authChoices": [ + { + "method": "oauth", + "choiceId": "chutes", + "choiceLabel": "Chutes (OAuth)", + "choiceHint": "Browser sign-in", + "groupId": "chutes", + "groupLabel": "Chutes", + "groupHint": "OAuth + API key", + "onboardingScopes": [ + "text-inference" + ] + }, + { + "method": "api-key", + "choiceId": "chutes-api-key", + "choiceLabel": "Chutes API key", + "choiceHint": "Open-source models including Llama, DeepSeek, and more", + "groupId": "chutes", + "groupLabel": "Chutes", + "groupHint": "OAuth + API key", + "optionKey": "chutesApiKey", + "cliFlag": "--chutes-api-key", + "cliOption": "--chutes-api-key ", + "cliDescription": "Chutes API key", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/chutes-provider", + "npmSpec": "@openclaw/chutes-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/cohere-provider", + "description": "OpenClaw Cohere provider plugin", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "cohere", + "label": "Cohere" + }, + "providers": [ + { + "id": "cohere", + "name": "Cohere", + "docs": "/providers/cohere", + "categories": [ + "cloud", + "llm" + ], + "authChoices": [ + { + "method": "api-key", + "choiceId": "cohere-api-key", + "choiceLabel": "Cohere API key", + "groupId": "cohere", + "groupLabel": "Cohere", + "groupHint": "OpenAI-compatible inference", + "optionKey": "cohereApiKey", + "cliFlag": "--cohere-api-key", + "cliOption": "--cohere-api-key ", + "cliDescription": "Cohere API key", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/cohere-provider", + "npmSpec": "@openclaw/cohere-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/cloudflare-ai-gateway-provider", + "description": "OpenClaw Cloudflare AI Gateway provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "cloudflare-ai-gateway", + "label": "Cloudflare AI Gateway" + }, + "providers": [ + { + "id": "cloudflare-ai-gateway", + "name": "Cloudflare AI Gateway", + "docs": "/providers/cloudflare-ai-gateway", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "CLOUDFLARE_AI_GATEWAY_API_KEY" + ], + "authChoices": [ + { + "method": "api-key", + "choiceId": "cloudflare-ai-gateway-api-key", + "choiceLabel": "Cloudflare AI Gateway", + "choiceHint": "Account ID + Gateway ID + API key", + "groupId": "cloudflare-ai-gateway", + "groupLabel": "Cloudflare AI Gateway", + "groupHint": "Account ID + Gateway ID + API key", + "optionKey": "cloudflareAiGatewayApiKey", + "cliFlag": "--cloudflare-ai-gateway-api-key", + "cliOption": "--cloudflare-ai-gateway-api-key ", + "cliDescription": "Cloudflare AI Gateway API key", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/cloudflare-ai-gateway-provider", + "npmSpec": "@openclaw/cloudflare-ai-gateway-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, { "name": "@openclaw/codex", "description": "OpenClaw Codex harness and model provider plugin", @@ -113,6 +387,122 @@ } } }, + { + "name": "@openclaw/deepinfra-provider", + "description": "OpenClaw DeepInfra provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "deepinfra", + "label": "DeepInfra" + }, + "providers": [ + { + "id": "deepinfra", + "name": "DeepInfra", + "docs": "/providers/deepinfra", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "DEEPINFRA_API_KEY" + ], + "authChoices": [ + { + "method": "api-key", + "choiceId": "deepinfra-api-key", + "choiceLabel": "DeepInfra API key", + "choiceHint": "Unified API for open source models", + "groupId": "deepinfra", + "groupLabel": "DeepInfra", + "groupHint": "Unified API for open source models", + "optionKey": "deepinfraApiKey", + "cliFlag": "--deepinfra-api-key", + "cliOption": "--deepinfra-api-key ", + "cliDescription": "DeepInfra API key", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "contracts": { + "mediaUnderstandingProviders": [ + "deepinfra" + ], + "memoryEmbeddingProviders": [ + "deepinfra" + ], + "imageGenerationProviders": [ + "deepinfra" + ], + "speechProviders": [ + "deepinfra" + ], + "videoGenerationProviders": [ + "deepinfra" + ] + }, + "install": { + "clawhubSpec": "clawhub:@openclaw/deepinfra-provider", + "npmSpec": "@openclaw/deepinfra-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/deepseek-provider", + "description": "OpenClaw DeepSeek provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "deepseek", + "label": "DeepSeek" + }, + "providers": [ + { + "id": "deepseek", + "name": "DeepSeek", + "docs": "/providers/deepseek", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "DEEPSEEK_API_KEY" + ], + "authChoices": [ + { + "method": "api-key", + "choiceId": "deepseek-api-key", + "choiceLabel": "DeepSeek API key", + "groupId": "deepseek", + "groupLabel": "DeepSeek", + "groupHint": "API key", + "optionKey": "deepseekApiKey", + "cliFlag": "--deepseek-api-key", + "cliOption": "--deepseek-api-key ", + "cliDescription": "DeepSeek API key", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/deepseek-provider", + "npmSpec": "@openclaw/deepseek-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, { "name": "@openclaw/gmi-provider", "description": "OpenClaw GMI Cloud provider plugin", @@ -157,41 +547,157 @@ } }, { - "name": "@openclaw/cohere-provider", - "description": "OpenClaw Cohere provider plugin", + "name": "@openclaw/groq-provider", + "description": "OpenClaw Groq media-understanding provider.", "source": "official", "kind": "provider", "openclaw": { "plugin": { - "id": "cohere", - "label": "Cohere" + "id": "groq", + "label": "Groq" }, "providers": [ { - "id": "cohere", - "name": "Cohere", - "docs": "/providers/cohere", - "categories": ["cloud", "llm"], + "id": "groq", + "name": "Groq", + "docs": "/providers/groq", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "GROQ_API_KEY" + ], "authChoices": [ { "method": "api-key", - "choiceId": "cohere-api-key", - "choiceLabel": "Cohere API key", - "groupId": "cohere", - "groupLabel": "Cohere", - "groupHint": "OpenAI-compatible inference", - "optionKey": "cohereApiKey", - "cliFlag": "--cohere-api-key", - "cliOption": "--cohere-api-key ", - "cliDescription": "Cohere API key", - "onboardingScopes": ["text-inference"] + "choiceId": "groq-api-key", + "choiceLabel": "Groq API key", + "groupId": "groq", + "groupLabel": "Groq", + "groupHint": "Fast OpenAI-compatible inference", + "optionKey": "groqApiKey", + "cliFlag": "--groq-api-key", + "cliOption": "--groq-api-key ", + "cliDescription": "Groq API key", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "contracts": { + "mediaUnderstandingProviders": [ + "groq" + ] + }, + "install": { + "clawhubSpec": "clawhub:@openclaw/groq-provider", + "npmSpec": "@openclaw/groq-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/kilocode-provider", + "description": "OpenClaw Kilo Gateway provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "kilocode", + "label": "Kilo Gateway" + }, + "providers": [ + { + "id": "kilocode", + "name": "Kilo Gateway", + "docs": "/providers/kilocode", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "KILOCODE_API_KEY" + ], + "authChoices": [ + { + "method": "api-key", + "choiceId": "kilocode-api-key", + "choiceLabel": "Kilo Gateway API key", + "choiceHint": "API key (OpenRouter-compatible)", + "groupId": "kilocode", + "groupLabel": "Kilo Gateway", + "groupHint": "API key (OpenRouter-compatible)", + "optionKey": "kilocodeApiKey", + "cliFlag": "--kilocode-api-key", + "cliOption": "--kilocode-api-key ", + "cliDescription": "Kilo Gateway API key", + "onboardingScopes": [ + "text-inference" + ] } ] } ], "install": { - "clawhubSpec": "clawhub:@openclaw/cohere-provider", - "npmSpec": "@openclaw/cohere-provider", + "clawhubSpec": "clawhub:@openclaw/kilocode-provider", + "npmSpec": "@openclaw/kilocode-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/kimi-provider", + "description": "OpenClaw Kimi provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "kimi", + "label": "Kimi Coding" + }, + "providers": [ + { + "id": "kimi", + "aliases": [ + "kimi-coding" + ], + "name": "Kimi Coding", + "docs": "/providers/moonshot", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "KIMI_API_KEY", + "KIMICODE_API_KEY" + ], + "authChoices": [ + { + "method": "api-key", + "choiceId": "kimi-code-api-key", + "choiceLabel": "Kimi Code API key (subscription)", + "groupId": "moonshot", + "groupLabel": "Moonshot AI (Kimi K2.6)", + "groupHint": "Kimi K2.6", + "optionKey": "kimiCodeApiKey", + "cliFlag": "--kimi-code-api-key", + "cliOption": "--kimi-code-api-key ", + "cliDescription": "Kimi Code API key (subscription)", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/kimi-provider", + "npmSpec": "@openclaw/kimi-provider", "defaultChoice": "npm", "minHostVersion": ">=2026.6.8" } @@ -237,6 +743,326 @@ "minHostVersion": ">=2026.5.26" } } + }, + { + "name": "@openclaw/qianfan-provider", + "description": "OpenClaw Qianfan provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "qianfan", + "label": "Qianfan" + }, + "providers": [ + { + "id": "qianfan", + "name": "Qianfan", + "docs": "/providers/qianfan", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "QIANFAN_API_KEY" + ], + "authChoices": [ + { + "method": "api-key", + "choiceId": "qianfan-api-key", + "choiceLabel": "Qianfan API key", + "groupId": "qianfan", + "groupLabel": "Qianfan", + "groupHint": "API key", + "optionKey": "qianfanApiKey", + "cliFlag": "--qianfan-api-key", + "cliOption": "--qianfan-api-key ", + "cliDescription": "QIANFAN API key", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/qianfan-provider", + "npmSpec": "@openclaw/qianfan-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/qwen-provider", + "description": "OpenClaw Qwen Cloud provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "qwen", + "label": "Qwen Cloud" + }, + "providers": [ + { + "id": "qwen", + "aliases": [ + "qwencloud", + "modelstudio", + "dashscope" + ], + "name": "Qwen Cloud", + "docs": "/providers/qwen", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "QWEN_API_KEY", + "MODELSTUDIO_API_KEY", + "DASHSCOPE_API_KEY" + ], + "authChoices": [ + { + "method": "standard-api-key-cn", + "choiceId": "qwen-standard-api-key-cn", + "deprecatedChoiceIds": [ + "modelstudio-standard-api-key-cn" + ], + "choiceLabel": "Standard API Key for China (pay-as-you-go)", + "choiceHint": "Endpoint: dashscope.aliyuncs.com", + "groupId": "qwen", + "groupLabel": "Qwen Cloud", + "groupHint": "Standard / Coding Plan (CN / Global) + multimodal roadmap", + "optionKey": "modelstudioStandardApiKeyCn", + "cliFlag": "--modelstudio-standard-api-key-cn", + "cliOption": "--modelstudio-standard-api-key-cn ", + "cliDescription": "Qwen Cloud standard API key (China)", + "onboardingScopes": [ + "text-inference" + ] + }, + { + "method": "standard-api-key", + "choiceId": "qwen-standard-api-key", + "deprecatedChoiceIds": [ + "modelstudio-standard-api-key" + ], + "choiceLabel": "Standard API Key for Global/Intl (pay-as-you-go)", + "choiceHint": "Endpoint: dashscope-intl.aliyuncs.com", + "groupId": "qwen", + "groupLabel": "Qwen Cloud", + "groupHint": "Standard / Coding Plan (CN / Global) + multimodal roadmap", + "optionKey": "modelstudioStandardApiKey", + "cliFlag": "--modelstudio-standard-api-key", + "cliOption": "--modelstudio-standard-api-key ", + "cliDescription": "Qwen Cloud standard API key (Global/Intl)", + "onboardingScopes": [ + "text-inference" + ] + }, + { + "method": "api-key-cn", + "choiceId": "qwen-api-key-cn", + "deprecatedChoiceIds": [ + "modelstudio-api-key-cn" + ], + "choiceLabel": "Coding Plan API Key for China (subscription)", + "choiceHint": "Endpoint: coding.dashscope.aliyuncs.com", + "groupId": "qwen", + "groupLabel": "Qwen Cloud", + "groupHint": "Standard / Coding Plan (CN / Global) + multimodal roadmap", + "optionKey": "modelstudioApiKeyCn", + "cliFlag": "--modelstudio-api-key-cn", + "cliOption": "--modelstudio-api-key-cn ", + "cliDescription": "Qwen Cloud Coding Plan API key (China)", + "onboardingScopes": [ + "text-inference" + ] + }, + { + "method": "api-key", + "choiceId": "qwen-api-key", + "deprecatedChoiceIds": [ + "modelstudio-api-key" + ], + "choiceLabel": "Coding Plan API Key for Global/Intl (subscription)", + "choiceHint": "Endpoint: coding-intl.dashscope.aliyuncs.com", + "groupId": "qwen", + "groupLabel": "Qwen Cloud", + "groupHint": "Standard / Coding Plan (CN / Global) + multimodal roadmap", + "optionKey": "modelstudioApiKey", + "cliFlag": "--modelstudio-api-key", + "cliOption": "--modelstudio-api-key ", + "cliDescription": "Qwen Cloud Coding Plan API key (Global/Intl)", + "onboardingScopes": [ + "text-inference" + ] + } + ] + }, + { + "id": "qwen-oauth", + "aliases": [ + "qwen-portal", + "qwen-cli" + ], + "name": "Qwen Cloud qwen oauth", + "docs": "/providers/qwen", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "QWEN_API_KEY" + ], + "authChoices": [ + { + "method": "api-key", + "choiceId": "qwen-oauth", + "choiceLabel": "Qwen OAuth", + "choiceHint": "Portal token for portal.qwen.ai", + "groupId": "qwen", + "groupLabel": "Qwen Cloud", + "groupHint": "Standard / Coding Plan / OAuth", + "optionKey": "qwenOauthToken", + "cliFlag": "--qwen-oauth-token", + "cliOption": "--qwen-oauth-token ", + "cliDescription": "Qwen OAuth token", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "contracts": { + "mediaUnderstandingProviders": [ + "qwen" + ], + "videoGenerationProviders": [ + "qwen" + ] + }, + "install": { + "clawhubSpec": "clawhub:@openclaw/qwen-provider", + "npmSpec": "@openclaw/qwen-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, + { + "name": "@openclaw/stepfun-provider", + "description": "OpenClaw StepFun provider plugin.", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "stepfun", + "label": "StepFun" + }, + "providers": [ + { + "id": "stepfun", + "name": "StepFun", + "docs": "/providers/stepfun", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "STEPFUN_API_KEY" + ], + "authChoices": [ + { + "method": "standard-api-key-cn", + "choiceId": "stepfun-standard-api-key-cn", + "choiceLabel": "StepFun Standard API key (China)", + "choiceHint": "Endpoint: api.stepfun.com/v1", + "groupId": "stepfun", + "groupLabel": "StepFun", + "groupHint": "Standard / Step Plan (China / Global)", + "optionKey": "stepfunApiKey", + "cliFlag": "--stepfun-api-key", + "cliOption": "--stepfun-api-key ", + "cliDescription": "StepFun API key", + "onboardingScopes": [ + "text-inference" + ] + }, + { + "method": "standard-api-key-intl", + "choiceId": "stepfun-standard-api-key-intl", + "choiceLabel": "StepFun Standard API key (Global/Intl)", + "choiceHint": "Endpoint: api.stepfun.ai/v1", + "groupId": "stepfun", + "groupLabel": "StepFun", + "groupHint": "Standard / Step Plan (China / Global)", + "optionKey": "stepfunApiKey", + "cliFlag": "--stepfun-api-key", + "cliOption": "--stepfun-api-key ", + "cliDescription": "StepFun API key", + "onboardingScopes": [ + "text-inference" + ] + } + ] + }, + { + "id": "stepfun-plan", + "name": "StepFun stepfun plan", + "docs": "/providers/stepfun", + "categories": [ + "cloud", + "llm" + ], + "envVars": [ + "STEPFUN_API_KEY" + ], + "authChoices": [ + { + "method": "plan-api-key-cn", + "choiceId": "stepfun-plan-api-key-cn", + "choiceLabel": "StepFun Step Plan API key (China)", + "choiceHint": "Endpoint: api.stepfun.com/step_plan/v1", + "groupId": "stepfun", + "groupLabel": "StepFun", + "groupHint": "Standard / Step Plan (China / Global)", + "optionKey": "stepfunApiKey", + "cliFlag": "--stepfun-api-key", + "cliOption": "--stepfun-api-key ", + "cliDescription": "StepFun API key", + "onboardingScopes": [ + "text-inference" + ] + }, + { + "method": "plan-api-key-intl", + "choiceId": "stepfun-plan-api-key-intl", + "choiceLabel": "StepFun Step Plan API key (Global/Intl)", + "choiceHint": "Endpoint: api.stepfun.ai/step_plan/v1", + "groupId": "stepfun", + "groupLabel": "StepFun", + "groupHint": "Standard / Step Plan (China / Global)", + "optionKey": "stepfunApiKey", + "cliFlag": "--stepfun-api-key", + "cliOption": "--stepfun-api-key ", + "cliDescription": "StepFun API key", + "onboardingScopes": [ + "text-inference" + ] + } + ] + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/stepfun-provider", + "npmSpec": "@openclaw/stepfun-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } } ] } diff --git a/src/cli/program/register.onboard.test.ts b/src/cli/program/register.onboard.test.ts index 469c0a7b49e..f6bfc98dc5c 100644 --- a/src/cli/program/register.onboard.test.ts +++ b/src/cli/program/register.onboard.test.ts @@ -36,7 +36,7 @@ vi.mock("../../commands/onboard-core-auth-flags.js", () => ({ })); vi.mock("../../plugins/provider-auth-choices.js", () => ({ - resolveManifestProviderOnboardAuthFlags: () => [ + resolveProviderOnboardAuthFlags: () => [ { cliOption: "--openai-api-key ", description: "OpenAI API key", diff --git a/src/cli/program/register.onboard.ts b/src/cli/program/register.onboard.ts index f3fd285d60d..0e2c6869413 100644 --- a/src/cli/program/register.onboard.ts +++ b/src/cli/program/register.onboard.ts @@ -14,7 +14,7 @@ import type { SecretInputMode, TailscaleMode, } from "../../commands/onboard-types.js"; -import { resolveManifestProviderOnboardAuthFlags } from "../../plugins/provider-auth-choices.js"; +import { resolveProviderOnboardAuthFlags } from "../../plugins/provider-auth-choices.js"; import { runCommandWithRuntime } from "../cli-utils.js"; import { parsePort } from "../shared/parse-port.js"; @@ -67,7 +67,7 @@ function resolveOnboardAuthFlags(): OnboardAuthFlag[] { // Provider manifests can add auth flags; keep duplicate CLI aliases out of Commander. const seenCliFlags = new Set(); const flags: OnboardAuthFlag[] = []; - for (const flag of [...CORE_ONBOARD_AUTH_FLAGS, ...resolveManifestProviderOnboardAuthFlags()]) { + for (const flag of [...CORE_ONBOARD_AUTH_FLAGS, ...resolveProviderOnboardAuthFlags()]) { const cliFlags = extractCliFlags(flag.cliOption); if (cliFlags.some((cliFlag) => seenCliFlags.has(cliFlag))) { continue; diff --git a/src/commands/auth-choice.apply.ts b/src/commands/auth-choice.apply.ts index c783a2f5052..772975352bb 100644 --- a/src/commands/auth-choice.apply.ts +++ b/src/commands/auth-choice.apply.ts @@ -55,10 +55,20 @@ async function formatDeprecatedProviderChoiceError( config: params.config, env: params.env, }); - if (!deprecatedChoice) { + if (deprecatedChoice) { + return `Auth choice ${JSON.stringify(authChoice)} is no longer supported. Use ${JSON.stringify(deprecatedChoice.choiceId)} instead, or run ${formatCliCommand("openclaw onboard")} to choose interactively.`; + } + const { resolveProviderInstallCatalogEntries } = + await import("../plugins/provider-install-catalog.js"); + const externalDeprecatedChoice = resolveProviderInstallCatalogEntries({ + config: params.config, + env: params.env, + includeUntrustedWorkspacePlugins: false, + }).find((entry) => entry.deprecatedChoiceIds?.includes(authChoice)); + if (!externalDeprecatedChoice) { return undefined; } - return `Auth choice ${JSON.stringify(authChoice)} is no longer supported. Use ${JSON.stringify(deprecatedChoice.choiceId)} instead, or run ${formatCliCommand("openclaw onboard")} to choose interactively.`; + return `Auth choice ${JSON.stringify(authChoice)} is no longer supported. Use ${JSON.stringify(externalDeprecatedChoice.choiceId)} instead, or run ${formatCliCommand("openclaw onboard")} to choose interactively.`; } /** Apply a selected auth choice, returning the mutated config or retry/model override signals. */ diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 175ce3b1fe5..54b98492feb 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -29,6 +29,8 @@ type DetectZaiEndpoint = (params: { modelId: string; note: string; } | null>; +type ResolveProviderInstallCatalogEntries = + typeof import("../plugins/provider-install-catalog.js").resolveProviderInstallCatalogEntries; const GOOGLE_GEMINI_DEFAULT_MODEL = "google/gemini-3.1-pro-preview"; const ZAI_CODING_GLOBAL_BASE_URL = "https://api.z.ai/api/coding/paas/v4"; @@ -36,9 +38,13 @@ const ZAI_CODING_CN_BASE_URL = "https://open.bigmodel.cn/api/coding/paas/v4"; const resolvePluginProviders = vi.hoisted(() => vi.fn<() => ProviderPlugin[]>(() => [])); const runProviderModelSelectedHook = vi.hoisted(() => vi.fn(async () => {})); +const resolveProviderInstallCatalogEntries = vi.hoisted(() => + vi.fn(() => []), +); vi.mock("../plugins/provider-install-catalog.js", () => ({ resolveProviderInstallCatalogEntry: vi.fn(() => undefined), + resolveProviderInstallCatalogEntries, })); vi.mock("./auth-choice.apply.api-providers.js", () => { @@ -691,6 +697,8 @@ describe("applyAuthChoice", () => { runProviderModelSelectedHook.mockClear(); detectZaiEndpoint.mockReset(); detectZaiEndpoint.mockResolvedValue(null); + resolveProviderInstallCatalogEntries.mockReset(); + resolveProviderInstallCatalogEntries.mockReturnValue([]); testAuthProfileStores.clear(); await lifecycle.cleanup(); }); @@ -796,6 +804,33 @@ describe("applyAuthChoice", () => { } }); + it("guides external provider auth-choice replacements before the plugin is installed", async () => { + const deprecatedChoiceSpy = vi + .spyOn(providerAuthChoices, "resolveManifestDeprecatedProviderAuthChoice") + .mockReturnValueOnce(undefined); + resolveProviderInstallCatalogEntries.mockReturnValueOnce([ + { + choiceId: "qwen-api-key", + deprecatedChoiceIds: ["modelstudio-api-key"], + }, + ] as never); + try { + await expect( + applyAuthChoice({ + authChoice: "modelstudio-api-key", + config: {}, + prompter: createPrompter({}), + runtime: createExitThrowingRuntime(), + setDefaultModel: true, + }), + ).rejects.toThrow( + 'Auth choice "modelstudio-api-key" is no longer supported. Use "qwen-api-key" instead, or run openclaw onboard to choose interactively.', + ); + } finally { + deprecatedChoiceSpy.mockRestore(); + } + }); + it("prompts and writes provider API key profiles for common providers", async () => { const scenarios: Array<{ authChoice: "huggingface-api-key"; diff --git a/src/commands/doctor/repair-sequencing.test.ts b/src/commands/doctor/repair-sequencing.test.ts index 0e02dafead6..8b56301ad9a 100644 --- a/src/commands/doctor/repair-sequencing.test.ts +++ b/src/commands/doctor/repair-sequencing.test.ts @@ -5,6 +5,7 @@ import { runDoctorRepairSequence } from "./repair-sequencing.js"; const mocks = vi.hoisted(() => ({ applyPluginAutoEnable: vi.fn(), + materializePluginAutoEnableCandidates: vi.fn(), collectActiveToolSchemaProjectionWarnings: vi.fn(), ensureAuthProfileStore: vi.fn(), evaluateStoredCredentialEligibility: vi.fn(), @@ -28,6 +29,7 @@ const mocks = vi.hoisted(() => ({ vi.mock("../../config/plugin-auto-enable.js", () => ({ applyPluginAutoEnable: mocks.applyPluginAutoEnable, + materializePluginAutoEnableCandidates: mocks.materializePluginAutoEnableCandidates, })); vi.mock("../doctor-plugin-registry.js", () => ({ @@ -212,6 +214,12 @@ describe("doctor repair sequencing", () => { config: params.config, changes: [], })); + mocks.materializePluginAutoEnableCandidates.mockImplementation( + (params: { config: OpenClawConfig }) => ({ + config: params.config, + changes: [], + }), + ); mocks.ensureAuthProfileStore.mockReturnValue({ profiles: {}, usageStats: {}, @@ -568,6 +576,50 @@ describe("doctor repair sequencing", () => { ]); }); + it("explicitly enables plugins repaired from env-only configuration", async () => { + mocks.repairMissingConfiguredPluginInstalls.mockResolvedValueOnce({ + changes: ['Installed missing configured plugin "exa" from @openclaw/exa-plugin.'], + warnings: [], + repairedPluginIds: ["exa"], + }); + mocks.materializePluginAutoEnableCandidates.mockImplementationOnce( + (params: { config: OpenClawConfig }) => ({ + config: { + ...params.config, + plugins: { + ...params.config.plugins, + entries: { + ...params.config.plugins?.entries, + exa: { enabled: true }, + }, + }, + }, + changes: ["exa installed for existing configuration, enabled automatically."], + }), + ); + + const result = await runDoctorRepairSequence({ + state: { + cfg: {} as OpenClawConfig, + candidate: {} as OpenClawConfig, + pendingChanges: false, + fixHints: [], + }, + doctorFixCommand: "openclaw doctor --fix", + }); + + expect(mocks.materializePluginAutoEnableCandidates).toHaveBeenCalledWith({ + config: {}, + env: process.env, + candidates: [{ pluginId: "exa", kind: "configured-plugin-repaired" }], + }); + expect(result.state.candidate.plugins?.entries?.exa).toEqual({ enabled: true }); + expect(result.changeNotes).toStrictEqual([ + 'Installed missing configured plugin "exa" from @openclaw/exa-plugin.', + "exa installed for existing configuration, enabled automatically.", + ]); + }); + it("moves legacy Codex routes to canonical OpenAI before missing plugin install repair", async () => { mocks.repairMissingConfiguredPluginInstalls.mockImplementationOnce( async (params: { cfg: OpenClawConfig }) => { diff --git a/src/commands/doctor/repair-sequencing.ts b/src/commands/doctor/repair-sequencing.ts index 6d01fc59f7e..7234b7751a2 100644 --- a/src/commands/doctor/repair-sequencing.ts +++ b/src/commands/doctor/repair-sequencing.ts @@ -1,6 +1,9 @@ // Doctor repair sequence coordinator for config, auth, plugin, and warning repairs. import { sanitizeForLog } from "../../../packages/terminal-core/src/ansi.js"; -import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js"; +import { + applyPluginAutoEnable, + materializePluginAutoEnableCandidates, +} from "../../config/plugin-auto-enable.js"; import { collectOpenAICodexAuthProfileStoreIdMap, maybeMigrateAuthProfileJsonStoresToSqlite, @@ -123,6 +126,19 @@ export async function runDoctorRepairSequence(params: { if (missingConfiguredPluginInstallRepair.changes.length > 0) { changeNotes.push(sanitizeLines(missingConfiguredPluginInstallRepair.changes)); applyMutation(applyPluginAutoEnable({ config: state.candidate, env })); + const repairedPluginIds = missingConfiguredPluginInstallRepair.repairedPluginIds ?? []; + if (repairedPluginIds.length > 0) { + applyMutation( + materializePluginAutoEnableCandidates({ + config: state.candidate, + env, + candidates: repairedPluginIds.map((pluginId) => ({ + pluginId, + kind: "configured-plugin-repaired" as const, + })), + }), + ); + } } if (missingConfiguredPluginInstallRepair.warnings.length > 0) { warningNotes.push(sanitizeLines(missingConfiguredPluginInstallRepair.warnings)); diff --git a/src/commands/doctor/shared/configured-provider-plugin-installs.ts b/src/commands/doctor/shared/configured-provider-plugin-installs.ts new file mode 100644 index 00000000000..3b6068f7ce5 --- /dev/null +++ b/src/commands/doctor/shared/configured-provider-plugin-installs.ts @@ -0,0 +1,119 @@ +// Resolves official provider plugins implied by configured auth and model selections. +import { collectConfiguredModelRefs } from "@openclaw/model-catalog-core/configured-model-refs"; +import { normalizeNullableString as normalizeId } from "@openclaw/normalization-core/string-coerce"; +import type { OpenClawConfig } from "../../../config/types.openclaw.js"; +import { + resolveOfficialExternalProviderContractPluginIds, + resolveOfficialExternalProviderPluginIds, + resolveOfficialExternalProviderPluginIdsForEnv, +} from "../../../plugins/official-external-plugin-catalog.js"; +import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js"; +import { asObjectRecord } from "./object.js"; + +function collectConfiguredProviderIds(cfg: OpenClawConfig): Set { + const ids = new Set(); + const add = (value: unknown) => { + const id = normalizeId(value); + if (id) { + ids.add(id.toLowerCase()); + } + }; + for (const profile of Object.values(asObjectRecord(cfg.auth?.profiles) ?? {})) { + add(asObjectRecord(profile)?.provider); + } + for (const providerId of Object.keys(asObjectRecord(cfg.models?.providers) ?? {})) { + add(providerId); + } + const modelByChannel = asObjectRecord(cfg.channels?.modelByChannel); + for (const [providerId, channelMap] of Object.entries(modelByChannel ?? {})) { + add(providerId); + for (const modelRef of Object.values(asObjectRecord(channelMap) ?? {})) { + if (typeof modelRef !== "string") { + continue; + } + const slash = modelRef.indexOf("/"); + if (slash > 0) { + add(modelRef.slice(0, slash)); + } + } + } + for (const { value } of collectConfiguredModelRefs(cfg, { + includeChannelModelOverrides: false, + })) { + const slash = value.indexOf("/"); + if (slash > 0) { + add(value.slice(0, slash)); + } + } + return ids; +} + +function collectConfiguredMediaProviderIds(cfg: OpenClawConfig): Set { + const ids = new Set(); + const add = (value: unknown) => { + const id = normalizeId(value); + if (id) { + ids.add(id.toLowerCase()); + } + }; + const addModels = (value: unknown) => { + if (!Array.isArray(value)) { + return; + } + for (const model of value) { + add(asObjectRecord(model)?.provider); + } + }; + const media = cfg.tools?.media; + addModels(media?.models); + addModels(media?.image?.models); + addModels(media?.audio?.models); + addModels(media?.video?.models); + return ids; +} + +/** Lists external provider plugins implied by configured auth profiles and model refs. */ +export function collectConfiguredProviderPluginIds(params: { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; +}): string[] { + const configuredProviderIds = collectConfiguredProviderIds(params.cfg); + const configuredMediaProviderIds = collectConfiguredMediaProviderIds(params.cfg); + const selectedProviderIds = new Set([...configuredProviderIds, ...configuredMediaProviderIds]); + const pluginIds = new Set( + resolveOfficialExternalProviderPluginIds({ + providerIds: selectedProviderIds, + }), + ); + for (const pluginId of resolveOfficialExternalProviderPluginIdsForEnv( + params.env ?? process.env, + )) { + pluginIds.add(pluginId); + } + for (const pluginId of resolveOfficialExternalProviderContractPluginIds({ + contract: "mediaUnderstandingProviders", + providerIds: configuredMediaProviderIds, + })) { + pluginIds.add(pluginId); + } + for (const pluginId of resolveOfficialExternalProviderContractPluginIds({ + contract: "speechProviders", + providerIds: configuredProviderIds, + })) { + pluginIds.add(pluginId); + } + for (const entry of resolveProviderInstallCatalogEntries({ + config: params.cfg, + env: params.env, + includeUntrustedWorkspacePlugins: false, + })) { + if ( + [entry.providerId, ...(entry.providerAliases ?? [])].some((providerId) => + selectedProviderIds.has(providerId.toLowerCase()), + ) + ) { + pluginIds.add(entry.pluginId); + } + } + return [...pluginIds].toSorted((left, right) => left.localeCompare(right)); +} diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.test.ts b/src/commands/doctor/shared/missing-configured-plugin-install.test.ts index f3040e37fd0..f1e62f56f63 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.test.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.test.ts @@ -65,6 +65,10 @@ const mocks = vi.hoisted(() => ({ resolveOfficialExternalPluginLabel: vi.fn( (entry: { label?: string; id?: string }) => entry.label ?? entry.id ?? "plugin", ), + resolveOfficialExternalProviderContractPluginIds: vi.fn(), + resolveOfficialExternalProviderPluginIds: vi.fn(), + resolveOfficialExternalProviderPluginIdsForEnv: vi.fn(), + resolveOfficialExternalWebProviderContractPluginIdsForEnv: vi.fn(), resolveDefaultPluginExtensionsDir: vi.fn(() => "/tmp/openclaw-plugins"), resolveDefaultPluginNpmDir: vi.fn(() => "/tmp/openclaw-npm"), resolvePluginNpmPackageDir: vi.fn( @@ -160,6 +164,13 @@ vi.mock("../../../plugins/official-external-plugin-catalog.js", () => ({ resolveOfficialExternalPluginId: mocks.resolveOfficialExternalPluginId, resolveOfficialExternalPluginInstall: mocks.resolveOfficialExternalPluginInstall, resolveOfficialExternalPluginLabel: mocks.resolveOfficialExternalPluginLabel, + resolveOfficialExternalProviderContractPluginIds: + mocks.resolveOfficialExternalProviderContractPluginIds, + resolveOfficialExternalProviderPluginIds: mocks.resolveOfficialExternalProviderPluginIds, + resolveOfficialExternalProviderPluginIdsForEnv: + mocks.resolveOfficialExternalProviderPluginIdsForEnv, + resolveOfficialExternalWebProviderContractPluginIdsForEnv: + mocks.resolveOfficialExternalWebProviderContractPluginIdsForEnv, })); vi.mock("../../../plugins/provider-install-catalog.js", () => ({ @@ -188,6 +199,77 @@ describe("repairMissingConfiguredPluginInstalls", () => { mocks.resolveDefaultPluginExtensionsDir.mockReturnValue("/tmp/openclaw-plugins"); mocks.resolveDefaultPluginNpmDir.mockReturnValue("/tmp/openclaw-npm"); mocks.resolveProviderInstallCatalogEntries.mockReturnValue([]); + mocks.resolveOfficialExternalProviderPluginIdsForEnv.mockReturnValue([]); + mocks.resolveOfficialExternalWebProviderContractPluginIdsForEnv.mockReturnValue([]); + mocks.resolveOfficialExternalProviderContractPluginIds.mockImplementation( + ({ contract, providerIds }: { contract: string; providerIds: ReadonlySet }) => { + const configuredProviderIds = new Set( + [...providerIds].map((providerId) => providerId.trim().toLowerCase()), + ); + const entries = mocks.listOfficialExternalPluginCatalogEntries.getMockImplementation()?.(); + if (!Array.isArray(entries)) { + return []; + } + return entries.flatMap((entry) => { + if (!entry || typeof entry !== "object") { + return []; + } + const candidate = entry as { + id?: string; + openclaw?: { + plugin?: { id?: string }; + contracts?: Record; + }; + }; + const pluginId = candidate.openclaw?.plugin?.id ?? candidate.id; + const ownedProviderIds = candidate.openclaw?.contracts?.[contract]; + if ( + !pluginId || + !Array.isArray(ownedProviderIds) || + !ownedProviderIds.some( + (providerId) => + typeof providerId === "string" && + configuredProviderIds.has(providerId.trim().toLowerCase()), + ) + ) { + return []; + } + return [pluginId]; + }); + }, + ); + mocks.resolveOfficialExternalProviderPluginIds.mockImplementation( + ({ providerIds }: { providerIds: ReadonlySet }) => { + const configuredProviderIds = new Set( + [...providerIds].map((providerId) => providerId.trim().toLowerCase()), + ); + const entries = mocks.listOfficialExternalPluginCatalogEntries.getMockImplementation()?.(); + if (!Array.isArray(entries)) { + return []; + } + return entries.flatMap((entry) => { + if (!entry || typeof entry !== "object") { + return []; + } + const candidate = entry as { + id?: string; + openclaw?: { + plugin?: { id?: string }; + providers?: Array<{ id?: string; aliases?: string[] }>; + }; + }; + const pluginId = candidate.openclaw?.plugin?.id ?? candidate.id; + const ownsConfiguredProvider = candidate.openclaw?.providers?.some((provider) => + [provider.id, ...(provider.aliases ?? [])].some( + (providerId) => + typeof providerId === "string" && + configuredProviderIds.has(providerId.trim().toLowerCase()), + ), + ); + return pluginId && ownsConfiguredProvider ? [pluginId] : []; + }); + }, + ); mocks.installPluginFromClawHub.mockResolvedValue({ ok: true, pluginId: "matrix", @@ -2920,6 +3002,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(result).toEqual({ changes: ['Repaired missing configured plugin "discord".'], warnings: [], + repairedPluginIds: ["discord"], records: { discord: { source: "npm", @@ -3116,6 +3199,7 @@ describe("repairMissingConfiguredPluginInstalls", () => { `Installed missing configured plugin "brave" from ${expectedNpmInstallSpec("@openclaw/brave-plugin")}.`, ], warnings: [], + repairedPluginIds: ["brave"], records: persistedRecords, }); }); @@ -3517,6 +3601,346 @@ describe("repairMissingConfiguredPluginInstalls", () => { ]); }); + it("installs configured external speech and web-fetch plugins from selected providers", async () => { + const packages = [ + ["firecrawl", "@openclaw/firecrawl-plugin"], + ["gradium", "@openclaw/gradium-speech"], + ["inworld", "@openclaw/inworld-speech"], + ] as const; + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue( + packages.map(([id, npmSpec]) => ({ + id, + label: id, + install: { + npmSpec, + defaultChoice: "npm", + }, + })), + ); + mocks.resolveOfficialExternalProviderContractPluginIds.mockImplementation( + ({ contract }: { contract: string }) => { + if (contract === "webFetchProviders") { + return ["firecrawl"]; + } + if (contract === "speechProviders") { + return ["gradium", "inworld"]; + } + return []; + }, + ); + for (const [pluginId, npmSpec] of packages) { + mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ + ok: true, + pluginId, + targetDir: `/tmp/openclaw-plugins/${pluginId}`, + version: "2026.6.8", + npmResolution: { + name: npmSpec, + version: "2026.6.8", + resolvedSpec: `${npmSpec}@2026.6.8`, + }, + }); + } + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: { + messages: { + tts: { + provider: "gradium", + providers: { + inworld: {}, + }, + }, + }, + tools: { + web: { + fetch: { + provider: "firecrawl", + }, + }, + }, + }, + env: {}, + }); + + expect( + mocks.installPluginFromNpmSpec.mock.calls.map( + ([params]) => (params as { expectedPluginId?: string }).expectedPluginId, + ), + ).toEqual(["firecrawl", "gradium", "inworld"]); + expect(result.changes).toEqual( + packages.map( + ([pluginId, npmSpec]) => + `Installed missing configured plugin "${pluginId}" from ${expectedNpmInstallSpec(npmSpec)}.`, + ), + ); + }); + + it("installs a configured external model provider without an auth choice", async () => { + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "groq", + label: "Groq", + install: { + npmSpec: "@openclaw/groq-provider", + defaultChoice: "npm", + }, + openclaw: { + plugin: { id: "groq", label: "Groq" }, + providers: [{ id: "groq" }], + }, + }, + ]); + mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ + ok: true, + pluginId: "groq", + targetDir: "/tmp/openclaw-plugins/groq", + version: "2026.6.8", + npmResolution: { + name: "@openclaw/groq-provider", + version: "2026.6.8", + resolvedSpec: "@openclaw/groq-provider@2026.6.8", + }, + }); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: { + agents: { + defaults: { + model: "groq/llama-3.3-70b-versatile", + }, + }, + }, + env: {}, + }); + + expectRecordFields(mockCallArg(mocks.installPluginFromNpmSpec), { + spec: expectedNpmInstallSpec("@openclaw/groq-provider"), + expectedPluginId: "groq", + trustedSourceLinkedOfficialInstall: true, + }); + expect(result.changes).toEqual([ + `Installed missing configured plugin "groq" from ${expectedNpmInstallSpec("@openclaw/groq-provider")}.`, + ]); + }); + + it("installs an external media-understanding provider selected only by media config", async () => { + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "groq", + label: "Groq", + install: { + npmSpec: "@openclaw/groq-provider", + defaultChoice: "npm", + }, + openclaw: { + plugin: { id: "groq", label: "Groq" }, + contracts: { mediaUnderstandingProviders: ["groq"] }, + }, + }, + ]); + mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ + ok: true, + pluginId: "groq", + targetDir: "/tmp/openclaw-plugins/groq", + version: "2026.6.8", + npmResolution: { + name: "@openclaw/groq-provider", + version: "2026.6.8", + resolvedSpec: "@openclaw/groq-provider@2026.6.8", + }, + }); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: { + tools: { + media: { + audio: { + models: [{ provider: "groq", model: "whisper-large-v3-turbo" }], + }, + }, + }, + }, + env: {}, + }); + + expectRecordFields(mockCallArg(mocks.installPluginFromNpmSpec), { + spec: expectedNpmInstallSpec("@openclaw/groq-provider"), + expectedPluginId: "groq", + trustedSourceLinkedOfficialInstall: true, + }); + expect(result.changes).toEqual([ + `Installed missing configured plugin "groq" from ${expectedNpmInstallSpec("@openclaw/groq-provider")}.`, + ]); + }); + + it("installs an external speech provider selected only by voiceModel", async () => { + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "gradium", + label: "Gradium", + install: { + npmSpec: "@openclaw/gradium-speech", + defaultChoice: "npm", + }, + openclaw: { + plugin: { id: "gradium", label: "Gradium" }, + contracts: { speechProviders: ["gradium"] }, + }, + }, + ]); + mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ + ok: true, + pluginId: "gradium", + targetDir: "/tmp/openclaw-plugins/gradium", + version: "2026.6.8", + npmResolution: { + name: "@openclaw/gradium-speech", + version: "2026.6.8", + resolvedSpec: "@openclaw/gradium-speech@2026.6.8", + }, + }); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: { + agents: { + defaults: { + voiceModel: { primary: "gradium/tts-default" }, + }, + }, + }, + env: {}, + }); + + expectRecordFields(mockCallArg(mocks.installPluginFromNpmSpec), { + spec: expectedNpmInstallSpec("@openclaw/gradium-speech"), + expectedPluginId: "gradium", + trustedSourceLinkedOfficialInstall: true, + }); + expect(result.changes).toEqual([ + `Installed missing configured plugin "gradium" from ${expectedNpmInstallSpec("@openclaw/gradium-speech")}.`, + ]); + }); + + it("installs env-only web provider plugins before auto-detection", async () => { + const packages = [ + ["exa", "@openclaw/exa-plugin", "EXA_API_KEY"], + ["firecrawl", "@openclaw/firecrawl-plugin", "FIRECRAWL_API_KEY"], + ] as const; + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue( + packages.map(([id, npmSpec, envVar]) => ({ + id, + label: id, + install: { + npmSpec, + defaultChoice: "npm", + }, + openclaw: { + plugin: { id, label: id }, + webSearchProviders: [ + { + id, + label: id, + hint: `${id} search`, + envVars: [envVar], + placeholder: `${id}-key`, + signupUrl: `https://example.com/${id}`, + }, + ], + }, + })), + ); + for (const [pluginId, npmSpec] of packages) { + mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ + ok: true, + pluginId, + targetDir: `/tmp/openclaw-plugins/${pluginId}`, + version: "2026.6.8", + npmResolution: { + name: npmSpec, + version: "2026.6.8", + resolvedSpec: `${npmSpec}@2026.6.8`, + }, + }); + } + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const result = await repairMissingConfiguredPluginInstalls({ + cfg: {}, + env: { + EXA_API_KEY: "exa-key", + FIRECRAWL_API_KEY: "firecrawl-key", + }, + }); + + expect( + mocks.installPluginFromNpmSpec.mock.calls.map( + ([params]) => (params as { expectedPluginId?: string }).expectedPluginId, + ), + ).toEqual(["exa", "firecrawl"]); + expect(result.changes).toEqual( + packages.map( + ([pluginId, npmSpec]) => + `Installed missing configured plugin "${pluginId}" from ${expectedNpmInstallSpec(npmSpec)}.`, + ), + ); + }); + + it("installs env-only provider plugins before model discovery", async () => { + mocks.resolveOfficialExternalProviderPluginIdsForEnv.mockReturnValue(["groq"]); + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "groq", + label: "Groq", + install: { + npmSpec: "@openclaw/groq-provider", + defaultChoice: "npm", + }, + openclaw: { + plugin: { id: "groq", label: "Groq" }, + }, + }, + ]); + mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ + ok: true, + pluginId: "groq", + targetDir: "/tmp/openclaw-plugins/groq", + version: "2026.6.8", + npmResolution: { + name: "@openclaw/groq-provider", + version: "2026.6.8", + resolvedSpec: "@openclaw/groq-provider@2026.6.8", + }, + }); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const env = { GROQ_API_KEY: "groq-key" }; + const result = await repairMissingConfiguredPluginInstalls({ + cfg: {}, + env, + }); + + expect(mocks.resolveOfficialExternalProviderPluginIdsForEnv).toHaveBeenCalledWith(env); + expectRecordFields(mockCallArg(mocks.installPluginFromNpmSpec), { + spec: expectedNpmInstallSpec("@openclaw/groq-provider"), + expectedPluginId: "groq", + trustedSourceLinkedOfficialInstall: true, + }); + expect(result.changes).toEqual([ + `Installed missing configured plugin "groq" from ${expectedNpmInstallSpec("@openclaw/groq-provider")}.`, + ]); + }); + it("installs configured external web search plugins from beta on the beta channel", async () => { mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ { @@ -3665,6 +4089,63 @@ describe("repairMissingConfiguredPluginInstalls", () => { expect(result.warnings).toStrictEqual([]); }); + it("installs Firecrawl for env-only web fetch when search is disabled", async () => { + mocks.resolveOfficialExternalWebProviderContractPluginIdsForEnv.mockReturnValue(["firecrawl"]); + mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ + { + id: "firecrawl", + label: "Firecrawl", + install: { + npmSpec: "@openclaw/firecrawl-plugin", + defaultChoice: "npm", + }, + openclaw: { + plugin: { id: "firecrawl", label: "Firecrawl" }, + }, + }, + ]); + mocks.installPluginFromNpmSpec.mockResolvedValueOnce({ + ok: true, + pluginId: "firecrawl", + targetDir: "/tmp/openclaw-plugins/firecrawl", + version: "2026.6.8", + npmResolution: { + name: "@openclaw/firecrawl-plugin", + version: "2026.6.8", + resolvedSpec: "@openclaw/firecrawl-plugin@2026.6.8", + }, + }); + + const { repairMissingConfiguredPluginInstalls } = + await import("./missing-configured-plugin-install.js"); + const env = { FIRECRAWL_API_KEY: "firecrawl-key" }; + const result = await repairMissingConfiguredPluginInstalls({ + cfg: { + tools: { + web: { + search: { + enabled: false, + }, + }, + }, + }, + env, + }); + + expect(mocks.resolveOfficialExternalWebProviderContractPluginIdsForEnv).toHaveBeenCalledWith({ + contract: "webFetchProviders", + env, + }); + expectRecordFields(mockCallArg(mocks.installPluginFromNpmSpec), { + spec: expectedNpmInstallSpec("@openclaw/firecrawl-plugin"), + expectedPluginId: "firecrawl", + trustedSourceLinkedOfficialInstall: true, + }); + expect(result.changes).toEqual([ + `Installed missing configured plugin "firecrawl" from ${expectedNpmInstallSpec("@openclaw/firecrawl-plugin")}.`, + ]); + }); + it("does not install a configured external web search plugin when search is disabled", async () => { mocks.listOfficialExternalPluginCatalogEntries.mockReturnValue([ { @@ -3720,7 +4201,9 @@ describe("repairMissingConfiguredPluginInstalls", () => { }, }, }, - env: {}, + env: { + BRAVE_API_KEY: "brave-key", + }, }); expect(mocks.installPluginFromClawHub).not.toHaveBeenCalled(); diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.ts b/src/commands/doctor/shared/missing-configured-plugin-install.ts index 762d634cfef..a1ead9ae6ec 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.ts @@ -25,6 +25,7 @@ import { resolveConfiguredChannelPresencePolicy } from "../../../plugins/channel import { buildClawHubPluginInstallRecordFields } from "../../../plugins/clawhub-install-records.js"; import { CLAWHUB_INSTALL_ERROR_CODE, installPluginFromClawHub } from "../../../plugins/clawhub.js"; import { collectConfiguredMemoryEmbeddingProviderIds } from "../../../plugins/gateway-startup-plugin-ids.js"; +import { collectConfiguredSpeechProviderIds } from "../../../plugins/gateway-startup-speech-providers.js"; import { resolveClawHubInstallSpecsForUpdateChannel, resolveNpmInstallSpecsForUpdateChannel, @@ -49,6 +50,8 @@ import type { PluginPackageInstall } from "../../../plugins/manifest.js"; import { listOfficialExternalPluginCatalogEntries, getOfficialExternalPluginCatalogManifest, + resolveOfficialExternalProviderContractPluginIds, + resolveOfficialExternalWebProviderContractPluginIdsForEnv, resolveOfficialExternalPluginId, resolveOfficialExternalPluginInstall, resolveOfficialExternalPluginLabel, @@ -56,9 +59,13 @@ import { import type { PluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.types.js"; import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js"; import { updateNpmInstalledPlugins } from "../../../plugins/update.js"; -import { resolveWebSearchInstallCatalogEntry } from "../../../plugins/web-search-install-catalog.js"; +import { + resolveWebSearchInstallCatalogEntriesForEnv, + resolveWebSearchInstallCatalogEntry, +} from "../../../plugins/web-search-install-catalog.js"; import { resolveUserPath } from "../../../utils.js"; import { VERSION } from "../../../version.js"; +import { collectConfiguredProviderPluginIds } from "./configured-provider-plugin-installs.js"; import { collectConfiguredRuntimePluginIds, CONFIGURED_RUNTIME_PLUGIN_INSTALL_CANDIDATES, @@ -143,28 +150,59 @@ function addConfiguredMemoryEmbeddingProviderPluginIds( if (configuredProviderIds.size === 0) { return; } - for (const entry of listOfficialExternalPluginCatalogEntries()) { - const manifest = getOfficialExternalPluginCatalogManifest(entry); - const pluginId = resolveOfficialExternalPluginId(entry); - if (!pluginId) { - continue; - } - const ownedProviderIds = [ - ...(manifest?.contracts?.embeddingProviders ?? []), - ...(manifest?.contracts?.memoryEmbeddingProviders ?? []), - ]; - if ( - ownedProviderIds.some((providerId) => { - const normalized = normalizeOptionalLowercaseString(providerId); - return normalized ? configuredProviderIds.has(normalized) : false; - }) - ) { + for (const contract of ["embeddingProviders", "memoryEmbeddingProviders"] as const) { + for (const pluginId of resolveOfficialExternalProviderContractPluginIds({ + contract, + providerIds: configuredProviderIds, + })) { ids.add(pluginId); } } } -function collectConfiguredPluginIds(cfg: OpenClawConfig): Set { +function addConfiguredSpeechProviderPluginIds(ids: Set, cfg: OpenClawConfig): void { + for (const pluginId of resolveOfficialExternalProviderContractPluginIds({ + contract: "speechProviders", + providerIds: collectConfiguredSpeechProviderIds(cfg), + })) { + ids.add(pluginId); + } +} + +function addConfiguredWebFetchProviderPluginIds(ids: Set, cfg: OpenClawConfig): void { + const webFetch = cfg.tools?.web?.fetch; + if (webFetch?.enabled === false) { + return; + } + const providerId = normalizeOptionalLowercaseString(webFetch?.provider); + if (!providerId) { + return; + } + for (const pluginId of resolveOfficialExternalProviderContractPluginIds({ + contract: "webFetchProviders", + providerIds: new Set([providerId]), + })) { + ids.add(pluginId); + } +} + +function addEnvWebFetchProviderPluginIds( + ids: Set, + cfg: OpenClawConfig, + env?: NodeJS.ProcessEnv, +): void { + if (cfg.tools?.web?.fetch?.enabled === false) { + return; + } + for (const pluginId of resolveOfficialExternalWebProviderContractPluginIdsForEnv({ + contract: "webFetchProviders", + env: env ?? process.env, + })) { + ids.add(pluginId); + } +} + +function collectConfiguredPluginIds(cfg: OpenClawConfig, env?: NodeJS.ProcessEnv): Set { const ids = new Set(); const plugins = asObjectRecord(cfg.plugins); if (plugins?.enabled === false) { @@ -184,8 +222,20 @@ function collectConfiguredPluginIds(cfg: OpenClawConfig): Set { ids.add(installEntry.pluginId); } } + if (cfg.tools?.web?.search?.enabled !== false) { + // Env-only web providers are valid auto-detect inputs and need their manifest installed first. + for (const entry of resolveWebSearchInstallCatalogEntriesForEnv(env ?? process.env)) { + ids.add(entry.pluginId); + } + } addConfiguredAgentRuntimePluginIds(ids, cfg); + for (const pluginId of collectConfiguredProviderPluginIds({ cfg, env })) { + ids.add(pluginId); + } addConfiguredMemoryEmbeddingProviderPluginIds(ids, cfg); + addConfiguredSpeechProviderPluginIds(ids, cfg); + addConfiguredWebFetchProviderPluginIds(ids, cfg); + addEnvWebFetchProviderPluginIds(ids, cfg, env); return ids; } @@ -1190,6 +1240,8 @@ export type RepairMissingPluginInstallsResult = { changes: string[]; /** User-facing warnings for failed or skipped plugin install repairs. */ warnings: string[]; + /** Plugin ids successfully repaired from current configuration. */ + repairedPluginIds?: string[]; /** User-facing details for repairs explicitly deferred until post-core convergence. */ deferredRepairDetails?: string[]; /** Plugin ids whose install repair failed and should be preserved from cleanup passes. */ @@ -1222,7 +1274,7 @@ export async function repairMissingConfiguredPluginInstalls(params: { return repairMissingPluginInstalls({ cfg: params.cfg, env: params.env, - pluginIds: collectConfiguredPluginIds(params.cfg), + pluginIds: collectConfiguredPluginIds(params.cfg, params.env), channelIds: collectConfiguredChannelIds(params.cfg, params.env), blockedPluginIds: collectBlockedPluginIds(params.cfg), ...(params.baselineRecords ? { baselineRecords: params.baselineRecords } : {}), @@ -1341,6 +1393,7 @@ async function repairMissingPluginInstalls(params: { const warnings: string[] = []; const deferredRepairDetails: string[] = []; const failedPluginIds = new Set(); + const repairedPluginIds = new Set(); const deferredPluginIds = new Set(); const preferNpmInstalls = isLegacyPackageUpdateDoctorPass(env); let nextRecords = records; @@ -1421,6 +1474,7 @@ async function repairMissingPluginInstalls(params: { }); for (const outcome of updateResult.outcomes) { if (outcome.status === "updated" || outcome.status === "unchanged") { + repairedPluginIds.add(outcome.pluginId); changes.push( installedPluginIdsWithStaleVersionBoundRuntimePackages.has(outcome.pluginId) ? `Refreshed stale configured plugin "${outcome.pluginId}".` @@ -1531,6 +1585,9 @@ async function repairMissingPluginInstalls(params: { nextRecords = installed.records; changes.push(...installed.changes); warnings.push(...installed.warnings); + if (!installed.failedPluginId && installed.records[candidate.pluginId]) { + repairedPluginIds.add(candidate.pluginId); + } if (installed.failedPluginId) { failedPluginIds.add(installed.failedPluginId); } @@ -1550,6 +1607,13 @@ async function repairMissingPluginInstalls(params: { changes, warnings, ...(deferredRepairDetails.length > 0 ? { deferredRepairDetails } : {}), + ...(repairedPluginIds.size > 0 + ? { + repairedPluginIds: [...repairedPluginIds].toSorted((left, right) => + left.localeCompare(right), + ), + } + : {}), ...(failedPluginIds.size > 0 ? { failedPluginIds: [...failedPluginIds].toSorted((left, right) => diff --git a/src/commands/doctor/shared/release-configured-plugin-installs.test.ts b/src/commands/doctor/shared/release-configured-plugin-installs.test.ts index c3a20ec0d0d..1e0ee10356d 100644 --- a/src/commands/doctor/shared/release-configured-plugin-installs.test.ts +++ b/src/commands/doctor/shared/release-configured-plugin-installs.test.ts @@ -257,6 +257,155 @@ describe("configured plugin install release step", () => { expect(result.channelIds).toStrictEqual([]); }); + it("collects external speech and web-fetch plugins selected by config", async () => { + const { collectReleaseConfiguredPluginIds } = + await import("./release-configured-plugin-installs.js"); + const result = collectReleaseConfiguredPluginIds({ + cfg: { + agents: { + defaults: { + model: "groq/llama-3.3-70b-versatile", + }, + }, + messages: { + tts: { + provider: "gradium", + providers: { + inworld: {}, + }, + }, + }, + tools: { + web: { + fetch: { + provider: "firecrawl", + }, + }, + }, + }, + env: {}, + }); + + expect(result.pluginIds).toEqual(["firecrawl", "gradium", "groq", "inworld"]); + expect(result.channelIds).toStrictEqual([]); + }); + + it("collects an external media-understanding plugin selected only by media config", async () => { + const { collectReleaseConfiguredPluginIds } = + await import("./release-configured-plugin-installs.js"); + const result = collectReleaseConfiguredPluginIds({ + cfg: { + tools: { + media: { + audio: { + models: [{ provider: "groq", model: "whisper-large-v3-turbo" }], + }, + }, + }, + }, + env: {}, + }); + + expect(result.pluginIds).toEqual(["groq"]); + expect(result.channelIds).toStrictEqual([]); + }); + + it("collects an external speech plugin selected only by voiceModel", async () => { + const { collectReleaseConfiguredPluginIds } = + await import("./release-configured-plugin-installs.js"); + const result = collectReleaseConfiguredPluginIds({ + cfg: { + agents: { + defaults: { + voiceModel: { primary: "gradium/tts-default" }, + }, + }, + }, + env: {}, + }); + + expect(result.pluginIds).toEqual(["gradium"]); + expect(result.channelIds).toStrictEqual([]); + }); + + it("collects env-only web provider plugins before auto-detection", async () => { + const { collectReleaseConfiguredPluginIds } = + await import("./release-configured-plugin-installs.js"); + const result = collectReleaseConfiguredPluginIds({ + cfg: {}, + env: { + EXA_API_KEY: "exa-key", + FIRECRAWL_API_KEY: "firecrawl-key", + }, + }); + + expect(result.pluginIds).toEqual(["exa", "firecrawl"]); + expect(result.channelIds).toStrictEqual([]); + }); + + it("does not collect env-only web provider plugins when search is disabled", async () => { + const { collectReleaseConfiguredPluginIds } = + await import("./release-configured-plugin-installs.js"); + const result = collectReleaseConfiguredPluginIds({ + cfg: { + tools: { + web: { + search: { + enabled: false, + }, + fetch: { + enabled: false, + }, + }, + }, + }, + env: { + EXA_API_KEY: "exa-key", + FIRECRAWL_API_KEY: "firecrawl-key", + }, + }); + + expect(result.pluginIds).toEqual([]); + expect(result.channelIds).toStrictEqual([]); + }); + + it("collects Firecrawl for env-only web fetch when search is disabled", async () => { + const { collectReleaseConfiguredPluginIds } = + await import("./release-configured-plugin-installs.js"); + const result = collectReleaseConfiguredPluginIds({ + cfg: { + tools: { + web: { + search: { + enabled: false, + }, + }, + }, + }, + env: { + FIRECRAWL_API_KEY: "firecrawl-key", + }, + }); + + expect(result.pluginIds).toEqual(["firecrawl"]); + expect(result.channelIds).toStrictEqual([]); + }); + + it("collects env-only external provider plugins before model discovery", async () => { + const { collectReleaseConfiguredPluginIds } = + await import("./release-configured-plugin-installs.js"); + const result = collectReleaseConfiguredPluginIds({ + cfg: {}, + env: { + GROQ_API_KEY: "groq-key", + MODELSTUDIO_API_KEY: "qwen-key", + }, + }); + + expect(result.pluginIds).toEqual(["groq", "qwen"]); + expect(result.channelIds).toStrictEqual([]); + }); + it("collects provider plugins from documented external provider aliases", async () => { mocks.resolveProviderInstallCatalogEntries.mockReturnValue([ { diff --git a/src/commands/doctor/shared/release-configured-plugin-installs.ts b/src/commands/doctor/shared/release-configured-plugin-installs.ts index 0d8489fdacc..30b206b7469 100644 --- a/src/commands/doctor/shared/release-configured-plugin-installs.ts +++ b/src/commands/doctor/shared/release-configured-plugin-installs.ts @@ -1,5 +1,4 @@ // Release-era repair for configs that imply official plugin installs before install records existed. -import { collectConfiguredModelRefs } from "@openclaw/model-catalog-core/configured-model-refs"; import { normalizeNullableString as normalizeId } from "@openclaw/normalization-core/string-coerce"; import { collectConfiguredAgentHarnessRuntimes } from "../../../agents/harness-runtimes.js"; import { listPotentialConfiguredChannelPresenceSignals } from "../../../channels/config-presence.js"; @@ -12,10 +11,18 @@ import { createDeferredConfiguredPluginRepairDoctorResult, type UpdatePostInstallDoctorResult, } from "../../../infra/update-doctor-result.js"; -import { getOfficialExternalPluginCatalogEntry } from "../../../plugins/official-external-plugin-catalog.js"; -import { resolveProviderInstallCatalogEntries } from "../../../plugins/provider-install-catalog.js"; -import { resolveWebSearchInstallCatalogEntry } from "../../../plugins/web-search-install-catalog.js"; +import { collectConfiguredSpeechProviderIds } from "../../../plugins/gateway-startup-speech-providers.js"; +import { + getOfficialExternalPluginCatalogEntry, + resolveOfficialExternalProviderContractPluginIds, + resolveOfficialExternalWebProviderContractPluginIdsForEnv, +} from "../../../plugins/official-external-plugin-catalog.js"; +import { + resolveWebSearchInstallCatalogEntriesForEnv, + resolveWebSearchInstallCatalogEntry, +} from "../../../plugins/web-search-install-catalog.js"; import { VERSION } from "../../../version.js"; +import { collectConfiguredProviderPluginIds } from "./configured-provider-plugin-installs.js"; import { repairMissingPluginInstallsForIds } from "./missing-configured-plugin-install.js"; import { asObjectRecord } from "./object.js"; import { shouldDeferConfiguredPluginInstallRepair } from "./update-phase.js"; @@ -143,66 +150,6 @@ function collectConfiguredChannelIds(cfg: OpenClawConfig, env: NodeJS.ProcessEnv return [...ids].toSorted((left, right) => left.localeCompare(right)); } -function collectConfiguredProviderIds(cfg: OpenClawConfig): Set { - const ids = new Set(); - const add = (value: unknown) => { - const id = normalizeId(value); - if (id) { - ids.add(id.toLowerCase()); - } - }; - for (const profile of Object.values(asObjectRecord(cfg.auth?.profiles) ?? {})) { - add(asObjectRecord(profile)?.provider); - } - for (const providerId of Object.keys(asObjectRecord(cfg.models?.providers) ?? {})) { - add(providerId); - } - const modelByChannel = asObjectRecord(cfg.channels?.modelByChannel); - for (const [providerId, channelMap] of Object.entries(modelByChannel ?? {})) { - add(providerId); - for (const modelRef of Object.values(asObjectRecord(channelMap) ?? {})) { - if (typeof modelRef !== "string") { - continue; - } - const slash = modelRef.indexOf("/"); - if (slash > 0) { - add(modelRef.slice(0, slash)); - } - } - } - for (const { value } of collectConfiguredModelRefs(cfg, { - includeChannelModelOverrides: false, - })) { - const slash = value.indexOf("/"); - if (slash > 0) { - add(value.slice(0, slash)); - } - } - return ids; -} - -function collectProviderPluginIds(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] { - const configuredProviders = collectConfiguredProviderIds(cfg); - if (configuredProviders.size === 0) { - return []; - } - const ids = new Set(); - for (const entry of resolveProviderInstallCatalogEntries({ - config: cfg, - env, - includeUntrustedWorkspacePlugins: false, - })) { - if ( - [entry.providerId, ...(entry.providerAliases ?? [])].some((providerId) => - configuredProviders.has(providerId.toLowerCase()), - ) - ) { - ids.add(entry.pluginId); - } - } - return [...ids].toSorted((left, right) => left.localeCompare(right)); -} - function collectAgentHarnessRuntimePluginIds( cfg: OpenClawConfig, _env: NodeJS.ProcessEnv, @@ -214,6 +161,9 @@ function collectAgentHarnessRuntimePluginIds( } function collectWebSearchPluginIds(cfg: OpenClawConfig): string[] { + if (cfg.tools?.web?.search?.enabled === false) { + return []; + } const providerId = cfg.tools?.web?.search?.provider; if (typeof providerId !== "string") { return []; @@ -222,6 +172,45 @@ function collectWebSearchPluginIds(cfg: OpenClawConfig): string[] { return entry?.pluginId ? [entry.pluginId] : []; } +function collectEnvWebSearchPluginIds(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] { + if (cfg.tools?.web?.search?.enabled === false) { + return []; + } + return resolveWebSearchInstallCatalogEntriesForEnv(env).map((entry) => entry.pluginId); +} + +function collectWebFetchPluginIds(cfg: OpenClawConfig): string[] { + const webFetch = cfg.tools?.web?.fetch; + if (webFetch?.enabled === false) { + return []; + } + const providerId = normalizeId(webFetch?.provider)?.toLowerCase(); + if (!providerId) { + return []; + } + return resolveOfficialExternalProviderContractPluginIds({ + contract: "webFetchProviders", + providerIds: new Set([providerId]), + }); +} + +function collectEnvWebFetchPluginIds(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] { + if (cfg.tools?.web?.fetch?.enabled === false) { + return []; + } + return resolveOfficialExternalWebProviderContractPluginIdsForEnv({ + contract: "webFetchProviders", + env, + }); +} + +function collectSpeechPluginIds(cfg: OpenClawConfig): string[] { + return resolveOfficialExternalProviderContractPluginIds({ + contract: "speechProviders", + providerIds: collectConfiguredSpeechProviderIds(cfg), + }); +} + function collectAcpRuntimePluginIds(cfg: OpenClawConfig): string[] { const acp = asObjectRecord(cfg.acp); if (!acp) { @@ -307,7 +296,7 @@ export function collectReleaseConfiguredPluginIds(params: { for (const pluginId of collectSlotPluginIds(params.cfg)) { addEligiblePluginId(params.cfg, pluginIds, pluginId); } - for (const pluginId of collectProviderPluginIds(params.cfg, env)) { + for (const pluginId of collectConfiguredProviderPluginIds({ cfg: params.cfg, env })) { addEligiblePluginId(params.cfg, pluginIds, pluginId); } for (const pluginId of collectAgentHarnessRuntimePluginIds(params.cfg, env)) { @@ -316,6 +305,18 @@ export function collectReleaseConfiguredPluginIds(params: { for (const pluginId of collectWebSearchPluginIds(params.cfg)) { addEligiblePluginId(params.cfg, pluginIds, pluginId); } + for (const pluginId of collectEnvWebSearchPluginIds(params.cfg, env)) { + addEligiblePluginId(params.cfg, pluginIds, pluginId); + } + for (const pluginId of collectWebFetchPluginIds(params.cfg)) { + addEligiblePluginId(params.cfg, pluginIds, pluginId); + } + for (const pluginId of collectEnvWebFetchPluginIds(params.cfg, env)) { + addEligiblePluginId(params.cfg, pluginIds, pluginId); + } + for (const pluginId of collectSpeechPluginIds(params.cfg)) { + addEligiblePluginId(params.cfg, pluginIds, pluginId); + } for (const pluginId of collectAcpRuntimePluginIds(params.cfg)) { addEligiblePluginId(params.cfg, pluginIds, pluginId); } diff --git a/src/commands/onboard-non-interactive/local/auth-choice-inference.test.ts b/src/commands/onboard-non-interactive/local/auth-choice-inference.test.ts index e4234b66dc7..9eefc19a54f 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice-inference.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice-inference.test.ts @@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OnboardOptions } from "../../onboard-types.js"; import { inferAuthChoiceFromFlags } from "./auth-choice-inference.js"; -const resolveManifestProviderOnboardAuthFlags = vi.hoisted(() => +const resolveProviderOnboardAuthFlags = vi.hoisted(() => vi.fn< () => ReadonlyArray<{ optionKey: string; @@ -14,17 +14,17 @@ const resolveManifestProviderOnboardAuthFlags = vi.hoisted(() => ); vi.mock("../../../plugins/provider-auth-choices.js", () => ({ - resolveManifestProviderOnboardAuthFlags, + resolveProviderOnboardAuthFlags, })); describe("inferAuthChoiceFromFlags", () => { beforeEach(() => { - resolveManifestProviderOnboardAuthFlags.mockReset(); - resolveManifestProviderOnboardAuthFlags.mockReturnValue([]); + resolveProviderOnboardAuthFlags.mockReset(); + resolveProviderOnboardAuthFlags.mockReturnValue([]); }); it("infers plugin-owned auth choices from manifest option keys", () => { - resolveManifestProviderOnboardAuthFlags.mockReturnValue([ + resolveProviderOnboardAuthFlags.mockReturnValue([ { optionKey: "pluginOwnedApiKey", authChoice: "plugin-api-key", diff --git a/src/commands/onboard-non-interactive/local/auth-choice-inference.ts b/src/commands/onboard-non-interactive/local/auth-choice-inference.ts index e2e5e0a6aca..8d8c8e046ae 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice-inference.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice-inference.ts @@ -6,7 +6,7 @@ */ import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; -import { resolveManifestProviderOnboardAuthFlags } from "../../../plugins/provider-auth-choices.js"; +import { resolveProviderOnboardAuthFlags } from "../../../plugins/provider-auth-choices.js"; import { CORE_ONBOARD_AUTH_FLAGS } from "../../onboard-core-auth-flags.js"; import type { AuthChoice, OnboardOptions } from "../../onboard-types.js"; @@ -39,7 +39,7 @@ export function inferAuthChoiceFromFlags( ...CORE_ONBOARD_AUTH_FLAGS, // Only trusted manifests can influence implicit auth choice; untrusted // workspace plugins require the user to choose them explicitly. - ...resolveManifestProviderOnboardAuthFlags({ + ...resolveProviderOnboardAuthFlags({ config: params?.config, workspaceDir: params?.workspaceDir, env: params?.env, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts index cbb54620a74..18f7aafc5e5 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts @@ -42,6 +42,14 @@ const resolveManifestProviderAuthChoice = vi.hoisted(() => vi.fn(() => undefined vi.mock("../../../plugins/provider-auth-choices.js", () => ({ resolveManifestProviderAuthChoice, })); +const resolveProviderInstallCatalogEntry = vi.hoisted(() => vi.fn(() => undefined)); +vi.mock("../../../plugins/provider-install-catalog.js", () => ({ + resolveProviderInstallCatalogEntry, +})); +const ensureOnboardingPluginInstalled = vi.hoisted(() => vi.fn()); +vi.mock("../../onboarding-plugin-install.js", () => ({ + ensureOnboardingPluginInstalled, +})); const resolveOwningPluginIdsForProvider = vi.hoisted(() => vi.fn(() => undefined)); const resolveProviderPluginChoice = vi.hoisted(() => vi.fn()); @@ -58,6 +66,8 @@ beforeEach(() => { vi.clearAllMocks(); resolvePreferredProviderForAuthChoice.mockResolvedValue(undefined); resolveManifestProviderAuthChoice.mockReturnValue(undefined); + resolveProviderInstallCatalogEntry.mockReturnValue(undefined); + ensureOnboardingPluginInstalled.mockResolvedValue(undefined); resolveOwningPluginIdsForProvider.mockReturnValue(undefined as never); resolveProviderPluginChoice.mockReturnValue(undefined); resolvePluginProviders.mockReturnValue([] as never); @@ -78,6 +88,7 @@ function createRuntime() { return { error: vi.fn(), exit: vi.fn(), + log: vi.fn(), }; } @@ -147,6 +158,91 @@ describe("applyNonInteractivePluginProviderChoice", () => { expect(result).toEqual({ plugins: { allow: ["vllm"] } }); }); + it("installs an official catalog provider before applying a cold auth choice", async () => { + const runtime = createRuntime(); + const runNonInteractive = vi.fn(async ({ config }: { config: OpenClawConfig }) => ({ + ...config, + agents: { + defaults: { + model: { primary: "groq/llama-3.3-70b-versatile" }, + }, + }, + })); + const provider = { id: "groq", pluginId: "groq", label: "Groq" }; + resolveProviderInstallCatalogEntry.mockReturnValue({ + pluginId: "groq", + label: "Groq", + origin: "bundled", + install: { + npmSpec: "@openclaw/groq-provider", + defaultChoice: "npm", + }, + }); + ensureOnboardingPluginInstalled.mockResolvedValue({ + cfg: { + plugins: { + entries: { + groq: { enabled: true }, + }, + }, + }, + installed: true, + pluginId: "groq", + status: "installed", + }); + resolvePluginProviders.mockReturnValue([provider] as never); + resolveProviderPluginChoice.mockReturnValueOnce(undefined).mockReturnValue({ + provider, + method: { runNonInteractive }, + }); + + const result = await applyNonInteractivePluginProviderChoice({ + nextConfig: { agents: { defaults: {} } } as OpenClawConfig, + authChoice: "groq-api-key", + opts: { groqApiKey: "groq-key" } as never, + runtime: runtime as never, + baseConfig: { agents: { defaults: {} } } as OpenClawConfig, + resolveApiKey: vi.fn(), + toApiKeyCredential: vi.fn(), + }); + + expect(resolveProviderInstallCatalogEntry).toHaveBeenCalledWith( + "groq-api-key", + expect.objectContaining({ + includeUntrustedWorkspacePlugins: false, + }), + ); + expect(ensureOnboardingPluginInstalled).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: { agents: { defaults: {} } }, + entry: { + pluginId: "groq", + label: "Groq", + install: { + npmSpec: "@openclaw/groq-provider", + defaultChoice: "npm", + }, + trustedSourceLinkedOfficialInstall: true, + }, + promptInstall: false, + }), + ); + expect(resolvePluginProviders).toHaveBeenCalledTimes(2); + expect(runNonInteractive).toHaveBeenCalledOnce(); + expect(result).toMatchObject({ + agents: { + defaults: { + model: { primary: "groq/llama-3.3-70b-versatile" }, + }, + }, + plugins: { + entries: { + groq: { enabled: true }, + }, + }, + }); + }); + it("fails explicitly when a provider-plugin auth choice resolves to no trusted setup provider", async () => { const runtime = createRuntime(); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts index 5ed6ac0e69c..c6e1f1ae4fc 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts @@ -16,6 +16,7 @@ import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { enablePluginInConfig } from "../../../plugins/enable.js"; import { resolvePreferredProviderForAuthChoice } from "../../../plugins/provider-auth-choice-preference.js"; import { resolveManifestProviderAuthChoice } from "../../../plugins/provider-auth-choices.js"; +import { resolveProviderInstallCatalogEntry } from "../../../plugins/provider-install-catalog.js"; import type { ProviderAuthOptionBag, ProviderNonInteractiveApiKeyCredentialParams, @@ -62,6 +63,7 @@ export async function applyNonInteractivePluginProviderChoice(params: { const agentDir = resolveAgentDir(params.nextConfig, agentId); const workspaceDir = resolveAgentWorkspaceDir(params.nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir(); + let nextConfig = params.nextConfig; const prefixedProviderId = params.authChoice.startsWith(PROVIDER_PLUGIN_CHOICE_PREFIX) ? params.authChoice.slice(PROVIDER_PLUGIN_CHOICE_PREFIX.length).split(":", 1)[0]?.trim() : undefined; @@ -69,7 +71,7 @@ export async function applyNonInteractivePluginProviderChoice(params: { prefixedProviderId || (await resolvePreferredProviderForAuthChoice({ choice: params.authChoice, - config: params.nextConfig, + config: nextConfig, workspaceDir, includeUntrustedWorkspacePlugins: false, })); @@ -83,13 +85,13 @@ export async function applyNonInteractivePluginProviderChoice(params: { const owningPluginIds = preferredProviderId ? resolveOwningPluginIdsForProviderRef({ provider: preferredProviderId, - config: params.nextConfig, + config: nextConfig, workspaceDir, }) : undefined; - const providerChoice = resolveProviderPluginChoice({ + let providerChoice = resolveProviderPluginChoice({ providers: resolvePluginProviders({ - config: params.nextConfig, + config: nextConfig, workspaceDir, onlyPluginIds: owningPluginIds, mode: "setup", @@ -112,14 +114,14 @@ export async function applyNonInteractivePluginProviderChoice(params: { } // Keep mismatch diagnostics metadata-only so untrusted workspace plugins are not loaded. const trustedManifestMatch = resolveManifestProviderAuthChoice(params.authChoice, { - config: params.nextConfig, + config: nextConfig, workspaceDir, includeUntrustedWorkspacePlugins: false, }); const untrustedOnlyManifestMatch = !trustedManifestMatch && resolveManifestProviderAuthChoice(params.authChoice, { - config: params.nextConfig, + config: nextConfig, workspaceDir, includeUntrustedWorkspacePlugins: true, }); @@ -135,11 +137,62 @@ export async function applyNonInteractivePluginProviderChoice(params: { params.runtime.exit(1); return null; } - return undefined; + const installCatalogEntry = resolveProviderInstallCatalogEntry(params.authChoice, { + config: nextConfig, + workspaceDir, + includeUntrustedWorkspacePlugins: false, + }); + if (!installCatalogEntry) { + return undefined; + } + const { ensureOnboardingPluginInstalled } = await import("../../onboarding-plugin-install.js"); + const installResult = await ensureOnboardingPluginInstalled({ + cfg: nextConfig, + entry: { + pluginId: installCatalogEntry.pluginId, + label: installCatalogEntry.label, + install: installCatalogEntry.install, + ...(installCatalogEntry.origin === "bundled" + ? { trustedSourceLinkedOfficialInstall: true } + : {}), + }, + prompter: createNonInteractiveLoggingPrompter( + params.runtime, + (message) => `Non-interactive setup cannot prompt for plugin install: ${message}`, + ), + runtime: params.runtime, + workspaceDir, + promptInstall: false, + }); + if (!installResult.installed) { + params.runtime.error( + `Unable to install the ${installCatalogEntry.label} plugin for non-interactive setup.`, + ); + params.runtime.exit(1); + return null; + } + nextConfig = installResult.cfg; + providerChoice = resolveProviderPluginChoice({ + providers: resolvePluginProviders({ + config: nextConfig, + workspaceDir, + onlyPluginIds: [installCatalogEntry.pluginId], + mode: "setup", + includeUntrustedWorkspacePlugins: false, + }), + choice: params.authChoice, + }); + if (!providerChoice) { + params.runtime.error( + `Installed plugin "${installCatalogEntry.label}" did not expose auth choice "${params.authChoice}".`, + ); + params.runtime.exit(1); + return null; + } } const enableResult = enablePluginInConfig( - params.nextConfig, + nextConfig, providerChoice.provider.pluginId ?? providerChoice.provider.id, ); if (!enableResult.enabled) { diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index 93ec3ff9322..09c92df4b21 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -218,6 +218,31 @@ describe("applyPluginAutoEnable core", () => { ).toBe("google auth configured"); }); + it("auto-enables external speech providers selected by TTS config", () => { + const result = applyPluginAutoEnable({ + config: { + messages: { tts: { provider: "gradium" } }, + plugins: { allow: ["telegram"] }, + }, + env, + manifestRegistry: makeRegistry([ + { + id: "gradium", + channels: [], + contracts: { speechProviders: ["gradium"] }, + origin: "global", + }, + ]), + }); + + expect(result.config.plugins?.allow).toEqual(["telegram", "gradium"]); + expect(result.config.plugins?.entries?.gradium).toEqual({ enabled: true }); + expect(result.autoEnabledReasons).toEqual({ + gradium: ["gradium speech provider selected"], + }); + expect(result.changes).toContain("gradium speech provider selected, enabled automatically."); + }); + it("treats an undefined config as empty", () => { const result = applyPluginAutoEnable({ config: undefined, diff --git a/src/config/plugin-auto-enable.shared.ts b/src/config/plugin-auto-enable.shared.ts index 9a45057be82..a9fc75c108f 100644 --- a/src/config/plugin-auto-enable.shared.ts +++ b/src/config/plugin-auto-enable.shared.ts @@ -12,9 +12,11 @@ import { listBundledChannelIdsWithConfiguredState, } from "../channels/plugins/configured-state.js"; import { getChatChannelMeta, normalizeChatChannelId } from "../channels/registry.js"; +import { isBlockedObjectKey } from "../infra/prototype-keys.js"; import { normalizePluginsConfig } from "../plugins/config-state.js"; import { getCurrentPluginMetadataSnapshot } from "../plugins/current-plugin-metadata-snapshot.js"; import type { PluginDiscoveryResult } from "../plugins/discovery.js"; +import { collectConfiguredSpeechProviderIds } from "../plugins/gateway-startup-speech-providers.js"; import { resolveInstalledPluginIndexPolicyHash } from "../plugins/installed-plugin-index-policy.js"; import type { PluginManifestRecord, PluginManifestRegistry } from "../plugins/manifest-registry.js"; import { loadPluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; @@ -28,7 +30,6 @@ import type { PluginAutoEnableResult, } from "./plugin-auto-enable.types.js"; import { ensurePluginAllowlisted } from "./plugins-allowlist.js"; -import { isBlockedObjectKey } from "../infra/prototype-keys.js"; import type { OpenClawConfig } from "./types.openclaw.js"; export type { PluginAutoEnableCandidate, @@ -173,6 +174,23 @@ function resolveProviderPluginsWithOwnedWebFetch( ); } +function resolvePluginIdsForConfiguredSpeechProvider( + providerId: string, + registry: PluginManifestRegistry, +): string[] { + const normalizedProviderId = normalizeOptionalLowercaseString(providerId); + if (!normalizedProviderId) { + return []; + } + return registry.plugins + .filter((plugin) => + (plugin.contracts?.speechProviders ?? []).some( + (candidate) => normalizeOptionalLowercaseString(candidate) === normalizedProviderId, + ), + ) + .map((plugin) => plugin.id); +} + function resolvePluginsWithOwnedToolConfig( registry: PluginManifestRegistry, ): PluginManifestRecord[] { @@ -348,6 +366,10 @@ function hasConfiguredWebFetchPluginEntry(cfg: OpenClawConfig): boolean { ); } +function hasConfiguredSpeechProviderSelection(cfg: OpenClawConfig): boolean { + return collectConfiguredSpeechProviderIds(cfg).size > 0; +} + function hasConfiguredPluginConfigEntry(cfg: OpenClawConfig): boolean { const entries = cfg.plugins?.entries; return ( @@ -521,6 +543,9 @@ function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig, env: NodeJS.Pr if (hasConfiguredProviderModelOrHarness(cfg, env)) { return true; } + if (hasConfiguredSpeechProviderSelection(cfg)) { + return true; + } if (hasConfiguredWebSearchProviderSelection(cfg)) { return true; } @@ -565,6 +590,9 @@ export function resolvePluginAutoEnableReadiness( if (hasConfiguredProviderModelOrHarness(cfg, env)) { return { mayNeedAutoEnable: true, configuredChannelIds }; } + if (hasConfiguredSpeechProviderSelection(cfg)) { + return { mayNeedAutoEnable: true, configuredChannelIds }; + } if ( hasConfiguredWebSearchProviderSelection(cfg) || hasConfiguredWebSearchPluginEntry(cfg) || @@ -596,6 +624,8 @@ export function resolvePluginAutoEnableCandidateReason( return `${candidate.providerId} auth configured`; case "provider-model-configured": return `${candidate.modelRef} model configured`; + case "speech-provider-selected": + return `${candidate.providerId} speech provider selected`; case "agent-harness-runtime-configured": return `${candidate.runtime} agent runtime configured`; case "web-search-provider-selected": @@ -608,6 +638,8 @@ export function resolvePluginAutoEnableCandidateReason( return `${candidate.pluginId} web fetch configured`; case "plugin-tool-configured": return `${candidate.pluginId} tool configured`; + case "configured-plugin-repaired": + return `${candidate.pluginId} installed for existing configuration`; case "setup-auto-enable": return candidate.reason; } @@ -654,6 +686,19 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: { } } + for (const providerId of collectConfiguredSpeechProviderIds(params.config)) { + for (const pluginId of resolvePluginIdsForConfiguredSpeechProvider( + providerId, + params.registry, + )) { + changes.push({ + pluginId, + kind: "speech-provider-selected", + providerId, + }); + } + } + for (const runtime of collectConfiguredAgentHarnessRuntimes(params.config)) { const pluginIds = resolveAgentHarnessOwnerPluginIds(params.registry, runtime); for (const pluginId of pluginIds) { diff --git a/src/config/plugin-auto-enable.test-helpers.ts b/src/config/plugin-auto-enable.test-helpers.ts index 2409bd8805d..3f25730354f 100644 --- a/src/config/plugin-auto-enable.test-helpers.ts +++ b/src/config/plugin-auto-enable.test-helpers.ts @@ -40,7 +40,12 @@ export function makeRegistry( activation?: { onAgentHarnesses?: string[] }; autoEnableWhenConfiguredProviders?: string[]; modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] }; - contracts?: { webSearchProviders?: string[]; webFetchProviders?: string[]; tools?: string[] }; + contracts?: { + speechProviders?: string[]; + webSearchProviders?: string[]; + webFetchProviders?: string[]; + tools?: string[]; + }; providers?: string[]; cliBackends?: string[]; origin?: PluginOrigin; diff --git a/src/config/plugin-auto-enable.types.ts b/src/config/plugin-auto-enable.types.ts index ec16acb9237..453baf160e2 100644 --- a/src/config/plugin-auto-enable.types.ts +++ b/src/config/plugin-auto-enable.types.ts @@ -18,6 +18,11 @@ export type PluginAutoEnableCandidate = kind: "provider-model-configured"; modelRef: string; } + | { + pluginId: string; + kind: "speech-provider-selected"; + providerId: string; + } | { pluginId: string; kind: "agent-harness-runtime-configured"; @@ -45,6 +50,10 @@ export type PluginAutoEnableCandidate = pluginId: string; kind: "plugin-tool-configured"; } + | { + pluginId: string; + kind: "configured-plugin-repaired"; + } | { pluginId: string; kind: "setup-auto-enable"; diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index c9bdafcf569..94013a0f3e5 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -152,6 +152,15 @@ function createManifestRegistryFixture(): PluginManifestRegistry { cliBackends: [], contracts: { speechProviders: ["tts-local-cli", "cli"] }, }, + { + id: "gradium", + channels: [], + origin: "global", + enabledByDefault: undefined, + providers: [], + cliBackends: [], + contracts: { speechProviders: ["gradium"] }, + }, { id: "anthropic", channels: [], @@ -809,6 +818,15 @@ describe("resolveGatewayStartupPluginIds", () => { } as OpenClawConfig, ["browser", "microsoft", "memory-core"], ], + [ + "includes explicitly enabled external speech providers at startup", + { + channels: {}, + messages: { tts: { provider: "gradium" } }, + plugins: { entries: { gradium: { enabled: true } } }, + } as OpenClawConfig, + ["browser", "gradium", "memory-core"], + ], [ "includes active persona speech providers at startup", { diff --git a/src/plugins/official-external-plugin-catalog.test.ts b/src/plugins/official-external-plugin-catalog.test.ts index 4d578e6a8f7..178873ccd34 100644 --- a/src/plugins/official-external-plugin-catalog.test.ts +++ b/src/plugins/official-external-plugin-catalog.test.ts @@ -3,6 +3,10 @@ import { type OfficialExternalPluginCatalogEntry, getOfficialExternalPluginCatalogEntry, listOfficialExternalPluginCatalogEntries, + resolveOfficialExternalProviderContractPluginIds, + resolveOfficialExternalProviderPluginIds, + resolveOfficialExternalProviderPluginIdsForEnv, + resolveOfficialExternalWebProviderContractPluginIdsForEnv, resolveOfficialExternalPluginId, resolveOfficialExternalPluginInstall, } from "./official-external-plugin-catalog.js"; @@ -16,6 +20,40 @@ function expectCatalogEntry(id: string): OfficialExternalPluginCatalogEntry { } describe("official external plugin catalog", () => { + it("lists the externalized provider and capability plugins with install metadata", () => { + const providers = [ + ["arcee", "@openclaw/arcee-provider"], + ["cerebras", "@openclaw/cerebras-provider"], + ["chutes", "@openclaw/chutes-provider"], + ["cloudflare-ai-gateway", "@openclaw/cloudflare-ai-gateway-provider"], + ["deepinfra", "@openclaw/deepinfra-provider"], + ["deepseek", "@openclaw/deepseek-provider"], + ["groq", "@openclaw/groq-provider"], + ["kilocode", "@openclaw/kilocode-provider"], + ["kimi", "@openclaw/kimi-provider"], + ["qianfan", "@openclaw/qianfan-provider"], + ["qwen", "@openclaw/qwen-provider"], + ["stepfun", "@openclaw/stepfun-provider"], + ] as const; + const plugins = [ + ["exa", "@openclaw/exa-plugin"], + ["firecrawl", "@openclaw/firecrawl-plugin"], + ["gradium", "@openclaw/gradium-speech"], + ["inworld", "@openclaw/inworld-speech"], + ["parallel", "@openclaw/parallel-plugin"], + ["perplexity", "@openclaw/perplexity-plugin"], + ] as const; + + for (const [id, npmSpec] of [...providers, ...plugins]) { + expect(resolveOfficialExternalPluginInstall(expectCatalogEntry(id))).toEqual({ + clawhubSpec: `clawhub:${npmSpec}`, + npmSpec, + defaultChoice: "npm", + minHostVersion: ">=2026.6.8", + }); + } + }); + it("resolves third-party channel lookup aliases to published plugin ids", () => { const wecomByChannel = expectCatalogEntry("wecom"); const wecomByPlugin = expectCatalogEntry("wecom-openclaw-plugin"); @@ -59,6 +97,7 @@ describe("official external plugin catalog", () => { const gmi = expectCatalogEntry("gmi"); expect(resolveOfficialExternalPluginId(gmi)).toBe("gmi"); + expect(getOfficialExternalPluginCatalogEntry("gmi-cloud")).toBe(gmi); expect(resolveOfficialExternalPluginInstall(gmi)).toEqual({ clawhubSpec: "clawhub:@openclaw/gmi-provider", npmSpec: "@openclaw/gmi-provider", @@ -79,6 +118,104 @@ describe("official external plugin catalog", () => { }); }); + it("resolves external provider aliases beyond the primary provider id", () => { + const qwen = expectCatalogEntry("qwen"); + + expect(getOfficialExternalPluginCatalogEntry("modelstudio")).toBe(qwen); + expect(getOfficialExternalPluginCatalogEntry("qwen-oauth")).toBe(qwen); + expect(getOfficialExternalPluginCatalogEntry("qwen-portal")).toBe(qwen); + }); + + it("maps external speech and web-fetch contracts to plugin owners", () => { + expect( + resolveOfficialExternalProviderContractPluginIds({ + contract: "speechProviders", + providerIds: new Set(["gradium", "inworld"]), + }), + ).toEqual(["gradium", "inworld"]); + expect( + resolveOfficialExternalProviderContractPluginIds({ + contract: "webFetchProviders", + providerIds: new Set(["firecrawl"]), + }), + ).toEqual(["firecrawl"]); + expect( + resolveOfficialExternalProviderContractPluginIds({ + contract: "mediaUnderstandingProviders", + providerIds: new Set(["groq"]), + }), + ).toEqual(["groq"]); + }); + + it("maps env-only web-fetch credentials to external plugin owners", () => { + expect( + resolveOfficialExternalWebProviderContractPluginIdsForEnv({ + contract: "webFetchProviders", + env: { FIRECRAWL_API_KEY: "firecrawl-key" }, + }), + ).toEqual(["firecrawl"]); + expect( + resolveOfficialExternalWebProviderContractPluginIdsForEnv({ + contract: "webFetchProviders", + env: { EXA_API_KEY: "exa-key" }, + }), + ).toEqual([]); + }); + + it("maps configured provider ids and aliases even without an auth choice", () => { + expect( + resolveOfficialExternalProviderPluginIds({ + providerIds: new Set(["groq", "modelstudio"]), + }), + ).toEqual(["groq", "qwen"]); + }); + + it("maps env-only provider credentials to external installs", () => { + expect( + resolveOfficialExternalProviderPluginIdsForEnv({ + ARCEEAI_API_KEY: "arcee-key", + CEREBRAS_API_KEY: "cerebras-key", + CHUTES_OAUTH_TOKEN: "chutes-token", + CLOUDFLARE_AI_GATEWAY_API_KEY: "cloudflare-key", + DEEPINFRA_API_KEY: "deepinfra-key", + DEEPSEEK_API_KEY: "deepseek-key", + GROQ_API_KEY: "groq-key", + KILOCODE_API_KEY: "kilocode-key", + KIMICODE_API_KEY: "kimi-key", + QIANFAN_API_KEY: "qianfan-key", + MODELSTUDIO_API_KEY: "qwen-key", + STEPFUN_API_KEY: "stepfun-key", + }), + ).toEqual([ + "arcee", + "cerebras", + "chutes", + "cloudflare-ai-gateway", + "deepinfra", + "deepseek", + "groq", + "kilocode", + "kimi", + "qianfan", + "qwen", + "stepfun", + ]); + expect(resolveOfficialExternalProviderPluginIdsForEnv({ GROQ_API_KEY: " " })).toEqual([]); + }); + + it("keeps Groq available through the cold-install auth catalog", () => { + const groq = expectCatalogEntry("groq"); + const authChoice = groq.openclaw?.providers?.find((provider) => provider.id === "groq") + ?.authChoices?.[0]; + + expect(authChoice).toMatchObject({ + choiceId: "groq-api-key", + optionKey: "groqApiKey", + cliFlag: "--groq-api-key", + cliOption: "--groq-api-key ", + }); + }); + it("allows invalid-config recovery for externalized stock plugins", () => { expect(resolveOfficialExternalPluginInstall(expectCatalogEntry("brave"))).toMatchObject({ npmSpec: "@openclaw/brave-plugin", diff --git a/src/plugins/official-external-plugin-catalog.ts b/src/plugins/official-external-plugin-catalog.ts index a834ef7522a..3ccbb3e89a4 100644 --- a/src/plugins/official-external-plugin-catalog.ts +++ b/src/plugins/official-external-plugin-catalog.ts @@ -17,6 +17,7 @@ type ManifestKey = typeof MANIFEST_KEY; export type OfficialExternalProviderAuthChoice = { method?: string; choiceId?: string; + deprecatedChoiceIds?: readonly string[]; choiceLabel?: string; choiceHint?: string; assistantPriority?: number; @@ -37,6 +38,7 @@ export type OfficialExternalProviderCatalogProvider = { name?: string; docs?: string; categories?: readonly string[]; + envVars?: readonly string[]; authChoices?: readonly OfficialExternalProviderAuthChoice[]; }; @@ -81,6 +83,13 @@ export type OfficialExternalPluginCatalogEntry = { kind?: string; } & Partial>; +type OfficialExternalProviderContract = + | "embeddingProviders" + | "mediaUnderstandingProviders" + | "memoryEmbeddingProviders" + | "speechProviders" + | "webFetchProviders"; + const OFFICIAL_CATALOG_SOURCES = [ officialExternalChannelCatalog, officialExternalProviderCatalog, @@ -132,7 +141,10 @@ function resolveOfficialExternalPluginLookupIds( [ normalizeOptionalString(manifest?.plugin?.id), normalizeOptionalString(manifest?.channel?.id), - normalizeOptionalString(manifest?.providers?.[0]?.id), + ...(manifest?.providers ?? []).flatMap((provider) => [ + normalizeOptionalString(provider.id), + ...(provider.aliases ?? []).map((alias) => normalizeOptionalString(alias)), + ]), ].filter((value): value is string => Boolean(value)), ); } @@ -189,6 +201,118 @@ export function listOfficialExternalPluginCatalogEntries(): OfficialExternalPlug return [...resolved.values()]; } +/** Resolves official external plugin owners for configured capability provider ids. */ +export function resolveOfficialExternalProviderContractPluginIds(params: { + contract: OfficialExternalProviderContract; + providerIds: ReadonlySet; +}): string[] { + const configuredProviderIds = new Set( + [...params.providerIds] + .map((providerId) => normalizeOptionalString(providerId)?.toLowerCase()) + .filter((providerId): providerId is string => Boolean(providerId)), + ); + if (configuredProviderIds.size === 0) { + return []; + } + const pluginIds = new Set(); + for (const entry of listOfficialExternalPluginCatalogEntries()) { + const pluginId = resolveOfficialExternalPluginId(entry); + const providerIds = + getOfficialExternalPluginCatalogManifest(entry)?.contracts?.[params.contract]; + if ( + pluginId && + providerIds?.some((providerId) => { + const normalized = normalizeOptionalString(providerId)?.toLowerCase(); + return normalized ? configuredProviderIds.has(normalized) : false; + }) + ) { + pluginIds.add(pluginId); + } + } + return [...pluginIds].toSorted((left, right) => left.localeCompare(right)); +} + +/** Resolves official web provider owners from matching documented environment credentials. */ +export function resolveOfficialExternalWebProviderContractPluginIdsForEnv(params: { + contract: OfficialExternalProviderContract; + env: NodeJS.ProcessEnv; +}): string[] { + const pluginIds = new Set(); + for (const entry of listOfficialExternalPluginCatalogEntries()) { + const pluginId = resolveOfficialExternalPluginId(entry); + const manifest = getOfficialExternalPluginCatalogManifest(entry); + const contractProviderIds = new Set( + (manifest?.contracts?.[params.contract] ?? []) + .map((providerId) => normalizeOptionalString(providerId)?.toLowerCase()) + .filter((providerId): providerId is string => Boolean(providerId)), + ); + if ( + pluginId && + contractProviderIds.size > 0 && + manifest?.webSearchProviders?.some((provider) => { + const providerId = normalizeOptionalString(provider.id)?.toLowerCase(); + return ( + providerId !== undefined && + contractProviderIds.has(providerId) && + provider.envVars?.some((envVar) => Boolean(params.env[envVar]?.trim())) + ); + }) + ) { + pluginIds.add(pluginId); + } + } + return [...pluginIds].toSorted((left, right) => left.localeCompare(right)); +} + +/** Resolves official external plugin owners for configured model provider ids. */ +export function resolveOfficialExternalProviderPluginIds(params: { + providerIds: ReadonlySet; +}): string[] { + const configuredProviderIds = new Set( + [...params.providerIds] + .map((providerId) => normalizeOptionalString(providerId)?.toLowerCase()) + .filter((providerId): providerId is string => Boolean(providerId)), + ); + if (configuredProviderIds.size === 0) { + return []; + } + const pluginIds = new Set(); + for (const entry of listOfficialExternalProviderCatalogEntries()) { + const pluginId = resolveOfficialExternalPluginId(entry); + const providers = getOfficialExternalPluginCatalogManifest(entry)?.providers; + if ( + pluginId && + providers?.some((provider) => + [provider.id, ...(provider.aliases ?? [])].some((providerId) => { + const normalized = normalizeOptionalString(providerId)?.toLowerCase(); + return normalized ? configuredProviderIds.has(normalized) : false; + }), + ) + ) { + pluginIds.add(pluginId); + } + } + return [...pluginIds].toSorted((left, right) => left.localeCompare(right)); +} + +/** Resolves official external provider owners with configured environment credentials. */ +export function resolveOfficialExternalProviderPluginIdsForEnv(env: NodeJS.ProcessEnv): string[] { + const pluginIds = new Set(); + for (const entry of listOfficialExternalProviderCatalogEntries()) { + const pluginId = resolveOfficialExternalPluginId(entry); + const providers = getOfficialExternalPluginCatalogManifest(entry)?.providers; + if ( + pluginId && + providers?.some((provider) => + provider.envVars?.some((envVar) => Boolean(env[envVar]?.trim())), + ) + ) { + pluginIds.add(pluginId); + } + } + return [...pluginIds].toSorted((left, right) => left.localeCompare(right)); +} + export function listOfficialExternalChannelCatalogEntries(): OfficialExternalPluginCatalogEntry[] { return listOfficialExternalPluginCatalogEntries().filter((entry) => Boolean(getOfficialExternalPluginCatalogManifest(entry)?.channel), diff --git a/src/plugins/provider-auth-choices.test.ts b/src/plugins/provider-auth-choices.test.ts index 78aea06b851..f007a72b5fd 100644 --- a/src/plugins/provider-auth-choices.test.ts +++ b/src/plugins/provider-auth-choices.test.ts @@ -8,6 +8,9 @@ const pluginRegistryMocks = vi.hoisted(() => ({ loadPluginMetadataSnapshot: vi.fn(), resolvePluginMetadataSnapshot: vi.fn(), })); +const officialCatalogMocks = vi.hoisted(() => ({ + listOfficialExternalProviderCatalogEntries: vi.fn(), +})); vi.mock("./manifest-registry-installed.js", () => ({ loadPluginManifestRegistryForInstalledIndex: @@ -35,6 +38,11 @@ vi.mock("../plugins/plugin-metadata-snapshot.js", () => ({ loadPluginMetadataSnapshot: pluginRegistryMocks.loadPluginMetadataSnapshot, resolvePluginMetadataSnapshot: pluginRegistryMocks.resolvePluginMetadataSnapshot, })); +vi.mock("./official-external-plugin-catalog.js", () => ({ + getOfficialExternalPluginCatalogManifest: (entry: { openclaw?: unknown }) => entry.openclaw, + listOfficialExternalProviderCatalogEntries: + officialCatalogMocks.listOfficialExternalProviderCatalogEntries, +})); vi.resetModules(); @@ -44,6 +52,7 @@ const { resolveManifestProviderAuthChoice, resolveManifestProviderAuthChoices, resolveManifestProviderOnboardAuthFlags, + resolveProviderOnboardAuthFlags, } = await import("./provider-auth-choices.js"); const { resetProviderAuthAliasMapCacheForTest, resolveProviderIdForAuth } = await import("../agents/provider-auth-aliases.js"); @@ -119,6 +128,8 @@ describe("provider auth choice manifest helpers", () => { (params?: { pluginMetadataSnapshot?: unknown }) => params?.pluginMetadataSnapshot ?? pluginRegistryMocks.loadPluginMetadataSnapshot(params), ); + officialCatalogMocks.listOfficialExternalProviderCatalogEntries.mockReset(); + officialCatalogMocks.listOfficialExternalProviderCatalogEntries.mockReturnValue([]); resetProviderAuthAliasMapCacheForTest(); }); @@ -158,6 +169,70 @@ describe("provider auth choice manifest helpers", () => { }); }); + it("keeps installed manifest flags ahead of official cold-install flags", () => { + setSingleManifestProviderAuthChoices("cerebras", [ + createProviderAuthChoice({ + provider: "cerebras", + method: "api-key", + choiceId: "cerebras-api-key", + choiceLabel: "Cerebras API key", + optionKey: "cerebrasApiKey", + cliFlag: "--cerebras-api-key", + cliOption: "--cerebras-api-key ", + cliDescription: "Installed Cerebras key", + }), + ]); + officialCatalogMocks.listOfficialExternalProviderCatalogEntries.mockReturnValue([ + { + openclaw: { + plugin: { id: "cerebras" }, + providers: [ + { + id: "cerebras", + authChoices: [ + { + method: "api-key", + choiceId: "cerebras-api-key", + choiceLabel: "Cerebras API key", + optionKey: "cerebrasApiKey", + cliFlag: "--cerebras-api-key", + cliOption: "--cerebras-api-key ", + cliDescription: "Catalog Cerebras key", + }, + { + method: "api-key", + choiceId: "groq-api-key", + choiceLabel: "Groq API key", + optionKey: "groqApiKey", + cliFlag: "--groq-api-key", + cliOption: "--groq-api-key ", + cliDescription: "Groq API key", + }, + ], + }, + ], + }, + }, + ]); + + expect(resolveProviderOnboardAuthFlags()).toEqual([ + { + optionKey: "cerebrasApiKey", + authChoice: "cerebras-api-key", + cliFlag: "--cerebras-api-key", + cliOption: "--cerebras-api-key ", + description: "Installed Cerebras key", + }, + { + optionKey: "groqApiKey", + authChoice: "groq-api-key", + cliFlag: "--groq-api-key", + cliOption: "--groq-api-key ", + description: "Groq API key", + }, + ]); + }); + it.each([ { name: "deduplicates flag metadata by option key + flag", diff --git a/src/plugins/provider-auth-choices.ts b/src/plugins/provider-auth-choices.ts index 506da83dbc5..ae18078014f 100644 --- a/src/plugins/provider-auth-choices.ts +++ b/src/plugins/provider-auth-choices.ts @@ -5,6 +5,10 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js"; import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js"; import type { PluginManifestRecord } from "./manifest-registry.js"; +import { + getOfficialExternalPluginCatalogManifest, + listOfficialExternalProviderCatalogEntries, +} from "./official-external-plugin-catalog.js"; import type { PluginOrigin } from "./plugin-origin.types.js"; export type ProviderAuthChoiceMetadata = { @@ -363,3 +367,46 @@ export function resolveManifestProviderOnboardAuthFlags( } return flags; } + +function resolveOfficialExternalProviderOnboardAuthFlags(): ProviderOnboardAuthFlag[] { + const flags: ProviderOnboardAuthFlag[] = []; + for (const entry of listOfficialExternalProviderCatalogEntries()) { + const manifest = getOfficialExternalPluginCatalogManifest(entry); + for (const provider of manifest?.providers ?? []) { + for (const choice of provider.authChoices ?? []) { + const optionKey = choice.optionKey?.trim(); + const authChoice = choice.choiceId?.trim(); + const cliFlag = choice.cliFlag?.trim(); + const cliOption = choice.cliOption?.trim(); + if (!optionKey || !authChoice || !cliFlag || !cliOption) { + continue; + } + flags.push({ + optionKey, + authChoice, + cliFlag, + cliOption, + description: choice.cliDescription?.trim() || choice.choiceLabel?.trim() || authChoice, + }); + } + } + } + return flags; +} + +/** Resolves onboard auth flags from installed manifests and official cold-install metadata. */ +export function resolveProviderOnboardAuthFlags( + params?: ManifestProviderAuthChoiceParams, +): ProviderOnboardAuthFlag[] { + const flags = resolveManifestProviderOnboardAuthFlags(params); + const seen = new Set(flags.map((flag) => `${flag.optionKey}::${flag.cliFlag}`)); + for (const flag of resolveOfficialExternalProviderOnboardAuthFlags()) { + const dedupeKey = `${flag.optionKey}::${flag.cliFlag}`; + if (seen.has(dedupeKey)) { + continue; + } + seen.add(dedupeKey); + flags.push(flag); + } + return flags; +} diff --git a/src/plugins/provider-install-catalog.ts b/src/plugins/provider-install-catalog.ts index fd297dc6d3e..6905cb179bb 100644 --- a/src/plugins/provider-install-catalog.ts +++ b/src/plugins/provider-install-catalog.ts @@ -367,6 +367,9 @@ function resolveOfficialExternalProviderInstallCatalogEntries(params: { cliDescription: choice.cliDescription, onboardingScopes: normalizeProviderAuthChoiceScopes(choice.onboardingScopes), }), + ...(choice.deprecatedChoiceIds?.length + ? { deprecatedChoiceIds: [...choice.deprecatedChoiceIds] } + : {}), label, origin: "bundled", install, diff --git a/src/plugins/web-fetch-providers.runtime.ts b/src/plugins/web-fetch-providers.runtime.ts index 39d748dd415..316e3be409e 100644 --- a/src/plugins/web-fetch-providers.runtime.ts +++ b/src/plugins/web-fetch-providers.runtime.ts @@ -23,6 +23,7 @@ function resolveWebFetchCandidatePluginIds(params: { env?: PluginLoadOptions["env"]; onlyPluginIds?: readonly string[]; origin?: PluginManifestRecord["origin"]; + sandboxed?: boolean; }): string[] | undefined { return resolveManifestDeclaredWebProviderCandidatePluginIds({ contract: "webFetchProviders", @@ -32,6 +33,7 @@ function resolveWebFetchCandidatePluginIds(params: { env: params.env, onlyPluginIds: params.onlyPluginIds, origin: params.origin, + sandboxed: params.sandboxed, }); } @@ -56,6 +58,7 @@ export function resolvePluginWebFetchProviders(params: { cache?: boolean; mode?: "runtime" | "setup"; origin?: PluginManifestRecord["origin"]; + sandboxed?: boolean; }): PluginWebFetchProviderEntry[] { return resolvePluginWebProviders(params, { resolveBundledResolutionConfig: resolveBundledWebFetchResolutionConfig, diff --git a/src/plugins/web-provider-resolution-candidates.test.ts b/src/plugins/web-provider-resolution-candidates.test.ts index fa75459a4b9..1f6be1ab472 100644 --- a/src/plugins/web-provider-resolution-candidates.test.ts +++ b/src/plugins/web-provider-resolution-candidates.test.ts @@ -116,6 +116,43 @@ describe("resolveManifestDeclaredWebProviderCandidatePluginIds", () => { ).toStrictEqual([]); }); + it("limits sandboxed web fetch candidates to bundled and trusted official installs", () => { + mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({ + plugins: [ + { + id: "bundled-fetch", + origin: "bundled", + contracts: { webFetchProviders: ["bundled-fetch"] }, + }, + { + id: "firecrawl", + origin: "global", + trustedOfficialInstall: true, + contracts: { webFetchProviders: ["firecrawl"] }, + }, + { + id: "third-party-fetch", + origin: "global", + contracts: { webFetchProviders: ["third-party"] }, + }, + { + id: "workspace-fetch", + origin: "workspace", + contracts: { webFetchProviders: ["workspace-fetch"] }, + }, + ], + diagnostics: [], + }); + + expect( + resolveManifestDeclaredWebProviderCandidatePluginIds({ + contract: "webFetchProviders", + configKey: "webFetch", + sandboxed: true, + }), + ).toEqual(["bundled-fetch", "firecrawl"]); + }); + it("derives provider candidates from a single manifest-registry read", () => { expect( resolveManifestDeclaredWebProviderCandidatePluginIds({ diff --git a/src/plugins/web-provider-resolution-shared.ts b/src/plugins/web-provider-resolution-shared.ts index 3e0286a2d39..3a9f60994a1 100644 --- a/src/plugins/web-provider-resolution-shared.ts +++ b/src/plugins/web-provider-resolution-shared.ts @@ -87,6 +87,7 @@ export function resolveManifestDeclaredWebProviderCandidatePluginIds(params: { env?: PluginLoadOptions["env"]; onlyPluginIds?: readonly string[]; origin?: PluginManifestRecord["origin"]; + sandboxed?: boolean; }): string[] | undefined { return resolveManifestDeclaredWebProviderCandidates(params).pluginIds; } @@ -100,6 +101,7 @@ export function resolveManifestDeclaredWebProviderCandidates(params: { env?: PluginLoadOptions["env"]; onlyPluginIds?: readonly string[]; origin?: PluginManifestRecord["origin"]; + sandboxed?: boolean; manifestRecords?: readonly PluginManifestRecord[]; }): WebProviderCandidateResolution { const scopedPluginIds = normalizePluginIdScope(params.onlyPluginIds); @@ -119,6 +121,11 @@ export function resolveManifestDeclaredWebProviderCandidates(params: { .filter( (plugin) => (!params.origin || plugin.origin === params.origin) && + // Sandboxed web tools may run bundled providers or a verified official install, + // never an arbitrary workspace or external plugin with the same contract. + (!params.sandboxed || + plugin.origin === "bundled" || + plugin.trustedOfficialInstall === true) && (!onlyPluginIdSet || onlyPluginIdSet.has(plugin.id)) && pluginManifestDeclaresProviderConfig(plugin, params.configKey, params.contract), ) @@ -129,7 +136,7 @@ export function resolveManifestDeclaredWebProviderCandidates(params: { } // Unscoped resolution falls back to runtime registry loading; scoped/origin-filtered // calls must return an explicit empty candidate set instead. - if (params.origin || scopedPluginIds !== undefined) { + if (params.origin || params.sandboxed || scopedPluginIds !== undefined) { return { pluginIds: [], manifestRecords }; } return { pluginIds: undefined, manifestRecords }; diff --git a/src/plugins/web-provider-runtime-shared.test.ts b/src/plugins/web-provider-runtime-shared.test.ts index 1a77a717d80..f7cb310c7fb 100644 --- a/src/plugins/web-provider-runtime-shared.test.ts +++ b/src/plugins/web-provider-runtime-shared.test.ts @@ -173,6 +173,7 @@ describe("web-provider-runtime-shared", () => { env: { BRAVE_API_KEY: "key" }, onlyPluginIds: ["brave", "firecrawl"], origin: "bundled", + sandboxed: true, workspaceDir: "/workspace", }, { @@ -193,6 +194,7 @@ describe("web-provider-runtime-shared", () => { env: { BRAVE_API_KEY: "key" }, onlyPluginIds: ["brave", "firecrawl"], origin: "bundled", + sandboxed: true, }); expect(mapRegistryProviders).toHaveBeenCalledWith({ registry: activeRegistry, diff --git a/src/plugins/web-provider-runtime-shared.ts b/src/plugins/web-provider-runtime-shared.ts index 8af5111cfe6..59a20a34a56 100644 --- a/src/plugins/web-provider-runtime-shared.ts +++ b/src/plugins/web-provider-runtime-shared.ts @@ -22,6 +22,7 @@ export type ResolvePluginWebProvidersParams = { cache?: boolean; mode?: "runtime" | "setup"; origin?: PluginManifestRecord["origin"]; + sandboxed?: boolean; }; type ResolveWebProviderRuntimeDeps = { @@ -40,6 +41,7 @@ type ResolveWebProviderRuntimeDeps = { env?: PluginLoadOptions["env"]; onlyPluginIds?: readonly string[]; origin?: PluginManifestRecord["origin"]; + sandboxed?: boolean; }) => string[] | undefined; mapRegistryProviders: (params: { registry: PluginRegistry; @@ -77,7 +79,8 @@ function resolveWebProviderRuntimeContext( const shouldFilterProviders = params.config !== undefined || params.onlyPluginIds !== undefined || - params.origin !== undefined; + params.origin !== undefined || + params.sandboxed === true; const { config, activationSourceConfig, autoEnabledReasons } = deps.resolveBundledResolutionConfig({ ...params, @@ -91,6 +94,7 @@ function resolveWebProviderRuntimeContext( env, onlyPluginIds: params.onlyPluginIds, origin: params.origin, + sandboxed: params.sandboxed, }), ); return { @@ -161,6 +165,7 @@ export function resolvePluginWebProviders( env, onlyPluginIds: params.onlyPluginIds, origin: params.origin, + sandboxed: params.sandboxed, }) ?? []; if (pluginIds.length === 0) { return []; diff --git a/src/plugins/web-search-install-catalog.test.ts b/src/plugins/web-search-install-catalog.test.ts new file mode 100644 index 00000000000..5b43640b354 --- /dev/null +++ b/src/plugins/web-search-install-catalog.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { + resolveWebSearchInstallCatalogEntry, + resolveWebSearchInstallCatalogEntries, + resolveWebSearchInstallCatalogEntriesForEnv, +} from "./web-search-install-catalog.js"; + +describe("web-search install catalog", () => { + it("keeps Parallel's keyless provider installable but opt-in", () => { + const entry = resolveWebSearchInstallCatalogEntry({ + providerId: "parallel-free", + pluginId: "parallel", + }); + + expect(entry).toMatchObject({ + pluginId: "parallel", + install: { + clawhubSpec: "clawhub:@openclaw/parallel-plugin", + npmSpec: "@openclaw/parallel-plugin", + }, + provider: { + id: "parallel-free", + requiresCredential: false, + envVars: [], + credentialPath: "", + }, + }); + expect(entry?.provider.autoDetectOrder).toBeUndefined(); + expect( + resolveWebSearchInstallCatalogEntries().some( + (candidate) => candidate.provider.id === "parallel", + ), + ).toBe(true); + }); + + it("resolves credential-backed plugins for env-only auto-detection", () => { + expect( + resolveWebSearchInstallCatalogEntriesForEnv({ + EXA_API_KEY: "exa-key", + FIRECRAWL_API_KEY: "firecrawl-key", + OPENROUTER_API_KEY: "openrouter-key", + PARALLEL_API_KEY: "parallel-key", + }).map((entry) => entry.pluginId), + ).toEqual(["exa", "firecrawl", "parallel", "perplexity"]); + }); +}); diff --git a/src/plugins/web-search-install-catalog.ts b/src/plugins/web-search-install-catalog.ts index df80c72858e..efd2737c8b6 100644 --- a/src/plugins/web-search-install-catalog.ts +++ b/src/plugins/web-search-install-catalog.ts @@ -74,13 +74,23 @@ function buildProviderEntry(params: { const providerId = normalizeString(params.provider.id); const label = normalizeString(params.provider.label); const hint = normalizeString(params.provider.hint); + const configuredCredentialPath = normalizeString(params.provider.credentialPath); const credentialPath = - normalizeString(params.provider.credentialPath) ?? - `plugins.entries.${params.pluginId}.config.webSearch.apiKey`; + params.provider.credentialPath === "" + ? "" + : (configuredCredentialPath ?? `plugins.entries.${params.pluginId}.config.webSearch.apiKey`); + const requiresCredential = params.provider.requiresCredential !== false; const envVars = normalizeTrimmedStringList(params.provider.envVars); const placeholder = normalizeString(params.provider.placeholder); const signupUrl = normalizeString(params.provider.signupUrl); - if (!providerId || !label || !hint || envVars.length === 0 || !placeholder || !signupUrl) { + if ( + !providerId || + !label || + !hint || + (requiresCredential && envVars.length === 0) || + !placeholder || + !signupUrl + ) { return null; } return { @@ -151,6 +161,17 @@ export function resolveWebSearchInstallCatalogEntries(): WebSearchInstallCatalog ); } +/** Lists credential-backed web provider plugins selected by documented environment variables. */ +export function resolveWebSearchInstallCatalogEntriesForEnv( + env: NodeJS.ProcessEnv, +): WebSearchInstallCatalogEntry[] { + return resolveWebSearchInstallCatalogEntries().filter( + (entry) => + entry.provider.requiresCredential !== false && + entry.provider.envVars.some((envVar) => Boolean(env[envVar]?.trim())), + ); +} + /** Resolves one web-search install catalog entry by provider id or plugin id. */ export function resolveWebSearchInstallCatalogEntry(params: { providerId?: string; diff --git a/src/secrets/runtime-web-tools.test.ts b/src/secrets/runtime-web-tools.test.ts index 7e851fc485b..1dbac68cee9 100644 --- a/src/secrets/runtime-web-tools.test.ts +++ b/src/secrets/runtime-web-tools.test.ts @@ -1434,33 +1434,56 @@ describe("runtime web tools resolution", () => { expect(resolvePluginWebFetchProvidersMock).not.toHaveBeenCalled(); }); - it("uses runtime web fetch discovery when the managed plugin index install records is populated", async () => { + it("resolves SecretRefs for verified installed Firecrawl fetch config", async () => { loadInstalledPluginIndexInstallRecordsSyncMock.mockReturnValue({ - "external-fetch": { + firecrawl: { source: "npm", - spec: "@openclaw/external-fetch", + spec: "@openclaw/firecrawl-plugin", }, }); + resolveManifestContractOwnerPluginIdMock.mockReturnValueOnce(undefined); - const { metadata } = await runRuntimeWebTools({ + const { metadata, resolvedConfig } = await runRuntimeWebTools({ config: asConfig({ + tools: { + web: { + fetch: { + provider: "firecrawl", + }, + }, + }, plugins: { entries: { firecrawl: { config: { webFetch: { - apiKey: "firecrawl-config-key", + apiKey: { + source: "env", + provider: "default", + id: "FIRECRAWL_API_KEY", + }, }, }, }, }, }, }), + env: { + FIRECRAWL_API_KEY: "firecrawl-config-key", + }, }); expect(metadata.fetch.selectedProvider).toBe("firecrawl"); + expect(metadata.fetch.selectedProviderKeySource).toBe("secretRef"); + expect( + ( + resolvedConfig.plugins?.entries?.firecrawl?.config as + | { webFetch?: { apiKey?: unknown } } + | undefined + )?.webFetch?.apiKey, + ).toBe("firecrawl-config-key"); expect(resolveBundledWebFetchProvidersFromPublicArtifactsMock).not.toHaveBeenCalled(); - expect(firstMockArg(resolvePluginWebFetchProvidersMock).origin).toBe("bundled"); + expect(firstMockArg(resolvePluginWebFetchProvidersMock).sandboxed).toBe(true); }); it("uses env fallback for unresolved web fetch provider SecretRef when active", async () => { diff --git a/src/secrets/runtime-web-tools.ts b/src/secrets/runtime-web-tools.ts index c02ac2628b7..efa7f20a130 100644 --- a/src/secrets/runtime-web-tools.ts +++ b/src/secrets/runtime-web-tools.ts @@ -447,7 +447,9 @@ async function resolveBundledWebFetchProviders(params: { return resolvePluginWebFetchProviders({ config: params.sourceConfig, env, - origin: "bundled", + // Runtime credential resolution may load only bundled providers or verified + // official installs. Arbitrary external providers must not gain SecretRef access. + sandboxed: true, }); } diff --git a/src/web-fetch/runtime.test.ts b/src/web-fetch/runtime.test.ts index 5f4e9259f8b..9fd6920755c 100644 --- a/src/web-fetch/runtime.test.ts +++ b/src/web-fetch/runtime.test.ts @@ -304,7 +304,7 @@ describe("web fetch runtime", () => { expect(requireResolvedWebFetch(resolved).provider.id).toBe("firecrawl"); }); - it("keeps sandboxed web fetch on bundled providers even when runtime providers are preferred", () => { + it("keeps sandboxed web fetch on trusted providers even when runtime providers are preferred", () => { const bundled = createFirecrawlProvider({ getConfiguredCredentialValue: () => "bundled-key", }); @@ -319,6 +319,11 @@ describe("web fetch runtime", () => { }); expect(requireResolvedWebFetch(resolved).provider.id).toBe("firecrawl"); + expect(resolvePluginWebFetchProvidersMock).toHaveBeenCalledWith({ + config: {}, + sandboxed: true, + }); + expect(resolveRuntimeWebFetchProvidersMock).not.toHaveBeenCalled(); }); it("uses runtime providers for non-sandboxed web fetch when runtime providers are preferred", () => { diff --git a/src/web-fetch/runtime.ts b/src/web-fetch/runtime.ts index 6dd0fc84e60..57fc1a2358c 100644 --- a/src/web-fetch/runtime.ts +++ b/src/web-fetch/runtime.ts @@ -193,7 +193,7 @@ export function resolveWebFetchDefinition( options?.sandboxed ? resolvePluginWebFetchProviders({ config: options?.config, - origin: "bundled", + sandboxed: true, }) : options?.preferRuntimeProviders ? resolveRuntimeWebFetchProviders({