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({