feat: add hosted model providers (#88247)

* feat(providers): add GMI provider

* feat(providers): add Novita provider

* feat(providers): add Qwen OAuth provider

* feat(providers): add Ollama Cloud provider

* docs: add hosted provider pages

* test(providers): align qwen catalog result typing
This commit is contained in:
Peter Steinberger
2026-05-30 08:26:16 +02:00
committed by GitHub
parent 311c1a05eb
commit 470fc879e8
45 changed files with 1777 additions and 78 deletions

10
.github/labeler.yml vendored
View File

@@ -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:

View File

@@ -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

View File

@@ -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",

View File

@@ -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`<br />included in OpenClaw | contracts: tools, webFetchProviders, webSearchProviders |
| [fireworks](/plugins/reference/fireworks) | Adds Fireworks model provider support to OpenClaw. | `@openclaw/fireworks-provider`<br />included in OpenClaw | providers: fireworks |
| [github-copilot](/plugins/reference/github-copilot) | Adds GitHub Copilot model provider support to OpenClaw. | `@openclaw/github-copilot-provider`<br />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`<br />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`<br />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`<br />included in OpenClaw | contracts: speechProviders |
| [groq](/plugins/reference/groq) | Adds Groq model provider support to OpenClaw. | `@openclaw/groq-provider`<br />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`<br />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`<br />included in OpenClaw | providers: mistral; contracts: mediaUnderstandingProviders, memoryEmbeddingProviders, realtimeTranscriptionProviders |
| [moonshot](/plugins/reference/moonshot) | Adds Moonshot model provider support to OpenClaw. | `@openclaw/moonshot-provider`<br />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`<br />included in OpenClaw | providers: novita, novita-ai, novitaai |
| [nvidia](/plugins/reference/nvidia) | Adds NVIDIA model provider support to OpenClaw. | `@openclaw/nvidia-provider`<br />included in OpenClaw | providers: nvidia |
| [oc-path](/plugins/reference/oc-path) | Adds the openclaw path CLI for oc:// workspace file addressing. | `@openclaw/oc-path`<br />included in OpenClaw | plugin |
| [ollama](/plugins/reference/ollama) | Adds Ollama model provider support to OpenClaw. | `@openclaw/ollama-provider`<br />included in OpenClaw | providers: ollama; contracts: memoryEmbeddingProviders, webSearchProviders |
| [ollama](/plugins/reference/ollama) | Adds Ollama, Ollama Cloud model provider support to OpenClaw. | `@openclaw/ollama-provider`<br />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`<br />included in OpenClaw | skills |
| [openai](/plugins/reference/openai) | Adds OpenAI, OpenAI Codex model provider support to OpenClaw. | `@openclaw/openai-provider`<br />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`<br />included in OpenClaw | providers: opencode; contracts: mediaUnderstandingProviders |
@@ -112,7 +114,7 @@ commands.
| [perplexity](/plugins/reference/perplexity) | Adds web search provider support. | `@openclaw/perplexity-plugin`<br />included in OpenClaw | contracts: webSearchProviders |
| [policy](/plugins/reference/policy) | Adds policy-backed doctor checks for workspace conformance. | `@openclaw/policy`<br />included in OpenClaw | plugin |
| [qianfan](/plugins/reference/qianfan) | Adds Qianfan model provider support to OpenClaw. | `@openclaw/qianfan-provider`<br />included in OpenClaw | providers: qianfan |
| [qwen](/plugins/reference/qwen) | Adds Qwen, Qwen Cloud, Model Studio, DashScope model provider support to OpenClaw. | `@openclaw/qwen-provider`<br />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`<br />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`<br />included in OpenClaw | contracts: videoGenerationProviders |
| [searxng](/plugins/reference/searxng) | Adds web search provider support. | `@openclaw/searxng-plugin`<br />included in OpenClaw | contracts: webSearchProviders |
| [senseaudio](/plugins/reference/senseaudio) | Adds media understanding provider support. | `@openclaw/senseaudio-provider`<br />included in OpenClaw | contracts: mediaUnderstandingProviders |

View File

@@ -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`<br />included in OpenClaw | contracts: tools, webFetchProviders, webSearchProviders |
| [fireworks](/plugins/reference/fireworks) | Adds Fireworks model provider support to OpenClaw. | `@openclaw/fireworks-provider`<br />included in OpenClaw | providers: fireworks |
| [github-copilot](/plugins/reference/github-copilot) | Adds GitHub Copilot model provider support to OpenClaw. | `@openclaw/github-copilot-provider`<br />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`<br />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`<br />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`<br />npm; ClawHub | contracts: tools |
| [googlechat](/plugins/reference/googlechat) | OpenClaw Google Chat channel plugin for spaces and direct messages. | `@openclaw/googlechat`<br />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`<br />npm; ClawHub | channels: msteams |
| [nextcloud-talk](/plugins/reference/nextcloud-talk) | OpenClaw Nextcloud Talk channel plugin for conversations. | `@openclaw/nextcloud-talk`<br />npm; ClawHub | channels: nextcloud-talk |
| [nostr](/plugins/reference/nostr) | OpenClaw Nostr channel plugin for NIP-04 encrypted direct messages. | `@openclaw/nostr`<br />npm; ClawHub | channels: nostr |
| [novita](/plugins/reference/novita) | Adds Novita, Novita AI, Novitaai model provider support to OpenClaw. | `@openclaw/novita-provider`<br />included in OpenClaw | providers: novita, novita-ai, novitaai |
| [nvidia](/plugins/reference/nvidia) | Adds NVIDIA model provider support to OpenClaw. | `@openclaw/nvidia-provider`<br />included in OpenClaw | providers: nvidia |
| [oc-path](/plugins/reference/oc-path) | Adds the openclaw path CLI for oc:// workspace file addressing. | `@openclaw/oc-path`<br />included in OpenClaw | plugin |
| [ollama](/plugins/reference/ollama) | Adds Ollama model provider support to OpenClaw. | `@openclaw/ollama-provider`<br />included in OpenClaw | providers: ollama; contracts: memoryEmbeddingProviders, webSearchProviders |
| [ollama](/plugins/reference/ollama) | Adds Ollama, Ollama Cloud model provider support to OpenClaw. | `@openclaw/ollama-provider`<br />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`<br />included in OpenClaw | skills |
| [openai](/plugins/reference/openai) | Adds OpenAI, OpenAI Codex model provider support to OpenClaw. | `@openclaw/openai-provider`<br />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`<br />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`<br />source checkout only | plugin |
| [qianfan](/plugins/reference/qianfan) | Adds Qianfan model provider support to OpenClaw. | `@openclaw/qianfan-provider`<br />included in OpenClaw | providers: qianfan |
| [qqbot](/plugins/reference/qqbot) | OpenClaw QQ Bot channel plugin for group and direct-message workflows. | `@openclaw/qqbot`<br />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`<br />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`<br />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`<br />included in OpenClaw | contracts: videoGenerationProviders |
| [searxng](/plugins/reference/searxng) | Adds web search provider support. | `@openclaw/searxng-plugin`<br />included in OpenClaw | contracts: webSearchProviders |
| [senseaudio](/plugins/reference/senseaudio) | Adds media understanding provider support. | `@openclaw/senseaudio-provider`<br />included in OpenClaw | contracts: mediaUnderstandingProviders |

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

47
docs/providers/gmi.md Normal file
View File

@@ -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="<your-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)

View File

@@ -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)

