diff --git a/.github/labeler.yml b/.github/labeler.yml index f82c50693cc..946413e75a2 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -221,10 +221,6 @@ - changed-files: - any-glob-to-any-file: - "extensions/open-prose/**" -"extensions: qwen-portal-auth": - - changed-files: - - any-glob-to-any-file: - - "extensions/qwen-portal-auth/**" "extensions: device-pair": - changed-files: - any-glob-to-any-file: diff --git a/CHANGELOG.md b/CHANGELOG.md index aaf57347d29..6a0d99260ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Docs: https://docs.openclaw.ai ### Breaking +- Providers/Qwen: remove the deprecated `qwen-portal-auth` OAuth integration for `portal.qwen.ai`; migrate to Model Studio with `openclaw onboard --auth-choice modelstudio-api-key`. (#52709) Thanks @pomelo-nwu. + ### Changes - MiniMax: add image generation provider for `image-01` model, supporting generate and image-to-image editing with aspect ratio control. (#54487) Thanks @liyuan97. diff --git a/docs/.generated/config-baseline.json b/docs/.generated/config-baseline.json index 5dd29d10ba8..712a3e33ce7 100644 --- a/docs/.generated/config-baseline.json +++ b/docs/.generated/config-baseline.json @@ -54228,127 +54228,6 @@ "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", "hasChildren": false }, - { - "path": "plugins.entries.qwen-portal-auth", - "kind": "plugin", - "type": "object", - "required": false, - "deprecated": false, - "sensitive": false, - "tags": [ - "advanced" - ], - "label": "qwen-portal-auth", - "help": "Plugin entry for qwen-portal-auth.", - "hasChildren": true - }, - { - "path": "plugins.entries.qwen-portal-auth.config", - "kind": "plugin", - "type": "object", - "required": false, - "deprecated": false, - "sensitive": false, - "tags": [ - "advanced" - ], - "label": "qwen-portal-auth Config", - "help": "Plugin-defined config payload for qwen-portal-auth.", - "hasChildren": false - }, - { - "path": "plugins.entries.qwen-portal-auth.enabled", - "kind": "plugin", - "type": "boolean", - "required": false, - "deprecated": false, - "sensitive": false, - "tags": [ - "advanced" - ], - "label": "Enable qwen-portal-auth", - "hasChildren": false - }, - { - "path": "plugins.entries.qwen-portal-auth.hooks", - "kind": "plugin", - "type": "object", - "required": false, - "deprecated": false, - "sensitive": false, - "tags": [ - "advanced" - ], - "label": "Plugin Hook Policy", - "help": "Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.", - "hasChildren": true - }, - { - "path": "plugins.entries.qwen-portal-auth.hooks.allowPromptInjection", - "kind": "plugin", - "type": "boolean", - "required": false, - "deprecated": false, - "sensitive": false, - "tags": [ - "access" - ], - "label": "Allow Prompt Injection Hooks", - "help": "Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.", - "hasChildren": false - }, - { - "path": "plugins.entries.qwen-portal-auth.subagent", - "kind": "plugin", - "type": "object", - "required": false, - "deprecated": false, - "sensitive": false, - "tags": [ - "advanced" - ], - "label": "Plugin Subagent Policy", - "help": "Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.", - "hasChildren": true - }, - { - "path": "plugins.entries.qwen-portal-auth.subagent.allowedModels", - "kind": "plugin", - "type": "array", - "required": false, - "deprecated": false, - "sensitive": false, - "tags": [ - "access" - ], - "label": "Plugin Subagent Allowed Models", - "help": "Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.", - "hasChildren": true - }, - { - "path": "plugins.entries.qwen-portal-auth.subagent.allowedModels.*", - "kind": "plugin", - "type": "string", - "required": false, - "deprecated": false, - "sensitive": false, - "tags": [], - "hasChildren": false - }, - { - "path": "plugins.entries.qwen-portal-auth.subagent.allowModelOverride", - "kind": "plugin", - "type": "boolean", - "required": false, - "deprecated": false, - "sensitive": false, - "tags": [ - "access" - ], - "label": "Allow Plugin Subagent Model Override", - "help": "Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.", - "hasChildren": false - }, { "path": "plugins.entries.sglang", "kind": "plugin", diff --git a/docs/.generated/config-baseline.jsonl b/docs/.generated/config-baseline.jsonl index d2e3d56c97c..4ea3903e805 100644 --- a/docs/.generated/config-baseline.jsonl +++ b/docs/.generated/config-baseline.jsonl @@ -1,4 +1,4 @@ -{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5648} +{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5639} {"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true} {"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true} {"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} @@ -4691,15 +4691,6 @@ {"recordType":"path","path":"plugins.entries.qianfan.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} {"recordType":"path","path":"plugins.entries.qianfan.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} {"recordType":"path","path":"plugins.entries.qianfan.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} -{"recordType":"path","path":"plugins.entries.qwen-portal-auth","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"qwen-portal-auth","help":"Plugin entry for qwen-portal-auth.","hasChildren":true} -{"recordType":"path","path":"plugins.entries.qwen-portal-auth.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"qwen-portal-auth Config","help":"Plugin-defined config payload for qwen-portal-auth.","hasChildren":false} -{"recordType":"path","path":"plugins.entries.qwen-portal-auth.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable qwen-portal-auth","hasChildren":false} -{"recordType":"path","path":"plugins.entries.qwen-portal-auth.hooks","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Hook Policy","help":"Per-plugin typed hook policy controls for core-enforced safety gates. Use this to constrain high-impact hook categories without disabling the entire plugin.","hasChildren":true} -{"recordType":"path","path":"plugins.entries.qwen-portal-auth.hooks.allowPromptInjection","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Prompt Injection Hooks","help":"Controls whether this plugin may mutate prompts through typed hooks. Set false to block `before_prompt_build` and ignore prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride` behavior.","hasChildren":false} -{"recordType":"path","path":"plugins.entries.qwen-portal-auth.subagent","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugin Subagent Policy","help":"Per-plugin subagent runtime controls for model override trust and allowlists. Keep this unset unless a plugin must explicitly steer subagent model selection.","hasChildren":true} -{"recordType":"path","path":"plugins.entries.qwen-portal-auth.subagent.allowedModels","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Plugin Subagent Allowed Models","help":"Allowed override targets for trusted plugin subagent runs as canonical \"provider/model\" refs. Use \"*\" only when you intentionally allow any model.","hasChildren":true} -{"recordType":"path","path":"plugins.entries.qwen-portal-auth.subagent.allowedModels.*","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false} -{"recordType":"path","path":"plugins.entries.qwen-portal-auth.subagent.allowModelOverride","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"Allow Plugin Subagent Model Override","help":"Explicitly allows this plugin to request provider/model overrides in background subagent runs. Keep false unless the plugin is trusted to steer model selection.","hasChildren":false} {"recordType":"path","path":"plugins.entries.sglang","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/sglang-provider","help":"OpenClaw SGLang provider plugin (plugin: sglang)","hasChildren":true} {"recordType":"path","path":"plugins.entries.sglang.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"@openclaw/sglang-provider Config","help":"Plugin-defined config payload for sglang.","hasChildren":false} {"recordType":"path","path":"plugins.entries.sglang.enabled","kind":"plugin","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Enable @openclaw/sglang-provider","hasChildren":false} diff --git a/docs/cli/models.md b/docs/cli/models.md index e023784cc5e..ceba129099b 100644 --- a/docs/cli/models.md +++ b/docs/cli/models.md @@ -38,7 +38,7 @@ Notes: - `models set ` accepts `provider/model` or an alias. - Model refs are parsed by splitting on the **first** `/`. If the model ID includes `/` (OpenRouter-style), include the provider prefix (example: `openrouter/moonshotai/kimi-k2`). - If you omit the provider, OpenClaw treats the input as an alias or a model for the **default provider** (only works when there is no `/` in the model ID). -- `models status` may show `marker()` in auth output for non-secret placeholders (for example `OPENAI_API_KEY`, `secretref-managed`, `minimax-oauth`, `qwen-oauth`, `ollama-local`) instead of masking them as secrets. +- `models status` may show `marker()` in auth output for non-secret placeholders (for example `OPENAI_API_KEY`, `secretref-managed`, `minimax-oauth`, `oauth:chutes`, `ollama-local`) instead of masking them as secrets. ### `models status` diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index ebcf7e49290..a1987aa8977 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -108,7 +108,6 @@ Current bundled examples: - `byteplus`, `cloudflare-ai-gateway`, `huggingface`, `kimi-coding`, `modelstudio`, `nvidia`, `qianfan`, `synthetic`, `together`, `venice`, `vercel-ai-gateway`, and `volcengine`: plugin-owned catalogs only -- `qwen-portal`: plugin-owned catalog, OAuth login, and OAuth refresh - `minimax` and `xiaomi`: plugin-owned catalogs plus usage auth/snapshot logic The bundled `openai` plugin now owns both provider ids: `openai` and @@ -348,22 +347,6 @@ Kimi Coding uses Moonshot AI's Anthropic-compatible endpoint: } ``` -### Qwen OAuth (free tier) - -Qwen provides OAuth access to Qwen Coder + Vision via a device-code flow. -The bundled provider plugin is enabled by default, so just log in: - -```bash -openclaw models auth login --provider qwen-portal --set-default -``` - -Model refs: - -- `qwen-portal/coder-model` -- `qwen-portal/vision-model` - -See [/providers/qwen](/providers/qwen) for setup details and notes. - ### Volcano Engine (Doubao) Volcano Engine (火山引擎) provides access to Doubao and other models in China. diff --git a/docs/help/testing.md b/docs/help/testing.md index b12b38234cb..7ebd66f94ca 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -469,7 +469,7 @@ Useful env vars: - `OPENCLAW_WORKSPACE_DIR=...` (default: `~/.openclaw/workspace`) mounted to `/home/node/.openclaw/workspace` - `OPENCLAW_PROFILE_FILE=...` (default: `~/.profile`) mounted to `/home/node/.profile` and sourced before running tests - External CLI auth dirs under `$HOME` are mounted read-only under `/host-auth/...`, then copied into `/home/node/...` before tests start - - Default: mount all supported dirs (`.codex`, `.claude`, `.qwen`, `.minimax`) + - Default: mount all supported dirs (`.codex`, `.claude`, `.minimax`) - Narrowed provider runs mount only the needed dirs inferred from `OPENCLAW_LIVE_PROVIDERS` / `OPENCLAW_LIVE_GATEWAY_PROVIDERS` - Override manually with `OPENCLAW_DOCKER_AUTH_DIRS=all`, `OPENCLAW_DOCKER_AUTH_DIRS=none`, or a comma list like `OPENCLAW_DOCKER_AUTH_DIRS=.claude,.codex` - `OPENCLAW_LIVE_GATEWAY_MODELS=...` / `OPENCLAW_LIVE_MODELS=...` to narrow the run diff --git a/docs/providers/index.md b/docs/providers/index.md index 6e639ecc27f..caf2ea90393 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -49,7 +49,6 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi - [Perplexity (web search)](/providers/perplexity-provider) - [Qianfan](/providers/qianfan) - [Qwen / Model Studio (Alibaba Cloud)](/providers/qwen_modelstudio) -- [Qwen (OAuth)](/providers/qwen) - [SGLang (local models)](/providers/sglang) - [Synthetic](/providers/synthetic) - [Together AI](/providers/together) diff --git a/docs/providers/qwen.md b/docs/providers/qwen.md index 6776c226e86..9a758e2db96 100644 --- a/docs/providers/qwen.md +++ b/docs/providers/qwen.md @@ -1,53 +1,33 @@ --- -summary: "Use Qwen OAuth (free tier) in OpenClaw" +summary: "Use Qwen models via Alibaba Cloud Model Studio" read_when: - You want to use Qwen with OpenClaw - - You want free-tier OAuth access to Qwen Coder + - You previously used Qwen OAuth title: "Qwen" --- # Qwen -Qwen provides a free-tier OAuth flow for Qwen Coder and Qwen Vision models -(2,000 requests/day, subject to Qwen rate limits). + -## Enable the plugin +**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. + + + +## Recommended: Model Studio (Alibaba Cloud Coding Plan) + +Use [Model Studio](/providers/modelstudio) for officially supported access to +Qwen models (Qwen 3.5 Plus, GLM-4.7, Kimi K2.5, MiniMax M2.5, and more). ```bash -openclaw plugins enable qwen-portal-auth +# Global endpoint +openclaw onboard --auth-choice modelstudio-api-key + +# China endpoint +openclaw onboard --auth-choice modelstudio-api-key-cn ``` -Restart the Gateway after enabling. - -## Authenticate - -```bash -openclaw models auth login --provider qwen-portal --set-default -``` - -This runs the Qwen device-code OAuth flow and writes a provider entry to your -`models.json` (plus a `qwen` alias for quick switching). - -## Model IDs - -- `qwen-portal/coder-model` -- `qwen-portal/vision-model` - -Switch models with: - -```bash -openclaw models set qwen-portal/coder-model -``` - -## Reuse Qwen Code CLI login - -If you already logged in with the Qwen Code CLI, OpenClaw will sync credentials -from `~/.qwen/oauth_creds.json` when it loads the auth store. You still need a -`models.providers.qwen-portal` entry (use the login command above to create one). - -## Notes - -- Tokens auto-refresh; re-run the login command if refresh fails or access is revoked. -- Default base URL: `https://portal.qwen.ai/v1` (override with - `models.providers.qwen-portal.baseUrl` if Qwen provides a different endpoint). -- See [Model providers](/concepts/model-providers) for provider-wide rules. +See [Model Studio](/providers/modelstudio) for full setup details. diff --git a/docs/tools/plugin.md b/docs/tools/plugin.md index 83bc123a4cf..cd58fa3e97a 100644 --- a/docs/tools/plugin.md +++ b/docs/tools/plugin.md @@ -90,7 +90,7 @@ and the [Plugin SDK Overview](/plugins/sdk-overview). `anthropic`, `byteplus`, `cloudflare-ai-gateway`, `github-copilot`, `google`, `huggingface`, `kilocode`, `kimi-coding`, `minimax`, `mistral`, `modelstudio`, `moonshot`, `nvidia`, `openai`, `opencode`, `opencode-go`, `openrouter`, - `qianfan`, `qwen-portal-auth`, `synthetic`, `together`, `venice`, + `qianfan`, `synthetic`, `together`, `venice`, `vercel-ai-gateway`, `volcengine`, `xiaomi`, `zai` diff --git a/docs/zh-CN/concepts/model-providers.md b/docs/zh-CN/concepts/model-providers.md index ebbe5af3271..bc58782f092 100644 --- a/docs/zh-CN/concepts/model-providers.md +++ b/docs/zh-CN/concepts/model-providers.md @@ -109,7 +109,6 @@ x-i18n: - `byteplus`、`cloudflare-ai-gateway`、`huggingface`、`kimi-coding`、 `modelstudio`、`nvidia`、`qianfan`、`synthetic`、`together`、`venice`、 `vercel-ai-gateway` 和 `volcengine`:仅插件接管的目录 -- `qwen-portal`:插件接管的目录、OAuth 登录和 OAuth 刷新 - `minimax` 和 `xiaomi`:插件接管的目录,以及使用量身份验证/快照逻辑 内置的 `openai` 插件现在接管两个提供商 ID:`openai` 和 @@ -348,22 +347,6 @@ Kimi Coding 使用 Moonshot AI 的 Anthropic 兼容端点: } ``` -### Qwen OAuth(免费层) - -Qwen 通过设备代码流程提供对 Qwen Coder + Vision 的 OAuth 访问。 -内置提供商插件默认启用,因此只需登录: - -```bash -openclaw models auth login --provider qwen-portal --set-default -``` - -模型引用: - -- `qwen-portal/coder-model` -- `qwen-portal/vision-model` - -设置详情和说明请参见 [/providers/qwen](/providers/qwen)。 - ### Volcano Engine(Doubao) Volcano Engine(火山引擎)为中国用户提供对 Doubao 和其他模型的访问。 diff --git a/docs/zh-CN/providers/qwen.md b/docs/zh-CN/providers/qwen.md index c755999d1e5..3156edaa42e 100644 --- a/docs/zh-CN/providers/qwen.md +++ b/docs/zh-CN/providers/qwen.md @@ -1,55 +1,36 @@ --- read_when: - 你想在 OpenClaw 中使用 Qwen - - 你想要免费层 OAuth 访问 Qwen Coder -summary: 在 OpenClaw 中使用 Qwen OAuth(免费层) + - 你之前使用过 Qwen OAuth +summary: 通过阿里云 Model Studio 使用 Qwen 模型 title: Qwen x-i18n: - generated_at: "2026-02-03T07:53:34Z" + generated_at: "2026-03-23T00:00:00Z" model: claude-opus-4-5 provider: pi - source_hash: 88b88e224e2fecbb1ca26e24fbccdbe25609be40b38335d0451343a5da53fdd4 + source_hash: "" source_path: providers/qwen.md workflow: 15 --- # Qwen -Qwen 为 Qwen Coder 和 Qwen Vision 模型提供免费层 OAuth 流程(每天 2,000 次请求,受 Qwen 速率限制约束)。 + -## 启用插件 +**Qwen OAuth 已移除。** 使用 `portal.qwen.ai` 端点的免费层 OAuth 集成(`qwen-portal`)已不再可用。详情请参见 [Issue #49557](https://github.com/openclaw/openclaw/issues/49557)。 + + + +## 推荐方案:Model Studio(阿里云 Coding Plan) + +使用 [Model Studio](/providers/modelstudio) 获取官方支持的 Qwen 模型访问(Qwen 3.5 Plus、GLM-4.7、Kimi K2.5、MiniMax M2.5 等)。 ```bash -openclaw plugins enable qwen-portal-auth +# 全球端点 +openclaw onboard --auth-choice modelstudio-api-key + +# 中国端点 +openclaw onboard --auth-choice modelstudio-api-key-cn ``` -启用后重启 Gateway 网关。 - -## 认证 - -```bash -openclaw models auth login --provider qwen-portal --set-default -``` - -这会运行 Qwen 设备码 OAuth 流程并将提供商条目写入你的 `models.json`(加上一个 `qwen` 别名以便快速切换)。 - -## 模型 ID - -- `qwen-portal/coder-model` -- `qwen-portal/vision-model` - -切换模型: - -```bash -openclaw models set qwen-portal/coder-model -``` - -## 复用 Qwen Code CLI 登录 - -如果你已经使用 Qwen Code CLI 登录,OpenClaw 会在加载认证存储时从 `~/.qwen/oauth_creds.json` 同步凭证。你仍然需要一个 `models.providers.qwen-portal` 条目(使用上面的登录命令创建一个)。 - -## 注意 - -- 令牌自动刷新;如果刷新失败或访问被撤销,请重新运行登录命令。 -- 默认基础 URL:`https://portal.qwen.ai/v1`(如果 Qwen 提供不同的端点,使用 `models.providers.qwen-portal.baseUrl` 覆盖)。 -- 参阅[模型提供商](/concepts/model-providers)了解提供商级别的规则。 +完整设置详情请参见 [Model Studio](/providers/modelstudio)。 diff --git a/docs/zh-CN/tools/plugin.md b/docs/zh-CN/tools/plugin.md index 775d94eb751..d286cffafd2 100644 --- a/docs/zh-CN/tools/plugin.md +++ b/docs/zh-CN/tools/plugin.md @@ -155,7 +155,6 @@ Bundle hook 支持仅限于常规 OpenClaw hook 目录格式(在声明的 hook - OpenCode Zen provider 能力 — 以 `opencode` 形式捆绑(默认启用) - OpenRouter provider 运行时 — 以 `openrouter` 形式捆绑(默认启用) - Qianfan provider catalog — 以 `qianfan` 形式捆绑(默认启用) -- Qwen OAuth(provider 身份验证 + catalog)— 以 `qwen-portal-auth` 形式捆绑(默认启用) - Synthetic provider catalog — 以 `synthetic` 形式捆绑(默认启用) - Together provider catalog — 以 `together` 形式捆绑(默认启用) - Venice provider catalog — 以 `venice` 形式捆绑(默认启用) @@ -497,7 +496,7 @@ api.registerHttpRoute({ `openclaw/plugin-sdk/minimax-portal-auth`、 `openclaw/plugin-sdk/nextcloud-talk`、`openclaw/plugin-sdk/nostr`、 `openclaw/plugin-sdk/open-prose`、`openclaw/plugin-sdk/phone-control`、 - `openclaw/plugin-sdk/qwen-portal-auth`、`openclaw/plugin-sdk/synology-chat`、 + `openclaw/plugin-sdk/synology-chat`、 `openclaw/plugin-sdk/talk-voice`、`openclaw/plugin-sdk/test-utils`、 `openclaw/plugin-sdk/thread-ownership`、`openclaw/plugin-sdk/tlon`、 `openclaw/plugin-sdk/twitch`、`openclaw/plugin-sdk/voice-call`、 @@ -613,7 +612,6 @@ OpenClaw 按以下顺序扫描: - `openrouter` - `phone-control` - `qianfan` -- `qwen-portal-auth` - `sglang` - `synthetic` - `talk-voice` diff --git a/extensions/qwen-portal-auth/README.md b/extensions/qwen-portal-auth/README.md deleted file mode 100644 index ab12233f008..00000000000 --- a/extensions/qwen-portal-auth/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Qwen OAuth (OpenClaw plugin) - -OAuth provider plugin for **Qwen** (free-tier OAuth). - -## Enable - -Bundled plugins are disabled by default. Enable this one: - -```bash -openclaw plugins enable qwen-portal-auth -``` - -Restart the Gateway after enabling. - -## Authenticate - -```bash -openclaw models auth login --provider qwen-portal --set-default -``` - -## Notes - -- Qwen OAuth uses a device-code login flow. -- Tokens auto-refresh; re-run login if refresh fails or access is revoked. diff --git a/extensions/qwen-portal-auth/index.ts b/extensions/qwen-portal-auth/index.ts deleted file mode 100644 index bcbc564dc33..00000000000 --- a/extensions/qwen-portal-auth/index.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { buildQwenPortalProvider, QWEN_PORTAL_BASE_URL } from "./provider-catalog.js"; -import { - buildOauthProviderAuthResult, - definePluginEntry, - ensureAuthProfileStore, - listProfilesForProvider, - QWEN_OAUTH_MARKER, - refreshQwenPortalCredentials, - type ProviderAuthContext, - type ProviderCatalogContext, -} from "./runtime-api.js"; - -const PROVIDER_ID = "qwen-portal"; -const PROVIDER_LABEL = "Qwen"; -const DEFAULT_MODEL = "qwen-portal/coder-model"; -const DEFAULT_BASE_URL = QWEN_PORTAL_BASE_URL; - -function normalizeBaseUrl(value: string | undefined): string { - const raw = value?.trim() || DEFAULT_BASE_URL; - const withProtocol = raw.startsWith("http") ? raw : `https://${raw}`; - return withProtocol.endsWith("/v1") ? withProtocol : `${withProtocol.replace(/\/+$/, "")}/v1`; -} - -function buildProviderCatalog(params: { baseUrl: string; apiKey: string }) { - return { - ...buildQwenPortalProvider(), - baseUrl: params.baseUrl, - apiKey: params.apiKey, - }; -} - -function resolveCatalog(ctx: ProviderCatalogContext) { - const explicitProvider = ctx.config.models?.providers?.[PROVIDER_ID]; - const envApiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey; - const authStore = ensureAuthProfileStore(ctx.agentDir, { - allowKeychainPrompt: false, - }); - const hasProfiles = listProfilesForProvider(authStore, PROVIDER_ID).length > 0; - const explicitApiKey = - typeof explicitProvider?.apiKey === "string" ? explicitProvider.apiKey.trim() : undefined; - const apiKey = envApiKey ?? explicitApiKey ?? (hasProfiles ? QWEN_OAUTH_MARKER : undefined); - if (!apiKey) { - return null; - } - - const explicitBaseUrl = - typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl : undefined; - - return { - provider: buildProviderCatalog({ - baseUrl: normalizeBaseUrl(explicitBaseUrl), - apiKey, - }), - }; -} - -export default definePluginEntry({ - id: "qwen-portal-auth", - name: "Qwen OAuth", - description: "OAuth flow for Qwen (free-tier) models", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: PROVIDER_LABEL, - docsPath: "/providers/qwen", - aliases: ["qwen"], - envVars: ["QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"], - catalog: { - run: async (ctx: ProviderCatalogContext) => resolveCatalog(ctx), - }, - auth: [ - { - id: "device", - label: "Qwen OAuth", - hint: "Device code login", - kind: "device_code", - run: async (ctx: ProviderAuthContext) => { - const progress = ctx.prompter.progress("Starting Qwen OAuth…"); - try { - const { loginQwenPortalOAuth } = await import("./oauth.runtime.js"); - const result = await loginQwenPortalOAuth({ - openUrl: ctx.openUrl, - note: ctx.prompter.note, - progress, - }); - - progress.stop("Qwen OAuth complete"); - - const baseUrl = normalizeBaseUrl(result.resourceUrl); - - return buildOauthProviderAuthResult({ - providerId: PROVIDER_ID, - defaultModel: DEFAULT_MODEL, - access: result.access, - refresh: result.refresh, - expires: result.expires, - configPatch: { - models: { - providers: { - [PROVIDER_ID]: { - baseUrl, - models: [], - }, - }, - }, - agents: { - defaults: { - models: { - "qwen-portal/coder-model": { alias: "qwen" }, - "qwen-portal/vision-model": {}, - }, - }, - }, - }, - notes: [ - "Qwen OAuth tokens auto-refresh. Re-run login if refresh fails or access is revoked.", - `Base URL defaults to ${DEFAULT_BASE_URL}. Override models.providers.${PROVIDER_ID}.baseUrl if needed.`, - ], - }); - } catch (err) { - progress.stop("Qwen OAuth failed"); - await ctx.prompter.note( - "If OAuth fails, verify your Qwen account has portal access and try again.", - "Qwen OAuth", - ); - throw err; - } - }, - }, - ], - wizard: { - setup: { - choiceId: "qwen-portal", - choiceLabel: "Qwen OAuth", - choiceHint: "Device code login", - methodId: "device", - }, - }, - refreshOAuth: async (cred) => ({ - ...cred, - ...(await refreshQwenPortalCredentials(cred)), - type: "oauth", - provider: PROVIDER_ID, - email: cred.email, - }), - }); - }, -}); diff --git a/extensions/qwen-portal-auth/oauth.runtime.ts b/extensions/qwen-portal-auth/oauth.runtime.ts deleted file mode 100644 index 8e2e3a0d5c7..00000000000 --- a/extensions/qwen-portal-auth/oauth.runtime.ts +++ /dev/null @@ -1 +0,0 @@ -export { loginQwenPortalOAuth } from "./oauth.js"; diff --git a/extensions/qwen-portal-auth/oauth.ts b/extensions/qwen-portal-auth/oauth.ts deleted file mode 100644 index d95273420e5..00000000000 --- a/extensions/qwen-portal-auth/oauth.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { generatePkceVerifierChallenge, toFormUrlEncoded } from "./runtime-api.js"; - -const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"; -const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`; -const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`; -const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56"; -const QWEN_OAUTH_SCOPE = "openid profile email model.completion"; -const QWEN_OAUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"; - -export type QwenDeviceAuthorization = { - device_code: string; - user_code: string; - verification_uri: string; - verification_uri_complete?: string; - expires_in: number; - interval?: number; -}; - -export type QwenOAuthToken = { - access: string; - refresh: string; - expires: number; - resourceUrl?: string; -}; - -type TokenPending = { status: "pending"; slowDown?: boolean }; - -type DeviceTokenResult = - | { status: "success"; token: QwenOAuthToken } - | TokenPending - | { status: "error"; message: string }; - -async function requestDeviceCode(params: { challenge: string }): Promise { - const response = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - "x-request-id": randomUUID(), - }, - body: toFormUrlEncoded({ - client_id: QWEN_OAUTH_CLIENT_ID, - scope: QWEN_OAUTH_SCOPE, - code_challenge: params.challenge, - code_challenge_method: "S256", - }), - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`Qwen device authorization failed: ${text || response.statusText}`); - } - - const payload = (await response.json()) as QwenDeviceAuthorization & { error?: string }; - if (!payload.device_code || !payload.user_code || !payload.verification_uri) { - throw new Error( - payload.error ?? - "Qwen device authorization returned an incomplete payload (missing user_code or verification_uri).", - ); - } - return payload; -} - -async function pollDeviceToken(params: { - deviceCode: string; - verifier: string; -}): Promise { - const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: toFormUrlEncoded({ - grant_type: QWEN_OAUTH_GRANT_TYPE, - client_id: QWEN_OAUTH_CLIENT_ID, - device_code: params.deviceCode, - code_verifier: params.verifier, - }), - }); - - if (!response.ok) { - let payload: { error?: string; error_description?: string } | undefined; - try { - payload = (await response.json()) as { error?: string; error_description?: string }; - } catch { - const text = await response.text(); - return { status: "error", message: text || response.statusText }; - } - - if (payload?.error === "authorization_pending") { - return { status: "pending" }; - } - - if (payload?.error === "slow_down") { - return { status: "pending", slowDown: true }; - } - - return { - status: "error", - message: payload?.error_description || payload?.error || response.statusText, - }; - } - - const tokenPayload = (await response.json()) as { - access_token?: string | null; - refresh_token?: string | null; - expires_in?: number | null; - token_type?: string; - resource_url?: string; - }; - - if (!tokenPayload.access_token || !tokenPayload.refresh_token || !tokenPayload.expires_in) { - return { status: "error", message: "Qwen OAuth returned incomplete token payload." }; - } - - return { - status: "success", - token: { - access: tokenPayload.access_token, - refresh: tokenPayload.refresh_token, - expires: Date.now() + tokenPayload.expires_in * 1000, - resourceUrl: tokenPayload.resource_url, - }, - }; -} - -export async function loginQwenPortalOAuth(params: { - openUrl: (url: string) => Promise; - note: (message: string, title?: string) => Promise; - progress: { update: (message: string) => void; stop: (message?: string) => void }; -}): Promise { - const { verifier, challenge } = generatePkceVerifierChallenge(); - const device = await requestDeviceCode({ challenge }); - const verificationUrl = device.verification_uri_complete || device.verification_uri; - - await params.note( - [ - `Open ${verificationUrl} to approve access.`, - `If prompted, enter the code ${device.user_code}.`, - ].join("\n"), - "Qwen OAuth", - ); - - try { - await params.openUrl(verificationUrl); - } catch { - // Fall back to manual copy/paste if browser open fails. - } - - const start = Date.now(); - let pollIntervalMs = device.interval ? device.interval * 1000 : 2000; - const timeoutMs = device.expires_in * 1000; - - while (Date.now() - start < timeoutMs) { - params.progress.update("Waiting for Qwen OAuth approval…"); - const result = await pollDeviceToken({ - deviceCode: device.device_code, - verifier, - }); - - if (result.status === "success") { - return result.token; - } - - if (result.status === "error") { - throw new Error(`Qwen OAuth failed: ${result.message}`); - } - - if (result.status === "pending" && result.slowDown) { - pollIntervalMs = Math.min(pollIntervalMs * 1.5, 10000); - } - - await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); - } - - throw new Error("Qwen OAuth timed out waiting for authorization."); -} diff --git a/extensions/qwen-portal-auth/openclaw.plugin.json b/extensions/qwen-portal-auth/openclaw.plugin.json deleted file mode 100644 index 5a6a8d555b7..00000000000 --- a/extensions/qwen-portal-auth/openclaw.plugin.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "id": "qwen-portal-auth", - "providers": ["qwen-portal"], - "providerAuthEnvVars": { - "qwen-portal": ["QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"] - }, - "providerAuthChoices": [ - { - "provider": "qwen-portal", - "method": "device", - "choiceId": "qwen-portal", - "choiceLabel": "Qwen OAuth", - "choiceHint": "Device code login", - "groupId": "qwen", - "groupLabel": "Qwen", - "groupHint": "OAuth" - } - ], - "configSchema": { - "type": "object", - "additionalProperties": false, - "properties": {} - } -} diff --git a/extensions/qwen-portal-auth/provider-catalog.ts b/extensions/qwen-portal-auth/provider-catalog.ts deleted file mode 100644 index f8d350fc2da..00000000000 --- a/extensions/qwen-portal-auth/provider-catalog.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { - ModelDefinitionConfig, - ModelProviderConfig, -} from "openclaw/plugin-sdk/provider-models"; - -export const QWEN_PORTAL_BASE_URL = "https://portal.qwen.ai/v1"; -const QWEN_PORTAL_DEFAULT_CONTEXT_WINDOW = 128000; -const QWEN_PORTAL_DEFAULT_MAX_TOKENS = 8192; -const QWEN_PORTAL_DEFAULT_COST = { - input: 0, - output: 0, - cacheRead: 0, - cacheWrite: 0, -}; - -function buildModelDefinition(params: { - id: string; - name: string; - input: ModelDefinitionConfig["input"]; -}): ModelDefinitionConfig { - return { - id: params.id, - name: params.name, - reasoning: false, - input: params.input, - cost: QWEN_PORTAL_DEFAULT_COST, - contextWindow: QWEN_PORTAL_DEFAULT_CONTEXT_WINDOW, - maxTokens: QWEN_PORTAL_DEFAULT_MAX_TOKENS, - }; -} - -export function buildQwenPortalProvider(): ModelProviderConfig { - return { - baseUrl: QWEN_PORTAL_BASE_URL, - api: "openai-completions", - models: [ - buildModelDefinition({ - id: "coder-model", - name: "Qwen Coder", - input: ["text"], - }), - buildModelDefinition({ - id: "vision-model", - name: "Qwen Vision", - input: ["text", "image"], - }), - ], - }; -} diff --git a/extensions/qwen-portal-auth/refresh.test.ts b/extensions/qwen-portal-auth/refresh.test.ts deleted file mode 100644 index c6276b4248c..00000000000 --- a/extensions/qwen-portal-auth/refresh.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; -import { withFetchPreconnect } from "../../test/helpers/extensions/fetch-mock.js"; -import { refreshQwenPortalCredentials } from "./refresh.js"; - -function expiredCredentials() { - return { - type: "oauth" as const, - provider: "qwen-portal", - access: "expired-access", - refresh: "refresh-token", - expires: Date.now() - 60_000, - }; -} - -describe("refreshQwenPortalCredentials", () => { - const originalFetch = globalThis.fetch; - - afterEach(() => { - globalThis.fetch = originalFetch; - vi.restoreAllMocks(); - }); - - const runRefresh = async () => await refreshQwenPortalCredentials(expiredCredentials()); - - it("refreshes oauth credentials and preserves existing refresh token when absent", async () => { - globalThis.fetch = withFetchPreconnect( - vi.fn(async () => { - return new Response( - JSON.stringify({ - access_token: "new-access", - expires_in: 3600, - }), - { - status: 200, - headers: { "Content-Type": "application/json" }, - }, - ); - }), - ); - - const result = await runRefresh(); - - expect(result.access).toBe("new-access"); - expect(result.refresh).toBe("refresh-token"); - expect(result.expires).toBeGreaterThan(Date.now()); - expect(globalThis.fetch).toHaveBeenCalledTimes(1); - expect(globalThis.fetch).toHaveBeenCalledWith( - "https://chat.qwen.ai/api/v1/oauth2/token", - expect.objectContaining({ - method: "POST", - body: expect.any(URLSearchParams), - }), - ); - }); - - it("replaces the refresh token when the server rotates it", async () => { - globalThis.fetch = withFetchPreconnect( - vi.fn(async () => { - return new Response( - JSON.stringify({ - access_token: "new-access", - refresh_token: "rotated-refresh", - expires_in: 1200, - }), - { - status: 200, - headers: { "Content-Type": "application/json" }, - }, - ); - }), - ); - - const result = await runRefresh(); - - expect(result.refresh).toBe("rotated-refresh"); - }); - - it("rejects invalid expires_in payloads", async () => { - globalThis.fetch = withFetchPreconnect( - vi.fn(async () => { - return new Response( - JSON.stringify({ - access_token: "new-access", - expires_in: 0, - }), - { - status: 200, - headers: { "Content-Type": "application/json" }, - }, - ); - }), - ); - - await expect(runRefresh()).rejects.toThrow( - "Qwen OAuth refresh response missing or invalid expires_in", - ); - }); - - it("turns 400 responses into a re-authenticate hint", async () => { - globalThis.fetch = withFetchPreconnect( - vi.fn(async () => new Response("bad refresh", { status: 400 })), - ); - - await expect(runRefresh()).rejects.toThrow("Qwen OAuth refresh token expired or invalid"); - }); - - it("requires a refresh token", async () => { - await expect( - refreshQwenPortalCredentials({ - type: "oauth", - provider: "qwen-portal", - access: "expired-access", - refresh: "", - expires: Date.now() - 60_000, - }), - ).rejects.toThrow("Qwen OAuth refresh token missing"); - }); - - it("rejects missing access tokens", async () => { - globalThis.fetch = withFetchPreconnect( - vi.fn(async () => { - return new Response( - JSON.stringify({ - expires_in: 3600, - }), - { - status: 200, - headers: { "Content-Type": "application/json" }, - }, - ); - }), - ); - - await expect(runRefresh()).rejects.toThrow("Qwen OAuth refresh response missing access token"); - }); - - it("surfaces non-400 refresh failures", async () => { - globalThis.fetch = withFetchPreconnect( - vi.fn(async () => new Response("gateway down", { status: 502 })), - ); - - await expect(runRefresh()).rejects.toThrow("Qwen OAuth refresh failed: gateway down"); - }); -}); diff --git a/extensions/qwen-portal-auth/refresh.ts b/extensions/qwen-portal-auth/refresh.ts deleted file mode 100644 index eee8421e011..00000000000 --- a/extensions/qwen-portal-auth/refresh.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { OAuthCredentials } from "@mariozechner/pi-ai"; -import { formatCliCommand } from "openclaw/plugin-sdk/setup-tools"; - -const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"; -const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`; -const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56"; - -export async function refreshQwenPortalCredentials( - credentials: OAuthCredentials, -): Promise { - const refreshToken = credentials.refresh?.trim(); - if (!refreshToken) { - throw new Error("Qwen OAuth refresh token missing; re-authenticate."); - } - - const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - Accept: "application/json", - }, - body: new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: QWEN_OAUTH_CLIENT_ID, - }), - }); - - if (!response.ok) { - const text = await response.text(); - if (response.status === 400) { - throw new Error( - `Qwen OAuth refresh token expired or invalid. Re-authenticate with \`${formatCliCommand("openclaw models auth login --provider qwen-portal")}\`.`, - ); - } - throw new Error(`Qwen OAuth refresh failed: ${text || response.statusText}`); - } - - const payload = (await response.json()) as { - access_token?: string; - refresh_token?: string; - expires_in?: number; - }; - const accessToken = payload.access_token?.trim(); - const newRefreshToken = payload.refresh_token?.trim(); - const expiresIn = payload.expires_in; - - if (!accessToken) { - throw new Error("Qwen OAuth refresh response missing access token."); - } - if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) { - throw new Error("Qwen OAuth refresh response missing or invalid expires_in."); - } - - return { - ...credentials, - // RFC 6749 section 6: new refresh token is optional; if present, replace old. - refresh: newRefreshToken || refreshToken, - access: accessToken, - expires: Date.now() + expiresIn * 1000, - }; -} diff --git a/extensions/qwen-portal-auth/runtime-api.ts b/extensions/qwen-portal-auth/runtime-api.ts deleted file mode 100644 index 5fbd1e571b4..00000000000 --- a/extensions/qwen-portal-auth/runtime-api.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth"; -export { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -export type { ProviderAuthContext, ProviderCatalogContext } from "openclaw/plugin-sdk/plugin-entry"; -export { ensureAuthProfileStore, listProfilesForProvider } from "openclaw/plugin-sdk/provider-auth"; -export { QWEN_OAUTH_MARKER } from "openclaw/plugin-sdk/agent-runtime"; -export { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk/provider-auth"; -export { refreshQwenPortalCredentials } from "./refresh.js"; diff --git a/scripts/check-no-raw-channel-fetch.mjs b/scripts/check-no-raw-channel-fetch.mjs index 18d801906b1..1d3df8f0923 100644 --- a/scripts/check-no-raw-channel-fetch.mjs +++ b/scripts/check-no-raw-channel-fetch.mjs @@ -34,8 +34,6 @@ const allowedRawFetchCallsites = new Set([ "extensions/nextcloud-talk/src/room-info.ts:92", "extensions/nextcloud-talk/src/send.ts:107", "extensions/nextcloud-talk/src/send.ts:198", - "extensions/qwen-portal-auth/oauth.ts:46", - "extensions/qwen-portal-auth/oauth.ts:80", "extensions/talk-voice/index.ts:27", "extensions/thread-ownership/index.ts:105", "extensions/voice-call/src/providers/plivo.ts:95", diff --git a/scripts/lib/live-docker-auth.sh b/scripts/lib/live-docker-auth.sh index c5021db2ac4..17008dd8d36 100644 --- a/scripts/lib/live-docker-auth.sh +++ b/scripts/lib/live-docker-auth.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -OPENCLAW_DOCKER_LIVE_AUTH_ALL=(.claude .codex .minimax .qwen) +OPENCLAW_DOCKER_LIVE_AUTH_ALL=(.claude .codex .minimax) openclaw_live_trim() { local value="${1:-}" @@ -30,9 +30,6 @@ openclaw_live_should_include_auth_dir_for_provider() { minimax | minimax-portal) printf '%s\n' ".minimax" ;; - qwen | qwen-portal-auth) - printf '%s\n' ".qwen" - ;; esac } diff --git a/src/agents/auth-profiles.doctor.test.ts b/src/agents/auth-profiles.doctor.test.ts new file mode 100644 index 00000000000..debf2e31f6a --- /dev/null +++ b/src/agents/auth-profiles.doctor.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { formatAuthDoctorHint } from "./auth-profiles/doctor.js"; +import type { AuthProfileStore } from "./auth-profiles/types.js"; + +const EMPTY_STORE: AuthProfileStore = { + version: 1, + profiles: {}, +}; + +describe("formatAuthDoctorHint", () => { + it("guides removed qwen portal users to model studio onboarding", async () => { + const hint = await formatAuthDoctorHint({ + store: EMPTY_STORE, + provider: "qwen-portal", + }); + + expect(hint).toContain("openclaw onboard --auth-choice modelstudio-api-key"); + expect(hint).toContain("modelstudio-api-key-cn"); + expect(hint).not.toContain("--provider modelstudio"); + }); +}); diff --git a/src/agents/auth-profiles.external-cli-sync.test.ts b/src/agents/auth-profiles.external-cli-sync.test.ts index 5883352a8d0..0240f4e38d9 100644 --- a/src/agents/auth-profiles.external-cli-sync.test.ts +++ b/src/agents/auth-profiles.external-cli-sync.test.ts @@ -3,7 +3,6 @@ import type { AuthProfileStore, OAuthCredential } from "./auth-profiles/types.js const mocks = vi.hoisted(() => ({ readCodexCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null), - readQwenCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null), readMiniMaxCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null), })); @@ -11,7 +10,6 @@ let syncExternalCliCredentials: typeof import("./auth-profiles/external-cli-sync let shouldReplaceStoredOAuthCredential: typeof import("./auth-profiles/external-cli-sync.js").shouldReplaceStoredOAuthCredential; let CODEX_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").CODEX_CLI_PROFILE_ID; let OPENAI_CODEX_DEFAULT_PROFILE_ID: typeof import("./auth-profiles/constants.js").OPENAI_CODEX_DEFAULT_PROFILE_ID; -let QWEN_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").QWEN_CLI_PROFILE_ID; let MINIMAX_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").MINIMAX_CLI_PROFILE_ID; function makeOAuthCredential( @@ -46,12 +44,6 @@ function getProviderCases() { readMock: mocks.readCodexCliCredentialsCached, legacyProfileId: CODEX_CLI_PROFILE_ID, }, - { - label: "Qwen", - profileId: QWEN_CLI_PROFILE_ID, - provider: "qwen-portal" as const, - readMock: mocks.readQwenCliCredentialsCached, - }, { label: "MiniMax", profileId: MINIMAX_CLI_PROFILE_ID, @@ -65,21 +57,15 @@ describe("syncExternalCliCredentials", () => { beforeEach(async () => { vi.resetModules(); mocks.readCodexCliCredentialsCached.mockReset().mockReturnValue(null); - mocks.readQwenCliCredentialsCached.mockReset().mockReturnValue(null); mocks.readMiniMaxCliCredentialsCached.mockReset().mockReturnValue(null); vi.doMock("./cli-credentials.js", () => ({ readCodexCliCredentialsCached: mocks.readCodexCliCredentialsCached, - readQwenCliCredentialsCached: mocks.readQwenCliCredentialsCached, readMiniMaxCliCredentialsCached: mocks.readMiniMaxCliCredentialsCached, })); ({ syncExternalCliCredentials, shouldReplaceStoredOAuthCredential } = await import("./auth-profiles/external-cli-sync.js")); - ({ - CODEX_CLI_PROFILE_ID, - OPENAI_CODEX_DEFAULT_PROFILE_ID, - QWEN_CLI_PROFILE_ID, - MINIMAX_CLI_PROFILE_ID, - } = await import("./auth-profiles/constants.js")); + ({ CODEX_CLI_PROFILE_ID, OPENAI_CODEX_DEFAULT_PROFILE_ID, MINIMAX_CLI_PROFILE_ID } = + await import("./auth-profiles/constants.js")); }); describe("shouldReplaceStoredOAuthCredential", () => { @@ -122,7 +108,7 @@ describe("syncExternalCliCredentials", () => { }); }); - it.each([{ providerLabel: "Codex" }, { providerLabel: "Qwen" }, { providerLabel: "MiniMax" }])( + it.each([{ providerLabel: "Codex" }, { providerLabel: "MiniMax" }])( "syncs $providerLabel CLI credentials into the target auth profile", ({ providerLabel }) => { const providerCase = getProviderCases().find((entry) => entry.label === providerLabel); @@ -195,7 +181,7 @@ describe("syncExternalCliCredentials", () => { }); }); - it.each([{ providerLabel: "Codex" }, { providerLabel: "Qwen" }, { providerLabel: "MiniMax" }])( + it.each([{ providerLabel: "Codex" }, { providerLabel: "MiniMax" }])( "does not overwrite newer stored $providerLabel credentials", ({ providerLabel }) => { const providerCase = getProviderCases().find((entry) => entry.label === providerLabel); diff --git a/src/agents/auth-profiles.readonly-sync.test.ts b/src/agents/auth-profiles.readonly-sync.test.ts index 441d0742308..098ad07c990 100644 --- a/src/agents/auth-profiles.readonly-sync.test.ts +++ b/src/agents/auth-profiles.readonly-sync.test.ts @@ -7,9 +7,9 @@ import type { AuthProfileStore } from "./auth-profiles/types.js"; const mocks = vi.hoisted(() => ({ syncExternalCliCredentials: vi.fn((store: AuthProfileStore) => { - store.profiles["qwen-portal:default"] = { + store.profiles["minimax-portal:default"] = { type: "oauth", - provider: "qwen-portal", + provider: "minimax-portal", access: "access-token", refresh: "refresh-token", expires: Date.now() + 60_000, @@ -61,13 +61,13 @@ describe("auth profiles read-only external CLI sync", () => { expect.any(Object), expect.objectContaining({ log: false }), ); - expect(loaded.profiles["qwen-portal:default"]).toMatchObject({ + expect(loaded.profiles["minimax-portal:default"]).toMatchObject({ type: "oauth", - provider: "qwen-portal", + provider: "minimax-portal", }); const persisted = JSON.parse(fs.readFileSync(authPath, "utf8")) as AuthProfileStore; - expect(persisted.profiles["qwen-portal:default"]).toBeUndefined(); + expect(persisted.profiles["minimax-portal:default"]).toBeUndefined(); expect(persisted.profiles["openai:default"]).toMatchObject({ type: "api_key", provider: "openai", diff --git a/src/agents/auth-profiles/constants.ts b/src/agents/auth-profiles/constants.ts index ace2c98dc81..b01067e30da 100644 --- a/src/agents/auth-profiles/constants.ts +++ b/src/agents/auth-profiles/constants.ts @@ -7,7 +7,6 @@ export const LEGACY_AUTH_FILENAME = "auth.json"; export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli"; export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli"; export const OPENAI_CODEX_DEFAULT_PROFILE_ID = "openai-codex:default"; -export const QWEN_CLI_PROFILE_ID = "qwen-portal:qwen-cli"; export const MINIMAX_CLI_PROFILE_ID = "minimax-portal:minimax-cli"; export const AUTH_STORE_LOCK_OPTIONS = { diff --git a/src/agents/auth-profiles/doctor.ts b/src/agents/auth-profiles/doctor.ts index 51fb5ed93f3..8e950574e03 100644 --- a/src/agents/auth-profiles/doctor.ts +++ b/src/agents/auth-profiles/doctor.ts @@ -3,6 +3,15 @@ import { buildProviderAuthDoctorHintWithPlugin } from "../../plugins/provider-ru import { normalizeProviderId } from "../model-selection.js"; import type { AuthProfileStore } from "./types.js"; +/** + * Migration hints for deprecated/removed OAuth providers. + * Users with stale credentials should be guided to migrate. + */ +const DEPRECATED_PROVIDER_MIGRATION_HINTS: Record = { + "qwen-portal": + "Qwen OAuth via portal.qwen.ai has been deprecated. Please migrate to Model Studio (Alibaba Cloud Coding Plan). Run: openclaw onboard --auth-choice modelstudio-api-key (or modelstudio-api-key-cn for the China endpoint).", +}; + export async function formatAuthDoctorHint(params: { cfg?: OpenClawConfig; store: AuthProfileStore; @@ -10,6 +19,13 @@ export async function formatAuthDoctorHint(params: { profileId?: string; }): Promise { const normalizedProvider = normalizeProviderId(params.provider); + + // Check for deprecated provider migration hints first + const migrationHint = DEPRECATED_PROVIDER_MIGRATION_HINTS[normalizedProvider]; + if (migrationHint) { + return migrationHint; + } + const pluginHint = await buildProviderAuthDoctorHintWithPlugin({ provider: normalizedProvider, context: { diff --git a/src/agents/auth-profiles/external-cli-sync.ts b/src/agents/auth-profiles/external-cli-sync.ts index 3551c33b71f..844c210cd40 100644 --- a/src/agents/auth-profiles/external-cli-sync.ts +++ b/src/agents/auth-profiles/external-cli-sync.ts @@ -1,12 +1,10 @@ import { readCodexCliCredentialsCached, - readQwenCliCredentialsCached, readMiniMaxCliCredentialsCached, } from "../cli-credentials.js"; import { EXTERNAL_CLI_SYNC_TTL_MS, OPENAI_CODEX_DEFAULT_PROFILE_ID, - QWEN_CLI_PROFILE_ID, MINIMAX_CLI_PROFILE_ID, log, } from "./constants.js"; @@ -70,11 +68,6 @@ export function shouldReplaceStoredOAuthCredential( } const EXTERNAL_CLI_SYNC_PROVIDERS: ExternalCliSyncProvider[] = [ - { - profileId: QWEN_CLI_PROFILE_ID, - provider: "qwen-portal", - readCredentials: () => readQwenCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }), - }, { profileId: MINIMAX_CLI_PROFILE_ID, provider: "minimax-portal", @@ -127,7 +120,7 @@ function syncExternalCliCredentialsForProvider( } /** - * Sync OAuth credentials from external CLI tools (Qwen Code CLI, MiniMax CLI, Codex CLI) + * Sync OAuth credentials from external CLI tools (MiniMax CLI, Codex CLI) * into the store. * * Returns true if any credentials were updated. diff --git a/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts b/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts index c7f16763a0a..4bfe4af02d6 100644 --- a/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts +++ b/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts @@ -23,7 +23,6 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => { vi.mock("../cli-credentials.js", () => ({ readCodexCliCredentialsCached: () => null, - readQwenCliCredentialsCached: () => null, readMiniMaxCliCredentialsCached: () => null, resetCliCredentialCachesForTest: () => undefined, })); diff --git a/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts b/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts index b5f54b141c9..ff648983a4f 100644 --- a/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts +++ b/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts @@ -32,7 +32,6 @@ const { vi.mock("../cli-credentials.js", () => ({ readCodexCliCredentialsCached: () => null, - readQwenCliCredentialsCached: () => null, readMiniMaxCliCredentialsCached: () => null, resetCliCredentialCachesForTest: () => undefined, })); diff --git a/src/agents/auth-profiles/oauth.test.ts b/src/agents/auth-profiles/oauth.test.ts index 279d816da17..6f0c2372d94 100644 --- a/src/agents/auth-profiles/oauth.test.ts +++ b/src/agents/auth-profiles/oauth.test.ts @@ -4,7 +4,6 @@ import type { AuthProfileStore } from "./types.js"; vi.mock("../cli-credentials.js", () => ({ readCodexCliCredentialsCached: () => null, - readQwenCliCredentialsCached: () => null, readMiniMaxCliCredentialsCached: () => null, resetCliCredentialCachesForTest: () => undefined, })); diff --git a/src/agents/cli-credentials.test.ts b/src/agents/cli-credentials.test.ts index 3e4b396f058..51f94f4d953 100644 --- a/src/agents/cli-credentials.test.ts +++ b/src/agents/cli-credentials.test.ts @@ -8,7 +8,6 @@ const execFileSyncMock = vi.fn(); const CLI_CREDENTIALS_CACHE_TTL_MS = 15 * 60 * 1000; let readClaudeCliCredentialsCached: typeof import("./cli-credentials.js").readClaudeCliCredentialsCached; let readCodexCliCredentialsCached: typeof import("./cli-credentials.js").readCodexCliCredentialsCached; -let readQwenCliCredentialsCached: typeof import("./cli-credentials.js").readQwenCliCredentialsCached; let resetCliCredentialCachesForTest: typeof import("./cli-credentials.js").resetCliCredentialCachesForTest; let writeClaudeCliKeychainCredentials: typeof import("./cli-credentials.js").writeClaudeCliKeychainCredentials; let writeClaudeCliCredentials: typeof import("./cli-credentials.js").writeClaudeCliCredentials; @@ -54,28 +53,11 @@ function createJwtWithExp(expSeconds: number): string { return `${encode({ alg: "RS256", typ: "JWT" })}.${encode({ exp: expSeconds })}.signature`; } -function writePortalCliCredentialFile( - filePath: string, - options: { access: string; refresh: string; expires: number }, -) { - fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 }); - fs.writeFileSync( - filePath, - JSON.stringify({ - access_token: options.access, - refresh_token: options.refresh, - expiry_date: options.expires, - }), - "utf8", - ); -} - describe("cli credentials", () => { beforeAll(async () => { ({ readClaudeCliCredentialsCached, readCodexCliCredentialsCached, - readQwenCliCredentialsCached, resetCliCredentialCachesForTest, writeClaudeCliKeychainCredentials, writeClaudeCliCredentials, @@ -372,50 +354,4 @@ describe("cli credentials", () => { fs.rmSync(tempHome, { recursive: true, force: true }); } }); - - it("invalidates cached Qwen credentials when oauth_creds.json changes within the TTL window", () => { - const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-qwen-cache-")); - const credPath = path.join(tempHome, ".qwen", "oauth_creds.json"); - try { - writePortalCliCredentialFile(credPath, { - access: "stale-access", - refresh: "stale-refresh", - expires: 1_000, - }); - fs.utimesSync(credPath, new Date("2026-03-24T10:00:00Z"), new Date("2026-03-24T10:00:00Z")); - vi.setSystemTime(new Date("2026-03-24T10:00:00Z")); - - const first = readQwenCliCredentialsCached({ - ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS, - homeDir: tempHome, - }); - - expect(first).toMatchObject({ - access: "stale-access", - refresh: "stale-refresh", - expires: 1_000, - }); - - writePortalCliCredentialFile(credPath, { - access: "fresh-access", - refresh: "fresh-refresh", - expires: 2_000, - }); - fs.utimesSync(credPath, new Date("2026-03-24T10:05:00Z"), new Date("2026-03-24T10:05:00Z")); - vi.advanceTimersByTime(60_000); - - const second = readQwenCliCredentialsCached({ - ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS, - homeDir: tempHome, - }); - - expect(second).toMatchObject({ - access: "fresh-access", - refresh: "fresh-refresh", - expires: 2_000, - }); - } finally { - fs.rmSync(tempHome, { recursive: true, force: true }); - } - }); }); diff --git a/src/agents/cli-credentials.ts b/src/agents/cli-credentials.ts index 44bf708085d..76cf465508e 100644 --- a/src/agents/cli-credentials.ts +++ b/src/agents/cli-credentials.ts @@ -11,7 +11,6 @@ const log = createSubsystemLogger("agents/auth-profiles"); const CLAUDE_CLI_CREDENTIALS_RELATIVE_PATH = ".claude/.credentials.json"; const CODEX_CLI_AUTH_FILENAME = "auth.json"; -const QWEN_CLI_CREDENTIALS_RELATIVE_PATH = ".qwen/oauth_creds.json"; const MINIMAX_CLI_CREDENTIALS_RELATIVE_PATH = ".minimax/oauth_creds.json"; const CLAUDE_CLI_KEYCHAIN_SERVICE = "Claude Code-credentials"; @@ -26,13 +25,11 @@ type CachedValue = { let claudeCliCache: CachedValue | null = null; let codexCliCache: CachedValue | null = null; -let qwenCliCache: CachedValue | null = null; let minimaxCliCache: CachedValue | null = null; export function resetCliCredentialCachesForTest(): void { claudeCliCache = null; codexCliCache = null; - qwenCliCache = null; minimaxCliCache = null; } @@ -60,14 +57,6 @@ export type CodexCliCredential = { accountId?: string; }; -export type QwenCliCredential = { - type: "oauth"; - provider: "qwen-portal"; - access: string; - refresh: string; - expires: number; -}; - export type MiniMaxCliCredential = { type: "oauth"; provider: "minimax-portal"; @@ -139,11 +128,6 @@ function resolveCodexHomePath() { } } -function resolveQwenCliCredentialsPath(homeDir?: string) { - const baseDir = homeDir ?? resolveUserPath("~"); - return path.join(baseDir, QWEN_CLI_CREDENTIALS_RELATIVE_PATH); -} - function resolveMiniMaxCliCredentialsPath(homeDir?: string) { const baseDir = homeDir ?? resolveUserPath("~"); return path.join(baseDir, MINIMAX_CLI_CREDENTIALS_RELATIVE_PATH); @@ -281,11 +265,6 @@ function readCodexKeychainCredentials(options?: { } } -function readQwenCliCredentials(options?: { homeDir?: string }): QwenCliCredential | null { - const credPath = resolveQwenCliCredentialsPath(options?.homeDir); - return readPortalCliOauthCredentials(credPath, "qwen-portal"); -} - function readPortalCliOauthCredentials( credPath: string, provider: TProvider, @@ -583,23 +562,6 @@ export function readCodexCliCredentialsCached(options?: { }); } -export function readQwenCliCredentialsCached(options?: { - ttlMs?: number; - homeDir?: string; -}): QwenCliCredential | null { - const credPath = resolveQwenCliCredentialsPath(options?.homeDir); - return readCachedCliCredential({ - ttlMs: options?.ttlMs ?? 0, - cache: qwenCliCache, - cacheKey: credPath, - read: () => readQwenCliCredentials({ homeDir: options?.homeDir }), - setCache: (next) => { - qwenCliCache = next; - }, - readSourceFingerprint: () => readFileMtimeMs(credPath), - }); -} - export function readMiniMaxCliCredentialsCached(options?: { ttlMs?: number; homeDir?: string; diff --git a/src/agents/context.lookup.test.ts b/src/agents/context.lookup.test.ts index a42a4ac1913..bcbcd3a9f83 100644 --- a/src/agents/context.lookup.test.ts +++ b/src/agents/context.lookup.test.ts @@ -278,7 +278,7 @@ describe("lookupContextTokens", () => { }); it("resolveContextTokensForModel prefers exact provider key over alias-normalized match", async () => { - // When both "qwen" and "qwen-portal" exist as config keys (alias pattern), + // When both "bedrock" and "amazon-bedrock" exist as config keys (alias pattern), // resolveConfiguredProviderContextWindow must return the exact-key match first, // not the first normalized hit — mirroring pi-embedded-runner/model.ts behaviour. mockDiscoveryDeps([]); @@ -286,8 +286,8 @@ describe("lookupContextTokens", () => { const cfg = { models: { providers: { - "qwen-portal": { models: [{ id: "qwen-max", contextWindow: 32_000 }] }, - qwen: { models: [{ id: "qwen-max", contextWindow: 128_000 }] }, + "amazon-bedrock": { models: [{ id: "claude-alias-test", contextWindow: 32_000 }] }, + bedrock: { models: [{ id: "claude-alias-test", contextWindow: 128_000 }] }, }, }, }; @@ -295,21 +295,21 @@ describe("lookupContextTokens", () => { const { resolveContextTokensForModel } = await import("./context.js"); await flushAsyncWarmup(); - // Exact key "qwen" wins over the alias-normalized match "qwen-portal". - const qwenResult = resolveContextTokensForModel({ + // Exact key "bedrock" wins over the alias-normalized match "amazon-bedrock". + const bedrockResult = resolveContextTokensForModel({ cfg: cfg as never, - provider: "qwen", - model: "qwen-max", + provider: "bedrock", + model: "claude-alias-test", }); - expect(qwenResult).toBe(128_000); + expect(bedrockResult).toBe(128_000); - // Exact key "qwen-portal" wins (no alias lookup needed). - const portalResult = resolveContextTokensForModel({ + // Exact key "amazon-bedrock" wins (no alias lookup needed). + const canonicalResult = resolveContextTokensForModel({ cfg: cfg as never, - provider: "qwen-portal", - model: "qwen-max", + provider: "amazon-bedrock", + model: "claude-alias-test", }); - expect(portalResult).toBe(32_000); + expect(canonicalResult).toBe(32_000); }); it("resolveContextTokensForModel(model-only) does not apply config scan for inferred provider", async () => { diff --git a/src/agents/context.ts b/src/agents/context.ts index 10560ac577c..695427936d6 100644 --- a/src/agents/context.ts +++ b/src/agents/context.ts @@ -324,9 +324,8 @@ function resolveConfiguredProviderContextWindow( } // Mirror the lookup order in pi-embedded-runner/model.ts: exact key first, - // then normalized fallback. This prevents alias collisions (e.g. when both - // "qwen" and "qwen-portal" exist as config keys) from picking the wrong - // contextWindow based on Object.entries iteration order. + // then normalized fallback. This prevents alias collisions from picking the + // wrong contextWindow based on Object.entries iteration order. function findContextWindow(matchProviderId: (id: string) => boolean): number | undefined { for (const [providerId, providerConfig] of Object.entries(providers!)) { if (!matchProviderId(providerId)) { @@ -355,7 +354,7 @@ function resolveConfiguredProviderContextWindow( return exactResult; } - // 2. Normalized fallback: covers alias keys such as "qwen" → "qwen-portal". + // 2. Normalized fallback: covers alias keys such as "z.ai" → "zai". const normalizedProvider = normalizeProviderId(provider); return findContextWindow((id) => normalizeProviderId(id) === normalizedProvider); } diff --git a/src/agents/model-auth-markers.test.ts b/src/agents/model-auth-markers.test.ts index 96b7aa96317..69a538b9600 100644 --- a/src/agents/model-auth-markers.test.ts +++ b/src/agents/model-auth-markers.test.ts @@ -11,12 +11,15 @@ import { describe("model auth markers", () => { it("recognizes explicit non-secret markers", () => { expect(isNonSecretApiKeyMarker(NON_ENV_SECRETREF_MARKER)).toBe(true); - expect(isNonSecretApiKeyMarker("qwen-oauth")).toBe(true); expect(isNonSecretApiKeyMarker(resolveOAuthApiKeyMarker("chutes"))).toBe(true); expect(isNonSecretApiKeyMarker("ollama-local")).toBe(true); expect(isNonSecretApiKeyMarker(GCP_VERTEX_CREDENTIALS_MARKER)).toBe(true); }); + it("does not treat removed provider markers as active auth markers", () => { + expect(isNonSecretApiKeyMarker("qwen-oauth")).toBe(false); + }); + it("recognizes known env marker names but not arbitrary all-caps keys", () => { expect(isNonSecretApiKeyMarker("OPENAI_API_KEY")).toBe(true); expect(isNonSecretApiKeyMarker("ALLCAPS_EXAMPLE")).toBe(false); diff --git a/src/agents/model-auth-markers.ts b/src/agents/model-auth-markers.ts index 4009630afc8..cac1662d5cb 100644 --- a/src/agents/model-auth-markers.ts +++ b/src/agents/model-auth-markers.ts @@ -3,7 +3,6 @@ import { listKnownProviderEnvApiKeyNames } from "./model-auth-env-vars.js"; export const MINIMAX_OAUTH_MARKER = "minimax-oauth"; export const OAUTH_API_KEY_MARKER_PREFIX = "oauth:"; -export const QWEN_OAUTH_MARKER = "qwen-oauth"; export const OLLAMA_LOCAL_AUTH_MARKER = "ollama-local"; export const CUSTOM_LOCAL_AUTH_MARKER = "custom-local"; export const GCP_VERTEX_CREDENTIALS_MARKER = "gcp-vertex-credentials"; @@ -80,7 +79,6 @@ export function isNonSecretApiKeyMarker( } const isKnownMarker = trimmed === MINIMAX_OAUTH_MARKER || - trimmed === QWEN_OAUTH_MARKER || isOAuthApiKeyMarker(trimmed) || trimmed === OLLAMA_LOCAL_AUTH_MARKER || trimmed === CUSTOM_LOCAL_AUTH_MARKER || diff --git a/src/agents/model-auth.profiles.test.ts b/src/agents/model-auth.profiles.test.ts index 3213ef7be32..607edfe4a56 100644 --- a/src/agents/model-auth.profiles.test.ts +++ b/src/agents/model-auth.profiles.test.ts @@ -466,20 +466,6 @@ describe("getApiKeyForModel", () => { ); }); - it("resolveEnvApiKey('qwen-portal') accepts QWEN_OAUTH_TOKEN", async () => { - await withEnvAsync( - { - QWEN_OAUTH_TOKEN: "qwen-oauth-token", - QWEN_PORTAL_API_KEY: undefined, - }, - async () => { - const resolved = resolveEnvApiKey("qwen"); - expect(resolved?.apiKey).toBe("qwen-oauth-token"); - expect(resolved?.source).toContain("QWEN_OAUTH_TOKEN"); - }, - ); - }); - it("resolveEnvApiKey('minimax-portal') accepts MINIMAX_OAUTH_TOKEN", async () => { await withEnvAsync( { diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index 05085bc1d1d..ae9df1e32fa 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -111,7 +111,7 @@ describe("model-selection", () => { expect(normalizeProviderId("Z.ai")).toBe("zai"); expect(normalizeProviderId("z-ai")).toBe("zai"); expect(normalizeProviderId("OpenCode-Zen")).toBe("opencode"); - expect(normalizeProviderId("qwen")).toBe("qwen-portal"); + expect(normalizeProviderId("qwen")).toBe("qwen"); expect(normalizeProviderId("kimi-code")).toBe("kimi"); expect(normalizeProviderId("kimi-coding")).toBe("kimi"); expect(normalizeProviderId("bedrock")).toBe("amazon-bedrock"); diff --git a/src/agents/models-config.e2e-harness.ts b/src/agents/models-config.e2e-harness.ts index bd01edc86be..d389827dd57 100644 --- a/src/agents/models-config.e2e-harness.ts +++ b/src/agents/models-config.e2e-harness.ts @@ -102,8 +102,6 @@ export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [ "PI_CODING_AGENT_DIR", "QIANFAN_API_KEY", "MODELSTUDIO_API_KEY", - "QWEN_OAUTH_TOKEN", - "QWEN_PORTAL_API_KEY", "SYNTHETIC_API_KEY", "TOGETHER_API_KEY", "VOLCANO_ENGINE_API_KEY", diff --git a/src/agents/models-config.providers.auth-provenance.test.ts b/src/agents/models-config.providers.auth-provenance.test.ts index 987f825932b..abea2d04e80 100644 --- a/src/agents/models-config.providers.auth-provenance.test.ts +++ b/src/agents/models-config.providers.auth-provenance.test.ts @@ -4,11 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { captureEnv } from "../test-utils/env.js"; -import { - MINIMAX_OAUTH_MARKER, - NON_ENV_SECRETREF_MARKER, - QWEN_OAUTH_MARKER, -} from "./model-auth-markers.js"; +import { MINIMAX_OAUTH_MARKER, NON_ENV_SECRETREF_MARKER } from "./model-auth-markers.js"; import { resolveImplicitProvidersForTest } from "./models-config.e2e-harness.js"; describe("models-config provider auth provenance", () => { @@ -84,7 +80,7 @@ describe("models-config provider auth provenance", () => { expect(providers?.together?.apiKey).toBe(NON_ENV_SECRETREF_MARKER); }); - it("keeps oauth compatibility markers for minimax-portal and qwen-portal", async () => { + it("keeps oauth compatibility markers for minimax-portal", async () => { const agentDir = mkdtempSync(join(tmpdir(), "openclaw-test-")); await writeFile( join(agentDir, "auth-profiles.json"), @@ -99,13 +95,6 @@ describe("models-config provider auth provenance", () => { refresh: "refresh-token", expires: Date.now() + 60_000, }, - "qwen-portal:default": { - type: "oauth", - provider: "qwen-portal", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - }, }, }, null, @@ -116,6 +105,5 @@ describe("models-config provider auth provenance", () => { const providers = await resolveImplicitProvidersForTest({ agentDir, env: {} }); expect(providers?.["minimax-portal"]?.apiKey).toBe(MINIMAX_OAUTH_MARKER); - expect(providers?.["qwen-portal"]?.apiKey).toBe(QWEN_OAUTH_MARKER); }); }); diff --git a/src/agents/models-config.providers.static.ts b/src/agents/models-config.providers.static.ts index f078456549c..8479c78b56a 100644 --- a/src/agents/models-config.providers.static.ts +++ b/src/agents/models-config.providers.static.ts @@ -22,7 +22,6 @@ export { QIANFAN_BASE_URL, QIANFAN_DEFAULT_MODEL_ID, buildQianfanProvider, - buildQwenPortalProvider, buildSyntheticProvider, buildTogetherProvider, buildDoubaoCodingProvider, diff --git a/src/agents/pi-embedded-runner/model.test.ts b/src/agents/pi-embedded-runner/model.test.ts index eb5ea440680..7386cea17d1 100644 --- a/src/agents/pi-embedded-runner/model.test.ts +++ b/src/agents/pi-embedded-runner/model.test.ts @@ -679,12 +679,12 @@ describe("resolveModel", () => { it("prefers exact provider config over normalized alias match when both keys exist", () => { mockDiscoveredModel({ - provider: "qwen", - modelId: "qwen3-coder-plus", + provider: "bedrock", + modelId: "bedrock-alias-exact-test", templateModel: { - id: "qwen3-coder-plus", - name: "Qwen3 Coder Plus", - provider: "qwen", + id: "bedrock-alias-exact-test", + name: "Bedrock alias test", + provider: "bedrock", api: "openai-completions", baseUrl: "https://default-provider.example.com/v1", reasoning: false, @@ -698,19 +698,19 @@ describe("resolveModel", () => { const cfg = { models: { providers: { - "qwen-portal": { - baseUrl: "https://canonical-provider.example.com/v1", + "amazon-bedrock": { + baseUrl: "https://canonical-bedrock.example.com/v1", api: "openai-completions", headers: { "X-Provider": "canonical" }, - models: [{ ...makeModel("qwen3-coder-plus"), reasoning: false }], + models: [{ ...makeModel("bedrock-alias-exact-test"), reasoning: false }], }, - qwen: { - baseUrl: "https://alias-provider.example.com/v1", + bedrock: { + baseUrl: "https://alias-bedrock.example.com/v1", api: "anthropic-messages", headers: { "X-Provider": "alias" }, models: [ { - ...makeModel("qwen3-coder-plus"), + ...makeModel("bedrock-alias-exact-test"), api: "anthropic-messages", reasoning: true, contextWindow: 262144, @@ -722,14 +722,14 @@ describe("resolveModel", () => { }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("qwen", "qwen3-coder-plus", "/tmp/agent", cfg); + const result = resolveModelForTest("bedrock", "bedrock-alias-exact-test", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model).toMatchObject({ - provider: "qwen", - id: "qwen3-coder-plus", + provider: "bedrock", + id: "bedrock-alias-exact-test", api: "anthropic-messages", - baseUrl: "https://alias-provider.example.com", + baseUrl: "https://alias-bedrock.example.com", reasoning: true, contextWindow: 262144, maxTokens: 32768, diff --git a/src/agents/provider-id.ts b/src/agents/provider-id.ts index bd82c3c3edd..b774785fc45 100644 --- a/src/agents/provider-id.ts +++ b/src/agents/provider-id.ts @@ -9,9 +9,6 @@ export function normalizeProviderId(provider: string): string { if (normalized === "opencode-go-auth") { return "opencode-go"; } - if (normalized === "qwen") { - return "qwen-portal"; - } if (normalized === "kimi" || normalized === "kimi-code" || normalized === "kimi-coding") { return "kimi"; } diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index 7e97e7b890f..aa992980140 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -111,15 +111,6 @@ describe("buildAuthChoiceOptions", () => { groupId: "together", groupLabel: "Together AI", }, - { - pluginId: "qwen-portal-auth", - providerId: "qwen-portal", - methodId: "device", - choiceId: "qwen-portal", - choiceLabel: "Qwen OAuth", - groupId: "qwen", - groupLabel: "Qwen", - }, { pluginId: "xai", providerId: "xai", @@ -200,7 +191,6 @@ describe("buildAuthChoiceOptions", () => { "moonshot-api-key", "together-api-key", "chutes", - "qwen-portal", "xai-api-key", "mistral-api-key", "volcengine-api-key", diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 89f7fa319f5..d80560ac663 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -16,7 +16,6 @@ import opencodeGoPlugin from "../../extensions/opencode-go/index.js"; import opencodePlugin from "../../extensions/opencode/index.js"; import openrouterPlugin from "../../extensions/openrouter/index.js"; import qianfanPlugin from "../../extensions/qianfan/index.js"; -import qwenPortalAuthPlugin from "../../extensions/qwen-portal-auth/index.js"; import syntheticPlugin from "../../extensions/synthetic/index.js"; import togetherPlugin from "../../extensions/together/index.js"; import venicePlugin from "../../extensions/venice/index.js"; @@ -104,7 +103,6 @@ function createDefaultProviderPlugins() { opencodePlugin, openrouterPlugin, qianfanPlugin, - qwenPortalAuthPlugin, syntheticPlugin, togetherPlugin, venicePlugin, @@ -1395,7 +1393,7 @@ describe("applyAuthChoice", () => { it("writes portal OAuth credentials for plugin providers", async () => { const scenarios: Array<{ - authChoice: "qwen-portal" | "minimax-global-oauth"; + authChoice: "minimax-global-oauth"; label: string; authId: string; authLabel: string; @@ -1407,18 +1405,6 @@ describe("applyAuthChoice", () => { apiKey: string; selectValue?: string; }> = [ - { - authChoice: "qwen-portal", - label: "Qwen", - authId: "device", - authLabel: "Qwen OAuth", - providerId: "qwen-portal", - profileId: "qwen-portal:default", - baseUrl: "https://portal.qwen.ai/v1", - api: "openai-completions", - defaultModel: "qwen-portal/coder-model", - apiKey: "qwen-oauth", // pragma: allowlist secret - }, { authChoice: "minimax-global-oauth", label: "MiniMax", @@ -1516,7 +1502,6 @@ describe("resolvePreferredProviderForAuthChoice", () => { it("maps known and unknown auth choices", async () => { const scenarios = [ { authChoice: "github-copilot" as const, expectedProvider: "github-copilot" }, - { authChoice: "qwen-portal" as const, expectedProvider: "qwen-portal" }, { authChoice: "mistral-api-key" as const, expectedProvider: "mistral" }, { authChoice: "ollama" as const, expectedProvider: "ollama" }, { authChoice: "unknown" as AuthChoice, expectedProvider: undefined }, diff --git a/src/commands/models.auth.provider-resolution.test.ts b/src/commands/models.auth.provider-resolution.test.ts index 19302e2ae1e..ef8794564df 100644 --- a/src/commands/models.auth.provider-resolution.test.ts +++ b/src/commands/models.auth.provider-resolution.test.ts @@ -15,7 +15,7 @@ describe("resolveRequestedLoginProviderOrThrow", () => { it("returns null and resolves provider by id/alias", () => { const providers = [ makeProvider({ id: "google-gemini-cli", aliases: ["gemini-cli"] }), - makeProvider({ id: "qwen-portal" }), + makeProvider({ id: "minimax-portal" }), ]; const scenarios = [ { requested: undefined, expectedId: null }, @@ -32,13 +32,13 @@ describe("resolveRequestedLoginProviderOrThrow", () => { it("throws when requested provider is not loaded", () => { const loadedProviders = [ makeProvider({ id: "google-gemini-cli" }), - makeProvider({ id: "qwen-portal" }), + makeProvider({ id: "minimax-portal" }), ]; expect(() => resolveRequestedLoginProviderOrThrow(loadedProviders, "google-antigravity"), ).toThrowError( - 'Unknown provider "google-antigravity". Loaded providers: google-gemini-cli, qwen-portal. Verify plugins via `openclaw plugins list --json`.', + 'Unknown provider "google-antigravity". Loaded providers: google-gemini-cli, minimax-portal. Verify plugins via `openclaw plugins list --json`.', ); }); }); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 85322122e1f..8ad83655ec5 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -328,7 +328,6 @@ export async function applyNonInteractiveAuthChoice(params: { if ( authChoice === "oauth" || authChoice === "chutes" || - authChoice === "qwen-portal" || authChoice === "minimax-global-oauth" || authChoice === "minimax-cn-oauth" ) { diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts index 7a2f8e0c626..fc2c0d59d9b 100644 --- a/src/commands/onboard-types.ts +++ b/src/commands/onboard-types.ts @@ -43,7 +43,6 @@ export type BuiltInAuthChoice = | "opencode-go" | "github-copilot" | "copilot-proxy" - | "qwen-portal" | "xai-api-key" | "mistral-api-key" | "volcengine-api-key" @@ -77,7 +76,6 @@ export type BuiltInAuthChoiceGroupId = | "synthetic" | "venice" | "mistral" - | "qwen" | "together" | "huggingface" | "qianfan" diff --git a/src/config/plugin-auto-enable.ts b/src/config/plugin-auto-enable.ts index 1892d882502..f0b93efd489 100644 --- a/src/config/plugin-auto-enable.ts +++ b/src/config/plugin-auto-enable.ts @@ -32,7 +32,6 @@ const EMPTY_PLUGIN_MANIFEST_REGISTRY: PluginManifestRegistry = { const PROVIDER_PLUGIN_IDS: Array<{ pluginId: string; providerId: string }> = [ { pluginId: "google", providerId: "google-gemini-cli" }, - { pluginId: "qwen-portal-auth", providerId: "qwen-portal" }, { pluginId: "copilot-proxy", providerId: "copilot-proxy" }, { pluginId: "minimax", providerId: "minimax-portal" }, ]; diff --git a/src/gateway/model-pricing-cache.ts b/src/gateway/model-pricing-cache.ts index 8307cef2f6e..447268fd923 100644 --- a/src/gateway/model-pricing-cache.ts +++ b/src/gateway/model-pricing-cache.ts @@ -40,8 +40,6 @@ const PROVIDER_ALIAS_TO_OPENROUTER: Record = { moonshot: "moonshotai", moonshotai: "moonshotai", "openai-codex": "openai", - qwen: "qwen", - "qwen-portal": "qwen", xai: "x-ai", zai: "z-ai", }; diff --git a/src/infra/provider-usage.auth.normalizes-keys.test.ts b/src/infra/provider-usage.auth.normalizes-keys.test.ts index 22f8f1ba281..7248f75cab8 100644 --- a/src/infra/provider-usage.auth.normalizes-keys.test.ts +++ b/src/infra/provider-usage.auth.normalizes-keys.test.ts @@ -66,7 +66,6 @@ vi.mock("../plugins/provider-runtime.ts", () => ({ vi.mock("../agents/cli-credentials.js", () => ({ readCodexCliCredentialsCached: () => null, readMiniMaxCliCredentialsCached: () => null, - readQwenCliCredentialsCached: () => null, })); vi.mock("../agents/auth-profiles/external-cli-sync.js", () => ({ diff --git a/src/plugin-sdk/channel-import-guardrails.test.ts b/src/plugin-sdk/channel-import-guardrails.test.ts index 46c8c0ed96b..339b13a2e14 100644 --- a/src/plugin-sdk/channel-import-guardrails.test.ts +++ b/src/plugin-sdk/channel-import-guardrails.test.ts @@ -134,7 +134,6 @@ const LOCAL_EXTENSION_API_BARREL_GUARDS = [ "phone-control", "copilot-proxy", "zai", - "qwen-portal-auth", "signal", "synology-chat", "talk-voice", diff --git a/src/plugin-sdk/provider-catalog.ts b/src/plugin-sdk/provider-catalog.ts index 9df2ada0b63..192cfdafd6b 100644 --- a/src/plugin-sdk/provider-catalog.ts +++ b/src/plugin-sdk/provider-catalog.ts @@ -39,7 +39,6 @@ export { QIANFAN_DEFAULT_MODEL_ID, buildQianfanProvider, } from "../../extensions/qianfan/provider-catalog.js"; -export { buildQwenPortalProvider } from "../../extensions/qwen-portal-auth/provider-catalog.js"; export { buildSyntheticProvider } from "../../extensions/synthetic/provider-catalog.js"; export { buildTogetherProvider } from "../../extensions/together/provider-catalog.js"; export { buildVeniceProvider } from "../../extensions/venice/provider-catalog.js"; diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index d8183d0eaf3..d0de0907b7b 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -127,7 +127,6 @@ describe("plugin-sdk subpath exports", () => { "lobster", "pairing-access", "provider-model-definitions", - "qwen-portal-auth", "reply-prefix", "secret-input-runtime", "secret-input-schema", diff --git a/src/plugins/bundled-provider-auth-env-vars.generated.ts b/src/plugins/bundled-provider-auth-env-vars.generated.ts index 101c97659d8..3af9f191601 100644 --- a/src/plugins/bundled-provider-auth-env-vars.generated.ts +++ b/src/plugins/bundled-provider-auth-env-vars.generated.ts @@ -30,7 +30,6 @@ export const BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES = { openrouter: ["OPENROUTER_API_KEY"], perplexity: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"], qianfan: ["QIANFAN_API_KEY"], - "qwen-portal": ["QWEN_OAUTH_TOKEN", "QWEN_PORTAL_API_KEY"], sglang: ["SGLANG_API_KEY"], synthetic: ["SYNTHETIC_API_KEY"], tavily: ["TAVILY_API_KEY"], diff --git a/src/plugins/bundled-provider-auth-env-vars.test.ts b/src/plugins/bundled-provider-auth-env-vars.test.ts index a6a28155a75..ff87f606ab9 100644 --- a/src/plugins/bundled-provider-auth-env-vars.test.ts +++ b/src/plugins/bundled-provider-auth-env-vars.test.ts @@ -34,10 +34,6 @@ describe("bundled provider auth env vars", () => { "PERPLEXITY_API_KEY", "OPENROUTER_API_KEY", ]); - expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES["qwen-portal"]).toEqual([ - "QWEN_OAUTH_TOKEN", - "QWEN_PORTAL_API_KEY", - ]); expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.tavily).toEqual(["TAVILY_API_KEY"]); expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES["minimax-portal"]).toEqual([ "MINIMAX_OAUTH_TOKEN", diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts index 0f4afead2f7..f723048b8e9 100644 --- a/src/plugins/config-state.ts +++ b/src/plugins/config-state.ts @@ -52,7 +52,6 @@ export const BUNDLED_ENABLED_BY_DEFAULT = new Set([ "openrouter", "phone-control", "qianfan", - "qwen-portal-auth", "sglang", "synthetic", "talk-voice", diff --git a/src/plugins/contracts/auth-choice.contract.test.ts b/src/plugins/contracts/auth-choice.contract.test.ts index b1a9a605ecd..df4fb84077e 100644 --- a/src/plugins/contracts/auth-choice.contract.test.ts +++ b/src/plugins/contracts/auth-choice.contract.test.ts @@ -1,18 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - createAuthTestLifecycle, - createExitThrowingRuntime, - createWizardPrompter, - readAuthProfilesForAgent, - requireOpenClawAgentDir, - setupAuthTestEnv, -} from "../../../test/helpers/auth-wizard.js"; import { clearRuntimeAuthProfileStoreSnapshots } from "../../agents/auth-profiles/store.js"; import { resolvePreferredProviderForAuthChoice } from "../../plugins/provider-auth-choice-preference.js"; -import { runProviderPluginAuthMethod } from "../../plugins/provider-auth-choice.js"; import { buildProviderPluginMethodChoice } from "../provider-wizard.js"; import { requireProviderContractProvider, uniqueProviderContractProviders } from "./registry.js"; -import { registerProviders, requireProvider } from "./testkit.js"; type ResolvePluginProviders = typeof import("../../plugins/provider-auth-choice.runtime.js").resolvePluginProviders; @@ -20,53 +10,19 @@ type ResolveProviderPluginChoice = typeof import("../../plugins/provider-auth-choice.runtime.js").resolveProviderPluginChoice; type RunProviderModelSelectedHook = typeof import("../../plugins/provider-auth-choice.runtime.js").runProviderModelSelectedHook; -const loginQwenPortalOAuthMock = vi.hoisted(() => vi.fn()); -const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn()); const resolvePluginProvidersMock = vi.hoisted(() => vi.fn(() => [])); const resolveProviderPluginChoiceMock = vi.hoisted(() => vi.fn()); const runProviderModelSelectedHookMock = vi.hoisted(() => vi.fn(async () => {}), ); -import qwenPortalPlugin from "../../../extensions/qwen-portal-auth/index.js"; -vi.mock("../../../extensions/qwen-portal-auth/oauth.js", () => ({ - loginQwenPortalOAuth: loginQwenPortalOAuthMock, -})); -vi.mock("../../../extensions/github-copilot/login.js", () => ({ - githubCopilotLoginCommand: githubCopilotLoginCommandMock, -})); vi.mock("../../plugins/provider-auth-choice.runtime.js", () => ({ resolvePluginProviders: resolvePluginProvidersMock, resolveProviderPluginChoice: resolveProviderPluginChoiceMock, runProviderModelSelectedHook: runProviderModelSelectedHookMock, })); -type StoredAuthProfile = { - type?: string; - provider?: string; - access?: string; - refresh?: string; - key?: string; - token?: string; -}; - describe("provider auth-choice contract", () => { - const lifecycle = createAuthTestLifecycle([ - "OPENCLAW_STATE_DIR", - "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", - ]); - let activeStateDir: string | null = null; - - async function setupTempState() { - if (activeStateDir) { - await lifecycle.cleanup(); - } - const env = await setupAuthTestEnv("openclaw-provider-auth-choice-"); - activeStateDir = env.stateDir; - lifecycle.setStateDir(env.stateDir); - } - beforeEach(() => { resolvePluginProvidersMock.mockReset(); resolvePluginProvidersMock.mockReturnValue(uniqueProviderContractProviders); @@ -90,22 +46,17 @@ describe("provider auth-choice contract", () => { afterEach(async () => { vi.restoreAllMocks(); - loginQwenPortalOAuthMock.mockReset(); - githubCopilotLoginCommandMock.mockReset(); resolvePluginProvidersMock.mockReset(); resolvePluginProvidersMock.mockReturnValue([]); resolveProviderPluginChoiceMock.mockReset(); resolveProviderPluginChoiceMock.mockReturnValue(null); runProviderModelSelectedHookMock.mockReset(); clearRuntimeAuthProfileStoreSnapshots(); - await lifecycle.cleanup(); - activeStateDir = null; }); it("maps provider-plugin choices through the shared preferred-provider fallback resolver", async () => { const pluginFallbackScenarios = [ "github-copilot", - "qwen-portal", "minimax-portal", "modelstudio", "ollama", @@ -131,114 +82,4 @@ describe("provider auth-choice contract", () => { ); expect(resolvePluginProvidersMock).toHaveBeenCalled(); }); - - it("runs qwen portal auth through the shared plugin auth-method helper", async () => { - await setupTempState(); - const qwenProvider = requireProvider(registerProviders(qwenPortalPlugin), "qwen-portal"); - loginQwenPortalOAuthMock.mockResolvedValueOnce({ - access: "access-token", - refresh: "refresh-token", - expires: 1_700_000_000_000, - resourceUrl: "portal.qwen.ai", - }); - - const note = vi.fn(async () => {}); - const result = await runProviderPluginAuthMethod({ - config: {}, - prompter: createWizardPrompter({ note }), - runtime: createExitThrowingRuntime(), - method: qwenProvider.auth[0], - allowSecretRefPrompt: false, - }); - - expect(result.config.auth?.profiles?.["qwen-portal:default"]).toMatchObject({ - provider: "qwen-portal", - mode: "oauth", - }); - expect(result.config.models?.providers?.["qwen-portal"]).toMatchObject({ - baseUrl: "https://portal.qwen.ai/v1", - models: [], - }); - expect(result.config.agents?.defaults?.models).toMatchObject({ - "qwen-portal/coder-model": { alias: "qwen" }, - "qwen-portal/vision-model": {}, - }); - expect(result.defaultModel).toBe("qwen-portal/coder-model"); - expect(note).toHaveBeenCalledWith( - expect.stringContaining("Qwen OAuth tokens auto-refresh."), - "Provider notes", - ); - - const stored = await readAuthProfilesForAgent<{ profiles?: Record }>( - requireOpenClawAgentDir(), - ); - expect(stored.profiles?.["qwen-portal:default"]).toMatchObject({ - type: "oauth", - provider: "qwen-portal", - access: "access-token", - refresh: "refresh-token", - }); - }); - - it("returns qwen portal default-model overrides for deferred callers", async () => { - await setupTempState(); - const qwenProvider = requireProvider(registerProviders(qwenPortalPlugin), "qwen-portal"); - loginQwenPortalOAuthMock.mockResolvedValueOnce({ - access: "access-token", - refresh: "refresh-token", - expires: 1_700_000_000_000, - resourceUrl: "portal.qwen.ai", - }); - - const result = await runProviderPluginAuthMethod({ - config: {}, - prompter: createWizardPrompter({}), - runtime: createExitThrowingRuntime(), - method: qwenProvider.auth[0], - allowSecretRefPrompt: false, - }); - - expect(githubCopilotLoginCommandMock).not.toHaveBeenCalled(); - expect(result).toEqual({ - config: { - agents: { - defaults: { - models: { - "qwen-portal/coder-model": { - alias: "qwen", - }, - "qwen-portal/vision-model": {}, - }, - }, - }, - auth: { - profiles: { - "qwen-portal:default": { - provider: "qwen-portal", - mode: "oauth", - }, - }, - }, - models: { - providers: { - "qwen-portal": { - baseUrl: "https://portal.qwen.ai/v1", - models: [], - }, - }, - }, - }, - defaultModel: "qwen-portal/coder-model", - }); - - const stored = await readAuthProfilesForAgent<{ - profiles?: Record; - }>(requireOpenClawAgentDir()); - expect(stored.profiles?.["qwen-portal:default"]).toMatchObject({ - type: "oauth", - provider: "qwen-portal", - access: "access-token", - refresh: "refresh-token", - }); - }); }); diff --git a/src/plugins/contracts/auth.contract.test.ts b/src/plugins/contracts/auth.contract.test.ts index 2f24f2c3af0..5caf3438a70 100644 --- a/src/plugins/contracts/auth.contract.test.ts +++ b/src/plugins/contracts/auth.contract.test.ts @@ -12,8 +12,6 @@ import { registerProviders, requireProvider } from "./testkit.js"; type LoginOpenAICodexOAuth = (typeof import("openclaw/plugin-sdk/provider-auth-login"))["loginOpenAICodexOAuth"]; -type LoginQwenPortalOAuth = - (typeof import("../../../extensions/qwen-portal-auth/oauth.js"))["loginQwenPortalOAuth"]; type GithubCopilotLoginCommand = (typeof import("openclaw/plugin-sdk/provider-auth-login"))["githubCopilotLoginCommand"]; type CreateVpsAwareHandlers = @@ -24,7 +22,6 @@ type ListProfilesForProvider = typeof import("openclaw/plugin-sdk/agent-runtime").listProfilesForProvider; const loginOpenAICodexOAuthMock = vi.hoisted(() => vi.fn()); -const loginQwenPortalOAuthMock = vi.hoisted(() => vi.fn()); const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn()); const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn()); const listProfilesForProviderMock = vi.hoisted(() => vi.fn()); @@ -47,13 +44,8 @@ vi.mock("openclaw/plugin-sdk/agent-runtime", async (importOriginal) => { }; }); -vi.mock("../../../extensions/qwen-portal-auth/oauth.js", () => ({ - loginQwenPortalOAuth: loginQwenPortalOAuthMock, -})); - import githubCopilotPlugin from "../../../extensions/github-copilot/index.js"; import openAIPlugin from "../../../extensions/openai/index.js"; -import qwenPortalPlugin from "../../../extensions/qwen-portal-auth/index.js"; function buildPrompter(): WizardPrompter { const progress: WizardProgress = { @@ -114,7 +106,6 @@ describe("provider auth contract", () => { afterEach(() => { loginOpenAICodexOAuthMock.mockReset(); - loginQwenPortalOAuthMock.mockReset(); githubCopilotLoginCommandMock.mockReset(); ensureAuthProfileStoreMock.mockReset(); listProfilesForProviderMock.mockReset(); @@ -377,50 +368,6 @@ describe("provider auth contract", () => { }); }); - it("keeps Qwen portal OAuth auth results provider-owned", async () => { - const provider = requireProvider(registerProviders(qwenPortalPlugin), "qwen-portal"); - loginQwenPortalOAuthMock.mockResolvedValueOnce({ - access: "access-token", - refresh: "refresh-token", - expires: 1_700_000_000_000, - resourceUrl: "portal.qwen.ai", - }); - - const result = await provider.auth[0]?.run(buildAuthContext() as never); - - expect(result).toMatchObject({ - profiles: [ - { - profileId: "qwen-portal:default", - credential: { - type: "oauth", - provider: "qwen-portal", - access: "access-token", - refresh: "refresh-token", - expires: 1_700_000_000_000, - }, - }, - ], - defaultModel: "qwen-portal/coder-model", - configPatch: { - models: { - providers: { - "qwen-portal": { - baseUrl: "https://portal.qwen.ai/v1", - models: [], - }, - }, - }, - }, - }); - expect(result?.notes).toEqual( - expect.arrayContaining([ - expect.stringContaining("auto-refresh"), - expect.stringContaining("Base URL defaults"), - ]), - ); - }); - it("keeps GitHub Copilot device auth results provider-owned", async () => { const provider = requireProvider(registerProviders(githubCopilotPlugin), "github-copilot"); authStore.profiles["github-copilot:github"] = { diff --git a/src/plugins/contracts/discovery.contract.test.ts b/src/plugins/contracts/discovery.contract.test.ts index f0accc1d526..cf3b24115de 100644 --- a/src/plugins/contracts/discovery.contract.test.ts +++ b/src/plugins/contracts/discovery.contract.test.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { AuthProfileStore } from "../../agents/auth-profiles/types.js"; -import { QWEN_OAUTH_MARKER } from "../../agents/model-auth-markers.js"; import type { ModelDefinitionConfig } from "../../config/types.models.js"; import { registerProviders, requireProvider } from "./testkit.js"; @@ -12,7 +11,6 @@ const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn()); const listProfilesForProviderMock = vi.hoisted(() => vi.fn()); let runProviderCatalog: typeof import("../provider-discovery.js").runProviderCatalog; -let qwenPortalProvider: Awaited>; let githubCopilotProvider: Awaited>; let ollamaProvider: Awaited>; let vllmProvider: Awaited>; @@ -53,21 +51,6 @@ function setRuntimeAuthStore(store?: AuthProfileStore) { ); } -function setQwenPortalOauthSnapshot() { - setRuntimeAuthStore({ - version: 1, - profiles: { - "qwen-portal:default": { - type: "oauth", - provider: "qwen-portal", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, - }, - }, - }); -} - function setGithubCopilotProfileSnapshot() { setRuntimeAuthStore({ version: 1, @@ -169,7 +152,6 @@ describe("provider discovery contract", () => { ({ runProviderCatalog } = await import("../provider-discovery.js")); const [ - { default: qwenPortalPlugin }, { default: githubCopilotPlugin }, { default: ollamaPlugin }, { default: vllmPlugin }, @@ -178,7 +160,6 @@ describe("provider discovery contract", () => { { default: modelStudioPlugin }, { default: cloudflareAiGatewayPlugin }, ] = await Promise.all([ - import("../../../extensions/qwen-portal-auth/index.js"), import("../../../extensions/github-copilot/index.js"), import("../../../extensions/ollama/index.js"), import("../../../extensions/vllm/index.js"), @@ -187,7 +168,6 @@ describe("provider discovery contract", () => { import("../../../extensions/modelstudio/index.js"), import("../../../extensions/cloudflare-ai-gateway/index.js"), ]); - qwenPortalProvider = requireProvider(registerProviders(qwenPortalPlugin), "qwen-portal"); githubCopilotProvider = requireProvider( registerProviders(githubCopilotPlugin), "github-copilot", @@ -215,42 +195,6 @@ describe("provider discovery contract", () => { listProfilesForProviderMock.mockReset(); }); - it("keeps qwen portal oauth marker fallback provider-owned", async () => { - setQwenPortalOauthSnapshot(); - - await expect( - runCatalog({ - provider: qwenPortalProvider, - }), - ).resolves.toEqual({ - provider: { - baseUrl: "https://portal.qwen.ai/v1", - apiKey: QWEN_OAUTH_MARKER, - api: "openai-completions", - models: [ - expect.objectContaining({ id: "coder-model", name: "Qwen Coder" }), - expect.objectContaining({ id: "vision-model", name: "Qwen Vision" }), - ], - }, - }); - }); - - it("keeps qwen portal env api keys higher priority than oauth markers", async () => { - setQwenPortalOauthSnapshot(); - - await expect( - runCatalog({ - provider: qwenPortalProvider, - env: { QWEN_PORTAL_API_KEY: "env-key" } as NodeJS.ProcessEnv, - resolveProviderApiKey: () => ({ apiKey: "env-key" }), - }), - ).resolves.toMatchObject({ - provider: { - apiKey: "env-key", - }, - }); - }); - it("keeps GitHub Copilot catalog disabled without env tokens or profiles", async () => { await expect(runCatalog({ provider: githubCopilotProvider })).resolves.toBeNull(); }); diff --git a/src/plugins/contracts/registry.ts b/src/plugins/contracts/registry.ts index d25f8a70a29..385c2605914 100644 --- a/src/plugins/contracts/registry.ts +++ b/src/plugins/contracts/registry.ts @@ -27,7 +27,6 @@ import opencodeGoPlugin from "../../../extensions/opencode-go/index.js"; import opencodePlugin from "../../../extensions/opencode/index.js"; import openrouterPlugin from "../../../extensions/openrouter/index.js"; import qianfanPlugin from "../../../extensions/qianfan/index.js"; -import qwenPortalAuthPlugin from "../../../extensions/qwen-portal-auth/index.js"; import sglangPlugin from "../../../extensions/sglang/index.js"; import syntheticPlugin from "../../../extensions/synthetic/index.js"; import togetherPlugin from "../../../extensions/together/index.js"; @@ -378,7 +377,6 @@ const bundledProviderPlugins = dedupePlugins([ opencodeGoPlugin, openrouterPlugin, qianfanPlugin, - qwenPortalAuthPlugin, sglangPlugin, syntheticPlugin, togetherPlugin, diff --git a/src/plugins/contracts/runtime.contract.test.ts b/src/plugins/contracts/runtime.contract.test.ts index 68c5249eccf..34b94732f5b 100644 --- a/src/plugins/contracts/runtime.contract.test.ts +++ b/src/plugins/contracts/runtime.contract.test.ts @@ -3,7 +3,6 @@ import os from "node:os"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; import openAIPlugin from "../../../extensions/openai/index.js"; -import qwenPortalPlugin from "../../../extensions/qwen-portal-auth/index.js"; import { createProviderUsageFetch, makeResponse } from "../../test-utils/provider-usage-fetch.js"; import type { ProviderPlugin, ProviderRuntimeModel } from "../types.js"; import { requireProviderContractProvider as requireBundledProviderContractProvider } from "./registry.js"; @@ -17,14 +16,8 @@ const getOAuthProvidersMock = vi.hoisted(() => { id: "anthropic", envApiKey: "ANTHROPIC_API_KEY", oauthTokenEnv: "ANTHROPIC_OAUTH_TOKEN" }, // pragma: allowlist secret { id: "google", envApiKey: "GOOGLE_API_KEY", oauthTokenEnv: "GOOGLE_OAUTH_TOKEN" }, // pragma: allowlist secret { id: "openai-codex", envApiKey: "OPENAI_API_KEY", oauthTokenEnv: "OPENAI_OAUTH_TOKEN" }, // pragma: allowlist secret - { - id: "qwen-portal", - envApiKey: "QWEN_PORTAL_API_KEY", - oauthTokenEnv: "QWEN_PORTAL_OAUTH_TOKEN", - }, // pragma: allowlist secret ]), ); -const refreshQwenPortalCredentialsMock = vi.hoisted(() => vi.fn()); vi.mock("@mariozechner/pi-ai/oauth", async () => { const actual = await vi.importActual( @@ -37,14 +30,6 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => { }; }); -vi.mock("../../../extensions/qwen-portal-auth/refresh.js", async () => { - const actual = await vi.importActual("../../../extensions/qwen-portal-auth/refresh.js"); - return { - ...actual, - refreshQwenPortalCredentials: refreshQwenPortalCredentialsMock, - }; -}); - function createModel(overrides: Partial & Pick) { return { id: overrides.id, @@ -64,9 +49,6 @@ function requireProviderContractProvider(providerId: string): ProviderPlugin { if (providerId === "openai-codex") { return requireProvider(registerProviders(openAIPlugin), providerId); } - if (providerId === "qwen-portal") { - return requireProvider(registerProviders(qwenPortalPlugin), providerId); - } return requireBundledProviderContractProvider(providerId); } @@ -74,7 +56,6 @@ describe("provider runtime contract", () => { beforeEach(() => { getOAuthApiKeyMock.mockReset(); getOAuthProvidersMock.mockClear(); - refreshQwenPortalCredentialsMock.mockReset(); }, CONTRACT_SETUP_TIMEOUT_MS); describe("anthropic", () => { @@ -633,29 +614,6 @@ describe("provider runtime contract", () => { }); }); - describe("qwen-portal", () => { - it("owns OAuth refresh", async () => { - const provider = requireProviderContractProvider("qwen-portal"); - const credential = { - type: "oauth" as const, - provider: "qwen-portal", - access: "stale-access-token", - refresh: "refresh-token", - expires: Date.now() - 60_000, - }; - const refreshed = { - ...credential, - access: "fresh-access-token", - expires: Date.now() + 60_000, - }; - - refreshQwenPortalCredentialsMock.mockReset(); - refreshQwenPortalCredentialsMock.mockResolvedValueOnce(refreshed); - - await expect(provider.refreshOAuth?.(credential)).resolves.toEqual(refreshed); - }); - }); - describe("zai", () => { it("owns glm-5 forward-compat resolution", () => { const provider = requireProviderContractProvider("zai");