diff --git a/packages/cli/src/constants/codingPlanTemplates.ts b/packages/cli/src/constants/codingPlanTemplates.ts index 99c034ecc..8cedab91f 100644 --- a/packages/cli/src/constants/codingPlanTemplates.ts +++ b/packages/cli/src/constants/codingPlanTemplates.ts @@ -23,15 +23,16 @@ export const CODING_PLAN_ENV_KEY = 'BAILIAN_CODING_PLAN_API_KEY'; export const CODING_PLAN_TEMPLATE: CodingPlanTemplate = [ { id: 'qwen3-coder-plus', - name: 'qwen3-coder-plur', + name: 'qwen3-coder-plus', baseUrl: 'https://coding.dashscope.aliyuncs.com/v1', - description: 'Qwen3 Coder Plus model from Bailian Coding Plan', + description: 'qwen3-coder-plus model from Bailian Coding Plan', envKey: CODING_PLAN_ENV_KEY, }, { id: 'qwen3-max-2026-01-23', name: 'qwen3-max-2026-01-23', - description: 'Qwen3 Max Thinking model from Bailian Coding Plan', + description: + 'qwen3 max model from Bailian Coding Plan with thinking enabled', baseUrl: 'https://coding.dashscope.aliyuncs.com/v1', envKey: CODING_PLAN_ENV_KEY, generationConfig: { diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 5466844d8..377d88511 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -946,6 +946,11 @@ export default { 'Terms of Services and Privacy Notice for Qwen Code': 'Nutzungsbedingungen und Datenschutzhinweis für Qwen Code', 'Qwen OAuth': 'Qwen OAuth', + 'Login with QwenChat account to use daily free quota.': + 'Melden Sie sich mit Ihrem QwenChat-Konto an, um das tägliche kostenlose Kontingent zu nutzen.', + 'API-KEY': 'API-KEY', + 'Use coding plan credentials or your own api-keys/providers.': + 'Verwenden Sie Coding Plan-Anmeldedaten oder Ihre eigenen API-Schlüssel/Anbieter.', OpenAI: 'OpenAI', 'Failed to login. Message: {{message}}': 'Anmeldung fehlgeschlagen. Meldung: {{message}}', @@ -1389,4 +1394,19 @@ export default { 'Verwenden Sie den /model-Befehl, um Ihr bevorzugtes Modell aus der konfigurierten Liste auszuwählen', 'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.': 'Unterstützte Authentifizierungstypen: openai, anthropic, gemini, vertex-ai, usw.', + + // ============================================================================ + // Auth Dialog - View Titles and Labels + // ============================================================================ + 'Coding Plan': 'Coding Plan', + "Paste your api key of Bailian Coding Plan and you're all set!": + 'Fügen Sie Ihren Bailian Coding Plan API-Schlüssel ein und Sie sind bereit!', + Custom: 'Benutzerdefiniert', + 'More instructions about configuring `modelProviders` manually.': + 'Weitere Anweisungen zur manuellen Konfiguration von `modelProviders`.', + 'Select API-KEY configuration mode:': + 'API-KEY-Konfigurationsmodus auswählen:', + '(Press Escape to go back)': '(Escape drücken zum Zurückgehen)', + '(Press Enter to submit, Escape to cancel)': + '(Enter zum Absenden, Escape zum Abbrechen)', }; diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index 78f57abce..9d27a84e9 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -937,6 +937,11 @@ export default { 'Terms of Services and Privacy Notice for Qwen Code': 'Terms of Services and Privacy Notice for Qwen Code', 'Qwen OAuth': 'Qwen OAuth', + 'Login with QwenChat account to use daily free quota.': + 'Login with QwenChat account to use daily free quota.', + 'API-KEY': 'API-KEY', + 'Use coding plan credentials or your own api-keys/providers.': + 'Use coding plan credentials or your own api-keys/providers.', OpenAI: 'OpenAI', 'Failed to login. Message: {{message}}': 'Failed to login. Message: {{message}}', @@ -1392,9 +1397,12 @@ export default { // ============================================================================ // Auth Dialog - View Titles and Labels // ============================================================================ - 'API-KEY': 'API-KEY', 'Coding Plan': 'Coding Plan', + "Paste your api key of Bailian Coding Plan and you're all set!": + "Paste your api key of Bailian Coding Plan and you're all set!", Custom: 'Custom', + 'More instructions about configuring `modelProviders` manually.': + 'More instructions about configuring `modelProviders` manually.', 'Select API-KEY configuration mode:': 'Select API-KEY configuration mode:', '(Press Escape to go back)': '(Press Escape to go back)', '(Press Enter to submit, Escape to cancel)': diff --git a/packages/cli/src/i18n/locales/ja.js b/packages/cli/src/i18n/locales/ja.js index e2c7306b7..6a4f728e9 100644 --- a/packages/cli/src/i18n/locales/ja.js +++ b/packages/cli/src/i18n/locales/ja.js @@ -679,6 +679,11 @@ export default { 'Terms of Services and Privacy Notice for Qwen Code': 'Qwen Code の利用規約とプライバシー通知', 'Qwen OAuth': 'Qwen OAuth', + 'Login with QwenChat account to use daily free quota.': + 'QwenChatアカウントでログインして、毎日の無料クォータをご利用ください。', + 'API-KEY': 'API-KEY', + 'Use coding plan credentials or your own api-keys/providers.': + 'Coding Planの認証情報またはご自身のAPIキー/プロバイダーをご利用ください。', OpenAI: 'OpenAI', 'Failed to login. Message: {{message}}': 'ログインに失敗しました。メッセージ: {{message}}', @@ -900,4 +905,18 @@ export default { '/model コマンドを使用して、設定済みリストからお好みのモデルを選択してください', 'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.': 'サポートされている認証タイプ:openai、anthropic、gemini、vertex-ai など', + + // ============================================================================ + // Auth Dialog - View Titles and Labels + // ============================================================================ + 'Coding Plan': 'Coding Plan', + "Paste your api key of Bailian Coding Plan and you're all set!": + 'Bailian Coding PlanのAPIキーを貼り付けるだけで準備完了です!', + Custom: 'カスタム', + 'More instructions about configuring `modelProviders` manually.': + '`modelProviders`を手動で設定する方法の詳細はこちら。', + 'Select API-KEY configuration mode:': 'API-KEY設定モードを選択してください:', + '(Press Escape to go back)': '(Escapeキーで戻る)', + '(Press Enter to submit, Escape to cancel)': + '(Enterで送信、Escapeでキャンセル)', }; diff --git a/packages/cli/src/i18n/locales/pt.js b/packages/cli/src/i18n/locales/pt.js index 3e0089fa1..96facbe6d 100644 --- a/packages/cli/src/i18n/locales/pt.js +++ b/packages/cli/src/i18n/locales/pt.js @@ -958,6 +958,11 @@ export default { 'Terms of Services and Privacy Notice for Qwen Code': 'Termos de Serviço e Aviso de Privacidade do Qwen Code', 'Qwen OAuth': 'Qwen OAuth', + 'Login with QwenChat account to use daily free quota.': + 'Faça login com sua conta QwenChat para usar a cota gratuita diária.', + 'API-KEY': 'API-KEY', + 'Use coding plan credentials or your own api-keys/providers.': + 'Use credenciais do Coding Plan ou suas próprias chaves API/provedores.', OpenAI: 'OpenAI', 'Failed to login. Message: {{message}}': 'Falha ao fazer login. Mensagem: {{message}}', @@ -1403,4 +1408,19 @@ export default { 'Use o comando /model para selecionar seu modelo preferido da lista configurada', 'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.': 'Tipos de autenticação suportados: openai, anthropic, gemini, vertex-ai, etc.', + + // ============================================================================ + // Auth Dialog - View Titles and Labels + // ============================================================================ + 'Coding Plan': 'Coding Plan', + "Paste your api key of Bailian Coding Plan and you're all set!": + 'Cole sua chave de API do Bailian Coding Plan e pronto!', + Custom: 'Personalizado', + 'More instructions about configuring `modelProviders` manually.': + 'Mais instruções sobre como configurar `modelProviders` manualmente.', + 'Select API-KEY configuration mode:': + 'Selecione o modo de configuração da API-KEY:', + '(Press Escape to go back)': '(Pressione Escape para voltar)', + '(Press Enter to submit, Escape to cancel)': + '(Pressione Enter para enviar, Escape para cancelar)', }; diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 4bf570351..f783b98b5 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -952,6 +952,11 @@ export default { 'Terms of Services and Privacy Notice for Qwen Code': 'Условия обслуживания и уведомление о конфиденциальности для Qwen Code', 'Qwen OAuth': 'Qwen OAuth', + 'Login with QwenChat account to use daily free quota.': + 'Войдите с помощью аккаунта QwenChat, чтобы использовать ежедневную бесплатную квоту.', + 'API-KEY': 'API-KEY', + 'Use coding plan credentials or your own api-keys/providers.': + 'Используйте учетные данные Coding Plan или свои собственные API-ключи/провайдеры.', OpenAI: 'OpenAI', 'Failed to login. Message: {{message}}': 'Не удалось войти. Сообщение: {{message}}', @@ -1393,4 +1398,18 @@ export default { 'Используйте команду /model, чтобы выбрать предпочитаемую модель из настроенного списка', 'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.': 'Поддерживаемые типы аутентификации: openai, anthropic, gemini, vertex-ai и др.', + + // ============================================================================ + // Auth Dialog - View Titles and Labels + // ============================================================================ + 'Coding Plan': 'Coding Plan', + "Paste your api key of Bailian Coding Plan and you're all set!": + 'Вставьте ваш API-ключ Bailian Coding Plan и всё готово!', + Custom: 'Пользовательский', + 'More instructions about configuring `modelProviders` manually.': + 'Дополнительные инструкции по ручной настройке `modelProviders`.', + 'Select API-KEY configuration mode:': 'Выберите режим конфигурации API-KEY:', + '(Press Escape to go back)': '(Нажмите Escape для возврата)', + '(Press Enter to submit, Escape to cancel)': + '(Нажмите Enter для отправки, Escape для отмены)', }; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index e314d5129..657919326 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -886,6 +886,10 @@ export default { 'Terms of Services and Privacy Notice for Qwen Code': 'Qwen Code 的服务条款和隐私声明', 'Qwen OAuth': 'Qwen OAuth (免费)', + 'Login with QwenChat account to use daily free quota.': + '使用 QwenChat 账号登录,享受每日免费额度。', + 'Use coding plan credentials or your own api-keys/providers.': + '使用 Coding Plan 凭证或您自己的 API 密钥/提供商。', OpenAI: 'OpenAI', 'Failed to login. Message: {{message}}': '登录失败。消息:{{message}}', 'Authentication is enforced to be {{enforcedType}}, but you are currently using {{currentType}}.': @@ -1230,7 +1234,11 @@ export default { // ============================================================================ 'API-KEY': 'API-KEY', 'Coding Plan': 'Coding Plan', + "Paste your api key of Bailian Coding Plan and you're all set!": + '粘贴您的百炼 Coding Plan API Key,即可完成设置!', Custom: '自定义', + 'More instructions about configuring `modelProviders` manually.': + '关于手动配置 `modelProviders` 的更多说明。', 'Select API-KEY configuration mode:': '选择 API-KEY 配置模式:', '(Press Escape to go back)': '(按 Escape 键返回)', '(Press Enter to submit, Escape to cancel)': '(按 Enter 提交,Escape 取消)', diff --git a/packages/cli/src/ui/auth/AuthDialog.tsx b/packages/cli/src/ui/auth/AuthDialog.tsx index f06ba360c..32d3af88f 100644 --- a/packages/cli/src/ui/auth/AuthDialog.tsx +++ b/packages/cli/src/ui/auth/AuthDialog.tsx @@ -203,6 +203,13 @@ export function AuthDialog(): React.JSX.Element { }} /> + + + {currentSelectedAuthType === AuthType.QWEN_OAUTH + ? t('Login with QwenChat account to use daily free quota.') + : t('Use coding plan credentials or your own api-keys/providers.')} + + ); @@ -225,6 +232,15 @@ export function AuthDialog(): React.JSX.Element { }} /> + + + {apiKeySubItems[apiKeySubModeIndex]?.value === 'coding-plan' + ? t("Paste your api key of Bailian Coding Plan and you're all set!") + : t( + 'More instructions about configuring `modelProviders` manually.', + )} + + {t('(Press Escape to go back)')} diff --git a/packages/core/src/models/modelRegistry.test.ts b/packages/core/src/models/modelRegistry.test.ts index 3a5993603..01ccc8207 100644 --- a/packages/core/src/models/modelRegistry.test.ts +++ b/packages/core/src/models/modelRegistry.test.ts @@ -320,4 +320,215 @@ describe('ModelRegistry', () => { expect(models.find((m) => m.id === 'invalid-model')).toBeUndefined(); }); }); + + describe('duplicate model id handling', () => { + it('should skip duplicate model ids and use first registered config', () => { + const registry = new ModelRegistry({ + openai: [ + { id: 'gpt-4', name: 'GPT-4 First', description: 'First config' }, + { id: 'gpt-4', name: 'GPT-4 Second', description: 'Second config' }, + { id: 'gpt-3.5', name: 'GPT-3.5' }, + ], + }); + + const models = registry.getModelsForAuthType(AuthType.USE_OPENAI); + expect(models.length).toBe(2); + + const gpt4 = registry.getModel(AuthType.USE_OPENAI, 'gpt-4'); + expect(gpt4).toBeDefined(); + expect(gpt4?.name).toBe('GPT-4 First'); + expect(gpt4?.description).toBe('First config'); + }); + + it('should handle multiple duplicate ids in same authType', () => { + const registry = new ModelRegistry({ + openai: [ + { id: 'model-a', name: 'Model A First' }, + { id: 'model-a', name: 'Model A Second' }, + { id: 'model-b', name: 'Model B First' }, + { id: 'model-b', name: 'Model B Second' }, + { id: 'model-c', name: 'Model C' }, + ], + }); + + const models = registry.getModelsForAuthType(AuthType.USE_OPENAI); + expect(models.length).toBe(3); + + expect(registry.getModel(AuthType.USE_OPENAI, 'model-a')?.name).toBe( + 'Model A First', + ); + expect(registry.getModel(AuthType.USE_OPENAI, 'model-b')?.name).toBe( + 'Model B First', + ); + expect(registry.getModel(AuthType.USE_OPENAI, 'model-c')?.name).toBe( + 'Model C', + ); + }); + + it('should treat same id in different authTypes as different models', () => { + const registry = new ModelRegistry({ + openai: [{ id: 'shared-model', name: 'OpenAI Shared' }], + gemini: [{ id: 'shared-model', name: 'Gemini Shared' }], + }); + + const openaiModel = registry.getModel( + AuthType.USE_OPENAI, + 'shared-model', + ); + const geminiModel = registry.getModel( + AuthType.USE_GEMINI, + 'shared-model', + ); + + expect(openaiModel?.name).toBe('OpenAI Shared'); + expect(geminiModel?.name).toBe('Gemini Shared'); + }); + }); + + describe('reloadModels', () => { + it('should reload models from new config', () => { + const registry = new ModelRegistry({ + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + }); + + expect(registry.getModelsForAuthType(AuthType.USE_OPENAI).length).toBe(1); + expect(registry.getModel(AuthType.USE_OPENAI, 'gpt-4')).toBeDefined(); + expect(registry.getModel(AuthType.USE_OPENAI, 'gpt-3.5')).toBeUndefined(); + + registry.reloadModels({ + openai: [{ id: 'gpt-3.5', name: 'GPT-3.5' }], + }); + + // After reload, only new models should exist + expect(registry.getModelsForAuthType(AuthType.USE_OPENAI).length).toBe(1); + expect(registry.getModel(AuthType.USE_OPENAI, 'gpt-4')).toBeUndefined(); + expect(registry.getModel(AuthType.USE_OPENAI, 'gpt-3.5')).toBeDefined(); + }); + + it('should preserve hard-coded qwen-oauth models after reload', () => { + const registry = new ModelRegistry({ + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + }); + + expect(registry.getModelsForAuthType(AuthType.QWEN_OAUTH).length).toBe( + QWEN_OAUTH_MODELS.length, + ); + + registry.reloadModels({ + openai: [{ id: 'gpt-3.5', name: 'GPT-3.5' }], + }); + + // qwen-oauth models should still exist + expect(registry.getModelsForAuthType(AuthType.QWEN_OAUTH).length).toBe( + QWEN_OAUTH_MODELS.length, + ); + expect( + registry.getModel(AuthType.QWEN_OAUTH, 'coder-model'), + ).toBeDefined(); + }); + + it('should clear user-configured models when reload with empty config', () => { + const registry = new ModelRegistry({ + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + gemini: [{ id: 'gemini-pro', name: 'Gemini Pro' }], + }); + + expect(registry.getModelsForAuthType(AuthType.USE_OPENAI).length).toBe(1); + expect(registry.getModelsForAuthType(AuthType.USE_GEMINI).length).toBe(1); + + registry.reloadModels({}); + + // All user-configured models should be cleared + expect(registry.getModelsForAuthType(AuthType.USE_OPENAI).length).toBe(0); + expect(registry.getModelsForAuthType(AuthType.USE_GEMINI).length).toBe(0); + + // qwen-oauth models should still exist + expect(registry.getModelsForAuthType(AuthType.QWEN_OAUTH).length).toBe( + QWEN_OAUTH_MODELS.length, + ); + }); + + it('should ignore qwen-oauth models in reload config', () => { + const registry = new ModelRegistry(); + + registry.reloadModels({ + 'qwen-oauth': [{ id: 'custom-qwen', name: 'Custom Qwen' }], + }); + + // qwen-oauth should still use hard-coded models + const qwenModels = registry.getModelsForAuthType(AuthType.QWEN_OAUTH); + expect(qwenModels.length).toBe(QWEN_OAUTH_MODELS.length); + expect(qwenModels.find((m) => m.id === 'custom-qwen')).toBeUndefined(); + }); + + it('should handle reload with multiple authTypes', () => { + const registry = new ModelRegistry({ + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + }); + + registry.reloadModels({ + openai: [ + { id: 'gpt-4', name: 'GPT-4 Updated' }, + { id: 'gpt-3.5', name: 'GPT-3.5' }, + ], + gemini: [{ id: 'gemini-pro', name: 'Gemini Pro' }], + }); + + const openaiModels = registry.getModelsForAuthType(AuthType.USE_OPENAI); + expect(openaiModels.length).toBe(2); + expect(registry.getModel(AuthType.USE_OPENAI, 'gpt-4')?.name).toBe( + 'GPT-4 Updated', + ); + + const geminiModels = registry.getModelsForAuthType(AuthType.USE_GEMINI); + expect(geminiModels.length).toBe(1); + }); + + it('should skip invalid authType keys during reload', () => { + const registry = new ModelRegistry({ + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + }); + + registry.reloadModels({ + openai: [{ id: 'gpt-3.5', name: 'GPT-3.5' }], + 'invalid-key': [{ id: 'invalid-model', name: 'Invalid Model' }], + } as unknown as ModelProvidersConfig); + + const openaiModels = registry.getModelsForAuthType(AuthType.USE_OPENAI); + expect(openaiModels.length).toBe(1); + expect(registry.getModel(AuthType.USE_OPENAI, 'gpt-3.5')).toBeDefined(); + }); + + it('should handle reload with undefined config', () => { + const registry = new ModelRegistry({ + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + }); + + registry.reloadModels(undefined); + + // All user-configured models should be cleared + expect(registry.getModelsForAuthType(AuthType.USE_OPENAI).length).toBe(0); + // qwen-oauth models should still exist + expect(registry.getModelsForAuthType(AuthType.QWEN_OAUTH).length).toBe( + QWEN_OAUTH_MODELS.length, + ); + }); + + it('should apply duplicate model id handling during reload', () => { + const registry = new ModelRegistry(); + + registry.reloadModels({ + openai: [ + { id: 'model-a', name: 'Model A First' }, + { id: 'model-a', name: 'Model A Second' }, + ], + }); + + const models = registry.getModelsForAuthType(AuthType.USE_OPENAI); + expect(models.length).toBe(1); + expect(registry.getModel(AuthType.USE_OPENAI, 'model-a')?.name).toBe( + 'Model A First', + ); + }); + }); }); diff --git a/packages/core/src/models/modelsConfig.test.ts b/packages/core/src/models/modelsConfig.test.ts index d0bb62f03..03a724829 100644 --- a/packages/core/src/models/modelsConfig.test.ts +++ b/packages/core/src/models/modelsConfig.test.ts @@ -1297,4 +1297,213 @@ describe('ModelsConfig', () => { }); }); }); + + describe('reloadModelProvidersConfig', () => { + it('should reload model providers configuration', async () => { + const modelsConfig = new ModelsConfig({ + initialAuthType: AuthType.USE_OPENAI, + modelProvidersConfig: { + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + }, + }); + + // Verify initial model + await modelsConfig.switchModel(AuthType.USE_OPENAI, 'gpt-4'); + expect(modelsConfig.getModel()).toBe('gpt-4'); + + // Reload with new config + modelsConfig.reloadModelProvidersConfig({ + openai: [{ id: 'gpt-3.5', name: 'GPT-3.5' }], + }); + + // After reload, old model should not exist + expect( + modelsConfig.getAllConfiguredModels().find((m) => m.id === 'gpt-4'), + ).toBeUndefined(); + expect( + modelsConfig.getAllConfiguredModels().find((m) => m.id === 'gpt-3.5'), + ).toBeDefined(); + }); + + it('should preserve current model selection if still available after reload', async () => { + const modelsConfig = new ModelsConfig({ + initialAuthType: AuthType.USE_OPENAI, + modelProvidersConfig: { + openai: [ + { id: 'gpt-4', name: 'GPT-4' }, + { id: 'gpt-3.5', name: 'GPT-3.5' }, + ], + }, + }); + + await modelsConfig.switchModel(AuthType.USE_OPENAI, 'gpt-4'); + expect(modelsConfig.getModel()).toBe('gpt-4'); + + // Reload with config that still includes gpt-4 + modelsConfig.reloadModelProvidersConfig({ + openai: [ + { id: 'gpt-4', name: 'GPT-4 Updated' }, + { id: 'new-model', name: 'New Model' }, + ], + }); + + // Current model should still be available + const availableModels = modelsConfig.getAllConfiguredModels(); + expect(availableModels.find((m) => m.id === 'gpt-4')).toBeDefined(); + expect(availableModels.find((m) => m.id === 'new-model')).toBeDefined(); + }); + + it('should update available models after reload', async () => { + const modelsConfig = new ModelsConfig({ + initialAuthType: AuthType.USE_OPENAI, + modelProvidersConfig: { + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + }, + }); + + const initialModels = modelsConfig.getAllConfiguredModels(); + expect(initialModels.some((m) => m.id === 'gpt-4')).toBe(true); + expect(initialModels.some((m) => m.id === 'gemini-pro')).toBe(false); + + // Reload with different config + modelsConfig.reloadModelProvidersConfig({ + openai: [{ id: 'gpt-3.5', name: 'GPT-3.5' }], + gemini: [{ id: 'gemini-pro', name: 'Gemini Pro' }], + }); + + const updatedModels = modelsConfig.getAllConfiguredModels(); + expect(updatedModels.some((m) => m.id === 'gpt-4')).toBe(false); + expect(updatedModels.some((m) => m.id === 'gpt-3.5')).toBe(true); + expect(updatedModels.some((m) => m.id === 'gemini-pro')).toBe(true); + }); + + it('should handle reload with empty config', async () => { + const modelsConfig = new ModelsConfig({ + initialAuthType: AuthType.USE_OPENAI, + modelProvidersConfig: { + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + gemini: [{ id: 'gemini-pro', name: 'Gemini Pro' }], + }, + }); + + expect( + modelsConfig + .getAllConfiguredModels() + .filter((m) => m.authType !== 'qwen-oauth').length, + ).toBeGreaterThan(0); + + // Reload with empty config + modelsConfig.reloadModelProvidersConfig({}); + + // Only qwen-oauth models should remain + const models = modelsConfig.getAllConfiguredModels(); + expect(models.every((m) => m.authType === 'qwen-oauth')).toBe(true); + }); + + it('should preserve qwen-oauth models after reload', () => { + const modelsConfig = new ModelsConfig({ + modelProvidersConfig: { + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + }, + }); + + const initialQwenModels = modelsConfig + .getAllConfiguredModels() + .filter((m) => m.authType === 'qwen-oauth'); + + modelsConfig.reloadModelProvidersConfig({ + gemini: [{ id: 'gemini-pro', name: 'Gemini Pro' }], + }); + + // qwen-oauth models should still exist + const qwenModelsAfterReload = modelsConfig + .getAllConfiguredModels() + .filter((m) => m.authType === 'qwen-oauth'); + expect(qwenModelsAfterReload.length).toBe(initialQwenModels.length); + }); + + it('should handle reload with undefined config', () => { + const modelsConfig = new ModelsConfig({ + modelProvidersConfig: { + openai: [{ id: 'gpt-4', name: 'GPT-4' }], + }, + }); + + expect( + modelsConfig + .getAllConfiguredModels() + .filter((m) => m.authType === 'openai').length, + ).toBeGreaterThan(0); + + modelsConfig.reloadModelProvidersConfig(undefined); + + // User-configured models should be cleared + expect( + modelsConfig + .getAllConfiguredModels() + .filter((m) => m.authType === 'openai').length, + ).toBe(0); + }); + + it('should support multiple reloads', () => { + const modelsConfig = new ModelsConfig(); + + // First reload + modelsConfig.reloadModelProvidersConfig({ + openai: [{ id: 'model-v1', name: 'Model V1' }], + }); + expect( + modelsConfig.getAllConfiguredModels().some((m) => m.id === 'model-v1'), + ).toBe(true); + + // Second reload + modelsConfig.reloadModelProvidersConfig({ + openai: [{ id: 'model-v2', name: 'Model V2' }], + }); + expect( + modelsConfig.getAllConfiguredModels().some((m) => m.id === 'model-v1'), + ).toBe(false); + expect( + modelsConfig.getAllConfiguredModels().some((m) => m.id === 'model-v2'), + ).toBe(true); + + // Third reload with empty config + modelsConfig.reloadModelProvidersConfig({}); + expect( + modelsConfig.getAllConfiguredModels().some((m) => m.id === 'model-v2'), + ).toBe(false); + }); + + it('should handle complex multi-authType reload', async () => { + const modelsConfig = new ModelsConfig({ + initialAuthType: AuthType.USE_OPENAI, + modelProvidersConfig: { + openai: [ + { id: 'gpt-4', name: 'GPT-4' }, + { id: 'gpt-3.5', name: 'GPT-3.5' }, + ], + gemini: [{ id: 'gemini-pro', name: 'Gemini Pro' }], + }, + }); + + // Reload with completely different config + modelsConfig.reloadModelProvidersConfig({ + openai: [{ id: 'new-openai', name: 'New OpenAI' }], + anthropic: [{ id: 'claude', name: 'Claude' }], + gemini: [{ id: 'gemini-ultra', name: 'Gemini Ultra' }], + }); + + const allModels = modelsConfig.getAllConfiguredModels(); + + // Old models should be gone + expect(allModels.some((m) => m.id === 'gpt-4')).toBe(false); + expect(allModels.some((m) => m.id === 'gpt-3.5')).toBe(false); + expect(allModels.some((m) => m.id === 'gemini-pro')).toBe(false); + + // New models should exist + expect(allModels.some((m) => m.id === 'new-openai')).toBe(true); + expect(allModels.some((m) => m.id === 'claude')).toBe(true); + expect(allModels.some((m) => m.id === 'gemini-ultra')).toBe(true); + }); + }); });