mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
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:
committed by
GitHub
parent
311c1a05eb
commit
470fc879e8
10
.github/labeler.yml
vendored
10
.github/labeler.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
23
docs/plugins/reference/gmi.md
Normal file
23
docs/plugins/reference/gmi.md
Normal 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)
|
||||
23
docs/plugins/reference/novita.md
Normal file
23
docs/plugins/reference/novita.md
Normal 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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
47
docs/providers/gmi.md
Normal 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)
|
||||
@@ -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
47
docs/providers/novita.md
Normal 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)
|
||||
80
docs/providers/ollama-cloud.md
Normal file
80
docs/providers/ollama-cloud.md
Normal 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)
|
||||
@@ -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">
|
||||
|
||||
76
docs/providers/qwen-oauth.md
Normal file
76
docs/providers/qwen-oauth.md
Normal 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)
|
||||
@@ -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
|
||||
|
||||
38
extensions/gmi/index.test.ts
Normal file
38
extensions/gmi/index.test.ts
Normal 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
49
extensions/gmi/index.ts
Normal 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
19
extensions/gmi/models.ts
Normal 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",
|
||||
};
|
||||
}
|
||||
162
extensions/gmi/openclaw.plugin.json
Normal file
162
extensions/gmi/openclaw.plugin.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
extensions/gmi/package.json
Normal file
13
extensions/gmi/package.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
10
extensions/gmi/provider-catalog.ts
Normal file
10
extensions/gmi/provider-catalog.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
16
extensions/gmi/tsconfig.json
Normal file
16
extensions/gmi/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
36
extensions/novita/index.test.ts
Normal file
36
extensions/novita/index.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
49
extensions/novita/index.ts
Normal file
49
extensions/novita/index.ts
Normal 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"),
|
||||
},
|
||||
});
|
||||
19
extensions/novita/models.ts
Normal file
19
extensions/novita/models.ts
Normal 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",
|
||||
};
|
||||
}
|
||||
162
extensions/novita/openclaw.plugin.json
Normal file
162
extensions/novita/openclaw.plugin.json
Normal 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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
extensions/novita/package.json
Normal file
13
extensions/novita/package.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
10
extensions/novita/provider-catalog.ts
Normal file
10
extensions/novita/provider-catalog.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
16
extensions/novita/tsconfig.json
Normal file
16
extensions/novita/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
},
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
12
pnpm-lock.yaml
generated
@@ -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':
|
||||
|
||||
@@ -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.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user