47
docs/providers/novita.md Normal file
View File

@@ -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="<your-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)

View File

@@ -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="<your-ollama-cloud-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="<your-ollama-cloud-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)

View File

@@ -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).
<Warning>
**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`).
</Warning>
@@ -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.
</Accordion>
<Accordion title="Remote and Ollama Cloud hosts">
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`.
</Accordion>
<Accordion title="Custom provider ids">
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
```
</Tab>
<Tab title="Local only">

View File

@@ -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="<your-qwen-portal-token>" # 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)

View File

@@ -6,21 +6,13 @@ read_when:
title: "Qwen"
---
<Warning>
**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.
</Warning>
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.
</Note>
</Tab>
<Tab title="Qwen OAuth / Portal">
**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.
<Steps>
<Step title="Provide your portal token">
```bash
openclaw onboard --auth-choice qwen-oauth
```
</Step>
<Step title="Set a default model">
```json5
{
agents: {
defaults: {
model: { primary: "qwen-oauth/qwen3.5-plus" },
},
},
}
```
</Step>
<Step title="Verify the model is available">
```bash
openclaw models list --provider qwen-oauth
```
</Step>
</Steps>
<Note>
`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.
</Note>
</Tab>
</Tabs>
## 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 |
<Note>
Availability can still vary by endpoint and billing plan even when a model is

