diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 1bdfb5f915e0..4d0b84afe0df 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 d4496f9f0343..cd5ddc7be561 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 bf6c282fad98..ac567de58dbe 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 f4817a823c04..1c761fc82503 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 fde1ff90338e..4152c16da254 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 ae9568606584..a80e272fa6c1 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 d44dda878f09..460939772110 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 606709147722..1db2f11e9f99 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 6116125182e3..495538545c18 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 435f0f4051bd..a0879363688d 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 7fbc3afd496a..db170be8a411 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 286a5b0424e9..7488c851f22f 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 9125f4d955d5..564c57c3e6d0 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 ecd3ef267749..9d289e33c1a7 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 71521d56e216..be9795726d48 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 fe102d650e1f..fbfa8c84d633 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 2956b72a7b8f..13a32d304b39 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 f0f89be9b12c..f88c36fbca29 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 60944c43eb42..63d324da4ee8 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 7197ed3d4ba1..74fb899ea4a3 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 58e81688c8bf..6036c48240dd 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 67e8edbdf6be..0db5f8989023 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 6ba03f483a47..661e8500ee4f 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 673c55a017bd..963f9bc6af57 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 2031f374b56c..1212e8e23280 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 f1dc343b536f..1b6fc46d0fe6 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 df8bca88ef5a..27266fedd052 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 6f600287e129..376646afe498 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 72f51073bf2d..c43540a3e99a 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 65fdd8a5a7ba..1bf90bf4d3b9 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 99dbd8287a5f..b8092e20a117 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 f1183f744121..32e7f4c1e160 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 f62225610c22..eb07cde9d125 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 c17d94219cbe..4d843bc0282e 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 50975a97e993..3a196139c938 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 bba2f0ba44cf..5da626b1fb49 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 84f486ccf8eb..683afc90b641 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 30c74f09ee87..e3b131c57ee7 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 913e936ddba1..e39f35bff1ee 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 8a101cc5cf85..cfe4dbc4a579 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 44896881c315..9c4d0e88b524 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 c114bdc5723a..dc219606f7e5 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 9920d2267d3e..29a17822f688 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 5cf8627bf4f7..b45913a39c02 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 13a7d12f206e..591303816418 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 c28bb38da709..f54180e8dd88 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 000000000000..20488012eced --- /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 000000000000..b531279f7563 --- /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 ab6e91f0ed3f..bee064336379 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 000000000000..3d62a3db0ffc --- /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 000000000000..2bb34e772c45 --- /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 baa6144d5da9..87a1b01d6e5f 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 000000000000..4c670d4bf0d0 --- /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 000000000000..485e3228003e --- /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 9101db988e1e..665bbe270d45 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 000000000000..91114608ae73 --- /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 000000000000..e584812be0ad --- /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 14182032e444..7c8097e130be 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 000000000000..6558794cf307 --- /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 000000000000..d70110cd5911 --- /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 2e3bc7defa74..2163fec661b7 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 000000000000..3aa324f9cbac --- /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 000000000000..11b14dc9b870 --- /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 cb398ddeb3fc..ed7ae5358e1e 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 000000000000..a7a55b09a57a --- /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 000000000000..043c861ad4ad --- /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 860ee8d71713..6ec949f56397 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 000000000000..bcf4489cea35 --- /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 000000000000..1897804771f1 --- /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 696d2063eff9..00c8a291cebc 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 000000000000..18a774bf70a8 --- /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 000000000000..cf37db272cbf --- /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 44e5f2dd3bbc..73d62c0f0db9 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 000000000000..532d4358a2de --- /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 201afcf87322..062449ef0637 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 b9cf2f928e32..f63c84bbaf84 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 000000000000..6e2a4041a642 --- /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 2b33e902aaf4..4889bd4d1cd7 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 000000000000..15edd341de42 --- /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 000000000000..df9647f48d9d --- /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 22347cf2fd2d..46f86d7d68f0 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 000000000000..a54b02c90135 --- /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 000000000000..3bc0a0dcc850 --- /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 f5e31485a95f..80f0ef9916a5 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 000000000000..fa75caf1e89c --- /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 000000000000..b7d358d4c363 --- /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 c52ce9cba594..41d2cf196068 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 000000000000..0b718066e331 --- /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 000000000000..4da26cdd13a0 --- /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 99a7d492dae1..5e52a6749095 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 000000000000..8d25dfdf8fa8 --- /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 000000000000..c1871b7de116 --- /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 367aea5955fd..1a5578c545c0 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 000000000000..64483d79333a --- /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 000000000000..9cd7f3360c1a --- /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 8322baee28de..ee18299d3f1d 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 000000000000..75d5db6a7de7 --- /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 000000000000..3251fe6e1b23 --- /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 f476859d32df..b2f1550dbed2 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 000000000000..08f960201db5 --- /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 000000000000..443acb31c582 --- /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 bab868a89a57..fefb90d923a3 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 d90b90562fff..e86c171afbcc 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 162e2149fc54..b2fe10012772 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 6c6c435e04b8..47bc23d4c980 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 469c0a7b49e5..f6bfc98dc5cb 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 f3fd285d60df..0e2c6869413c 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 c783a2f5052f..772975352bbd 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 175ce3b1fe5d..54b98492febc 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 0e02dafead66..8b56301ad9a7 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 6d01fc59f7eb..7234b7751a2e 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 000000000000..3b6068f7ce56 --- /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 f3040e37fd03..f1e62f56f63f 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 762d634cfef1..a1ead9ae6ecd 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 c3a20ec0d0dc..1e0ee10356db 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 0d8489fdaccf..30b206b7469b 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 e4234b66dc7d..9eefc19a54fe 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 e2e5e0a6aca8..8d8c8e046ae5 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 cbb54620a749..18f7aafc5e50 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 5ed6ac0e69cf..c6e1f1ae4fc3 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 93ec3ff93228..09c92df4b21d 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 9a45057be829..a9fc75c108f2 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 2409bd8805d3..3f25730354fb 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 ec16acb9237d..453baf160e2c 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 c9bdafcf569e..94013a0f3e52 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 4d578e6a8f77..178873ccd345 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 a834ef7522a3..3ccbb3e89a42 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 78aea06b851b..f007a72b5fd5 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 506da83dbc5d..ae18078014fa 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 fd297dc6d3e6..6905cb179bbd 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 39d748dd4150..316e3be409e5 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 fa75459a4b9f..1f6be1ab472f 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 3e0286a2d392..3a9f60994a11 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 1a77a717d802..f7cb310c7fb9 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 8af5111cfe6b..59a20a34a566 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 000000000000..5b43640b3549 --- /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 df80c72858eb..efd2737c8b6c 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 7e851fc485b1..1dbac68cee91 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 c02ac2628b77..efa7f20a1309 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 5f4e9259f8b0..9fd6920755c1 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 6dd0fc84e601..57fc1a2358c4 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({