From dad68d319b190e3d8df2d4a4be73051bfc5280cd Mon Sep 17 00:00:00 2001 From: pomelo Date: Thu, 26 Mar 2026 16:32:34 +0800 Subject: [PATCH] Remove Qwen OAuth integration (qwen-portal-auth) (#52709) * Remove Qwen OAuth integration (qwen-portal-auth) Qwen OAuth via portal.qwen.ai is being deprecated by the Qwen team due to traffic impact on their primary Qwen Code user base. Users should migrate to the officially supported Model Studio (Alibaba Cloud Coding Plan) provider instead. Ref: https://github.com/openclaw/openclaw/issues/49557 - Delete extensions/qwen-portal-auth/ plugin entirely - Remove qwen-portal from onboarding auth choices, provider aliases, auto-enable list, bundled plugin defaults, and pricing cache - Remove Qwen CLI credential sync (external-cli-sync, cli-credentials) - Remove QWEN_OAUTH_MARKER from model auth markers - Update docs/providers/qwen.md to redirect to Model Studio - Update model-providers docs (EN + zh-CN) to remove Qwen OAuth section - Regenerate config and plugin-sdk baselines - Update all affected tests Co-authored-by: Qwen-Coder * Clean up residual qwen-portal references after OAuth removal * Add migration hint for deprecated qwen-portal OAuth provider * fix: finish qwen oauth removal follow-up --------- Co-authored-by: Qwen-Coder Co-authored-by: Frank Yang --- .github/labeler.yml | 4 - CHANGELOG.md | 2 + docs/.generated/config-baseline.json | 121 ------------ docs/.generated/config-baseline.jsonl | 11 +- docs/cli/models.md | 2 +- docs/concepts/model-providers.md | 17 -- docs/help/testing.md | 2 +- docs/providers/index.md | 1 - docs/providers/qwen.md | 60 ++---- docs/tools/plugin.md | 2 +- docs/zh-CN/concepts/model-providers.md | 17 -- docs/zh-CN/providers/qwen.md | 55 ++---- docs/zh-CN/tools/plugin.md | 4 +- extensions/qwen-portal-auth/README.md | 24 --- extensions/qwen-portal-auth/index.ts | 148 --------------- extensions/qwen-portal-auth/oauth.runtime.ts | 1 - extensions/qwen-portal-auth/oauth.ts | 179 ------------------ .../qwen-portal-auth/openclaw.plugin.json | 24 --- .../qwen-portal-auth/provider-catalog.ts | 49 ----- extensions/qwen-portal-auth/refresh.test.ts | 144 -------------- extensions/qwen-portal-auth/refresh.ts | 62 ------ extensions/qwen-portal-auth/runtime-api.ts | 7 - scripts/check-no-raw-channel-fetch.mjs | 2 - scripts/lib/live-docker-auth.sh | 5 +- src/agents/auth-profiles.doctor.test.ts | 21 ++ .../auth-profiles.external-cli-sync.test.ts | 22 +-- .../auth-profiles.readonly-sync.test.ts | 10 +- src/agents/auth-profiles/constants.ts | 1 - src/agents/auth-profiles/doctor.ts | 16 ++ src/agents/auth-profiles/external-cli-sync.ts | 9 +- .../oauth.fallback-to-main-agent.test.ts | 1 - ...auth.openai-codex-refresh-fallback.test.ts | 1 - src/agents/auth-profiles/oauth.test.ts | 1 - src/agents/cli-credentials.test.ts | 64 ------- src/agents/cli-credentials.ts | 38 ---- src/agents/context.lookup.test.ts | 26 +-- src/agents/context.ts | 7 +- src/agents/model-auth-markers.test.ts | 5 +- src/agents/model-auth-markers.ts | 2 - src/agents/model-auth.profiles.test.ts | 14 -- src/agents/model-selection.test.ts | 2 +- src/agents/models-config.e2e-harness.ts | 2 - ...s-config.providers.auth-provenance.test.ts | 16 +- src/agents/models-config.providers.static.ts | 1 - src/agents/pi-embedded-runner/model.test.ts | 30 +-- src/agents/provider-id.ts | 3 - src/commands/auth-choice-options.test.ts | 10 - src/commands/auth-choice.test.ts | 17 +- .../models.auth.provider-resolution.test.ts | 6 +- .../local/auth-choice.ts | 1 - src/commands/onboard-types.ts | 2 - src/config/plugin-auto-enable.ts | 1 - src/gateway/model-pricing-cache.ts | 2 - ...rovider-usage.auth.normalizes-keys.test.ts | 1 - .../channel-import-guardrails.test.ts | 1 - src/plugin-sdk/provider-catalog.ts | 1 - src/plugin-sdk/subpaths.test.ts | 1 - ...undled-provider-auth-env-vars.generated.ts | 1 - .../bundled-provider-auth-env-vars.test.ts | 4 - src/plugins/config-state.ts | 1 - .../contracts/auth-choice.contract.test.ts | 159 ---------------- src/plugins/contracts/auth.contract.test.ts | 53 ------ .../contracts/discovery.contract.test.ts | 56 ------ src/plugins/contracts/registry.ts | 2 - .../contracts/runtime.contract.test.ts | 42 ---- 65 files changed, 135 insertions(+), 1461 deletions(-) delete mode 100644 extensions/qwen-portal-auth/README.md delete mode 100644 extensions/qwen-portal-auth/index.ts delete mode 100644 extensions/qwen-portal-auth/oauth.runtime.ts delete mode 100644 extensions/qwen-portal-auth/oauth.ts delete mode 100644 extensions/qwen-portal-auth/openclaw.plugin.json delete mode 100644 extensions/qwen-portal-auth/provider-catalog.ts delete mode 100644 extensions/qwen-portal-auth/refresh.test.ts delete mode 100644 extensions/qwen-portal-auth/refresh.ts delete mode 100644 extensions/qwen-portal-auth/runtime-api.ts create mode 100644 src/agents/auth-profiles.doctor.test.ts 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");