diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index 91f4569ac..ac26caad9 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -1001,9 +1001,43 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { return this.config.maxConcurrentStreams } - async check(providerId: string): Promise<{ isOk: boolean; errorMsg: string | null }> { - const provider = this.getProviderInstance(providerId) - return provider.check() + async check( + providerId: string, + modelId?: string + ): Promise<{ isOk: boolean; errorMsg: string | null }> { + try { + const provider = this.getProviderInstance(providerId) + + // 如果提供了modelId,使用completions方法进行测试 + if (modelId) { + try { + const testMessage = [{ role: 'user' as const, content: 'hi' }] + const response: LLMResponse | null = await Promise.race([ + provider.completions(testMessage, modelId, 0.1, 10), + new Promise((resolve) => setTimeout(() => resolve(null), 60000)) + ]) + // 检查响应是否有效 + if ( + response && + (response.content || response.content === '' || response.reasoning_content) + ) { + return { isOk: true, errorMsg: null } + } else { + return { isOk: false, errorMsg: 'Model response is invalid' } + } + } catch (error) { + console.error(`Model ${modelId} check failed:`, error) + const errorMessage = error instanceof Error ? error.message : String(error) + return { isOk: false, errorMsg: `Model test failed: ${errorMessage}` } + } + } else { + return { isOk: false, errorMsg: 'Model ID is required' } + } + } catch (error) { + console.error(`Provider ${providerId} check failed:`, error) + const errorMessage = error instanceof Error ? error.message : String(error) + return { isOk: false, errorMsg: `Provider check failed: ${errorMessage}` } + } } async getKeyStatus(providerId: string): Promise { diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue index 0a5954439..280ae61ff 100644 --- a/src/renderer/src/App.vue +++ b/src/renderer/src/App.vue @@ -13,6 +13,8 @@ import { useSettingsStore } from '@/stores/settings' import { useThemeStore } from '@/stores/theme' import { useLanguageStore } from '@/stores/language' import TranslatePopup from '@/components/popup/TranslatePopup.vue' +import ModelCheckDialog from '@/components/settings/ModelCheckDialog.vue' +import { useModelCheckStore } from '@/stores/modelCheck' const route = useRoute() const configPresenter = usePresenter('configPresenter') @@ -22,6 +24,7 @@ const { toast } = useToast() const settingsStore = useSettingsStore() const themeStore = useThemeStore() const langStore = useLanguageStore() +const modelCheckStore = useModelCheckStore() // 错误通知队列及当前正在显示的错误 const errorQueue = ref>([]) const currentErrorId = ref(null) @@ -298,5 +301,15 @@ onBeforeUnmount(() => { + + diff --git a/src/renderer/src/components/settings/ModelCheckDialog.vue b/src/renderer/src/components/settings/ModelCheckDialog.vue new file mode 100644 index 000000000..6a2d4ac0a --- /dev/null +++ b/src/renderer/src/components/settings/ModelCheckDialog.vue @@ -0,0 +1,206 @@ + + + diff --git a/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue b/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue index 761768df9..a7ba3199f 100644 --- a/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue +++ b/src/renderer/src/components/settings/ModelProviderSettingsDetail.vue @@ -8,7 +8,7 @@ :provider-websites="providerWebsites" @api-host-change="handleApiHostChange" @api-key-change="handleApiKeyChange" - @validate-key="handleApiKeyEnter" + @validate-key="openModelCheckDialog" @delete-provider="showDeleteProviderDialog = true" @oauth-success="handleOAuthSuccess" @oauth-error="handleOAuthError" @@ -73,6 +73,7 @@ import AzureProviderConfig from './AzureProviderConfig.vue' import GeminiSafetyConfig from './GeminiSafetyConfig.vue' import ProviderModelManager from './ProviderModelManager.vue' import ProviderDialogContainer from './ProviderDialogContainer.vue' +import { useModelCheckStore } from '@/stores/modelCheck' import { levelToValueMap, safetyCategories } from '@/lib/gemini' interface ProviderWebsites { @@ -105,6 +106,7 @@ const props = defineProps<{ }>() const settingsStore = useSettingsStore() +const modelCheckStore = useModelCheckStore() const apiKey = ref(props.provider.apiKey || '') const apiHost = ref(props.provider.baseUrl || '') const azureApiVersion = ref('') @@ -227,14 +229,6 @@ watch( { immediate: true } // Removed deep: true as provider object itself changes ) -const handleApiKeyEnter = async (value: string) => { - const inputElement = document.getElementById(`${props.provider.id}-apikey`) - if (inputElement) { - inputElement.blur() - } - await settingsStore.updateProviderApi(props.provider.id, value, undefined) - await validateApiKey() -} const handleApiKeyChange = async (value: string) => { await settingsStore.updateProviderApi(props.provider.id, value, undefined) } @@ -350,4 +344,8 @@ const handleConfigChanged = async () => { // 模型配置变更后重新初始化数据 await initData() } + +const openModelCheckDialog = () => { + modelCheckStore.openDialog(props.provider.id) +} diff --git a/src/renderer/src/components/settings/OllamaProviderSettingsDetail.vue b/src/renderer/src/components/settings/OllamaProviderSettingsDetail.vue index 72f4f7fca..f32a8692e 100644 --- a/src/renderer/src/components/settings/OllamaProviderSettingsDetail.vue +++ b/src/renderer/src/components/settings/OllamaProviderSettingsDetail.vue @@ -36,7 +36,7 @@ variant="outline" size="xs" class="text-xs text-normal rounded-lg" - @click="validateApiKey" + @click="openModelCheckDialog" > {{ t('settings.provider.verifyKey') }} @@ -244,6 +244,7 @@ import { DialogFooter } from '@/components/ui/dialog' import { useSettingsStore } from '@/stores/settings' +import { useModelCheckStore } from '@/stores/modelCheck' import type { LLM_PROVIDER } from '@shared/presenter' const { t } = useI18n() @@ -253,6 +254,7 @@ const props = defineProps<{ }>() const settingsStore = useSettingsStore() +const modelCheckStore = useModelCheckStore() const apiHost = ref(props.provider.baseUrl || '') const apiKey = ref(props.provider.apiKey || '') const showPullModelDialog = ref(false) @@ -521,6 +523,10 @@ const validateApiKey = async () => { } } +const openModelCheckDialog = () => { + modelCheckStore.openDialog(props.provider.id) +} + // 监听 provider 变化 watch( () => props.provider, diff --git a/src/renderer/src/components/settings/ProviderApiConfig.vue b/src/renderer/src/components/settings/ProviderApiConfig.vue index f757079ce..6909b093e 100644 --- a/src/renderer/src/components/settings/ProviderApiConfig.vue +++ b/src/renderer/src/components/settings/ProviderApiConfig.vue @@ -56,7 +56,7 @@ variant="outline" size="xs" class="text-xs text-normal rounded-lg" - @click="$emit('validate-key', apiKey)" + @click="openModelCheckDialog" > {{ t('settings.provider.verifyKey') @@ -113,6 +113,7 @@ import { Button } from '@/components/ui/button' import { Icon } from '@iconify/vue' import GitHubCopilotOAuth from './GitHubCopilotOAuth.vue' import { usePresenter } from '@/composables/usePresenter' +import { useModelCheckStore } from '@/stores/modelCheck' import type { LLM_PROVIDER, KeyStatus } from '@shared/presenter' interface ProviderWebsites { @@ -125,6 +126,7 @@ interface ProviderWebsites { const { t } = useI18n() const llmProviderPresenter = usePresenter('llmproviderPresenter') +const modelCheckStore = useModelCheckStore() const props = defineProps<{ provider: LLM_PROVIDER @@ -176,6 +178,10 @@ const handleOAuthError = (error: string) => { emit('oauth-error', error) } +const openModelCheckDialog = () => { + modelCheckStore.openDialog(props.provider.id) +} + const getKeyStatus = async () => { if ( ['ppio', 'openrouter', 'siliconcloud', 'silicon', 'deepseek', '302ai'].includes( diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index e849600b3..233c0ece4 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -224,6 +224,17 @@ "pullModel": { "title": "Pull Model", "pull": "Pull" + }, + "modelCheck": { + "title": "Model Check", + "description": "Select a model to test connectivity and availability", + "model": "Select Model", + "modelPlaceholder": "Please select a model to test", + "test": "Start Test", + "checking": "Testing...", + "success": "Model test successful", + "failed": "Model test failed", + "noModels": "No available models for this provider" } }, "pullModels": "Pull Models", diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index 666c414a3..662cde6d3 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -224,6 +224,17 @@ "pullModel": { "title": "دریافت مدل", "pull": "دریافت" + }, + "modelCheck": { + "checking": "تست ...", + "description": "مدلی را برای تست اتصال و قابلیت استفاده انتخاب کنید", + "failed": "آزمون مدل انجام نشد", + "model": "یک مدل را انتخاب کنید", + "modelPlaceholder": "لطفاً مدل را برای آزمایش انتخاب کنید", + "noModels": "هیچ مدلی برای این ارائه دهنده خدمات در دسترس نیست", + "success": "تست مدل موفق شد", + "test": "آزمون را شروع کنید", + "title": "بررسی مدل" } }, "pullModels": "دریافت مدل‌ها", diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index 01116252b..212e11a82 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -224,6 +224,17 @@ "pullModel": { "title": "Récupérer le modèle", "pull": "Récupérer" + }, + "modelCheck": { + "checking": "Essai...", + "description": "Sélectionnez un modèle pour les tests de connectivité et d'utilisation", + "failed": "Échec du test du modèle", + "model": "Sélectionnez un modèle", + "modelPlaceholder": "Veuillez sélectionner le modèle à tester", + "noModels": "Il n'y a aucun modèle disponible pour ce fournisseur de services", + "success": "Le test du modèle a réussi", + "test": "Commencer le test", + "title": "Chèque de modèle" } }, "pullModels": "Récupérer les modèles", diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index ce39e1df7..3b15f62b6 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -224,6 +224,17 @@ "pullModel": { "title": "モデルを取得", "pull": "取得" + }, + "modelCheck": { + "checking": "テスト...", + "description": "接続性とユーザビリティテストのモデルを選択します", + "failed": "モデルテストに失敗しました", + "model": "モデルを選択します", + "modelPlaceholder": "テストするモデルを選択してください", + "noModels": "このサービスプロバイダーが利用できるモデルはありません", + "success": "モデルテストが成功しました", + "test": "テストを開始します", + "title": "モデルチェック" } }, "pullModels": "モデルを取得", diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index 97b4d7a91..dcb9b7877 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -223,6 +223,17 @@ "pullModel": { "title": "모델 가져오기", "pull": "가져오기" + }, + "modelCheck": { + "checking": "테스트 ...", + "description": "연결 및 유용성 테스트 모델을 선택하십시오", + "failed": "모델 테스트가 실패했습니다", + "model": "모델을 선택하십시오", + "modelPlaceholder": "테스트 할 모델을 선택하십시오", + "noModels": "이 서비스 제공 업체에는 사용할 수있는 모델이 없습니다", + "success": "모델 테스트가 성공했습니다", + "test": "테스트를 시작하십시오", + "title": "모델 점검" } }, "pullModels": "모델 가져오기", diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index 02c9b7bc8..98e08acd2 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -223,6 +223,17 @@ "pullModel": { "title": "Скачивание модели", "pull": "Скачать" + }, + "modelCheck": { + "checking": "Тестирование ...", + "description": "Выберите модель для подключения и удобства использования", + "failed": "Тест модели не удался", + "model": "Выберите модель", + "modelPlaceholder": "Пожалуйста, выберите модель для тестирования", + "noModels": "Для этого поставщика услуг нет модели", + "success": "Модельный тест преуспел", + "test": "Начните тест", + "title": "Модель проверка" } }, "pullModels": "Скачать модели", diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 629298b56..f6b54c62f 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -224,6 +224,17 @@ "pullModel": { "title": "拉取模型", "pull": "拉取" + }, + "modelCheck": { + "title": "模型检查", + "description": "选择一个模型进行连接性和可用性测试", + "model": "选择模型", + "modelPlaceholder": "请选择要测试的模型", + "test": "开始测试", + "checking": "测试中...", + "success": "模型测试成功", + "failed": "模型测试失败", + "noModels": "该服务商没有可用的模型" } }, "pullModels": "拉取模型", diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index 72130dc41..138fd970f 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -223,6 +223,17 @@ "pullModel": { "title": "拉取模型", "pull": "拉取" + }, + "modelCheck": { + "checking": "測試中...", + "description": "選擇一個模型進行連接性和可用性測試", + "failed": "模型測試失敗", + "model": "選擇模型", + "modelPlaceholder": "請選擇要測試的模型", + "noModels": "該服務商沒有可用的模型", + "success": "模型測試成功", + "test": "開始測試", + "title": "模型檢查" } }, "pullModels": "拉取模型", diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index 27134e4ec..49b92c9d1 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -224,6 +224,17 @@ "pullModel": { "title": "下載模型", "pull": "下載" + }, + "modelCheck": { + "checking": "測試中...", + "description": "選擇一個模型進行連接性和可用性測試", + "failed": "模型測試失敗", + "model": "選擇模型", + "modelPlaceholder": "請選擇要測試的模型", + "noModels": "該服務商沒有可用的模型", + "success": "模型測試成功", + "test": "開始測試", + "title": "模型檢查" } }, "pullModels": "下載模型", diff --git a/src/renderer/src/stores/modelCheck.ts b/src/renderer/src/stores/modelCheck.ts new file mode 100644 index 000000000..0ba0be5b9 --- /dev/null +++ b/src/renderer/src/stores/modelCheck.ts @@ -0,0 +1,24 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +export const useModelCheckStore = defineStore('modelCheck', () => { + const isDialogOpen = ref(false) + const currentProviderId = ref('') + + const openDialog = (providerId: string) => { + currentProviderId.value = providerId + isDialogOpen.value = true + } + + const closeDialog = () => { + isDialogOpen.value = false + currentProviderId.value = '' + } + + return { + isDialogOpen, + currentProviderId, + openDialog, + closeDialog + } +}) diff --git a/src/renderer/src/stores/settings.ts b/src/renderer/src/stores/settings.ts index dc33aec53..0aa15d765 100644 --- a/src/renderer/src/stores/settings.ts +++ b/src/renderer/src/stores/settings.ts @@ -723,8 +723,8 @@ export const useSettingsStore = defineStore('settings', () => { } } - const checkProvider = async (providerId: string) => { - return await llmP.check(providerId) + const checkProvider = async (providerId: string, modelId?: string) => { + return await llmP.check(providerId, modelId) } // 删除自定义模型 diff --git a/src/renderer/src/views/WelcomeView.vue b/src/renderer/src/views/WelcomeView.vue index f0fdc419c..f10daeca8 100644 --- a/src/renderer/src/views/WelcomeView.vue +++ b/src/renderer/src/views/WelcomeView.vue @@ -65,6 +65,9 @@ const DialogDescription = defineAsyncComponent(() => const DialogFooter = defineAsyncComponent(() => import('@/components/ui/dialog').then((mod) => mod.DialogFooter) ) +const ModelCheckDialog = defineAsyncComponent( + () => import('@/components/settings/ModelCheckDialog.vue') +) const settingsStore = useSettingsStore() const languageStore = useLanguageStore() @@ -123,6 +126,9 @@ const showErrorDialog = ref(false) const showSuccessDialog = ref(false) const dialogMessage = ref('') +// 模型检查弹窗状态 +const showModelCheckDialog = ref(false) + const nextStep = async () => { if (currentStep.value < steps.length - 1) { if (currentStep.value === 1) { @@ -212,29 +218,10 @@ const handleModelEnabledChange = async (model: MODEL_META, enabled: boolean) => console.log('handleModelEnabledChange', model, enabled) } -const validateApiKey = async () => { - if ((!apiKey.value || !baseUrl.value) && selectedProvider.value !== 'ollama') { - showErrorDialog.value = true - dialogMessage.value = t('settings.provider.dialog.verify.missingFields') - return - } - await settingsStore.updateProvider(selectedProvider.value, { - apiKey: apiKey.value, - baseUrl: baseUrl.value, - id: settingsStore.providers.find((p) => p.id === selectedProvider.value)!.id, - name: settingsStore.providers.find((p) => p.id === selectedProvider.value)!.name, - apiType: settingsStore.providers.find((p) => p.id === selectedProvider.value)!.apiType, - enable: false - }) - const result = await settingsStore.checkProvider(selectedProvider.value) - if (!result.isOk) { - showErrorDialog.value = true - dialogMessage.value = t('settings.provider.dialog.verify.failed') - } else { - showSuccessDialog.value = true - dialogMessage.value = t('settings.provider.dialog.verify.success') - } +const openModelCheckDialog = () => { + showModelCheckDialog.value = true } + const isLastStep = computed(() => currentStep.value === steps.length - 1) const isFirstStep = computed(() => currentStep.value === 0) @@ -327,24 +314,6 @@ const isFirstStep = computed(() => currentStep.value === 0) {{ t('settings.provider.getKeyTipEnd') }} -
- - -
@@ -352,6 +321,22 @@ const isFirstStep = computed(() => currentStep.value === 0) diff --git a/src/shared/presenter.d.ts b/src/shared/presenter.d.ts index 51f6b8aa8..d12bd6c2d 100644 --- a/src/shared/presenter.d.ts +++ b/src/shared/presenter.d.ts @@ -512,7 +512,7 @@ export interface ILlmProviderPresenter { maxTokens?: number ): Promise stopStream(eventId: string): Promise - check(providerId: string): Promise<{ isOk: boolean; errorMsg: string | null }> + check(providerId: string, modelId?: string): Promise<{ isOk: boolean; errorMsg: string | null }> getKeyStatus(providerId: string): Promise summaryTitles( messages: { role: 'system' | 'user' | 'assistant'; content: string }[],