View File

@@ -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<string, unknown> }
| 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",
);
});
});

49
extensions/gmi/index.ts Normal file
View File

@@ -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"),
},
});

19
extensions/gmi/models.ts Normal file
View File

@@ -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",
};
}

View File

@@ -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 <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
}
}
]
}
}
}
}

View File

@@ -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"]
}
}

View File

@@ -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),
};
}

View File

@@ -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"
]
}

View File

@@ -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<string, unknown> }
| 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");
});
});

View File

@@ -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"),
},
});

View File

@@ -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",
};
}

View File

@@ -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 <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
}
}
]
}
}
}
}

View File

@@ -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"]
}
}

View File

@@ -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),
};
}

View File

@@ -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"
]
}

View File

@@ -76,10 +76,10 @@ beforeEach(() => {
});
function registerProvider() {
return registerProviderWithPluginConfig({});
return registerProvidersWithPluginConfig({}).find((provider) => provider.id === "ollama");
}
function registerProviderWithPluginConfig(pluginConfig: Record<string, unknown>) {
function registerProvidersWithPluginConfig(pluginConfig: Record<string, unknown>) {
const registerProviderMock = vi.fn();
plugin.register(
@@ -94,8 +94,18 @@ function registerProviderWithPluginConfig(pluginConfig: Record<string, unknown>)
}),
);
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<string, unknown>) {
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<string, unknown> {
@@ -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();

View File

@@ -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<string, ProviderRuntimeModel[]>();
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<ModelProviderConfig> {
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",

View File

@@ -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 <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"]

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<string, unknown> | undefined;
const streamFn = wrapQwenProviderStream({
provider: providerId,
thinkingLevel: "off",
streamFn: ((
_model: unknown,
_context: unknown,
options?: {
onPayload?: (payload: Record<string, unknown>, 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<unknown>;
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" },
],
},
]);
},
);
});

View File

@@ -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());
},

View File

@@ -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<ModelDefinitionConfig> = [
{
@@ -178,6 +181,10 @@ export function buildQwenDefaultModelDefinition(): ModelDefinitionConfig {
return buildQwenModelDefinition({ id: QWEN_DEFAULT_MODEL_ID });
}
export function buildQwenOAuthModelCatalog(): ReadonlyArray<ModelDefinitionConfig> {
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. */

View File

@@ -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;

View File

@@ -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 <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 <token>",
"cliDescription": "Qwen OAuth token"
}
],
"configSchema": {

View File

@@ -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;

View File

@@ -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<string, unknown> => Boolean(part));
return normalized.length > 0 ? normalized : content;
}
function patchQwenOAuthPayload(payload: Record<string, unknown>): 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<string, unknown>;
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<string, unknown>).cache_control = { type: "ephemeral" };
}
}
payload.vl_high_resolution_images = true;
}
function setQwenChatTemplateThinking(payload: Record<string, unknown>, 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);
}
});
}

12
pnpm-lock.yaml generated
View File

@@ -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':

View File

@@ -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.",
);
});
});

View File

@@ -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<string, string> = {
"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<string> {
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({