diff --git a/.github/labeler.yml b/.github/labeler.yml index 8535f0e0789e..425c28b22c0b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -355,6 +355,11 @@ - any-glob-to-any-file: - "extensions/deepinfra/**" - "docs/providers/deepinfra.md" +"extensions: gmi": + - changed-files: + - any-glob-to-any-file: + - "extensions/gmi/**" + - "docs/providers/gmi.md" "extensions: tencent": - changed-files: - any-glob-to-any-file: @@ -436,6 +441,11 @@ - changed-files: - any-glob-to-any-file: - "extensions/nvidia/**" +"extensions: novita": + - changed-files: + - any-glob-to-any-file: + - "extensions/novita/**" + - "docs/providers/novita.md" "extensions: phone-control": - changed-files: - any-glob-to-any-file: diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index c26bb269e886..bc7460151447 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -290,32 +290,36 @@ 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` | -| 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-M2.7` | -| 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-super-120b-a12b` | -| OpenRouter | `openrouter` | `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` | -| 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_API_KEY` | `xiaomi/mimo-v2-flash` | +| 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` | +| 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` | - | +| GMI Cloud | `gmi` | `GMI_API_KEY` | `gmi/google/gemini-3.1-flash-lite` | +| 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-M2.7` | +| 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-super-120b-a12b` | +| 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_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_API_KEY` | `xiaomi/mimo-v2-flash` | #### Quirks worth knowing diff --git a/docs/docs.json b/docs/docs.json index b2329fed890f..47d5d76b44a6 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1405,6 +1405,7 @@ "providers/fal", "providers/fireworks", "providers/github-copilot", + "providers/gmi", "providers/google", "providers/gradium", "providers/groq", @@ -1417,8 +1418,10 @@ "providers/minimax", "providers/mistral", "providers/moonshot", + "providers/novita", "providers/nvidia", "providers/ollama", + "providers/ollama-cloud", "providers/openai", "providers/opencode", "providers/opencode-go", @@ -1427,6 +1430,7 @@ "providers/pixverse", "providers/qianfan", "providers/qwen", + "providers/qwen-oauth", "providers/runway", "providers/senseaudio", "providers/sglang", diff --git a/docs/plugins/plugin-inventory.md b/docs/plugins/plugin-inventory.md index fec4fd0a0ccc..d07a076ea44c 100644 --- a/docs/plugins/plugin-inventory.md +++ b/docs/plugins/plugin-inventory.md @@ -79,6 +79,7 @@ commands. | [firecrawl](/plugins/reference/firecrawl) | Adds agent-callable tools. Adds web fetch provider support. Adds web search provider support. | `@openclaw/firecrawl-plugin`
included in OpenClaw | contracts: tools, webFetchProviders, webSearchProviders | | [fireworks](/plugins/reference/fireworks) | Adds Fireworks model provider support to OpenClaw. | `@openclaw/fireworks-provider`
included in OpenClaw | providers: fireworks | | [github-copilot](/plugins/reference/github-copilot) | Adds GitHub Copilot model provider support to OpenClaw. | `@openclaw/github-copilot-provider`
included in OpenClaw | providers: github-copilot; contracts: memoryEmbeddingProviders | +| [gmi](/plugins/reference/gmi) | Adds Gmi, Gmi Cloud, Gmicloud model provider support to OpenClaw. | `@openclaw/gmi-provider`
included in OpenClaw | providers: gmi, gmi-cloud, gmicloud | | [google](/plugins/reference/google) | Adds Google, Google Gemini CLI, Google Vertex model provider support to OpenClaw. | `@openclaw/google-plugin`
included in OpenClaw | providers: google, google-gemini-cli, google-vertex; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, musicGenerationProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders, webSearchProviders | | [gradium](/plugins/reference/gradium) | Adds text-to-speech provider support. | `@openclaw/gradium-speech`
included in OpenClaw | contracts: speechProviders | | [groq](/plugins/reference/groq) | Adds Groq model provider support to OpenClaw. | `@openclaw/groq-provider`
included in OpenClaw | providers: groq; contracts: mediaUnderstandingProviders | @@ -101,9 +102,10 @@ commands. | [minimax](/plugins/reference/minimax) | Adds MiniMax, MiniMax Portal model provider support to OpenClaw. | `@openclaw/minimax-provider`
included in OpenClaw | providers: minimax, minimax-portal; contracts: imageGenerationProviders, mediaUnderstandingProviders, musicGenerationProviders, speechProviders, videoGenerationProviders, webSearchProviders | | [mistral](/plugins/reference/mistral) | Adds Mistral model provider support to OpenClaw. | `@openclaw/mistral-provider`
included in OpenClaw | providers: mistral; contracts: mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders | | [moonshot](/plugins/reference/moonshot) | Adds Moonshot model provider support to OpenClaw. | `@openclaw/moonshot-provider`
included in OpenClaw | providers: moonshot; contracts: mediaUnderstandingProviders, webSearchProviders | +| [novita](/plugins/reference/novita) | Adds Novita, Novita AI, Novitaai model provider support to OpenClaw. | `@openclaw/novita-provider`
included in OpenClaw | providers: novita, novita-ai, novitaai | | [nvidia](/plugins/reference/nvidia) | Adds NVIDIA model provider support to OpenClaw. | `@openclaw/nvidia-provider`
included in OpenClaw | providers: nvidia | | [oc-path](/plugins/reference/oc-path) | Adds the openclaw path CLI for oc:// workspace file addressing. | `@openclaw/oc-path`
included in OpenClaw | plugin | -| [ollama](/plugins/reference/ollama) | Adds Ollama model provider support to OpenClaw. | `@openclaw/ollama-provider`
included in OpenClaw | providers: ollama; contracts: memoryEmbeddingProviders, webSearchProviders | +| [ollama](/plugins/reference/ollama) | Adds Ollama, Ollama Cloud model provider support to OpenClaw. | `@openclaw/ollama-provider`
included in OpenClaw | providers: ollama, ollama-cloud; contracts: memoryEmbeddingProviders, webSearchProviders | | [open-prose](/plugins/reference/open-prose) | OpenProse VM skill pack with a /prose slash command. | `@openclaw/open-prose`
included in OpenClaw | skills | | [openai](/plugins/reference/openai) | Adds OpenAI, OpenAI Codex model provider support to OpenClaw. | `@openclaw/openai-provider`
included in OpenClaw | providers: openai, openai-codex; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders | | [opencode](/plugins/reference/opencode) | Adds OpenCode model provider support to OpenClaw. | `@openclaw/opencode-provider`
included in OpenClaw | providers: opencode; contracts: mediaUnderstandingProviders | @@ -112,7 +114,7 @@ commands. | [perplexity](/plugins/reference/perplexity) | Adds web search provider support. | `@openclaw/perplexity-plugin`
included in OpenClaw | contracts: webSearchProviders | | [policy](/plugins/reference/policy) | Adds policy-backed doctor checks for workspace conformance. | `@openclaw/policy`
included in OpenClaw | plugin | | [qianfan](/plugins/reference/qianfan) | Adds Qianfan model provider support to OpenClaw. | `@openclaw/qianfan-provider`
included in OpenClaw | providers: qianfan | -| [qwen](/plugins/reference/qwen) | Adds Qwen, Qwen Cloud, Model Studio, DashScope model provider support to OpenClaw. | `@openclaw/qwen-provider`
included in OpenClaw | providers: qwen, qwencloud, modelstudio, dashscope; contracts: mediaUnderstandingProviders, videoGenerationProviders | +| [qwen](/plugins/reference/qwen) | Adds Qwen, Qwen Cloud, Model Studio, DashScope, Qwen Oauth, Qwen Portal, Qwen CLI model provider support to OpenClaw. | `@openclaw/qwen-provider`
included in OpenClaw | providers: qwen, qwencloud, modelstudio, dashscope, qwen-oauth, qwen-portal, qwen-cli; contracts: mediaUnderstandingProviders, videoGenerationProviders | | [runway](/plugins/reference/runway) | Adds video generation provider support. | `@openclaw/runway-provider`
included in OpenClaw | contracts: videoGenerationProviders | | [searxng](/plugins/reference/searxng) | Adds web search provider support. | `@openclaw/searxng-plugin`
included in OpenClaw | contracts: webSearchProviders | | [senseaudio](/plugins/reference/senseaudio) | Adds media understanding provider support. | `@openclaw/senseaudio-provider`
included in OpenClaw | contracts: mediaUnderstandingProviders | diff --git a/docs/plugins/reference.md b/docs/plugins/reference.md index 0de6467b787a..8536ab77cfd9 100644 --- a/docs/plugins/reference.md +++ b/docs/plugins/reference.md @@ -58,6 +58,7 @@ pnpm plugins:inventory:gen | [firecrawl](/plugins/reference/firecrawl) | Adds agent-callable tools. Adds web fetch provider support. Adds web search provider support. | `@openclaw/firecrawl-plugin`
included in OpenClaw | contracts: tools, webFetchProviders, webSearchProviders | | [fireworks](/plugins/reference/fireworks) | Adds Fireworks model provider support to OpenClaw. | `@openclaw/fireworks-provider`
included in OpenClaw | providers: fireworks | | [github-copilot](/plugins/reference/github-copilot) | Adds GitHub Copilot model provider support to OpenClaw. | `@openclaw/github-copilot-provider`
included in OpenClaw | providers: github-copilot; contracts: memoryEmbeddingProviders | +| [gmi](/plugins/reference/gmi) | Adds Gmi, Gmi Cloud, Gmicloud model provider support to OpenClaw. | `@openclaw/gmi-provider`
included in OpenClaw | providers: gmi, gmi-cloud, gmicloud | | [google](/plugins/reference/google) | Adds Google, Google Gemini CLI, Google Vertex model provider support to OpenClaw. | `@openclaw/google-plugin`
included in OpenClaw | providers: google, google-gemini-cli, google-vertex; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, musicGenerationProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders, webSearchProviders | | [google-meet](/plugins/reference/google-meet) | OpenClaw Google Meet participant plugin for joining calls through Chrome or Twilio transports. | `@openclaw/google-meet`
npm; ClawHub | contracts: tools | | [googlechat](/plugins/reference/googlechat) | OpenClaw Google Chat channel plugin for spaces and direct messages. | `@openclaw/googlechat`
npm; ClawHub | channels: googlechat | @@ -89,9 +90,10 @@ pnpm plugins:inventory:gen | [msteams](/plugins/reference/msteams) | OpenClaw Microsoft Teams channel plugin for bot conversations. | `@openclaw/msteams`
npm; ClawHub | channels: msteams | | [nextcloud-talk](/plugins/reference/nextcloud-talk) | OpenClaw Nextcloud Talk channel plugin for conversations. | `@openclaw/nextcloud-talk`
npm; ClawHub | channels: nextcloud-talk | | [nostr](/plugins/reference/nostr) | OpenClaw Nostr channel plugin for NIP-04 encrypted direct messages. | `@openclaw/nostr`
npm; ClawHub | channels: nostr | +| [novita](/plugins/reference/novita) | Adds Novita, Novita AI, Novitaai model provider support to OpenClaw. | `@openclaw/novita-provider`
included in OpenClaw | providers: novita, novita-ai, novitaai | | [nvidia](/plugins/reference/nvidia) | Adds NVIDIA model provider support to OpenClaw. | `@openclaw/nvidia-provider`
included in OpenClaw | providers: nvidia | | [oc-path](/plugins/reference/oc-path) | Adds the openclaw path CLI for oc:// workspace file addressing. | `@openclaw/oc-path`
included in OpenClaw | plugin | -| [ollama](/plugins/reference/ollama) | Adds Ollama model provider support to OpenClaw. | `@openclaw/ollama-provider`
included in OpenClaw | providers: ollama; contracts: memoryEmbeddingProviders, webSearchProviders | +| [ollama](/plugins/reference/ollama) | Adds Ollama, Ollama Cloud model provider support to OpenClaw. | `@openclaw/ollama-provider`
included in OpenClaw | providers: ollama, ollama-cloud; contracts: memoryEmbeddingProviders, webSearchProviders | | [open-prose](/plugins/reference/open-prose) | OpenProse VM skill pack with a /prose slash command. | `@openclaw/open-prose`
included in OpenClaw | skills | | [openai](/plugins/reference/openai) | Adds OpenAI, OpenAI Codex model provider support to OpenClaw. | `@openclaw/openai-provider`
included in OpenClaw | providers: openai, openai-codex; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders, realtimeVoiceProviders, speechProviders, videoGenerationProviders | | [opencode](/plugins/reference/opencode) | Adds OpenCode model provider support to OpenClaw. | `@openclaw/opencode-provider`
included in OpenClaw | providers: opencode; contracts: mediaUnderstandingProviders | @@ -106,7 +108,7 @@ pnpm plugins:inventory:gen | [qa-matrix](/plugins/reference/qa-matrix) | Matrix QA transport runner and substrate. | `@openclaw/qa-matrix`
source checkout only | plugin | | [qianfan](/plugins/reference/qianfan) | Adds Qianfan model provider support to OpenClaw. | `@openclaw/qianfan-provider`
included in OpenClaw | providers: qianfan | | [qqbot](/plugins/reference/qqbot) | OpenClaw QQ Bot channel plugin for group and direct-message workflows. | `@openclaw/qqbot`
npm; ClawHub | channels: qqbot; contracts: tools; skills | -| [qwen](/plugins/reference/qwen) | Adds Qwen, Qwen Cloud, Model Studio, DashScope model provider support to OpenClaw. | `@openclaw/qwen-provider`
included in OpenClaw | providers: qwen, qwencloud, modelstudio, dashscope; contracts: mediaUnderstandingProviders, videoGenerationProviders | +| [qwen](/plugins/reference/qwen) | Adds Qwen, Qwen Cloud, Model Studio, DashScope, Qwen Oauth, Qwen Portal, Qwen CLI model provider support to OpenClaw. | `@openclaw/qwen-provider`
included in OpenClaw | providers: qwen, qwencloud, modelstudio, dashscope, qwen-oauth, qwen-portal, qwen-cli; contracts: mediaUnderstandingProviders, videoGenerationProviders | | [runway](/plugins/reference/runway) | Adds video generation provider support. | `@openclaw/runway-provider`
included in OpenClaw | contracts: videoGenerationProviders | | [searxng](/plugins/reference/searxng) | Adds web search provider support. | `@openclaw/searxng-plugin`
included in OpenClaw | contracts: webSearchProviders | | [senseaudio](/plugins/reference/senseaudio) | Adds media understanding provider support. | `@openclaw/senseaudio-provider`
included in OpenClaw | contracts: mediaUnderstandingProviders | diff --git a/docs/plugins/reference/gmi.md b/docs/plugins/reference/gmi.md new file mode 100644 index 000000000000..c5fece02dcf1 --- /dev/null +++ b/docs/plugins/reference/gmi.md @@ -0,0 +1,23 @@ +--- +summary: "Adds Gmi, Gmi Cloud, Gmicloud model provider support to OpenClaw." +read_when: + - You are installing, configuring, or auditing the gmi plugin +title: "Gmi plugin" +--- + +# Gmi plugin + +Adds Gmi, Gmi Cloud, Gmicloud model provider support to OpenClaw. + +## Distribution + +- Package: `@openclaw/gmi-provider` +- Install route: included in OpenClaw + +## Surface + +providers: gmi, gmi-cloud, gmicloud + +## Related docs + +- [gmi](/providers/gmi) diff --git a/docs/plugins/reference/novita.md b/docs/plugins/reference/novita.md new file mode 100644 index 000000000000..911a20b9a7ad --- /dev/null +++ b/docs/plugins/reference/novita.md @@ -0,0 +1,23 @@ +--- +summary: "Adds Novita, Novita AI, Novitaai model provider support to OpenClaw." +read_when: + - You are installing, configuring, or auditing the novita plugin +title: "Novita plugin" +--- + +# Novita plugin + +Adds Novita, Novita AI, Novitaai model provider support to OpenClaw. + +## Distribution + +- Package: `@openclaw/novita-provider` +- Install route: included in OpenClaw + +## Surface + +providers: novita, novita-ai, novitaai + +## Related docs + +- [novita](/providers/novita) diff --git a/docs/plugins/reference/ollama.md b/docs/plugins/reference/ollama.md index a2eac70d141e..c22a34cebfe7 100644 --- a/docs/plugins/reference/ollama.md +++ b/docs/plugins/reference/ollama.md @@ -1,5 +1,5 @@ --- -summary: "Adds Ollama model provider support to OpenClaw." +summary: "Adds Ollama, Ollama Cloud model provider support to OpenClaw." read_when: - You are installing, configuring, or auditing the ollama plugin title: "Ollama plugin" @@ -7,7 +7,7 @@ title: "Ollama plugin" # Ollama plugin -Adds Ollama model provider support to OpenClaw. +Adds Ollama, Ollama Cloud model provider support to OpenClaw. ## Distribution @@ -16,7 +16,7 @@ Adds Ollama model provider support to OpenClaw. ## Surface -providers: ollama; contracts: memoryEmbeddingProviders, webSearchProviders +providers: ollama, ollama-cloud; contracts: memoryEmbeddingProviders, webSearchProviders ## Related docs diff --git a/docs/plugins/reference/qwen.md b/docs/plugins/reference/qwen.md index 489261229b98..c5b73b4e8ca1 100644 --- a/docs/plugins/reference/qwen.md +++ b/docs/plugins/reference/qwen.md @@ -1,5 +1,5 @@ --- -summary: "Adds Qwen, Qwen Cloud, Model Studio, DashScope model provider support to OpenClaw." +summary: "Adds Qwen, Qwen Cloud, Model Studio, DashScope, Qwen Oauth, Qwen Portal, Qwen CLI model provider support to OpenClaw." read_when: - You are installing, configuring, or auditing the qwen plugin title: "Qwen plugin" @@ -7,7 +7,7 @@ title: "Qwen plugin" # Qwen plugin -Adds Qwen, Qwen Cloud, Model Studio, DashScope model provider support to OpenClaw. +Adds Qwen, Qwen Cloud, Model Studio, DashScope, Qwen Oauth, Qwen Portal, Qwen CLI model provider support to OpenClaw. ## Distribution @@ -16,7 +16,7 @@ Adds Qwen, Qwen Cloud, Model Studio, DashScope model provider support to OpenCla ## Surface -providers: qwen, qwencloud, modelstudio, dashscope; contracts: mediaUnderstandingProviders, videoGenerationProviders +providers: qwen, qwencloud, modelstudio, dashscope, qwen-oauth, qwen-portal, qwen-cli; contracts: mediaUnderstandingProviders, videoGenerationProviders ## Related docs diff --git a/docs/providers/gmi.md b/docs/providers/gmi.md new file mode 100644 index 000000000000..5cc64e41b381 --- /dev/null +++ b/docs/providers/gmi.md @@ -0,0 +1,47 @@ +--- +summary: "Use GMI Cloud's OpenAI-compatible API with OpenClaw" +read_when: + - You want to run OpenClaw with GMI Cloud models + - You need the GMI provider id, key, or endpoint +title: "GMI Cloud" +--- + +GMI Cloud is a bundled OpenAI-compatible provider. Use provider id `gmi` and model refs like `gmi/google/gemini-3.1-flash-lite`. + +## Setup + +Create an API key in GMI Cloud, then run: + +```bash +openclaw onboard --auth-choice gmi-api-key +``` + +Or set: + +```bash +export GMI_API_KEY="" # pragma: allowlist secret +``` + +## Defaults + +- Provider: `gmi` +- Aliases: `gmi-cloud`, `gmicloud` +- Base URL: `https://api.gmi-serving.com/v1` +- Env var: `GMI_API_KEY` +- Default model: `gmi/google/gemini-3.1-flash-lite` + +## Models + +The bundled catalog seeds commonly available GMI Cloud route ids, including: + +- `gmi/zai-org/GLM-5.1-FP8` +- `gmi/deepseek-ai/DeepSeek-V3.2` +- `gmi/moonshotai/Kimi-K2.5` +- `gmi/google/gemini-3.1-flash-lite` +- `gmi/anthropic/claude-sonnet-4.6` +- `gmi/openai/gpt-5.4` + +## Related + +- [Model providers](/concepts/model-providers) +- [All providers](/providers/index) diff --git a/docs/providers/index.md b/docs/providers/index.md index a800de392657..02bcafa299df 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -41,6 +41,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi - [fal](/providers/fal) - [Fireworks](/providers/fireworks) - [GitHub Copilot](/providers/github-copilot) +- [GMI Cloud](/providers/gmi) - [Google (Gemini)](/providers/google) - [Gradium](/providers/gradium) - [Groq (LPU inference)](/providers/groq) @@ -53,7 +54,9 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi - [Mistral](/providers/mistral) - [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot) - [NVIDIA](/providers/nvidia) +- [NovitaAI](/providers/novita) - [Ollama (cloud + local models)](/providers/ollama) +- [Ollama Cloud](/providers/ollama-cloud) - [OpenAI (API + Codex)](/providers/openai) - [OpenCode](/providers/opencode) - [OpenCode Go](/providers/opencode-go) @@ -61,6 +64,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi - [Perplexity (web search)](/providers/perplexity-provider) - [Qianfan](/providers/qianfan) - [Qwen Cloud](/providers/qwen) +- [Qwen OAuth / Portal](/providers/qwen-oauth) - [Runway](/providers/runway) - [SenseAudio](/providers/senseaudio) - [SGLang (local models)](/providers/sglang) diff --git a/docs/providers/novita.md b/docs/providers/novita.md new file mode 100644 index 000000000000..a0954f9b3e78 --- /dev/null +++ b/docs/providers/novita.md @@ -0,0 +1,47 @@ +--- +summary: "Use NovitaAI's OpenAI-compatible API with OpenClaw" +read_when: + - You want to run OpenClaw with NovitaAI models + - You need the Novita provider id, key, or endpoint +title: "NovitaAI" +--- + +NovitaAI is a bundled OpenAI-compatible provider. Use provider id `novita` and model refs like `novita/deepseek/deepseek-v3-0324`. + +## Setup + +Create an API key at [novita.ai/settings/key-management](https://novita.ai/settings/key-management), then run: + +```bash +openclaw onboard --auth-choice novita-api-key +``` + +Or set: + +```bash +export NOVITA_API_KEY="" # pragma: allowlist secret +``` + +## Defaults + +- Provider: `novita` +- Aliases: `novita-ai`, `novitaai` +- Base URL: `https://api.novita.ai/openai/v1` +- Env var: `NOVITA_API_KEY` +- Default model: `novita/deepseek/deepseek-v3-0324` + +## Models + +The bundled catalog seeds commonly available NovitaAI route ids, including: + +- `novita/moonshotai/kimi-k2.5` +- `novita/minimax/minimax-m2.7` +- `novita/zai-org/glm-5` +- `novita/deepseek/deepseek-v3-0324` +- `novita/deepseek/deepseek-r1-0528` +- `novita/qwen/qwen3-235b-a22b-fp8` + +## Related + +- [Model providers](/concepts/model-providers) +- [All providers](/providers/index) diff --git a/docs/providers/ollama-cloud.md b/docs/providers/ollama-cloud.md new file mode 100644 index 000000000000..111ac751b05e --- /dev/null +++ b/docs/providers/ollama-cloud.md @@ -0,0 +1,80 @@ +--- +summary: "Use Ollama Cloud directly with OpenClaw" +read_when: + - You want to use hosted Ollama models without a local Ollama server + - You need the ollama-cloud provider id, key, or endpoint +title: "Ollama Cloud" +--- + +Ollama Cloud is a first-class hosted provider id for Ollama's cloud API. Use +provider id `ollama-cloud` and model refs like `ollama-cloud/kimi-k2.6`. + +Use this page when you want cloud-only routing. For local Ollama, hybrid +cloud-plus-local routing, embeddings, and custom host details, see +[Ollama](/providers/ollama). + +## Setup + +Create an Ollama Cloud API key at [ollama.com/settings/keys](https://ollama.com/settings/keys), then run: + +```bash +openclaw onboard --auth-choice ollama-cloud +``` + +Or set: + +```bash +export OLLAMA_API_KEY="" # pragma: allowlist secret +``` + +## Defaults + +- Provider: `ollama-cloud` +- Base URL: `https://ollama.com` +- Env var: `OLLAMA_API_KEY` +- API style: Ollama native `/api/chat` +- Example model: `ollama-cloud/kimi-k2.6` + +## Models + +OpenClaw discovers Ollama Cloud models from the live hosted catalog. Commonly +available hosted ids include: + +- `ollama-cloud/gpt-oss:20b` +- `ollama-cloud/kimi-k2.6` +- `ollama-cloud/deepseek-v4-flash` +- `ollama-cloud/minimax-m2.7` +- `ollama-cloud/glm-5` + +Use a model id from your current hosted catalog: + +```bash +openclaw models list --provider ollama-cloud +openclaw models set ollama-cloud/kimi-k2.6 +``` + +## Live test + +For Ollama Cloud API-key smoke tests, point the Ollama live test at the hosted +endpoint and choose a model from your current catalog: + +```bash +export OLLAMA_API_KEY="" # pragma: allowlist secret + +OPENCLAW_LIVE_TEST=1 \ +OPENCLAW_LIVE_OLLAMA=1 \ +OPENCLAW_LIVE_OLLAMA_BASE_URL=https://ollama.com \ +OPENCLAW_LIVE_OLLAMA_MODEL=kimi-k2.6 \ +OPENCLAW_LIVE_OLLAMA_WEB_SEARCH=1 \ +pnpm test:live -- extensions/ollama/ollama.live.test.ts +``` + +The cloud smoke runs text, native stream, and web search. It skips embeddings by +default for `https://ollama.com` because Ollama Cloud API keys may not authorize +`/api/embed`. + +## Related + +- [Ollama](/providers/ollama) +- [Model providers](/concepts/model-providers) +- [All providers](/providers/index) diff --git a/docs/providers/ollama.md b/docs/providers/ollama.md index 5717e1d92725..1051dbdb0783 100644 --- a/docs/providers/ollama.md +++ b/docs/providers/ollama.md @@ -9,6 +9,12 @@ title: "Ollama" OpenClaw integrates with Ollama's native API (`/api/chat`) for hosted cloud models and local/self-hosted Ollama servers. You can use Ollama in three modes: `Cloud + Local` through a reachable Ollama host, `Cloud only` against `https://ollama.com`, or `Local only` against a reachable Ollama host. +OpenClaw also registers `ollama-cloud` as a first-class hosted provider id for +direct Ollama Cloud use. Use refs like `ollama-cloud/kimi-k2.5:cloud` when you +want cloud-only routing without sharing the local `ollama` provider id. + +For the dedicated cloud-only setup page, see [Ollama Cloud](/providers/ollama-cloud). + **Remote Ollama users**: Do not use the `/v1` OpenAI-compatible URL (`http://host:11434/v1`) with OpenClaw. This breaks tool calling and models may output raw tool JSON as plain text. Use the native Ollama API URL instead: `baseUrl: "http://host:11434"` (no `/v1`). @@ -22,7 +28,7 @@ Ollama provider config uses `baseUrl` as the canonical key. OpenClaw also accept Local and LAN Ollama hosts do not need a real bearer token. OpenClaw uses the local `ollama-local` marker only for loopback, private-network, `.local`, and bare-hostname Ollama base URLs. - Remote public hosts and Ollama Cloud (`https://ollama.com`) require a real credential through `OLLAMA_API_KEY`, an auth profile, or the provider's `apiKey`. + Remote public hosts and Ollama Cloud (`https://ollama.com`) require a real credential through `OLLAMA_API_KEY`, an auth profile, or the provider's `apiKey`. For direct hosted use, prefer provider `ollama-cloud`. Custom provider ids that set `api: "ollama"` follow the same rules. For example, an `ollama-remote` provider that points at a private LAN Ollama host can use `apiKey: "ollama-local"` and sub-agents will resolve that marker through the Ollama provider hook instead of treating it as a missing credential. Memory search can also set `agents.defaults.memorySearch.provider` to that custom provider id so embeddings use the matching Ollama endpoint. @@ -167,6 +173,13 @@ Choose your preferred setup method and mode. The cloud model list shown during `openclaw onboard` is populated live from `https://ollama.com/api/tags`, capped at 500 entries, so the picker reflects the current hosted catalog rather than a static seed. If `ollama.com` is unreachable or returns no models at setup time, OpenClaw falls back to the previous hardcoded suggestions so onboarding still completes. + You can also configure the first-class cloud provider directly: + + ```bash + openclaw onboard --auth-choice ollama-cloud + openclaw models set ollama-cloud/kimi-k2.5:cloud + ``` + diff --git a/docs/providers/qwen-oauth.md b/docs/providers/qwen-oauth.md new file mode 100644 index 000000000000..3c7ce2549916 --- /dev/null +++ b/docs/providers/qwen-oauth.md @@ -0,0 +1,76 @@ +--- +summary: "Use the Qwen Portal provider id with OpenClaw" +read_when: + - You want to configure the qwen-oauth provider id + - You previously used Qwen Portal OAuth credentials + - You need the Qwen Portal endpoint or migration guidance +title: "Qwen OAuth / Portal" +--- + +`qwen-oauth` is the Qwen Portal provider id. It targets the Qwen Portal endpoint +and keeps older Qwen OAuth / portal setups addressable through a distinct +provider id. + +For new Qwen Cloud setups, prefer [Qwen](/providers/qwen) with the Standard +ModelStudio endpoint unless you specifically have a current Qwen Portal token. + +## Setup + +Provide your portal token through onboarding: + +```bash +openclaw onboard --auth-choice qwen-oauth +``` + +Or set: + +```bash +export QWEN_API_KEY="" # pragma: allowlist secret +``` + +## Defaults + +- Provider: `qwen-oauth` +- Aliases: `qwen-portal`, `qwen-cli` +- Base URL: `https://portal.qwen.ai/v1` +- Env var: `QWEN_API_KEY` +- API style: OpenAI-compatible +- Default model: `qwen-oauth/qwen3.5-plus` + +## Models + +The bundled catalog seeds the Qwen Portal default: + +- `qwen-oauth/qwen3.5-plus` + +Availability depends on the current Qwen Portal account and token. If your +account uses ModelStudio / DashScope API keys instead, configure the canonical +`qwen` provider: + +```bash +openclaw onboard --auth-choice qwen-standard-api-key +openclaw models set qwen/qwen3-coder-plus +``` + +## Migration + +Legacy Qwen Portal OAuth profiles may not be refreshable. If a portal profile +stops working, re-authenticate with a current token or switch to the Standard +Qwen provider: + +```bash +openclaw onboard --auth-choice qwen-standard-api-key +``` + +Standard global ModelStudio uses: + +```text +https://dashscope-intl.aliyuncs.com/compatible-mode/v1 +``` + +## Related + +- [Qwen](/providers/qwen) +- [Alibaba Model Studio](/providers/alibaba) +- [Model providers](/concepts/model-providers) +- [All providers](/providers/index) diff --git a/docs/providers/qwen.md b/docs/providers/qwen.md index 869de6575dfa..30c74f09ee87 100644 --- a/docs/providers/qwen.md +++ b/docs/providers/qwen.md @@ -6,21 +6,13 @@ read_when: title: "Qwen" --- - - -**Qwen OAuth has been removed.** The free-tier OAuth integration -(`qwen-portal`) that used `portal.qwen.ai` endpoints is no longer available. -See [Issue #49557](https://github.com/openclaw/openclaw/issues/49557) for -background. - - - OpenClaw now treats Qwen as a first-class bundled provider with canonical id `qwen`. The bundled provider targets the Qwen Cloud / Alibaba DashScope and -Coding Plan endpoints and keeps legacy `modelstudio` ids working as a -compatibility alias. +Coding Plan endpoints, keeps legacy `modelstudio` ids working as a compatibility +alias, and also exposes the Qwen Portal token flow as provider `qwen-oauth`. - Provider: `qwen` +- Portal provider: [`qwen-oauth`](/providers/qwen-oauth) - Preferred env var: `QWEN_API_KEY` - Also accepted for compatibility: `MODELSTUDIO_API_KEY`, `DASHSCOPE_API_KEY` - API style: OpenAI-compatible @@ -132,6 +124,44 @@ Choose your plan type and follow the setup steps. + + + **Best for:** a Qwen Portal token against `https://portal.qwen.ai/v1`. + + See [Qwen OAuth / Portal](/providers/qwen-oauth) for the dedicated provider + page and migration notes. + + + + ```bash + openclaw onboard --auth-choice qwen-oauth + ``` + + + ```json5 + { + agents: { + defaults: { + model: { primary: "qwen-oauth/qwen3.5-plus" }, + }, + }, + } + ``` + + + ```bash + openclaw models list --provider qwen-oauth + ``` + + + + + `qwen-oauth` uses the same `QWEN_API_KEY` env var name as the DashScope + provider, but stores auth under the `qwen-oauth` provider id when configured + through OpenClaw onboarding. + + + ## Plan types and endpoints @@ -142,6 +172,7 @@ Choose your plan type and follow the setup steps. | Standard (pay-as-you-go) | Global | `qwen-standard-api-key` | `dashscope-intl.aliyuncs.com/compatible-mode/v1` | | Coding Plan (subscription) | China | `qwen-api-key-cn` | `coding.dashscope.aliyuncs.com/v1` | | Coding Plan (subscription) | Global | `qwen-api-key` | `coding-intl.dashscope.aliyuncs.com/v1` | +| Qwen Portal | Global | `qwen-oauth` | `portal.qwen.ai/v1` | The provider auto-selects the endpoint based on your auth choice. Canonical choices use the `qwen-*` family; `modelstudio-*` remains compatibility-only. @@ -169,6 +200,7 @@ the Standard endpoint. | `qwen/glm-5` | text | 202,752 | GLM | | `qwen/glm-4.7` | text | 202,752 | GLM | | `qwen/kimi-k2.5` | text, image | 262,144 | Moonshot AI via Alibaba | +| `qwen-oauth/qwen3.5-plus` | text, image | 1,000,000 | Qwen Portal default | Availability can still vary by endpoint and billing plan even when a model is diff --git a/extensions/gmi/index.test.ts b/extensions/gmi/index.test.ts new file mode 100644 index 000000000000..feddcd782c64 --- /dev/null +++ b/extensions/gmi/index.test.ts @@ -0,0 +1,38 @@ +import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime"; +import { describe, expect, it } from "vitest"; +import plugin from "./index.js"; + +function requireCatalogProvider( + result: + | { provider: { baseUrl?: string; models?: Array<{ id: string }> } } + | { providers: Record } + | null + | undefined, +): { baseUrl?: string; models?: Array<{ id: string }> } { + if (!result || !("provider" in result)) { + throw new Error("single provider catalog result missing"); + } + return result.provider; +} + +describe("gmi provider plugin", () => { + it("registers GMI Cloud as an OpenAI-compatible provider", async () => { + const provider = await registerSingleProviderPlugin(plugin); + + expect(provider.id).toBe("gmi"); + expect(provider.aliases).toEqual(["gmi-cloud", "gmicloud"]); + expect(provider.envVars).toEqual(["GMI_API_KEY"]); + expect(provider.auth?.map((method) => method.id)).toEqual(["api-key"]); + + const result = await provider.staticCatalog?.run({ + config: {}, + env: {}, + resolveProviderApiKey: () => ({}), + } as never); + const catalogProvider = requireCatalogProvider(result); + expect(catalogProvider.baseUrl).toBe("https://api.gmi-serving.com/v1"); + expect(catalogProvider.models?.map((model) => model.id)).toContain( + "google/gemini-3.1-flash-lite", + ); + }); +}); diff --git a/extensions/gmi/index.ts b/extensions/gmi/index.ts new file mode 100644 index 000000000000..541b7f4dc4a6 --- /dev/null +++ b/extensions/gmi/index.ts @@ -0,0 +1,49 @@ +import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; +import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools"; +import { GMI_DEFAULT_MODEL_REF } from "./models.js"; +import { buildGmiProvider } from "./provider-catalog.js"; + +const PROVIDER_ID = "gmi"; + +export default defineSingleProviderPluginEntry({ + id: PROVIDER_ID, + name: "GMI Cloud Provider", + description: "Bundled GMI Cloud provider plugin", + provider: { + label: "GMI Cloud", + docsPath: "/providers/gmi", + aliases: ["gmi-cloud", "gmicloud"], + envVars: ["GMI_API_KEY"], + auth: [ + { + methodId: "api-key", + label: "GMI Cloud API key", + hint: "OpenAI-compatible GMI Cloud endpoint", + optionKey: "gmiApiKey", + flagName: "--gmi-api-key", + envVar: "GMI_API_KEY", + promptMessage: "Enter GMI Cloud API key", + defaultModel: GMI_DEFAULT_MODEL_REF, + noteTitle: "GMI Cloud", + noteMessage: "Manage API keys at https://www.gmicloud.ai/", + }, + ], + catalog: { + buildProvider: buildGmiProvider, + buildStaticProvider: buildGmiProvider, + allowExplicitBaseUrl: true, + }, + augmentModelCatalog: ({ config }) => + readConfiguredProviderCatalogEntries({ + config, + providerId: PROVIDER_ID, + }), + ...buildProviderReplayFamilyHooks({ + family: "openai-compatible", + dropReasoningFromHistory: false, + }), + ...buildProviderToolCompatFamilyHooks("openai"), + }, +}); diff --git a/extensions/gmi/models.ts b/extensions/gmi/models.ts new file mode 100644 index 000000000000..b324f453ad28 --- /dev/null +++ b/extensions/gmi/models.ts @@ -0,0 +1,19 @@ +import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared"; +import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import manifest from "./openclaw.plugin.json" with { type: "json" }; + +const GMI_MANIFEST_PROVIDER = buildManifestModelProviderConfig({ + providerId: "gmi", + catalog: manifest.modelCatalog.providers.gmi, +}); + +export const GMI_BASE_URL = GMI_MANIFEST_PROVIDER.baseUrl; +export const GMI_MODEL_CATALOG: ModelDefinitionConfig[] = GMI_MANIFEST_PROVIDER.models; +export const GMI_DEFAULT_MODEL_REF = "gmi/google/gemini-3.1-flash-lite"; + +export function buildGmiModelDefinition(model: ModelDefinitionConfig): ModelDefinitionConfig { + return { + ...model, + api: "openai-completions", + }; +} diff --git a/extensions/gmi/openclaw.plugin.json b/extensions/gmi/openclaw.plugin.json new file mode 100644 index 000000000000..dca4a21493db --- /dev/null +++ b/extensions/gmi/openclaw.plugin.json @@ -0,0 +1,162 @@ +{ + "id": "gmi", + "activation": { + "onStartup": false + }, + "enabledByDefault": true, + "providers": ["gmi", "gmi-cloud", "gmicloud"], + "providerAuthAliases": { + "gmi-cloud": "gmi", + "gmicloud": "gmi" + }, + "providerEndpoints": [ + { + "endpointClass": "gmi-native", + "hosts": ["api.gmi-serving.com"] + } + ], + "providerRequest": { + "providers": { + "gmi": { + "family": "gmi" + }, + "gmi-cloud": { + "family": "gmi" + }, + "gmicloud": { + "family": "gmi" + } + } + }, + "setup": { + "providers": [ + { + "id": "gmi", + "envVars": ["GMI_API_KEY"] + } + ] + }, + "providerAuthChoices": [ + { + "provider": "gmi", + "method": "api-key", + "choiceId": "gmi-api-key", + "choiceLabel": "GMI Cloud API key", + "choiceHint": "OpenAI-compatible GMI Cloud endpoint", + "groupId": "gmi", + "groupLabel": "GMI Cloud", + "groupHint": "OpenAI-compatible GMI Cloud endpoint", + "optionKey": "gmiApiKey", + "cliFlag": "--gmi-api-key", + "cliOption": "--gmi-api-key ", + "cliDescription": "GMI Cloud API key" + } + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "modelCatalog": { + "aliases": { + "gmi-cloud": { + "provider": "gmi" + }, + "gmicloud": { + "provider": "gmi" + } + }, + "providers": { + "gmi": { + "baseUrl": "https://api.gmi-serving.com/v1", + "api": "openai-completions", + "models": [ + { + "id": "zai-org/GLM-5.1-FP8", + "name": "GLM-5.1 FP8", + "reasoning": true, + "input": ["text"], + "contextWindow": 202752, + "maxTokens": 65536, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "deepseek-ai/DeepSeek-V3.2", + "name": "DeepSeek V3.2", + "reasoning": false, + "input": ["text"], + "contextWindow": 163840, + "maxTokens": 65536, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "moonshotai/Kimi-K2.5", + "name": "Kimi K2.5", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 262144, + "maxTokens": 65536, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "google/gemini-3.1-flash-lite", + "name": "Gemini 3.1 Flash Lite", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 1048576, + "maxTokens": 65536, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "anthropic/claude-sonnet-4.6", + "name": "Claude Sonnet 4.6", + "reasoning": false, + "input": ["text", "image"], + "contextWindow": 200000, + "maxTokens": 64000, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "openai/gpt-5.4", + "name": "GPT-5.4", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 400000, + "maxTokens": 128000, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + } + ] + } + } + } +} diff --git a/extensions/gmi/package.json b/extensions/gmi/package.json new file mode 100644 index 000000000000..d87f40ee7cb9 --- /dev/null +++ b/extensions/gmi/package.json @@ -0,0 +1,13 @@ +{ + "name": "@openclaw/gmi-provider", + "version": "2026.5.28", + "private": true, + "description": "OpenClaw GMI Cloud provider plugin", + "type": "module", + "devDependencies": { + "@openclaw/plugin-sdk": "workspace:*" + }, + "openclaw": { + "extensions": ["./index.ts"] + } +} diff --git a/extensions/gmi/provider-catalog.ts b/extensions/gmi/provider-catalog.ts new file mode 100644 index 000000000000..cc7a87ea246a --- /dev/null +++ b/extensions/gmi/provider-catalog.ts @@ -0,0 +1,10 @@ +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import { GMI_BASE_URL, GMI_MODEL_CATALOG, buildGmiModelDefinition } from "./models.js"; + +export function buildGmiProvider(): ModelProviderConfig { + return { + baseUrl: GMI_BASE_URL, + api: "openai-completions", + models: GMI_MODEL_CATALOG.map(buildGmiModelDefinition), + }; +} diff --git a/extensions/gmi/tsconfig.json b/extensions/gmi/tsconfig.json new file mode 100644 index 000000000000..b8a85a99ac3d --- /dev/null +++ b/extensions/gmi/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.package-boundary.base.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["./*.ts", "./src/**/*.ts"], + "exclude": [ + "./**/*.test.ts", + "./dist/**", + "./node_modules/**", + "./src/test-support/**", + "./src/**/*test-helpers.ts", + "./src/**/*test-harness.ts", + "./src/**/*test-support.ts" + ] +} diff --git a/extensions/novita/index.test.ts b/extensions/novita/index.test.ts new file mode 100644 index 000000000000..bb392296f9d9 --- /dev/null +++ b/extensions/novita/index.test.ts @@ -0,0 +1,36 @@ +import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime"; +import { describe, expect, it } from "vitest"; +import plugin from "./index.js"; + +function requireCatalogProvider( + result: + | { provider: { baseUrl?: string; models?: Array<{ id: string }> } } + | { providers: Record } + | null + | undefined, +): { baseUrl?: string; models?: Array<{ id: string }> } { + if (!result || !("provider" in result)) { + throw new Error("single provider catalog result missing"); + } + return result.provider; +} + +describe("novita provider plugin", () => { + it("registers NovitaAI as an OpenAI-compatible provider", async () => { + const provider = await registerSingleProviderPlugin(plugin); + + expect(provider.id).toBe("novita"); + expect(provider.aliases).toEqual(["novita-ai", "novitaai"]); + expect(provider.envVars).toEqual(["NOVITA_API_KEY"]); + expect(provider.auth?.map((method) => method.id)).toEqual(["api-key"]); + + const result = await provider.staticCatalog?.run({ + config: {}, + env: {}, + resolveProviderApiKey: () => ({}), + } as never); + const catalogProvider = requireCatalogProvider(result); + expect(catalogProvider.baseUrl).toBe("https://api.novita.ai/openai/v1"); + expect(catalogProvider.models?.map((model) => model.id)).toContain("deepseek/deepseek-v3-0324"); + }); +}); diff --git a/extensions/novita/index.ts b/extensions/novita/index.ts new file mode 100644 index 000000000000..9c5bc37e33fc --- /dev/null +++ b/extensions/novita/index.ts @@ -0,0 +1,49 @@ +import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provider-catalog-shared"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; +import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools"; +import { NOVITA_DEFAULT_MODEL_REF } from "./models.js"; +import { buildNovitaProvider } from "./provider-catalog.js"; + +const PROVIDER_ID = "novita"; + +export default defineSingleProviderPluginEntry({ + id: PROVIDER_ID, + name: "NovitaAI Provider", + description: "Bundled NovitaAI provider plugin", + provider: { + label: "NovitaAI", + docsPath: "/providers/novita", + aliases: ["novita-ai", "novitaai"], + envVars: ["NOVITA_API_KEY"], + auth: [ + { + methodId: "api-key", + label: "NovitaAI API key", + hint: "OpenAI-compatible NovitaAI endpoint", + optionKey: "novitaApiKey", + flagName: "--novita-api-key", + envVar: "NOVITA_API_KEY", + promptMessage: "Enter NovitaAI API key", + defaultModel: NOVITA_DEFAULT_MODEL_REF, + noteTitle: "NovitaAI", + noteMessage: "Manage API keys at https://novita.ai/settings/key-management", + }, + ], + catalog: { + buildProvider: buildNovitaProvider, + buildStaticProvider: buildNovitaProvider, + allowExplicitBaseUrl: true, + }, + augmentModelCatalog: ({ config }) => + readConfiguredProviderCatalogEntries({ + config, + providerId: PROVIDER_ID, + }), + ...buildProviderReplayFamilyHooks({ + family: "openai-compatible", + dropReasoningFromHistory: false, + }), + ...buildProviderToolCompatFamilyHooks("openai"), + }, +}); diff --git a/extensions/novita/models.ts b/extensions/novita/models.ts new file mode 100644 index 000000000000..69310397fd73 --- /dev/null +++ b/extensions/novita/models.ts @@ -0,0 +1,19 @@ +import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared"; +import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import manifest from "./openclaw.plugin.json" with { type: "json" }; + +const NOVITA_MANIFEST_PROVIDER = buildManifestModelProviderConfig({ + providerId: "novita", + catalog: manifest.modelCatalog.providers.novita, +}); + +export const NOVITA_BASE_URL = NOVITA_MANIFEST_PROVIDER.baseUrl; +export const NOVITA_MODEL_CATALOG: ModelDefinitionConfig[] = NOVITA_MANIFEST_PROVIDER.models; +export const NOVITA_DEFAULT_MODEL_REF = "novita/deepseek/deepseek-v3-0324"; + +export function buildNovitaModelDefinition(model: ModelDefinitionConfig): ModelDefinitionConfig { + return { + ...model, + api: "openai-completions", + }; +} diff --git a/extensions/novita/openclaw.plugin.json b/extensions/novita/openclaw.plugin.json new file mode 100644 index 000000000000..b74faa7658b7 --- /dev/null +++ b/extensions/novita/openclaw.plugin.json @@ -0,0 +1,162 @@ +{ + "id": "novita", + "activation": { + "onStartup": false + }, + "enabledByDefault": true, + "providers": ["novita", "novita-ai", "novitaai"], + "providerAuthAliases": { + "novita-ai": "novita", + "novitaai": "novita" + }, + "providerEndpoints": [ + { + "endpointClass": "novita-native", + "hosts": ["api.novita.ai"] + } + ], + "providerRequest": { + "providers": { + "novita": { + "family": "novita" + }, + "novita-ai": { + "family": "novita" + }, + "novitaai": { + "family": "novita" + } + } + }, + "setup": { + "providers": [ + { + "id": "novita", + "envVars": ["NOVITA_API_KEY"] + } + ] + }, + "providerAuthChoices": [ + { + "provider": "novita", + "method": "api-key", + "choiceId": "novita-api-key", + "choiceLabel": "NovitaAI API key", + "choiceHint": "OpenAI-compatible NovitaAI endpoint", + "groupId": "novita", + "groupLabel": "NovitaAI", + "groupHint": "OpenAI-compatible NovitaAI endpoint", + "optionKey": "novitaApiKey", + "cliFlag": "--novita-api-key", + "cliOption": "--novita-api-key ", + "cliDescription": "NovitaAI API key" + } + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + }, + "modelCatalog": { + "aliases": { + "novita-ai": { + "provider": "novita" + }, + "novitaai": { + "provider": "novita" + } + }, + "providers": { + "novita": { + "baseUrl": "https://api.novita.ai/openai/v1", + "api": "openai-completions", + "models": [ + { + "id": "moonshotai/kimi-k2.5", + "name": "Kimi K2.5", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 262144, + "maxTokens": 65536, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "minimax/minimax-m2.7", + "name": "MiniMax M2.7", + "reasoning": true, + "input": ["text"], + "contextWindow": 1000000, + "maxTokens": 65536, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "zai-org/glm-5", + "name": "GLM-5", + "reasoning": true, + "input": ["text"], + "contextWindow": 202752, + "maxTokens": 65536, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "deepseek/deepseek-v3-0324", + "name": "DeepSeek V3 0324", + "reasoning": false, + "input": ["text"], + "contextWindow": 163840, + "maxTokens": 65536, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "deepseek/deepseek-r1-0528", + "name": "DeepSeek R1 0528", + "reasoning": true, + "input": ["text"], + "contextWindow": 163840, + "maxTokens": 65536, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "qwen/qwen3-235b-a22b-fp8", + "name": "Qwen3 235B A22B FP8", + "reasoning": true, + "input": ["text"], + "contextWindow": 262144, + "maxTokens": 65536, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + } + ] + } + } + } +} diff --git a/extensions/novita/package.json b/extensions/novita/package.json new file mode 100644 index 000000000000..4ab6638583df --- /dev/null +++ b/extensions/novita/package.json @@ -0,0 +1,13 @@ +{ + "name": "@openclaw/novita-provider", + "version": "2026.5.28", + "private": true, + "description": "OpenClaw NovitaAI provider plugin", + "type": "module", + "devDependencies": { + "@openclaw/plugin-sdk": "workspace:*" + }, + "openclaw": { + "extensions": ["./index.ts"] + } +} diff --git a/extensions/novita/provider-catalog.ts b/extensions/novita/provider-catalog.ts new file mode 100644 index 000000000000..35434b414360 --- /dev/null +++ b/extensions/novita/provider-catalog.ts @@ -0,0 +1,10 @@ +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import { NOVITA_BASE_URL, NOVITA_MODEL_CATALOG, buildNovitaModelDefinition } from "./models.js"; + +export function buildNovitaProvider(): ModelProviderConfig { + return { + baseUrl: NOVITA_BASE_URL, + api: "openai-completions", + models: NOVITA_MODEL_CATALOG.map(buildNovitaModelDefinition), + }; +} diff --git a/extensions/novita/tsconfig.json b/extensions/novita/tsconfig.json new file mode 100644 index 000000000000..b8a85a99ac3d --- /dev/null +++ b/extensions/novita/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.package-boundary.base.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["./*.ts", "./src/**/*.ts"], + "exclude": [ + "./**/*.test.ts", + "./dist/**", + "./node_modules/**", + "./src/test-support/**", + "./src/**/*test-helpers.ts", + "./src/**/*test-harness.ts", + "./src/**/*test-support.ts" + ] +} diff --git a/extensions/ollama/index.test.ts b/extensions/ollama/index.test.ts index 38ab28527015..5b670b44ed70 100644 --- a/extensions/ollama/index.test.ts +++ b/extensions/ollama/index.test.ts @@ -76,10 +76,10 @@ beforeEach(() => { }); function registerProvider() { - return registerProviderWithPluginConfig({}); + return registerProvidersWithPluginConfig({}).find((provider) => provider.id === "ollama"); } -function registerProviderWithPluginConfig(pluginConfig: Record) { +function registerProvidersWithPluginConfig(pluginConfig: Record) { const registerProviderMock = vi.fn(); plugin.register( @@ -94,8 +94,18 @@ function registerProviderWithPluginConfig(pluginConfig: Record) }), ); - expect(registerProviderMock).toHaveBeenCalledTimes(1); - return registerProviderMock.mock.calls[0]?.[0]; + expect(registerProviderMock).toHaveBeenCalledTimes(2); + return registerProviderMock.mock.calls.map((call) => call[0]); +} + +function registerProviderWithPluginConfig(pluginConfig: Record) { + return registerProvidersWithPluginConfig(pluginConfig).find( + (provider) => provider.id === "ollama", + ); +} + +function registerOllamaCloudProvider() { + return registerProvidersWithPluginConfig({}).find((provider) => provider.id === "ollama-cloud"); } function requireRecord(value: unknown, label: string): Record { @@ -694,6 +704,36 @@ describe("ollama plugin", () => { expect(auth).toBeUndefined(); }); + it("registers ollama-cloud as a hosted provider", async () => { + const provider = registerOllamaCloudProvider(); + + expect(provider.id).toBe("ollama-cloud"); + expect(provider.envVars).toEqual(["OLLAMA_API_KEY"]); + expect(provider.auth?.map((method: { id: string }) => method.id)).toEqual(["api-key"]); + + const result = await provider.staticCatalog?.run({ + config: {}, + env: {}, + resolveProviderApiKey: () => ({}), + } as never); + if (!result || !("provider" in result)) { + throw new Error("single provider catalog result missing"); + } + expect(result.provider.baseUrl).toBe("https://ollama.com"); + expect(result.provider.models?.map((model: { id: string }) => model.id)).toEqual([ + "kimi-k2.5:cloud", + "minimax-m2.7:cloud", + "glm-5.1:cloud", + ]); + + provider.createStreamFn?.({ + config: {}, + model: { id: "kimi-k2.5:cloud" }, + provider: "ollama-cloud", + } as never); + expect(requireConfiguredStreamParams().providerBaseUrl).toBe("https://ollama.com"); + }); + it("does not mint synthetic auth for public IPv4 baseUrl", () => { const provider = registerProvider(); diff --git a/extensions/ollama/index.ts b/extensions/ollama/index.ts index 5b3e4a740f64..1acc895daf4a 100644 --- a/extensions/ollama/index.ts +++ b/extensions/ollama/index.ts @@ -11,6 +11,7 @@ import { type ProviderRuntimeModel, } from "openclaw/plugin-sdk/plugin-entry"; import { buildApiKeyCredential } from "openclaw/plugin-sdk/provider-auth"; +import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import type { ModelDefinitionConfig, ModelProviderConfig, @@ -29,6 +30,11 @@ import { queryOllamaModelShowInfo, } from "./api.js"; import { resolveThinkingProfile as resolveOllamaThinkingProfile } from "./provider-policy-api.js"; +import { + OLLAMA_CLOUD_BASE_URL, + OLLAMA_CLOUD_DEFAULT_MODELS, + OLLAMA_CLOUD_PROVIDER_ID, +} from "./src/defaults.js"; import { OLLAMA_DEFAULT_API_KEY, OLLAMA_PROVIDER_ID, @@ -61,6 +67,7 @@ function buildNativeOllamaReplayPolicy(): ProviderReplayPolicy { } const dynamicModelCache = new Map(); +const OLLAMA_CLOUD_DEFAULT_MODEL_REF = `${OLLAMA_CLOUD_PROVIDER_ID}/${OLLAMA_CLOUD_DEFAULT_MODELS[0]}`; function buildDynamicCacheKey(provider: string, baseUrl: string | undefined): string { return `${provider}\0${baseUrl ?? ""}`; @@ -98,6 +105,19 @@ function toDynamicOllamaModel(params: { }; } +function buildStaticOllamaCloudProvider(): ModelProviderConfig { + return { + baseUrl: OLLAMA_CLOUD_BASE_URL, + api: "ollama", + models: OLLAMA_CLOUD_DEFAULT_MODELS.map((model) => buildOllamaModelDefinition(model)), + }; +} + +async function buildOllamaCloudProvider(): Promise { + const discovered = await buildOllamaProvider(OLLAMA_CLOUD_BASE_URL, { quiet: true }); + return discovered.models?.length ? discovered : buildStaticOllamaCloudProvider(); +} + async function resolveRequestedDynamicOllamaModel(params: { provider: string; providerConfig: ModelProviderConfig; @@ -140,6 +160,80 @@ export default definePluginEntry({ return config ? {} : startupPluginConfig; }; api.registerWebSearchProvider(createOllamaWebSearchProvider()); + api.registerProvider({ + id: OLLAMA_CLOUD_PROVIDER_ID, + label: "Ollama Cloud", + docsPath: "/providers/ollama", + envVars: ["OLLAMA_API_KEY"], + auth: [ + createProviderApiKeyAuthMethod({ + providerId: OLLAMA_CLOUD_PROVIDER_ID, + methodId: "api-key", + label: "Ollama Cloud API key", + hint: "Hosted models via ollama.com", + optionKey: "ollamaCloudApiKey", + flagName: "--ollama-cloud-api-key", + envVar: "OLLAMA_API_KEY", + promptMessage: "Enter Ollama Cloud API key", + defaultModel: OLLAMA_CLOUD_DEFAULT_MODEL_REF, + noteTitle: "Ollama Cloud", + noteMessage: "Manage API keys at https://ollama.com/settings/keys", + wizard: { + choiceId: "ollama-cloud", + choiceLabel: "Ollama Cloud", + choiceHint: "Hosted models via ollama.com", + groupId: "ollama", + groupLabel: "Ollama", + groupHint: "Cloud and local open models", + }, + }), + ], + catalog: { + order: "simple", + run: async (ctx: ProviderCatalogContext) => { + const apiKey = ctx.resolveProviderApiKey(OLLAMA_CLOUD_PROVIDER_ID).apiKey; + if (!apiKey) { + return null; + } + return { + provider: { + ...(await buildOllamaCloudProvider()), + apiKey, + }, + }; + }, + }, + staticCatalog: { + order: "simple", + run: async () => ({ + provider: buildStaticOllamaCloudProvider(), + }), + }, + createStreamFn: ({ config, model, provider }) => { + return createConfiguredOllamaStreamFn({ + model, + providerBaseUrl: + readProviderBaseUrl( + resolveConfiguredOllamaProviderConfig({ config, providerId: provider }), + ) ?? OLLAMA_CLOUD_BASE_URL, + }); + }, + ...OPENAI_COMPATIBLE_REPLAY_HOOKS, + buildReplayPolicy: (ctx) => + ctx.modelApi === "ollama" + ? buildNativeOllamaReplayPolicy() + : buildOpenAICompatibleReplayPolicy(ctx.modelApi), + resolveReasoningOutputMode: () => "native", + resolveThinkingProfile: resolveOllamaThinkingProfile, + wrapStreamFn: createConfiguredOllamaCompatStreamWrapper, + matchesContextOverflowError: ({ errorMessage }) => + /\bollama\b.*(?:context length|too many tokens|context window)/i.test(errorMessage) || + /\btruncating input\b.*\btoo long\b/i.test(errorMessage), + buildUnknownModelHint: () => + "Ollama Cloud requires an API key. " + + 'Set OLLAMA_API_KEY or run "openclaw onboard --auth-choice ollama-cloud". ' + + "See: https://docs.openclaw.ai/providers/ollama", + }); api.registerProvider({ id: OLLAMA_PROVIDER_ID, label: "Ollama", diff --git a/extensions/ollama/openclaw.plugin.json b/extensions/ollama/openclaw.plugin.json index 7139097144cc..467ca5ef9ee0 100644 --- a/extensions/ollama/openclaw.plugin.json +++ b/extensions/ollama/openclaw.plugin.json @@ -4,12 +4,15 @@ "onStartup": false }, "enabledByDefault": true, - "providers": ["ollama"], + "providers": ["ollama", "ollama-cloud"], "providerCatalogEntry": "./provider-discovery.ts", "providerRequest": { "providers": { "ollama": { "family": "ollama" + }, + "ollama-cloud": { + "family": "ollama-cloud" } } }, @@ -17,6 +20,9 @@ "providers": { "ollama": { "external": false + }, + "ollama-cloud": { + "external": true } } }, @@ -27,6 +33,10 @@ { "id": "ollama", "envVars": ["OLLAMA_API_KEY"] + }, + { + "id": "ollama-cloud", + "envVars": ["OLLAMA_API_KEY"] } ] }, @@ -40,8 +50,89 @@ "groupId": "ollama", "groupLabel": "Ollama", "groupHint": "Cloud and local open models" + }, + { + "provider": "ollama-cloud", + "method": "api-key", + "choiceId": "ollama-cloud", + "choiceLabel": "Ollama Cloud", + "choiceHint": "Hosted models via ollama.com", + "groupId": "ollama", + "groupLabel": "Ollama", + "groupHint": "Cloud and local open models", + "optionKey": "ollamaCloudApiKey", + "cliFlag": "--ollama-cloud-api-key", + "cliOption": "--ollama-cloud-api-key ", + "cliDescription": "Ollama Cloud API key" } ], + "modelCatalog": { + "providers": { + "ollama-cloud": { + "baseUrl": "https://ollama.com", + "api": "ollama", + "models": [ + { + "id": "kimi-k2.5:cloud", + "name": "kimi-k2.5:cloud", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 128000, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "minimax-m2.7:cloud", + "name": "minimax-m2.7:cloud", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 128000, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + }, + { + "id": "glm-5.1:cloud", + "name": "glm-5.1:cloud", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 128000, + "maxTokens": 8192, + "compat": { + "supportsTools": true, + "supportsUsageInStreaming": true + } + } + ] + } + }, + "discovery": { + "ollama-cloud": "refreshable" + } + }, "contracts": { "memoryEmbeddingProviders": ["ollama"], "webSearchProviders": ["ollama"] diff --git a/extensions/ollama/src/defaults.ts b/extensions/ollama/src/defaults.ts index 815a54d14bc0..c1e420674d88 100644 --- a/extensions/ollama/src/defaults.ts +++ b/extensions/ollama/src/defaults.ts @@ -1,6 +1,12 @@ export const OLLAMA_DEFAULT_BASE_URL = "http://127.0.0.1:11434"; export const OLLAMA_DOCKER_HOST_BASE_URL = "http://host.docker.internal:11434"; export const OLLAMA_CLOUD_BASE_URL = "https://ollama.com"; +export const OLLAMA_CLOUD_PROVIDER_ID = "ollama-cloud"; +export const OLLAMA_CLOUD_DEFAULT_MODELS = [ + "kimi-k2.5:cloud", + "minimax-m2.7:cloud", + "glm-5.1:cloud", +] as const; export const OLLAMA_DEFAULT_CONTEXT_WINDOW = 128000; export const OLLAMA_DEFAULT_MAX_TOKENS = 8192; diff --git a/extensions/ollama/src/setup.ts b/extensions/ollama/src/setup.ts index bed20607d100..c58fac7ff075 100644 --- a/extensions/ollama/src/setup.ts +++ b/extensions/ollama/src/setup.ts @@ -22,6 +22,7 @@ import { } from "openclaw/plugin-sdk/string-coerce-runtime"; import { OLLAMA_CLOUD_BASE_URL, + OLLAMA_CLOUD_DEFAULT_MODELS, OLLAMA_DEFAULT_BASE_URL, OLLAMA_DOCKER_HOST_BASE_URL, OLLAMA_DEFAULT_MODEL, @@ -40,7 +41,7 @@ import { export { buildOllamaProvider }; const OLLAMA_SUGGESTED_MODELS_LOCAL = [OLLAMA_DEFAULT_MODEL]; -const OLLAMA_SUGGESTED_MODELS_CLOUD = ["kimi-k2.5:cloud", "minimax-m2.7:cloud", "glm-5.1:cloud"]; +const OLLAMA_SUGGESTED_MODELS_CLOUD = [...OLLAMA_CLOUD_DEFAULT_MODELS]; const OLLAMA_CONTEXT_ENRICH_LIMIT = 200; const OLLAMA_CLOUD_MAX_DISCOVERED_MODELS = 500; const OLLAMA_PULL_RESPONSE_TIMEOUT_MS = 30_000; diff --git a/extensions/qwen/index.test.ts b/extensions/qwen/index.test.ts index 4f9b02f02956..b0ee33f7b6ab 100644 --- a/extensions/qwen/index.test.ts +++ b/extensions/qwen/index.test.ts @@ -1,11 +1,28 @@ -import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime"; +import { + registerProviderPlugin, + requireRegisteredProvider, +} from "openclaw/plugin-sdk/plugin-test-runtime"; +import type { ProviderCatalogResult } from "openclaw/plugin-sdk/provider-catalog-shared"; +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; import { describe, expect, it } from "vitest"; import { QWEN_36_PLUS_MODEL_ID, QWEN_BASE_URL } from "./api.js"; import qwenPlugin from "./index.js"; +import { wrapQwenProviderStream } from "./stream.js"; + +function requireCatalogProvider(result: ProviderCatalogResult): ModelProviderConfig { + if (!result || !("provider" in result)) { + throw new Error("single provider catalog result missing"); + } + return result.provider; +} async function registerQwenProvider() { - // The test runtime asserts the plugin registers exactly one provider and returns it. - return registerSingleProviderPlugin(qwenPlugin); + const { providers } = await registerProviderPlugin({ + plugin: qwenPlugin, + id: "qwen", + name: "Qwen Provider", + }); + return requireRegisteredProvider(providers, "qwen"); } describe("qwen provider plugin", () => { @@ -28,4 +45,103 @@ describe("qwen provider plugin", () => { expect(provider.suppressBuiltInModel).toBeUndefined(); }); + + it("registers qwen-oauth as a portal provider", async () => { + const { providers } = await registerProviderPlugin({ + plugin: qwenPlugin, + id: "qwen", + name: "Qwen Provider", + }); + const provider = requireRegisteredProvider(providers, "qwen-oauth"); + + expect(provider.aliases).toEqual(["qwen-portal", "qwen-cli"]); + expect(provider.envVars).toEqual(["QWEN_API_KEY"]); + expect(provider.auth?.map((method) => method.id)).toEqual(["api-key"]); + + const result = await provider.staticCatalog?.run({ + config: {}, + env: {}, + resolveProviderApiKey: () => ({}), + } as never); + const catalogProvider = requireCatalogProvider(result); + expect(catalogProvider.baseUrl).toBe("https://portal.qwen.ai/v1"); + expect(catalogProvider.models?.map((model) => model.id)).toContain("qwen3.5-plus"); + }); + + it("reuses legacy qwen portal auth profiles for qwen-oauth catalog", async () => { + const { providers } = await registerProviderPlugin({ + plugin: qwenPlugin, + id: "qwen", + name: "Qwen Provider", + }); + const provider = requireRegisteredProvider(providers, "qwen-oauth"); + + const result = await provider.catalog?.run({ + config: {}, + env: {}, + resolveProviderApiKey: (providerId: string) => + providerId === "qwen-portal" ? { apiKey: "portal-token" } : {}, + } as never); + + const catalogProvider = requireCatalogProvider(result); + expect(catalogProvider.apiKey).toBe("portal-token"); + expect(catalogProvider.baseUrl).toBe("https://portal.qwen.ai/v1"); + }); + + it.each([["qwen-oauth"], ["qwen-portal"], ["qwen-cli"]])( + "patches %s message payloads for portal compatibility", + async (providerId) => { + let patchedPayload: Record | undefined; + const streamFn = wrapQwenProviderStream({ + provider: providerId, + thinkingLevel: "off", + streamFn: (( + _model: unknown, + _context: unknown, + options?: { + onPayload?: (payload: Record, model: unknown) => void; + }, + ) => { + const payload = { + messages: [ + { role: "system", content: "system text" }, + { role: "user", content: ["hello", { type: "text", text: "world" }] }, + ], + }; + options?.onPayload?.(payload, _model); + patchedPayload = payload; + return (async function* () {})(); + }) as never, + } as never); + + const stream = streamFn!( + { + provider: providerId, + api: "openai-completions", + id: "qwen3.5-plus", + } as never, + {} as never, + {}, + ) as AsyncIterable; + for await (const event of stream) { + void event; + // Drain stream so the payload hook runs. + } + + expect(patchedPayload?.vl_high_resolution_images).toBe(true); + expect(patchedPayload?.messages).toEqual([ + { + role: "system", + content: [{ type: "text", text: "system text", cache_control: { type: "ephemeral" } }], + }, + { + role: "user", + content: [ + { type: "text", text: "hello" }, + { type: "text", text: "world" }, + ], + }, + ]); + }, + ); }); diff --git a/extensions/qwen/index.ts b/extensions/qwen/index.ts index a8519ef3874e..c0ee5bb2441c 100644 --- a/extensions/qwen/index.ts +++ b/extensions/qwen/index.ts @@ -1,3 +1,4 @@ +import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { applyQwenNativeStreamingUsageCompat } from "./api.js"; import { buildQwenMediaUnderstandingProvider } from "./media-understanding-provider.js"; @@ -6,19 +7,23 @@ import { QWEN_36_PLUS_MODEL_ID, QWEN_BASE_URL, QWEN_DEFAULT_MODEL_REF, + QWEN_OAUTH_DEFAULT_MODEL_REF, + QWEN_OAUTH_PROVIDER_ID, } from "./models.js"; import { applyQwenConfig, applyQwenConfigCn, + applyQwenOAuthConfig, applyQwenStandardConfig, applyQwenStandardConfigCn, } from "./onboard.js"; -import { buildQwenProvider } from "./provider-catalog.js"; +import { buildQwenOAuthProvider, buildQwenProvider } from "./provider-catalog.js"; import { wrapQwenProviderStream } from "./stream.js"; import { buildQwenVideoGenerationProvider } from "./video-generation-provider.js"; const PROVIDER_ID = "qwen"; const LEGACY_PROVIDER_ID = "modelstudio"; +const QWEN_OAUTH_AUTH_PROVIDER_IDS = [QWEN_OAUTH_PROVIDER_ID, "qwen-portal", "qwen-cli"] as const; function normalizeProviderId(value: string): string { return value.trim().toLowerCase(); @@ -175,6 +180,62 @@ export default defineSingleProviderPluginEntry({ }, }, register(api) { + api.registerProvider({ + id: QWEN_OAUTH_PROVIDER_ID, + label: "Qwen OAuth", + docsPath: "/providers/qwen", + aliases: ["qwen-portal", "qwen-cli"], + envVars: ["QWEN_API_KEY"], + auth: [ + createProviderApiKeyAuthMethod({ + providerId: QWEN_OAUTH_PROVIDER_ID, + methodId: "api-key", + label: "Qwen OAuth token", + hint: "Portal token for portal.qwen.ai", + optionKey: "qwenOauthToken", + flagName: "--qwen-oauth-token", + envVar: "QWEN_API_KEY", + promptMessage: "Enter Qwen OAuth token", + defaultModel: QWEN_OAUTH_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyQwenOAuthConfig(cfg), + wizard: { + choiceId: QWEN_OAUTH_PROVIDER_ID, + choiceLabel: "Qwen OAuth", + choiceHint: "Portal token for portal.qwen.ai", + groupId: "qwen", + groupLabel: "Qwen Cloud", + groupHint: "Standard / Coding Plan / OAuth", + }, + }), + ], + catalog: { + order: "simple", + run: async (ctx) => { + const apiKey = QWEN_OAUTH_AUTH_PROVIDER_IDS.map( + (providerId) => ctx.resolveProviderApiKey(providerId).apiKey, + ).find( + (candidate): candidate is string => + typeof candidate === "string" && candidate.length > 0, + ); + if (!apiKey) { + return null; + } + return { + provider: { + ...buildQwenOAuthProvider(), + apiKey, + }, + }; + }, + }, + staticCatalog: { + order: "simple", + run: async () => ({ + provider: buildQwenOAuthProvider(), + }), + }, + wrapStreamFn: wrapQwenProviderStream, + }); api.registerMediaUnderstandingProvider(buildQwenMediaUnderstandingProvider()); api.registerVideoGenerationProvider(buildQwenVideoGenerationProvider()); }, diff --git a/extensions/qwen/models.ts b/extensions/qwen/models.ts index 8f3d5ee17488..4ec1510b4094 100644 --- a/extensions/qwen/models.ts +++ b/extensions/qwen/models.ts @@ -13,6 +13,8 @@ export const QWEN_CN_BASE_URL = "https://coding.dashscope.aliyuncs.com/v1"; export const QWEN_STANDARD_CN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"; export const QWEN_STANDARD_GLOBAL_BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"; +export const QWEN_OAUTH_PROVIDER_ID = "qwen-oauth"; +export const QWEN_OAUTH_BASE_URL = "https://portal.qwen.ai/v1"; export const QWEN_DEFAULT_MODEL_ID = "qwen3.5-plus"; export const QWEN_36_PLUS_MODEL_ID = "qwen3.6-plus"; @@ -23,6 +25,7 @@ export const QWEN_DEFAULT_COST = { cacheWrite: 0, }; export const QWEN_DEFAULT_MODEL_REF = `qwen/${QWEN_DEFAULT_MODEL_ID}`; +export const QWEN_OAUTH_DEFAULT_MODEL_REF = `qwen-oauth/${QWEN_DEFAULT_MODEL_ID}`; export const QWEN_MODEL_CATALOG: ReadonlyArray = [ { @@ -178,6 +181,10 @@ export function buildQwenDefaultModelDefinition(): ModelDefinitionConfig { return buildQwenModelDefinition({ id: QWEN_DEFAULT_MODEL_ID }); } +export function buildQwenOAuthModelCatalog(): ReadonlyArray { + return QWEN_MODEL_CATALOG.map((model) => ({ ...model, maxTokens: 65_536 })); +} + /** @deprecated Use QWEN_BASE_URL. */ export const MODELSTUDIO_BASE_URL = QWEN_BASE_URL; /** @deprecated Use QWEN_GLOBAL_BASE_URL. */ diff --git a/extensions/qwen/onboard.ts b/extensions/qwen/onboard.ts index 39edf9778d5f..6b7970bb486b 100644 --- a/extensions/qwen/onboard.ts +++ b/extensions/qwen/onboard.ts @@ -6,10 +6,12 @@ import { QWEN_CN_BASE_URL, QWEN_DEFAULT_MODEL_REF, QWEN_GLOBAL_BASE_URL, + QWEN_OAUTH_DEFAULT_MODEL_REF, + QWEN_OAUTH_PROVIDER_ID, QWEN_STANDARD_CN_BASE_URL, QWEN_STANDARD_GLOBAL_BASE_URL, } from "./models.js"; -import { buildQwenProvider } from "./provider-catalog.js"; +import { buildQwenOAuthProvider, buildQwenProvider } from "./provider-catalog.js"; const qwenPresetAppliers = createModelCatalogPresetAppliers<[string]>({ primaryModelRef: QWEN_DEFAULT_MODEL_REF, @@ -31,6 +33,23 @@ const qwenPresetAppliers = createModelCatalogPresetAppliers<[string]>({ }, }); +const qwenOAuthPresetAppliers = createModelCatalogPresetAppliers<[]>({ + primaryModelRef: QWEN_OAUTH_DEFAULT_MODEL_REF, + resolveParams: () => { + const provider = buildQwenOAuthProvider(); + return { + providerId: QWEN_OAUTH_PROVIDER_ID, + api: provider.api ?? "openai-completions", + baseUrl: provider.baseUrl, + catalogModels: provider.models ?? [], + aliases: [ + ...(provider.models ?? []).map((model) => `qwen-oauth/${model.id}`), + { modelRef: QWEN_OAUTH_DEFAULT_MODEL_REF, alias: "Qwen OAuth" }, + ], + }; + }, +}); + function applyQwenProviderConfig(cfg: OpenClawConfig): OpenClawConfig { return qwenPresetAppliers.applyProviderConfig(cfg, QWEN_GLOBAL_BASE_URL); } @@ -63,6 +82,10 @@ export function applyQwenStandardConfigCn(cfg: OpenClawConfig): OpenClawConfig { return qwenPresetAppliers.applyConfig(cfg, QWEN_STANDARD_CN_BASE_URL); } +export function applyQwenOAuthConfig(cfg: OpenClawConfig): OpenClawConfig { + return qwenOAuthPresetAppliers.applyConfig(cfg); +} + export const applyModelStudioProviderConfig = applyQwenProviderConfig; export const applyModelStudioProviderConfigCn = applyQwenProviderConfigCn; export const applyModelStudioConfig = applyQwenConfig; diff --git a/extensions/qwen/openclaw.plugin.json b/extensions/qwen/openclaw.plugin.json index 1215eadf5ab0..99979da71669 100644 --- a/extensions/qwen/openclaw.plugin.json +++ b/extensions/qwen/openclaw.plugin.json @@ -4,7 +4,11 @@ "onStartup": false }, "enabledByDefault": true, - "providers": ["qwen", "qwencloud", "modelstudio", "dashscope"], + "providers": ["qwen", "qwencloud", "modelstudio", "dashscope", "qwen-oauth", "qwen-portal", "qwen-cli"], + "providerAuthAliases": { + "qwen-portal": "qwen-oauth", + "qwen-cli": "qwen-oauth" + }, "providerEndpoints": [ { "endpointClass": "modelstudio-native", @@ -14,6 +18,10 @@ "https://dashscope.aliyuncs.com/compatible-mode/v1", "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" ] + }, + { + "endpointClass": "qwen-portal-native", + "baseUrls": ["https://portal.qwen.ai/v1"] } ], "providerRequest": { @@ -29,10 +37,27 @@ }, "dashscope": { "family": "modelstudio" + }, + "qwen-oauth": { + "family": "qwen-portal" + }, + "qwen-portal": { + "family": "qwen-portal" + }, + "qwen-cli": { + "family": "qwen-portal" } } }, "modelCatalog": { + "aliases": { + "qwen-portal": { + "provider": "qwen-oauth" + }, + "qwen-cli": { + "provider": "qwen-oauth" + } + }, "suppressions": [ { "provider": "qwen", @@ -52,7 +77,141 @@ "providerConfigApiIn": ["qwen", "modelstudio"] } } - ] + ], + "providers": { + "qwen-oauth": { + "baseUrl": "https://portal.qwen.ai/v1", + "api": "openai-completions", + "models": [ + { + "id": "qwen3.5-plus", + "name": "qwen3.5-plus", + "reasoning": false, + "input": ["text", "image"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 65536 + }, + { + "id": "qwen3.6-plus", + "name": "qwen3.6-plus", + "reasoning": false, + "input": ["text", "image"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 65536 + }, + { + "id": "qwen3-max-2026-01-23", + "name": "qwen3-max-2026-01-23", + "reasoning": false, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 65536 + }, + { + "id": "qwen3-coder-next", + "name": "qwen3-coder-next", + "reasoning": false, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 65536 + }, + { + "id": "qwen3-coder-plus", + "name": "qwen3-coder-plus", + "reasoning": false, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 65536 + }, + { + "id": "MiniMax-M2.5", + "name": "MiniMax-M2.5", + "reasoning": true, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 1000000, + "maxTokens": 65536 + }, + { + "id": "glm-5", + "name": "glm-5", + "reasoning": false, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 202752, + "maxTokens": 65536 + }, + { + "id": "glm-4.7", + "name": "glm-4.7", + "reasoning": false, + "input": ["text"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 202752, + "maxTokens": 65536 + }, + { + "id": "kimi-k2.5", + "name": "kimi-k2.5", + "reasoning": false, + "input": ["text", "image"], + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + }, + "contextWindow": 262144, + "maxTokens": 65536 + } + ] + } + } }, "contracts": { "mediaUnderstandingProviders": ["qwen"], @@ -75,6 +234,10 @@ { "id": "qwen", "envVars": ["QWEN_API_KEY", "MODELSTUDIO_API_KEY", "DASHSCOPE_API_KEY"] + }, + { + "id": "qwen-oauth", + "envVars": ["QWEN_API_KEY"] } ] }, @@ -138,6 +301,20 @@ "cliFlag": "--modelstudio-api-key", "cliOption": "--modelstudio-api-key ", "cliDescription": "Qwen Cloud Coding Plan API key (Global/Intl)" + }, + { + "provider": "qwen-oauth", + "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" } ], "configSchema": { diff --git a/extensions/qwen/provider-catalog.ts b/extensions/qwen/provider-catalog.ts index 01b19974cb31..dff177b1ec11 100644 --- a/extensions/qwen/provider-catalog.ts +++ b/extensions/qwen/provider-catalog.ts @@ -1,5 +1,10 @@ import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; -import { buildQwenModelCatalogForBaseUrl, QWEN_BASE_URL } from "./models.js"; +import { + buildQwenModelCatalogForBaseUrl, + buildQwenOAuthModelCatalog, + QWEN_BASE_URL, + QWEN_OAUTH_BASE_URL, +} from "./models.js"; export function buildQwenProvider(params?: { baseUrl?: string }): ModelProviderConfig { const baseUrl = params?.baseUrl ?? QWEN_BASE_URL; @@ -10,4 +15,12 @@ export function buildQwenProvider(params?: { baseUrl?: string }): ModelProviderC }; } +export function buildQwenOAuthProvider(): ModelProviderConfig { + return { + baseUrl: QWEN_OAUTH_BASE_URL, + api: "openai-completions", + models: buildQwenOAuthModelCatalog().map((model) => Object.assign({}, model)), + }; +} + export const buildModelStudioProvider = buildQwenProvider; diff --git a/extensions/qwen/stream.ts b/extensions/qwen/stream.ts index c806e81ff4d3..8c31c8961841 100644 --- a/extensions/qwen/stream.ts +++ b/extensions/qwen/stream.ts @@ -13,12 +13,60 @@ function isQwenProviderId(providerId: string): boolean { const normalized = normalizeProviderId(providerId); return ( normalized === "qwen" || + normalized === "qwen-oauth" || + normalized === "qwen-portal" || + normalized === "qwen-cli" || normalized === "modelstudio" || normalized === "qwencloud" || normalized === "dashscope" ); } +function isQwenOAuthProviderId(providerId: string): boolean { + const normalized = normalizeProviderId(providerId); + return normalized === "qwen-oauth" || normalized === "qwen-portal" || normalized === "qwen-cli"; +} + +function normalizeQwenOAuthContent(content: unknown): unknown { + if (typeof content === "string") { + return [{ type: "text", text: content }]; + } + if (!Array.isArray(content)) { + return content; + } + const normalized = content + .map((part) => { + if (typeof part === "string") { + return { type: "text", text: part }; + } + return part && typeof part === "object" ? part : undefined; + }) + .filter((part): part is Record => Boolean(part)); + return normalized.length > 0 ? normalized : content; +} + +function patchQwenOAuthPayload(payload: Record): void { + const messages = payload.messages; + if (!Array.isArray(messages)) { + return; + } + for (const message of messages) { + if (!message || typeof message !== "object") { + continue; + } + const record = message as Record; + record.content = normalizeQwenOAuthContent(record.content); + if (record.role !== "system" || !Array.isArray(record.content) || record.content.length === 0) { + continue; + } + const last = record.content[record.content.length - 1]; + if (last && typeof last === "object") { + (last as Record).cache_control = { type: "ephemeral" }; + } + } + payload.vl_high_resolution_images = true; +} + function setQwenChatTemplateThinking(payload: Record, enabled: boolean): void { const existing = payload.chat_template_kwargs; if (existing && typeof existing === "object" && !Array.isArray(existing)) { @@ -79,9 +127,17 @@ export function wrapQwenProviderStream(ctx: ProviderWrapStreamFnContext): Stream if (!isQwenProviderId(ctx.provider) || (ctx.model && ctx.model.api !== "openai-completions")) { return undefined; } - return createQwenThinkingWrapper( + const streamFn = createQwenThinkingWrapper( ctx.streamFn, ctx.thinkingLevel, ctx.model ? readQwenThinkingFormatFromModel(ctx.model) : undefined, ); + if (!isQwenOAuthProviderId(ctx.provider)) { + return streamFn; + } + return createPayloadPatchStreamWrapper(streamFn, ({ payload, model }) => { + if (model.api === "openai-completions") { + patchQwenOAuthPayload(payload); + } + }); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ace48322421..953be4a222e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -798,6 +798,12 @@ importers: specifier: workspace:* version: link:../../packages/plugin-sdk + extensions/gmi: + devDependencies: + '@openclaw/plugin-sdk': + specifier: workspace:* + version: link:../../packages/plugin-sdk + extensions/google: dependencies: '@google/genai': @@ -1177,6 +1183,12 @@ importers: specifier: workspace:* version: link:../.. + extensions/novita: + devDependencies: + '@openclaw/plugin-sdk': + specifier: workspace:* + version: link:../../packages/plugin-sdk + extensions/nvidia: devDependencies: '@openclaw/plugin-sdk': diff --git a/src/agents/auth-profiles.doctor.test.ts b/src/agents/auth-profiles.doctor.test.ts index 7ecc86851f4c..3fbc2158c553 100644 --- a/src/agents/auth-profiles.doctor.test.ts +++ b/src/agents/auth-profiles.doctor.test.ts @@ -8,14 +8,35 @@ const EMPTY_STORE: AuthProfileStore = { }; describe("formatAuthDoctorHint", () => { - it("guides removed qwen portal users to model studio onboarding", async () => { + it("does not report restored qwen portal auth as removed", async () => { const hint = await formatAuthDoctorHint({ store: EMPTY_STORE, provider: "qwen-portal", }); + expect(hint).toBe(""); + }); + + it("guides legacy qwen portal oauth profiles to re-authenticate", async () => { + const hint = await formatAuthDoctorHint({ + store: { + version: 1, + profiles: { + "qwen-portal-auth": { + type: "oauth", + provider: "qwen-portal", + access: "old-access", + refresh: "old-refresh", + expires: 0, + }, + }, + }, + provider: "qwen-portal", + profileId: "qwen-portal-auth", + }); + expect(hint).toBe( - "Qwen OAuth via portal.qwen.ai has been deprecated. Please migrate to Qwen Cloud Coding Plan. Run: openclaw onboard --auth-choice qwen-api-key (or qwen-api-key-cn for the China endpoint). Legacy modelstudio auth-choice ids still work.", + "Legacy Qwen Portal OAuth profiles are not refreshable. Re-authenticate with a current portal token: openclaw onboard --auth-choice qwen-oauth.", ); }); }); diff --git a/src/agents/auth-profiles/doctor.ts b/src/agents/auth-profiles/doctor.ts index c07f70443911..74efc815bb66 100644 --- a/src/agents/auth-profiles/doctor.ts +++ b/src/agents/auth-profiles/doctor.ts @@ -3,14 +3,16 @@ import { buildProviderAuthDoctorHintWithPlugin } from "../../plugins/provider-ru import { normalizeProviderId } from "../provider-id.js"; import type { AuthProfileStore } from "./types.js"; -/** - * Migration hints for deprecated/removed OAuth providers. - * Users with stale credentials should be guided to migrate. - */ -const DEPRECATED_PROVIDER_MIGRATION_HINTS: Record = { - "qwen-portal": - "Qwen OAuth via portal.qwen.ai has been deprecated. Please migrate to Qwen Cloud Coding Plan. Run: openclaw onboard --auth-choice qwen-api-key (or qwen-api-key-cn for the China endpoint). Legacy modelstudio auth-choice ids still work.", -}; +const QWEN_PORTAL_OAUTH_MIGRATION_HINT = + "Legacy Qwen Portal OAuth profiles are not refreshable. Re-authenticate with a current portal token: openclaw onboard --auth-choice qwen-oauth."; + +function hasLegacyQwenPortalOAuthProfile(store: AuthProfileStore, profileId?: string): boolean { + const profiles = profileId ? [store.profiles[profileId]] : Object.values(store.profiles); + return profiles.some( + (profile) => + profile?.type === "oauth" && normalizeProviderId(profile.provider) === "qwen-portal", + ); +} export async function formatAuthDoctorHint(params: { cfg?: OpenClawConfig; @@ -19,11 +21,11 @@ export async function formatAuthDoctorHint(params: { profileId?: string; }): Promise { const normalizedProvider = normalizeProviderId(params.provider); - - // Check for deprecated provider migration hints first - const migrationHint = DEPRECATED_PROVIDER_MIGRATION_HINTS[normalizedProvider]; - if (migrationHint) { - return migrationHint; + if ( + normalizedProvider === "qwen-portal" && + hasLegacyQwenPortalOAuthProfile(params.store, params.profileId) + ) { + return QWEN_PORTAL_OAUTH_MIGRATION_HINT; } const pluginHint = await buildProviderAuthDoctorHintWithPlugin({