Repository navigation
refactor(core): extract Protocol enum and decouple model identity from auth type - #5089
Conversation
…m auth type
- AuthType: enum -> string (custom provider IDs supported)
- New Protocol enum: OPENAI, QWEN_OAUTH, GEMINI, ANTHROPIC
- ContentGeneratorConfig: add `protocol` field for SDK routing
- ProviderConfig: { protocol, models[], baseUrl?, envKey? }
- ModelProvidersConfig: Record<string, ProviderConfig>
- createContentGenerator: dispatch by `protocol` not `authType`
- v4->v5 migration: authType maps to protocol, models wrapped in ProviderConfig
|
#5039 only added fields to CLI settings. External channels (ACP, IDE extensions) still send a single #5089 decouples at the core:
Could you take a look? @wenshao |
…iderConfig format
|
Thanks for the review! @wenshao R1 results: Fixed:
Addressed differently:
|
qqqys
left a comment
There was a problem hiding this comment.
Critical-only re-review: the prior protocol propagation blocker is still unresolved for non-modelProviders auth paths. The new fix only injects protocol in ModelsConfig.getGenerationConfig() when ModelRegistry has a registered provider for the current authType. For legacy/env/CLI/manual setups such as OPENAI_API_KEY + OPENAI_MODEL + OPENAI_BASE_URL with no modelProviders entry, resolveModelConfig() still returns authType/model/apiKey/baseUrl but no protocol; ModelRegistry only registers qwen-oauth by default, so getGenerationConfig() cannot fill it. refreshAuth() then passes that config into createContentGenerator(), which now throws "ContentGeneratorConfig must have a protocol" before creating any generator. Impact: existing non-registry OpenAI/Gemini/Anthropic/Vertex auth flows fail at startup/auth refresh/model switch. Please derive protocol in the general resolution path as well, not only from provider registry entries.
wenshao
left a comment
There was a problem hiding this comment.
New findings beyond existing 6 inline comments.
[Critical] packages/cli/src/ui/hooks/useProviderUpdates.ts:167 — getInstalledOwnedModelIds casts modelProviders[protocol] to ProviderModelConfig[] and calls .map() / .filter(). After v5 migration, the value is a ProviderConfig object (not an array), so this throws TypeError: allModels.map is not a function at runtime. This file was not updated in the PR. Fix: unwrap like acpAgent.ts does:
const entry = modelProviders[protocol];
const allModels: ProviderModelConfig[] = Array.isArray(entry) ? entry : (entry?.models ?? []);[Suggestion] packages/cli/src/ui/auth/useProviderSetupFlow.ts:448 — The settings preview JSON emits v4 array format (modelProviders: { [protocol]: models }) instead of v5 ProviderConfig format ({ [protocol]: { protocol, models } }). Display-only, but misleads users who inspect the preview.
— qwen3.7-max via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
Additional findings not mappable to diff lines:
[Critical] packages/core/src/models/content-generator-config.ts — buildAgentContentGeneratorConfig() spreads parentConfig into nextConfig (inheriting the parent's protocol) but never updates protocol for the target authType. When a subagent targets a different provider (e.g., parent uses OpenAI, subagent targets Anthropic), createContentGenerator dispatches on the parent's protocol, creating the wrong content generator. Fix: after constructing nextConfig, resolve the protocol for authOverrides.authType via modelRegistry.getProtocolForAuthType() and set it on nextConfig.protocol.
[Critical] packages/cli/src/ui/hooks/useProviderUpdates.ts:155-169 — getInstalledOwnedModelIds() was not updated for v5 format. It casts modelProviders as Record<string, ProviderModelConfig[]> (v4 array) and calls .map() / .filter() on the value. After migration to v5, each entry is a {protocol, models} object, so allModels.map(m => m.id) will crash with TypeError: allModels.map is not a function.
— qwen3.7-max via Qwen Code /review
…havior - Update test: any non-empty token in trailing parens is now treated as authType - Add test: empty parens fallback returns whole input as modelId - Update JSDoc: remove "valid AuthType" since authType is now an arbitrary string No logic changes — test and docs only.
|
CI fix: ff36dd0 Changes:
Other:
Verified the decoupling changes locally against all three test plan scenarios (v4→v5 migration, same ID with different envKeys, |
wenshao
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
Verification —
|
| Area | Suites | Tests |
|---|---|---|
| migration | v4-to-v5, migration/index |
19, 26 ✅ |
| settings / config | settings (cli), config (core) |
133, 234 ✅ |
| models | modelsConfig, modelRegistry, content-generator-config |
73, 55, 14 ✅ |
| routing | contentGenerator (protocol dispatch) |
10 ✅ |
| providers | install, provider-config, custom-provider |
21, 52, 11 ✅ |
| model / auth utils | modelConfigUtils, acpModelUtils, useAuth |
53, 6, 23 ✅ |
Real CLI E2E — v4→v5 settings migration (Reviewer Test Plan Scenario 1)
Seeded the PR's exact v4 ~/.qwen/settings.json (5 providers in the old array format, $version: 4), then ran the real binary:
$version: 4 → 5
openai → { protocol: "openai", models: […] }
qwen-oauth → { protocol: "qwen-oauth", models: […] }
gemini → { protocol: "gemini", models: […] }
vertex-ai → { protocol: "gemini", models: […] } ← note: vertex-ai maps to the gemini protocol
anthropic → { protocol: "anthropic", models: […] }
- Every model entry preserved byte-for-byte; unrelated settings (
security,privacy) preserved. - Idempotent: a second run on the now-v5 file leaves it unchanged (no double-migration / no churn).
- The
-prun then exited on "No auth type is selected" — expected, since the migration runs at settings-load before the prompt; the point is the file transformed cleanly with no parse/crash.
Real CLI E2E — protocol-dispatch routing
createContentGenerator now dispatches on protocol instead of authType. Drove the real binary with openai auth against a mock endpoint → it returned the reply (PROTOCOL_ROUTED_OK, exit 0). So the decoupled routing works end-to-end in the binary, not just in unit tests.
Notes
- HEAD
660ea26dis a merge ofmaininto the branch; the 75-file scope is mostly mechanical type/import updates plus test updates to the newProviderConfig { protocol, models }shape — the green suites confirm the shape is consistent across core / cli / providers. - Verified on Linux.
No issues found — the refactor is behavior-preserving, the v4→v5 migration is correct (including the vertex-ai → gemini mapping), content-preserving, and idempotent, and protocol routing works in the real binary. LGTM. 🚀
中文版(合并参考)
验证 —— 提取 Protocol 枚举 + 将模型身份与鉴权类型解耦 ✅
这是一个大型重构(75 个文件)外加一个 v4→v5 设置迁移,所以我用两种方式验证:跑了大量被改动的测试套件(重构必须保持行为不变),并端到端驱动真实的 dist/cli.js 二进制来验证头部的迁移和新的 protocol 路由。基于 HEAD 660ea26d,在 Linux 上 npm ci + npm run bundle 构建。
这里 tmux/交互模式没有额外价值 —— 迁移在启动时发生,与模式无关 —— 所以我用 headless 方式驱动真实二进制,直接观察磁盘上的转换。
重构保持行为不变 —— 改动最多的套件 ~730 个用例全绿
| 范围 | 套件 | 用例数 |
|---|---|---|
| 迁移 | v4-to-v5、migration/index |
19、26 ✅ |
| 设置 / 配置 | settings(cli)、config(core) |
133、234 ✅ |
| 模型 | modelsConfig、modelRegistry、content-generator-config |
73、55、14 ✅ |
| 路由 | contentGenerator(按 protocol 分发) |
10 ✅ |
| providers | install、provider-config、custom-provider |
21、52、11 ✅ |
| 模型 / 鉴权工具 | modelConfigUtils、acpModelUtils、useAuth |
53、6、23 ✅ |
真实 CLI 端到端 —— v4→v5 设置迁移(Reviewer Test Plan 场景 1)
放入 PR 给出的那份 v4 ~/.qwen/settings.json(5 个 provider,旧数组格式,$version: 4),然后跑真实二进制:
$version: 4 → 5
openai → { protocol: "openai", models: […] }
qwen-oauth → { protocol: "qwen-oauth", models: […] }
gemini → { protocol: "gemini", models: […] }
vertex-ai → { protocol: "gemini", models: […] } ← 注意:vertex-ai 映射到 gemini 协议
anthropic → { protocol: "anthropic", models: […] }
- 每个 model 条目逐字节保留;无关设置(
security、privacy)保留。 - 幂等:对已是 v5 的文件再跑一次不会改变它(不会重复迁移 / 无抖动)。
- 之后
-p运行以 "No auth type is selected" 退出 —— 符合预期,因为迁移在设置加载时、prompt 之前发生;关键是文件干净转换、无解析/崩溃。
真实 CLI 端到端 —— 按 protocol 分发的路由
createContentGenerator 现在按 protocol 而非 authType 分发。用 openai 鉴权对接 mock 接口驱动真实二进制 → 返回了回复(PROTOCOL_ROUTED_OK,exit 0)。所以解耦后的路由在二进制层面端到端可用,而不仅是单测。
说明
- HEAD
660ea26d是把main合并进分支的提交;75 文件的范围大多是机械的类型/import 更新,以及对新ProviderConfig { protocol, models }形状的测试更新 —— 全绿的套件证明该形状在 core / cli / providers 间一致。 - 在 Linux 上验证。
未发现问题 —— 重构保持行为不变,v4→v5 迁移正确(含 vertex-ai → gemini 映射)、保留内容、且幂等,protocol 路由在真实二进制上可用。LGTM 🚀
…s listing (#5729) #5089 switched getAllConfiguredModels from Object.values(AuthType) to modelRegistry.getAuthTypes(), which only returns authTypes that have registry models. A runtime model resolved from env/CLI overrides (e.g. OPENAI_API_KEY / OPENAI_BASE_URL / OPENAI_MODEL with no modelProviders entry) uses an authType that has no registry models, so it was dropped from the default listing — even when it is the active/current model. This hid the active model from the ACP `availableModels` list and the interactive `/model` picker, and deterministically failed the ACP `set_config_option` integration test (`expect(openaiModel).toBeDefined()`) on every main run since #5089. Include the active runtime model's authType in the default (no-filter) case so it is enumerated alongside registry models. Explicit-authType callers are unchanged. Add a core unit regression test.
Add Stage 0b that blocks community-contributed refactor PRs touching core infrastructure (packages/core, auth, providers, models, config, tools, services) unless maintainer-initiated with prior design discussion. Triggered by PR #5089: 75-file refactor across core/auth/providers/models that should never have been accepted from a community contributor.
Add Stage 0b that flatly rejects community-contributed refactor PRs touching core infrastructure (packages/core, auth, providers, models, config, tools, services). No exceptions — core refactors must be maintainer-initiated with prior design discussion. Triggered by PR #5089: 75-file refactor across core/auth/providers/models that should never have been accepted from a community contributor.
A fork (cross-repository) PR whose title is a `refactor` type could be auto-approved by the /triage skill and merged without a maintainer reviewing the structural changes (this happened with #5089). Add a deterministic approval guardrail to Stage 3: before approving, check `isCrossRepository && title ~ /^refactor/i`; on a match, skip `gh pr review --approve` and escalate to the maintainer instead. Approval is now a positive condition (the guard must explicitly pass), so a blocked or empty check never approves. Document the rule in the skill's global Rules section as well.
Addresses /qreview feedback on the revert:
- vscode findOpenaiModels: restore read-side tolerance for the V5
{ protocol, models } shape. The extension reads/writes settings.json
without running the CLI v5->v4 migration, so a not-yet-downgraded
$version:5 file would otherwise return [] and silently drop existing
OpenAI models on the next write. (Critical)
- modelRegistry.registerAuthTypeModels: guard against a non-array provider
value (skip + warn) instead of throwing an opaque "models is not
iterable" — covers hand-edited or unmigrated files the downgrade misses.
- needsMigration JSDoc: update the stale ">= SETTINGS_VERSION" wording to
match the "=== SETTINGS_VERSION, else fall through" logic the downgrade
path depends on.
- settings.test.ts: also assert the v5->v4 downgrade is persisted to disk
(.tmp write-back), not just the in-memory merged result.
Adds tests for the registry guard and the vscode V5 read tolerance.
…M#5089) Reverts the structural changes from QwenLM#5089 back to the pre-QwenLM#5089 shape: AuthType stays a fixed enum (not `string`), the Protocol enum is removed, modelProviders is `Record<authType, ModelConfig[]>` again (not `{ protocol, models }`), and createContentGenerator dispatches on authType. The v4->v5 settings migration is removed and SETTINGS_VERSION reverts to 4. Features merged on top of QwenLM#5089 are kept and re-adapted to the old enum+array structure (not reverted): - QwenLM#5632 fastOnly/voiceOnly model flags (test fixtures reshaped to arrays) - QwenLM#5638 workspace provider defaults (readProviderModels already tolerates both shapes; test fixtures reshaped to arrays) - QwenLM#5729 active-runtime-model listing (pre-QwenLM#5089 getAllConfiguredModels already enumerates Object.values(AuthType), so the runtime model is included natively) - QwenLM#5728 ACP set_config_option deterministic provider fixture (reshaped to array; the flake fix is preserved) KNOWN DOWNGRADE CAVEAT: settings already migrated to $version:5 (shipped in v0.19.0) retain the v5 `{ protocol, models }` modelProviders shape, which the reverted ModelRegistry consumes as an array. Such settings will throw on load until re-configured. A v5->v4 downgrade guard/migration is a separate follow-up if backward compatibility for migrated users is needed.
…vert After reverting QwenLM#5089, settings already migrated to $version:5 (shipped in v0.19.0) carry a modelProviders `{ protocol, models }` shape that the reverted v4 readers consume as arrays, throwing "models is not iterable" on load. This adds the inverse migration so those configs auto-converge to v4 on load (the user-facing "automatically migrate $version:5 to 4"). - V5ToV4Migration: unwraps each modelProviders `{ protocol, models }` back to its `models` array, drops the now-implicit protocol (warning only when the explicit protocol differs from the key-derived one), and resets $version to 4. - DOWNGRADE_MIGRATIONS keeps the downgrade out of the ascending forward ALL_MIGRATIONS chain (preserving its invariants); runMigrations and needsMigration consider both via a combined convergence set. - needsMigration now gates on `=== SETTINGS_VERSION` instead of `>=`, so a newer-but-handled version (v5) is reported as needing migration while a genuinely unknown newer version (v6+) is still left untouched. Covered by unit tests for the migration, the framework wiring, and an end-to-end loadSettings downgrade-on-load test.
Addresses /qreview feedback on the revert:
- vscode findOpenaiModels: restore read-side tolerance for the V5
{ protocol, models } shape. The extension reads/writes settings.json
without running the CLI v5->v4 migration, so a not-yet-downgraded
$version:5 file would otherwise return [] and silently drop existing
OpenAI models on the next write. (Critical)
- modelRegistry.registerAuthTypeModels: guard against a non-array provider
value (skip + warn) instead of throwing an opaque "models is not
iterable" — covers hand-edited or unmigrated files the downgrade misses.
- needsMigration JSDoc: update the stale ">= SETTINGS_VERSION" wording to
match the "=== SETTINGS_VERSION, else fall through" logic the downgrade
path depends on.
- settings.test.ts: also assert the v5->v4 downgrade is persisted to disk
(.tmp write-back), not just the in-memory merged result.
Adds tests for the registry guard and the vscode V5 read tolerance.
… paths Addresses /review suggestions on the revert: - contentGenerator: import PROVIDER_SOURCED_FIELDS from constants.js (where it is actually defined) instead of modelsConfig.js, breaking the runtime import cycle contentGenerator -> modelsConfig -> contentGenerator. constants.js only references contentGenerator at the type level, which is erased at runtime, so no cycle remains. - contentGenerator.test: add coverage for the two authType error paths the revert restored (missing authType -> "must have an authType"; unknown authType -> "Unsupported authType"), which QwenLM#5089's protocol-based tests had replaced. Neither was covered before. The acpAgent z.nativeEnum(AuthType).parse(methodId) suggestion is left as-is: that line is byte-identical to pre-QwenLM#5089, so it is pre-existing behavior the revert faithfully restores rather than a regression of this PR.
#5745) * revert(core): revert Protocol enum & model-identity decoupling (#5089) Reverts the structural changes from #5089 back to the pre-#5089 shape: AuthType stays a fixed enum (not `string`), the Protocol enum is removed, modelProviders is `Record<authType, ModelConfig[]>` again (not `{ protocol, models }`), and createContentGenerator dispatches on authType. The v4->v5 settings migration is removed and SETTINGS_VERSION reverts to 4. Features merged on top of #5089 are kept and re-adapted to the old enum+array structure (not reverted): - #5632 fastOnly/voiceOnly model flags (test fixtures reshaped to arrays) - #5638 workspace provider defaults (readProviderModels already tolerates both shapes; test fixtures reshaped to arrays) - #5729 active-runtime-model listing (pre-#5089 getAllConfiguredModels already enumerates Object.values(AuthType), so the runtime model is included natively) - #5728 ACP set_config_option deterministic provider fixture (reshaped to array; the flake fix is preserved) KNOWN DOWNGRADE CAVEAT: settings already migrated to $version:5 (shipped in v0.19.0) retain the v5 `{ protocol, models }` modelProviders shape, which the reverted ModelRegistry consumes as an array. Such settings will throw on load until re-configured. A v5->v4 downgrade guard/migration is a separate follow-up if backward compatibility for migrated users is needed. * feat(cli): add v5->v4 settings downgrade migration for #5089 revert After reverting #5089, settings already migrated to $version:5 (shipped in v0.19.0) carry a modelProviders `{ protocol, models }` shape that the reverted v4 readers consume as arrays, throwing "models is not iterable" on load. This adds the inverse migration so those configs auto-converge to v4 on load (the user-facing "automatically migrate $version:5 to 4"). - V5ToV4Migration: unwraps each modelProviders `{ protocol, models }` back to its `models` array, drops the now-implicit protocol (warning only when the explicit protocol differs from the key-derived one), and resets $version to 4. - DOWNGRADE_MIGRATIONS keeps the downgrade out of the ascending forward ALL_MIGRATIONS chain (preserving its invariants); runMigrations and needsMigration consider both via a combined convergence set. - needsMigration now gates on `=== SETTINGS_VERSION` instead of `>=`, so a newer-but-handled version (v5) is reported as needing migration while a genuinely unknown newer version (v6+) is still left untouched. Covered by unit tests for the migration, the framework wiring, and an end-to-end loadSettings downgrade-on-load test. * fix(test): align integration settings-version constant with reverted v4 The integration suites hard-coded CURRENT_SETTINGS_VERSION = 5 (introduced by #5676), which mismatched the reverted SETTINGS_VERSION = 4 and failed the migration assertions ($version now writes 4, not 5). Revert the constant to 4 in both settings-migration and qwen-config-dir integration tests. Verified: QWEN_SANDBOX=false vitest run --root ./integration-tests cli/settings-migration.test.ts cli/qwen-config-dir.test.ts → 21 passed. * fix: harden v5-era settings handling on the #5089 revert path Addresses /qreview feedback on the revert: - vscode findOpenaiModels: restore read-side tolerance for the V5 { protocol, models } shape. The extension reads/writes settings.json without running the CLI v5->v4 migration, so a not-yet-downgraded $version:5 file would otherwise return [] and silently drop existing OpenAI models on the next write. (Critical) - modelRegistry.registerAuthTypeModels: guard against a non-array provider value (skip + warn) instead of throwing an opaque "models is not iterable" — covers hand-edited or unmigrated files the downgrade misses. - needsMigration JSDoc: update the stale ">= SETTINGS_VERSION" wording to match the "=== SETTINGS_VERSION, else fall through" logic the downgrade path depends on. - settings.test.ts: also assert the v5->v4 downgrade is persisted to disk (.tmp write-back), not just the in-memory merged result. Adds tests for the registry guard and the vscode V5 read tolerance. * fix(core): break contentGenerator import cycle + cover reverted error paths Addresses /review suggestions on the revert: - contentGenerator: import PROVIDER_SOURCED_FIELDS from constants.js (where it is actually defined) instead of modelsConfig.js, breaking the runtime import cycle contentGenerator -> modelsConfig -> contentGenerator. constants.js only references contentGenerator at the type level, which is erased at runtime, so no cycle remains. - contentGenerator.test: add coverage for the two authType error paths the revert restored (missing authType -> "must have an authType"; unknown authType -> "Unsupported authType"), which #5089's protocol-based tests had replaced. Neither was covered before. The acpAgent z.nativeEnum(AuthType).parse(methodId) suggestion is left as-is: that line is byte-identical to pre-#5089, so it is pre-existing behavior the revert faithfully restores rather than a regression of this PR.
…wenLM#5758) (QwenLM#5793) * feat(config): map provider id to SDK protocol via providerProtocol (QwenLM#5758) Decouple provider identity from SDK routing in a backward-compatible way (issue QwenLM#5758, Approach A), without re-introducing the modelProviders structural change that got QwenLM#5089 reverted. - Add a `providerProtocol` settings dictionary mapping a provider id to the built-in protocol (AuthType) that should route it. `modelProviders` stays a Record<providerId, ModelConfig[]> array; old versions ignore the new key. - ModelRegistry resolves each provider id to a protocol (explicit map entry, or the id itself when built-in) and registers custom ids under that protocol, merging providers that share one protocol. Unmapped unknown ids are skipped (typo guard); the resolver is a pure function. - Honor a custom provider's envKey/metadata: auth pre-flight and CLI model resolution now look up models by resolved protocol across all provider ids, not just the protocol key. - Surface skipped providers (no mapping / unknown protocol) through the visible CLI warnings path instead of debug-only logging. - reloadModels preserves the protocol map when omitted (undefined) and replaces it when given; ACP workspaceReload threads providerProtocol so long-lived sessions pick up changes. End-to-end providerId identity and the ACP/VSCode wire format are deferred to a follow-up. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(config): update generated settings schema * fix(config): refresh provider protocol reloads * fix(config): cover provider protocol auth reloads * fix(config): improve provider protocol warnings * test(serve): cover provider protocol status * fix(config): guard provider protocol lookups * fix(config): clarify invalid provider protocol warnings --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Add Stage 0b that flatly rejects community-contributed refactor PRs touching core infrastructure (packages/core, auth, providers, models, config, tools, services). No exceptions — core refactors must be maintainer-initiated with prior design discussion. Triggered by PR #5089: 75-file refactor across core/auth/providers/models that should never have been accepted from a community contributor.
What this PR does
Extracts a standalone
Protocolenum from the auth layer so that model identity (provider ID) and SDK routing (protocol) are no longer coupled.Concrete changes:
AuthTypechanged from a fixed 5-value enum totype AuthType = string, allowing arbitrary provider IDs (including custom providers).enum Protocol { OPENAI, QWEN_OAUTH, GEMINI, ANTHROPIC }added tocontentGenerator.ts. This controls which SDK handles a request.ContentGeneratorConfiggains aprotocolfield.createContentGeneratornow dispatches onprotocolinstead ofauthType.ModelProvidersConfigvalues changed fromModelConfig[]toProviderConfig { protocol, models, baseUrl?, envKey? }. Each provider entry now carries its own protocol.ProviderConfig.protocolinproviders/types.tschanged fromAuthTypetoProtocol;protocolOptionsandenvKeyfunction signatures updated accordingly.Protocol.*instead ofAuthType.USE_*.acpAgent.ts,runQwenServe.ts,server.ts,ProviderSetupSteps.tsx,useAuth.ts,useProviderSetupFlow.ts,modelConfigUtils.ts) and VSCode extension (AuthMessageHandler.ts).ProviderConfigshape andProtocolenum usage.packages/cli/src/config/migration/versions/v4-to-v5.ts): auto-converts oldmodelProvidersarray format to{ protocol, models }on config load.settingsWriter.ts:writeModelProvidersConfigandwriteCodingPlanConfigupdated to write V5 format;findOpenaiModelsupdated to read both formats.Why it's needed
Previously,
authTypeserved double duty: it was both the provider identity and the SDK selector. This made it impossible to add custom providers (arbitrary string IDs) without breaking SDK routing. SeparatingProtocolfromauthTypelets us support custom provider IDs while keeping SDK dispatch stable and explicit.Reviewer Test Plan
Before / After
Before:
{ "modelProviders": { "openai": [ { "id": "gpt-4o", "name": "GPT-4o", "baseUrl": "...", "envKey": "..." } ] } }authTypewas an enum ('openai'|'gemini'|'anthropic'| etc.), used for both provider identity and SDK routing.After:
{ "modelProviders": { "openai": { "protocol": "openai", "models": [ { "id": "gpt-4o", "name": "GPT-4o", "baseUrl": "...", "envKey": "..." } ] } } }authTypeis now any string (provider identity).protocolis a separate field that controls SDK routing.How to verify
npm run buildScenario 1: v4 to v5 Config Migration
Goal: Old config auto-migrates to new format.
Steps:
~/.qwen/settings.json:{ "modelProviders": { "openai": [ {"id": "qwen-model", "name": "qwen-model", "baseUrl": "http://127.0.0.1:8001", "envKey": "OPENAI_API_KEY"} ], "qwen-oauth": [ {"id": "qwen-oauth-model", "name": "Qwen OAuth", "baseUrl": "http://127.0.0.1:8001", "envKey": "QWEN_OAUTH_API_KEY"} ], "gemini": [ {"id": "gemini-model", "name": "Gemini", "baseUrl": "http://127.0.0.1:8001", "envKey": "GEMINI_API_KEY"} ], "vertex-ai": [ {"id": "vertex-model", "name": "Vertex AI", "baseUrl": "http://127.0.0.1:8001", "envKey": "VERTEX_API_KEY"} ], "anthropic": [ {"id": "anthropic-model", "name": "Anthropic", "baseUrl": "http://127.0.0.1:8001", "envKey": "ANTHROPIC_API_KEY"} ] }, "$version": 4, "env": { "OPENAI_API_KEY": "sk-openai-xxx", "QWEN_OAUTH_API_KEY": "sk-qwen-oauth-xxx", "GEMINI_API_KEY": "sk-gemini-xxx", "VERTEX_API_KEY": "sk-vertex-xxx", "ANTHROPIC_API_KEY": "sk-anthropic-xxx" } }npm start.~/.qwen/settings.jsonbecomes v5 format:{ "modelProviders": { "openai": { "protocol": "openai", "models": [ {"id": "qwen-model", "name": "qwen-model", "baseUrl": "http://127.0.0.1:8001", "envKey": "OPENAI_API_KEY"} ] }, "qwen-oauth": { "protocol": "qwen-oauth", "models": [ {"id": "qwen-oauth-model", "name": "Qwen OAuth", "baseUrl": "http://127.0.0.1:8001", "envKey": "QWEN_OAUTH_API_KEY"} ] }, "gemini": { "protocol": "gemini", "models": [ {"id": "gemini-model", "name": "Gemini", "baseUrl": "http://127.0.0.1:8001", "envKey": "GEMINI_API_KEY"} ] }, "vertex-ai": { "protocol": "gemini", "models": [ {"id": "vertex-model", "name": "Vertex AI", "baseUrl": "http://127.0.0.1:8001", "envKey": "VERTEX_API_KEY"} ] }, "anthropic": { "protocol": "anthropic", "models": [ {"id": "anthropic-model", "name": "Anthropic", "baseUrl": "http://127.0.0.1:8001", "envKey": "ANTHROPIC_API_KEY"} ] } }, "$version": 5, "env": { "OPENAI_API_KEY": "sk-openai-xxx", "QWEN_OAUTH_API_KEY": "sk-qwen-oauth-xxx", "GEMINI_API_KEY": "sk-gemini-xxx", "VERTEX_API_KEY": "sk-vertex-xxx", "ANTHROPIC_API_KEY": "sk-anthropic-xxx" } }Expected:
$versionchanges from 4 to 5.protocolfield (mapping: openai to openai, qwen-oauth to qwen-oauth, gemini to gemini, vertex-ai to gemini, anthropic to anthropic).modelsfield contains the original array contents.Scenario 2: Same ID, Same BaseUrl, Different envKey
Goal: Multiple providers sharing the same model ID and baseUrl but different envKeys get correctly distinguished.
Steps:
~/.qwen/settings.json:{ "modelProviders": { "openai": [ {"id": "qwen3.7-max", "baseUrl": "http://127.0.0.1:8001", "envKey": "TOKEN_PLAN_KEY"}, {"id": "qwen3.7-max", "baseUrl": "http://127.0.0.1:8001", "envKey": "IDEALAB_KEY"} ] }, "$version": 4, "env": { "TOKEN_PLAN_KEY": "sk-token-plan-xxx", "IDEALAB_KEY": "sk-idealab-xxx" } }qwen./modelto select a model.Expected (before PR):
/modelonly shows the first model (TOKEN_PLAN_KEY).Expected (after PR):
/modelshows both models.openaiorqwen3.7-idealab).Scenario 3: /model Command with Provider Specifier
Goal:
/modelcommand correctly switches providers when given a provider qualifier.Steps:
/model qwen3.7-max.Before switch:
/statusshowsAuth: API Key - openai./model qwen3.7-max(qwen3.7-idealab).After switch:
/statusshowsAuth: API Key - qwen3.7-idealab.settings.jsonupdatessecurity.auth.selectedTypeto"qwen3.7-idealab".Tested on
Risk & Scope
createContentGeneratordispatch logic is functionally identical — same branches, just keyed onprotocolinstead ofauthType.modelProvidersstructure change from array to{ protocol, models }is a breaking format change. A v4→v5 settings migration is included in this PR — old configs are auto-converted on load.Follow-up tasks
modelProvidersarray format to{ protocol, models }on config load. Done in this PR.ModelDialog.tsxcurrently hardcodes the 5 built-in AuthType values. Needs to support arbitrary provider IDs for custom providers. (Separate PR)model.name→model.id: Rename the settings field for clarityLinked Issues
Fixes: #5090 #4877 #5080
Closes: #4814
Resolves: #4813 #4722
中文
这个 PR 做了什么
将
Protocol从认证层中独立出来,使模型身份(提供商 ID)和 SDK 路由(协议)解耦。具体改动:
AuthType从固定 5 值枚举改为type AuthType = string,支持任意提供商 ID(包括自定义提供商)。contentGenerator.ts中新增enum Protocol { OPENAI, QWEN_OAUTH, GEMINI, ANTHROPIC },用于控制请求走哪个 SDK。ContentGeneratorConfig新增protocol字段,createContentGenerator按protocol分发而非authType。ModelProvidersConfig的值从ModelConfig[]改为ProviderConfig { protocol, models, baseUrl?, envKey? }。providers/types.ts中ProviderConfig.protocol从AuthType改为Protocol;protocolOptions和envKey函数签名同步更新。Protocol.*。packages/cli/src/config/migration/versions/v4-to-v5.ts):加载旧配置时自动将modelProviders数组格式转换为{ protocol, models }。settingsWriter.ts:writeModelProvidersConfig和writeCodingPlanConfig更新为写入 V5 格式;findOpenaiModels兼容新旧两种格式。为什么需要这个改动
之前
authType同时承担提供商身份和 SDK 选择器两个职责,导致无法支持自定义提供商(任意字符串 ID)。将Protocol从authType中分离后,可以支持自定义提供商 ID,同时保持 SDK 分发稳定。Reviewer 测试计划
修改前 / 修改后
修改前:
{ "modelProviders": { "openai": [ { "id": "gpt-4o", "name": "GPT-4o" } ] } }authType是枚举,同时用于提供商身份和 SDK 路由。修改后:
{ "modelProviders": { "openai": { "protocol": "openai", "models": [ { "id": "gpt-4o", "name": "GPT-4o" } ] } } }authType可以是任意字符串(提供商身份),protocol单独控制 SDK 路由。如何验证
npm run build场景 1:v4→v5 配置迁移
目标:验证旧配置自动迁移为新格式。
步骤:
~/.qwen/settings.json:{ "modelProviders": { "openai": [ {"id": "qwen-model", "name": "qwen-model", "baseUrl": "http://127.0.0.1:8001", "envKey": "OPENAI_API_KEY"} ], "qwen-oauth": [ {"id": "qwen-oauth-model", "name": "Qwen OAuth", "baseUrl": "http://127.0.0.1:8001", "envKey": "QWEN_OAUTH_API_KEY"} ], "gemini": [ {"id": "gemini-model", "name": "Gemini", "baseUrl": "http://127.0.0.1:8001", "envKey": "GEMINI_API_KEY"} ], "vertex-ai": [ {"id": "vertex-model", "name": "Vertex AI", "baseUrl": "http://127.0.0.1:8001", "envKey": "VERTEX_API_KEY"} ], "anthropic": [ {"id": "anthropic-model", "name": "Anthropic", "baseUrl": "http://127.0.0.1:8001", "envKey": "ANTHROPIC_API_KEY"} ] }, "$version": 4, "env": { "OPENAI_API_KEY": "sk-openai-xxx", "QWEN_OAUTH_API_KEY": "sk-qwen-oauth-xxx", "GEMINI_API_KEY": "sk-gemini-xxx", "VERTEX_API_KEY": "sk-vertex-xxx", "ANTHROPIC_API_KEY": "sk-anthropic-xxx" } }npm start。~/.qwen/settings.json是否变为 v5 格式:{ "modelProviders": { "openai": { "protocol": "openai", "models": [ {"id": "qwen-model", "name": "qwen-model", "baseUrl": "http://127.0.0.1:8001", "envKey": "OPENAI_API_KEY"} ] }, "qwen-oauth": { "protocol": "qwen-oauth", "models": [ {"id": "qwen-oauth-model", "name": "Qwen OAuth", "baseUrl": "http://127.0.0.1:8001", "envKey": "QWEN_OAUTH_API_KEY"} ] }, "gemini": { "protocol": "gemini", "models": [ {"id": "gemini-model", "name": "Gemini", "baseUrl": "http://127.0.0.1:8001", "envKey": "GEMINI_API_KEY"} ] }, "vertex-ai": { "protocol": "gemini", "models": [ {"id": "vertex-model", "name": "Vertex AI", "baseUrl": "http://127.0.0.1:8001", "envKey": "VERTEX_API_KEY"} ] }, "anthropic": { "protocol": "anthropic", "models": [ {"id": "anthropic-model", "name": "Anthropic", "baseUrl": "http://127.0.0.1:8001", "envKey": "ANTHROPIC_API_KEY"} ] } }, "$version": 5, "env": { "OPENAI_API_KEY": "sk-openai-xxx", "QWEN_OAUTH_API_KEY": "sk-qwen-oauth-xxx", "GEMINI_API_KEY": "sk-gemini-xxx", "VERTEX_API_KEY": "sk-vertex-xxx", "ANTHROPIC_API_KEY": "sk-anthropic-xxx" } }预期结果:
$version从 4 变为 5。protocol字段(映射规则:openai→openai, qwen-oauth→qwen-oauth, gemini→gemini, vertex-ai→gemini, anthropic→anthropic)。models字段包含原来的数组内容。场景 2:相同 ID、相同 baseUrl、不同 envKey
目标:验证多个提供商共享同一 model ID 和 baseUrl,但使用不同 envKey 时,系统能正确区分。
步骤:
~/.qwen/settings.json:{ "modelProviders": { "openai": [ {"id": "qwen3.7-max", "baseUrl": "http://127.0.0.1:8001", "envKey": "TOKEN_PLAN_KEY"}, {"id": "qwen3.7-max", "baseUrl": "http://127.0.0.1:8001", "envKey": "IDEALAB_KEY"} ] }, "$version": 4, "env": { "TOKEN_PLAN_KEY": "sk-token-plan-xxx", "IDEALAB_KEY": "sk-idealab-xxx" } }qwen。/model选择模型。预期结果(此 PR 前):
/model只显示第一个模型(TOKEN_PLAN_KEY)。预期结果(此 PR 后):
/model显示两个模型。openai或qwen3.7-idealab)。场景 3:/model 命令直接指定提供商
目标:验证通过
/model命令指定提供商和模型时,系统能正确切换。步骤:
/model qwen3.7-max。切换前:
/status显示Auth: API Key - openai。/model qwen3.7-max(qwen3.7-idealab)。切换后:
/status显示Auth: API Key - qwen3.7-idealab。settings.json中security.auth.selectedType更新为"qwen3.7-idealab"。测试环境
风险与范围
createContentGenerator分发逻辑功能不变。modelProviders从数组改为{ protocol, models }是破坏性格式变更。本 PR 已包含 v4→v5 迁移逻辑,旧配置会在加载时自动转换。后续任务
modelProviders数组格式为{ protocol, models }。已在本 PR 中完成。ModelDialog.tsx目前硬编码 5 个 AuthType 值,需要支持任意提供商 ID。(单独 PR)model.name→model.id:重命名 settings 字段以提高语义清晰度