From a6612940f856a9fadd1c65139528f2edfbe568b3 Mon Sep 17 00:00:00 2001 From: tanzhenxin Date: Wed, 15 Apr 2026 23:17:32 +0800 Subject: [PATCH] fix(cli): block discontinued qwen-oauth model selection in ModelDialog (#3299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #3291 discontinued the Qwen OAuth free tier but intentionally left the ModelDialog unchanged, relying on server rejection for qwen-oauth models. This follow-up adds proper UI handling consistent with the AuthDialog: - Mark qwen-oauth model entries with "(Discontinued)" label and warning color - Replace descriptions with "Discontinued — switch to Coding Plan or API Key" - Block selection with inline error message instead of calling switchModel - Show ⚠ discontinuation notice in the detail panel for highlighted entries - Runtime OAuth models (existing cached tokens) remain selectable until server rejects them (soft cutoff principle from PR #3291) - Add i18n strings for the new error message across all 7 locale files Co-authored-by: Qwen-Coder --- packages/cli/src/i18n/locales/de.js | 2 + packages/cli/src/i18n/locales/en.js | 2 + packages/cli/src/i18n/locales/fr.js | 2 + packages/cli/src/i18n/locales/ja.js | 2 + packages/cli/src/i18n/locales/pt.js | 2 + packages/cli/src/i18n/locales/ru.js | 2 + packages/cli/src/i18n/locales/zh.js | 2 + .../src/ui/components/ModelDialog.test.tsx | 98 +++++++++++++------ .../cli/src/ui/components/ModelDialog.tsx | 45 ++++++++- 9 files changed, 126 insertions(+), 31 deletions(-) diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 3893bfe04..b2444fa7b 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -1251,6 +1251,8 @@ export default { 'Das kostenlose Qwen OAuth-Kontingent wurde am 2026-04-15 eingestellt. Führen Sie /auth aus, um den Anbieter zu wechseln.', 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select Coding Plan or API Key instead.': 'Das kostenlose Qwen OAuth-Kontingent wurde am 2026-04-15 eingestellt. Bitte wählen Sie Coding Plan oder API Key.', + 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider or run /auth to switch.': + 'Das kostenlose Qwen OAuth-Angebot wurde am 2026-04-15 eingestellt. Bitte wählen Sie ein Modell eines anderen Anbieter oder führen Sie /auth aus, um zu wechseln.', '\n⚠ Qwen OAuth free tier was discontinued on 2026-04-15. Please select another option.\n': '\n⚠ Das kostenlose Qwen OAuth-Kontingent wurde am 2026-04-15 eingestellt. Bitte wählen Sie eine andere Option.\n', 'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models': diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 4d3971859..1c99dcf7d 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -1304,6 +1304,8 @@ export default { 'Qwen OAuth free tier was discontinued on 2026-04-15. Run /auth to switch provider.', 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select Coding Plan or API Key instead.': 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select Coding Plan or API Key instead.', + 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider or run /auth to switch.': + 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider or run /auth to switch.', '\n⚠ Qwen OAuth free tier was discontinued on 2026-04-15. Please select another option.\n': '\n⚠ Qwen OAuth free tier was discontinued on 2026-04-15. Please select another option.\n', 'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models': diff --git a/packages/cli/src/i18n/locales/fr.js b/packages/cli/src/i18n/locales/fr.js index 6c0d01b9c..95cf5f935 100644 --- a/packages/cli/src/i18n/locales/fr.js +++ b/packages/cli/src/i18n/locales/fr.js @@ -1335,6 +1335,8 @@ export default { 'Le niveau gratuit Qwen OAuth a été abandonné le 2026-04-15. Exécutez /auth pour changer de fournisseur.', 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select Coding Plan or API Key instead.': 'Le niveau gratuit Qwen OAuth a été abandonné le 2026-04-15. Veuillez sélectionner Coding Plan ou API Key.', + 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider or run /auth to switch.': + "Le niveau gratuit de Qwen OAuth a été abandonné le 2026-04-15. Veuillez sélectionner un modèle d'un autre fournisseur ou exécuter /auth pour changer.", '\n⚠ Qwen OAuth free tier was discontinued on 2026-04-15. Please select another option.\n': '\n⚠ Le niveau gratuit Qwen OAuth a été abandonné le 2026-04-15. Veuillez sélectionner une autre option.\n', 'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models': diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js index a6ba3c4cb..f0a1e9846 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -972,6 +972,8 @@ export default { 'Qwen OAuth 無料枠は 2026-04-15 に終了しました。/auth を実行してプロバイダーを切り替えてください。', 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select Coding Plan or API Key instead.': 'Qwen OAuth 無料枠は 2026-04-15 に終了しました。Coding Plan または API Key を選択してください。', + 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider or run /auth to switch.': + 'Qwen OAuth無料プランは2026-04-15に終了しました。他のプロバイダーのモデルを選択するか、/authを実行して切り替えてください。', '\n⚠ Qwen OAuth free tier was discontinued on 2026-04-15. Please select another option.\n': '\n⚠ Qwen OAuth 無料枠は 2026-04-15 に終了しました。他のオプションを選択してください。\n', 'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models': diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js index 4bd728890..935db9f40 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -1257,6 +1257,8 @@ export default { 'O nível gratuito do Qwen OAuth foi descontinuado em 2026-04-15. Execute /auth para trocar de provedor.', 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select Coding Plan or API Key instead.': 'O nível gratuito do Qwen OAuth foi descontinuado em 2026-04-15. Selecione Coding Plan ou API Key.', + 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider or run /auth to switch.': + 'O nível gratuito do Qwen OAuth foi descontinuado em 2026-04-15. Por favor, selecione um modelo de outro provedor ou execute /auth para trocar.', '\n⚠ Qwen OAuth free tier was discontinued on 2026-04-15. Please select another option.\n': '\n⚠ O nível gratuito do Qwen OAuth foi descontinuado em 2026-04-15. Selecione outra opção.\n', 'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models': diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 02a3d234a..5ee2bb6dc 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -1181,6 +1181,8 @@ export default { 'Бесплатный уровень Qwen OAuth прекращён 2026-04-15. Выполните /auth для смены провайдера.', 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select Coding Plan or API Key instead.': 'Бесплатный уровень Qwen OAuth прекращён 2026-04-15. Выберите Coding Plan или API Key.', + 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider or run /auth to switch.': + 'Бесплатный уровень Qwen OAuth был прекращен 2026-04-15. Пожалуйста, выберите модель от другого провайдера или выполните /auth для переключения.', '\n⚠ Qwen OAuth free tier was discontinued on 2026-04-15. Please select another option.\n': '\n⚠ Бесплатный уровень Qwen OAuth прекращён 2026-04-15. Выберите другую опцию.\n', 'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models': diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 39eabbe1b..59c05bf1a 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -1233,6 +1233,8 @@ export default { 'Qwen OAuth 免费额度已于 2026-04-15 停用。请运行 /auth 切换服务商。', 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select Coding Plan or API Key instead.': 'Qwen OAuth 免费额度已于 2026-04-15 停用。请选择 Coding Plan 或 API Key。', + 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider or run /auth to switch.': + 'Qwen OAuth免费层已于2026-04-15停止服务。请选择其他提供商的模型或运行 /auth 切换。', '\n⚠ Qwen OAuth free tier was discontinued on 2026-04-15. Please select another option.\n': '\n⚠ Qwen OAuth 免费额度已于 2026-04-15 停用。请选择其他选项。\n', 'Paid \u00B7 Up to 6,000 requests/5 hrs \u00B7 All Alibaba Cloud Coding Plan Models': diff --git a/packages/cli/src/ui/components/ModelDialog.test.tsx b/packages/cli/src/ui/components/ModelDialog.test.tsx index dc5cc108a..d9b563302 100644 --- a/packages/cli/src/ui/components/ModelDialog.test.tsx +++ b/packages/cli/src/ui/components/ModelDialog.test.tsx @@ -192,8 +192,8 @@ describe('', () => { expect(mockedSelect).toHaveBeenCalledTimes(1); }); - it('calls config.switchModel and onClose when DescriptiveRadioButtonSelect.onSelect is triggered', async () => { - const { props, mockConfig, mockSettings } = renderComponent( + it('blocks qwen-oauth model selection with an error message (discontinued)', async () => { + const { props, mockConfig } = renderComponent( {}, { getAvailableModelsForAuthType: vi.fn((t: AuthType) => { @@ -214,25 +214,79 @@ describe('', () => { await childOnSelect(`${AuthType.QWEN_OAUTH}::${DEFAULT_QWEN_MODEL}`); - expect(mockConfig?.switchModel).toHaveBeenCalledWith( - AuthType.QWEN_OAUTH, - DEFAULT_QWEN_MODEL, + // qwen-oauth is discontinued — switchModel should NOT be called + expect(mockConfig?.switchModel).not.toHaveBeenCalled(); + // Dialog should NOT close (user stays in the dialog to see the error) + expect(props.onClose).not.toHaveBeenCalled(); + }); + + it('calls config.switchModel and onClose when selecting a non-OAuth model', async () => { + const switchModel = vi.fn().mockResolvedValue(undefined); + const getAuthType = vi.fn(() => AuthType.USE_OPENAI); + const getAvailableModelsForAuthType = vi.fn((t: AuthType) => { + if (t === AuthType.USE_OPENAI) { + return [{ id: 'gpt-4', label: 'GPT-4', authType: t }]; + } + if (t === AuthType.QWEN_OAUTH) { + return getFilteredQwenModels().map((m) => ({ + id: m.id, + label: m.label, + authType: AuthType.QWEN_OAUTH, + })); + } + return []; + }); + + const { props, mockSettings } = renderComponent({}, { + getModel: vi.fn(() => 'gpt-4'), + getAuthType, + switchModel, + getAvailableModelsForAuthType, + getAllConfiguredModels: vi.fn(() => [ + ...getFilteredQwenModels().map((m) => ({ + id: m.id, + label: m.label, + description: m.description || '', + authType: AuthType.QWEN_OAUTH, + })), + { + id: 'gpt-4', + label: 'GPT-4', + description: 'GPT-4 model', + authType: AuthType.USE_OPENAI, + }, + ]), + getContentGeneratorConfig: vi.fn(() => ({ + authType: AuthType.USE_OPENAI, + model: 'gpt-4', + })), + } as unknown as Partial); + + const childOnSelect = mockedSelect.mock.calls[0][0].onSelect; + expect(childOnSelect).toBeDefined(); + + // Select a non-OAuth model (USE_OPENAI) + await childOnSelect(`${AuthType.USE_OPENAI}::gpt-4`); + + expect(switchModel).toHaveBeenCalledWith( + AuthType.USE_OPENAI, + 'gpt-4', undefined, ); expect(mockSettings.setValue).toHaveBeenCalledWith( SettingScope.User, 'model.name', - DEFAULT_QWEN_MODEL, + 'gpt-4', ); expect(mockSettings.setValue).toHaveBeenCalledWith( SettingScope.User, 'security.auth.selectedType', - AuthType.QWEN_OAUTH, + AuthType.USE_OPENAI, ); expect(props.onClose).toHaveBeenCalledTimes(1); }); - it('calls config.switchModel and persists authType+model when selecting a different authType', async () => { + it('blocks switching to qwen-oauth from another authType (discontinued)', async () => { const switchModel = vi.fn().mockResolvedValue(undefined); const getAuthType = vi.fn(() => AuthType.USE_OPENAI); const getAvailableModelsForAuthType = vi.fn((t: AuthType) => { @@ -253,39 +307,25 @@ describe('', () => { getAuthType, getModel: vi.fn(() => 'gpt-4'), getContentGeneratorConfig: vi.fn(() => ({ - authType: AuthType.QWEN_OAUTH, - model: DEFAULT_QWEN_MODEL, + authType: AuthType.USE_OPENAI, + model: 'gpt-4', })), - // Add switchModel to the mock object (not the type) switchModel, getAvailableModelsForAuthType, }; - const { props, mockSettings } = renderComponent( + const { props } = renderComponent( {}, - // Cast to Config to bypass type checking, matching the runtime behavior mockConfigWithSwitchAuthType as unknown as Partial, ); const childOnSelect = mockedSelect.mock.calls[0][0].onSelect; await childOnSelect(`${AuthType.QWEN_OAUTH}::${DEFAULT_QWEN_MODEL}`); - expect(switchModel).toHaveBeenCalledWith( - AuthType.QWEN_OAUTH, - DEFAULT_QWEN_MODEL, - { requireCachedCredentials: true }, - ); - expect(mockSettings.setValue).toHaveBeenCalledWith( - SettingScope.User, - 'model.name', - DEFAULT_QWEN_MODEL, - ); - expect(mockSettings.setValue).toHaveBeenCalledWith( - SettingScope.User, - 'security.auth.selectedType', - AuthType.QWEN_OAUTH, - ); - expect(props.onClose).toHaveBeenCalledTimes(1); + // qwen-oauth is discontinued — switchModel should NOT be called + expect(switchModel).not.toHaveBeenCalled(); + // Dialog should NOT close + expect(props.onClose).not.toHaveBeenCalled(); }); it('passes onHighlight to DescriptiveRadioButtonSelect', () => { diff --git a/packages/cli/src/ui/components/ModelDialog.tsx b/packages/cli/src/ui/components/ModelDialog.tsx index e8ca53b59..a6488d5aa 100644 --- a/packages/cli/src/ui/components/ModelDialog.tsx +++ b/packages/cli/src/ui/components/ModelDialog.tsx @@ -213,11 +213,19 @@ export function ModelDialog({ const value = isRuntime && snapshotId ? snapshotId : `${t2}::${model.id}`; + const isQwenOAuth = t2 === AuthType.QWEN_OAUTH; + const title = ( [{t2}] @@ -225,16 +233,22 @@ export function ModelDialog({ {isRuntime && ( (Runtime) )} + {isQwenOAuth && !isRuntime && ( + ({t('Discontinued')}) + )} ); - // Include runtime indicator in description + // Include runtime / discontinued indicator in description let description = model.description || ''; if (isRuntime) { description = description ? `${description} (Runtime)` : 'Runtime model'; } + if (isQwenOAuth && !isRuntime) { + description = t('Discontinued — switch to Coding Plan or API Key'); + } return { value, @@ -323,6 +337,25 @@ export function ModelDialog({ return; } + // Block selection of discontinued qwen-oauth models + // (only block non-runtime OAuth; runtime OAuth models from existing + // cached tokens are still allowed to work until the server rejects them) + const isQwenOAuthSelection = + selected.startsWith(`${AuthType.QWEN_OAUTH}::`) || + (selected.startsWith('$runtime|') && + selected.split('|')[1] === AuthType.QWEN_OAUTH); + const isRuntimeOAuthSelection = selected.startsWith( + `$runtime|${AuthType.QWEN_OAUTH}|`, + ); + if (isQwenOAuthSelection && !isRuntimeOAuthSelection) { + setErrorMessage( + t( + 'Qwen OAuth free tier was discontinued on 2026-04-15. Please select a model from another provider or run /auth to switch.', + ), + ); + return; + } + let after: ContentGeneratorConfig | undefined; let effectiveAuthType: AuthType | undefined; let effectiveModelId = selected; @@ -461,6 +494,14 @@ export function ModelDialog({ borderRight={false} borderColor={theme.border.default} /> + {highlightedEntry.authType === AuthType.QWEN_OAUTH && + !highlightedEntry.isRuntime && ( + + + ⚠ {t('Discontinued — switch to Coding Plan or API Key')} + + + )}