From 623f35a7e321e43798813e4bb4f8ac18b55e05ce Mon Sep 17 00:00:00 2001 From: Rin Date: Sun, 15 Mar 2026 04:39:30 +0800 Subject: [PATCH] feat(stage-pages,stage-ui,i18n): disable auto trigger for chatcompletion check and use explict button (#1336) Co-authored-by-agent: Claude Sonnet 4.6 Co-authored-by: Neko --- packages/i18n/src/locales/en/settings.yaml | 4 + packages/i18n/src/locales/es/settings.yaml | 4 + packages/i18n/src/locales/fr/settings.yaml | 4 + packages/i18n/src/locales/ja/settings.yaml | 4 + packages/i18n/src/locales/ko/settings.yaml | 4 + packages/i18n/src/locales/ru/settings.yaml | 4 + packages/i18n/src/locales/vi/settings.yaml | 4 + .../i18n/src/locales/zh-Hans/settings.yaml | 4 + .../i18n/src/locales/zh-Hant/settings.yaml | 4 + .../settings/providers/chat/[providerId].vue | 52 +++------ .../providers/chat/azure-ai-foundry.vue | 43 +++----- .../providers/chat/cloudflare-workers-ai.vue | 43 +++----- .../settings/providers/chat/lm-studio.vue | 43 +++----- .../pages/settings/providers/chat/ollama.vue | 43 +++----- .../components/scenarios/providers/index.ts | 1 + .../providers/provider-validation-alerts.vue | 103 ++++++++++++++++++ .../composables/use-provider-validation.ts | 49 ++++++++- packages/stage-ui/src/libs/providers/types.ts | 6 + .../providers/validators/openai-compatible.ts | 1 + .../src/libs/providers/validators/run.ts | 77 ++++++++++++- packages/stage-ui/src/stores/providers.ts | 9 ++ .../src/stores/providers/converters.ts | 26 ++++- 22 files changed, 393 insertions(+), 139 deletions(-) create mode 100644 packages/stage-ui/src/components/scenarios/providers/provider-validation-alerts.vue diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index 9cea061ff..477204e4c 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -20,8 +20,12 @@ dialogs: baseUrlHelp: API endpoint URL (use default if unsure) accountId: Account ID validationSuccess: Configuration validation success + validationPartial: Configuration partially validated validationFailed: Configuration validation failed validationError: 'Validation error: {error}' + testGeneration: Test Generation + testGenerationRunning: Testing... + testGenerationFailed: Generation test failed skipForNow: Skip for now saveAndContinue: Save and Continue next: Next diff --git a/packages/i18n/src/locales/es/settings.yaml b/packages/i18n/src/locales/es/settings.yaml index e78e5a76a..706c15014 100644 --- a/packages/i18n/src/locales/es/settings.yaml +++ b/packages/i18n/src/locales/es/settings.yaml @@ -19,8 +19,12 @@ dialogs: baseUrlHelp: URL del endpoint de la API (usa el predeterminado si no estás seguro) accountId: ID de Cuenta validationSuccess: La validación de la configuración fue exitosa + validationPartial: Configuración parcialmente validada validationFailed: La validación de la configuración falló validationError: 'Error de validación: {error}' + testGeneration: Prueba de Generación + testGenerationRunning: Probando... + testGenerationFailed: Prueba de generación fallida skipForNow: Omitir por ahora saveAndContinue: Guardar y Continuar next: Siguiente diff --git a/packages/i18n/src/locales/fr/settings.yaml b/packages/i18n/src/locales/fr/settings.yaml index 5b3ac2c8c..a97caeee5 100644 --- a/packages/i18n/src/locales/fr/settings.yaml +++ b/packages/i18n/src/locales/fr/settings.yaml @@ -19,8 +19,12 @@ dialogs: baseUrlHelp: URL de l'endpoint API (utilisez la valeur par défaut si vous n’êtes pas sûr) accountId: ID du compte validationSuccess: Validation de la configuration réussie + validationPartial: Configuration partiellement validée validationFailed: La validation de la configuration a échouée. validationError: 'Erreur de validation: {error}' + testGeneration: Test de génération + testGenerationRunning: Test en cours... + testGenerationFailed: Test de génération échoué skipForNow: Passer pour l'instant saveAndContinue: Sauvegarder et Continuer next: Suivant diff --git a/packages/i18n/src/locales/ja/settings.yaml b/packages/i18n/src/locales/ja/settings.yaml index 0ce5eb43e..8b43cd390 100644 --- a/packages/i18n/src/locales/ja/settings.yaml +++ b/packages/i18n/src/locales/ja/settings.yaml @@ -19,8 +19,12 @@ dialogs: baseUrlHelp: APIエンドポイントURL(不明な場合はデフォルトを使用) accountId: アカウントID validationSuccess: 設定検証に成功 + validationPartial: 設定が部分的に検証されました validationFailed: 設定検証に失敗 validationError: '検証エラー: {error}' + testGeneration: 生成テスト + testGenerationRunning: テスト中... + testGenerationFailed: 生成テストに失敗 skipForNow: 今はスキップ saveAndContinue: 保存して続行 next: 次へ diff --git a/packages/i18n/src/locales/ko/settings.yaml b/packages/i18n/src/locales/ko/settings.yaml index 555ce7891..31fcbc489 100644 --- a/packages/i18n/src/locales/ko/settings.yaml +++ b/packages/i18n/src/locales/ko/settings.yaml @@ -19,8 +19,12 @@ dialogs: baseUrlHelp: API 엔드포인트 URL(확실하지 않은 경우 기본값 사용) accountId: 계정 ID validationSuccess: 구성 검증 성공 + validationPartial: 구성이 부분적으로 검증됨 validationFailed: 구성 검증 실패 validationError: '검증 오류: {error}' + testGeneration: 생성 테스트 + testGenerationRunning: 테스트 중... + testGenerationFailed: 생성 테스트 실패 skipForNow: 지금은 건너뛰기 saveAndContinue: 저장 및 계속 next: 다음 diff --git a/packages/i18n/src/locales/ru/settings.yaml b/packages/i18n/src/locales/ru/settings.yaml index 69ca57b6d..2329cdf0c 100644 --- a/packages/i18n/src/locales/ru/settings.yaml +++ b/packages/i18n/src/locales/ru/settings.yaml @@ -19,8 +19,12 @@ dialogs: baseUrlHelp: URL конечной точки API (оставьте по умолчанию, если не уверены) accountId: ID аккаунта validationSuccess: Configuration validation success + validationPartial: Конфигурация частично проверена validationFailed: Проверка конфигурации не удалась validationError: 'Ошибка проверки: {error}' + testGeneration: Тест генерации + testGenerationRunning: Тестирование... + testGenerationFailed: Тест генерации не удался skipForNow: Пропустить на сейчас saveAndContinue: Сохранить и продолжить next: Далее diff --git a/packages/i18n/src/locales/vi/settings.yaml b/packages/i18n/src/locales/vi/settings.yaml index 72aaebc05..9a682c001 100644 --- a/packages/i18n/src/locales/vi/settings.yaml +++ b/packages/i18n/src/locales/vi/settings.yaml @@ -19,8 +19,12 @@ dialogs: baseUrlHelp: Địa chỉ API đầu cuối (dùng mặc định nếu không chắc) accountId: ID tài khoản validationSuccess: Xác thực cấu hình thành công + validationPartial: Cấu hình được xác thực một phần validationFailed: Xác thực cấu hình thất bại validationError: 'Lỗi xác thực: {error}' + testGeneration: Kiểm tra tạo + testGenerationRunning: Đang kiểm tra... + testGenerationFailed: Kiểm tra tạo thất bại skipForNow: Bỏ qua saveAndContinue: Lưu và tiếp tục next: Tiếp diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index c9f8bd5ba..dacc31344 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -19,8 +19,12 @@ dialogs: baseUrlHelp: API 端点 URL(如果不确定请使用默认值) accountId: 账户 ID validationSuccess: 配置验证成功 + validationPartial: 配置部分验证通过 validationFailed: 配置验证失败 validationError: '验证错误:{error}' + testGeneration: 测试生成 + testGenerationRunning: 测试中... + testGenerationFailed: 生成测试失败 skipForNow: 暂时跳过 saveAndContinue: 保存并继续 next: 下一步 diff --git a/packages/i18n/src/locales/zh-Hant/settings.yaml b/packages/i18n/src/locales/zh-Hant/settings.yaml index b09fd77eb..91dab73ba 100644 --- a/packages/i18n/src/locales/zh-Hant/settings.yaml +++ b/packages/i18n/src/locales/zh-Hant/settings.yaml @@ -19,8 +19,12 @@ dialogs: baseUrlHelp: API 端點 URL(如果不確定請使用預設值) accountId: 帳號 ID validationSuccess: 設定驗證成功 + validationPartial: 設定部分驗證通過 validationFailed: 設定驗證失敗 validationError: '驗證錯誤:{error}' + testGeneration: 測試生成 + testGenerationRunning: 測試中... + testGenerationFailed: 生成測試失敗 skipForNow: 先略過 saveAndContinue: 儲存並繼續 next: 下一步 diff --git a/packages/stage-pages/src/pages/settings/providers/chat/[providerId].vue b/packages/stage-pages/src/pages/settings/providers/chat/[providerId].vue index 48a49d4c5..360a65bd3 100644 --- a/packages/stage-pages/src/pages/settings/providers/chat/[providerId].vue +++ b/packages/stage-pages/src/pages/settings/providers/chat/[providerId].vue @@ -2,13 +2,13 @@ import type { RemovableRef } from '@vueuse/core' import { - Alert, ProviderAdvancedSettings, ProviderApiKeyInput, ProviderBaseUrlInput, ProviderBasicSettings, ProviderSettingsContainer, ProviderSettingsLayout, + ProviderValidationAlerts, } from '@proj-airi/stage-ui/components' import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation' import { useConsciousnessStore } from '@proj-airi/stage-ui/stores/modules/consciousness' @@ -53,6 +53,11 @@ const { validationMessage, handleResetSettings, forceValid, + hasManualValidators, + isManualTesting, + manualTestPassed, + manualTestMessage, + runManualTest, } = useProviderValidation(providerId) function goToModelSelection() { @@ -88,39 +93,18 @@ function goToModelSelection() { - - - - - - - + diff --git a/packages/stage-pages/src/pages/settings/providers/chat/azure-ai-foundry.vue b/packages/stage-pages/src/pages/settings/providers/chat/azure-ai-foundry.vue index 30d01595a..18fe3c0a4 100644 --- a/packages/stage-pages/src/pages/settings/providers/chat/azure-ai-foundry.vue +++ b/packages/stage-pages/src/pages/settings/providers/chat/azure-ai-foundry.vue @@ -2,13 +2,13 @@ import type { RemovableRef } from '@vueuse/core' import { - Alert, ProviderAccountIdInput, ProviderAdvancedSettings, ProviderApiKeyInput, ProviderBasicSettings, ProviderSettingsContainer, ProviderSettingsLayout, + ProviderValidationAlerts, } from '@proj-airi/stage-ui/components' import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation' import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers' @@ -66,6 +66,11 @@ const { validationMessage, handleResetSettings, forceValid, + hasManualValidators, + isManualTesting, + manualTestPassed, + manualTestMessage, + runManualTest, } = useProviderValidation(providerId) @@ -113,30 +118,18 @@ const { - - - - - - - + diff --git a/packages/stage-pages/src/pages/settings/providers/chat/cloudflare-workers-ai.vue b/packages/stage-pages/src/pages/settings/providers/chat/cloudflare-workers-ai.vue index d5276aa3d..4928729b8 100644 --- a/packages/stage-pages/src/pages/settings/providers/chat/cloudflare-workers-ai.vue +++ b/packages/stage-pages/src/pages/settings/providers/chat/cloudflare-workers-ai.vue @@ -2,12 +2,12 @@ import type { RemovableRef } from '@vueuse/core' import { - Alert, ProviderAccountIdInput, ProviderApiKeyInput, ProviderBasicSettings, ProviderSettingsContainer, ProviderSettingsLayout, + ProviderValidationAlerts, } from '@proj-airi/stage-ui/components' import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation' import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers' @@ -47,6 +47,11 @@ const { validationMessage, handleResetSettings, forceValid, + hasManualValidators, + isManualTesting, + manualTestPassed, + manualTestMessage, + runManualTest, } = useProviderValidation(providerId) @@ -78,30 +83,18 @@ const { - - - - - - - + diff --git a/packages/stage-pages/src/pages/settings/providers/chat/lm-studio.vue b/packages/stage-pages/src/pages/settings/providers/chat/lm-studio.vue index 64311ae0d..79b7eac14 100644 --- a/packages/stage-pages/src/pages/settings/providers/chat/lm-studio.vue +++ b/packages/stage-pages/src/pages/settings/providers/chat/lm-studio.vue @@ -2,11 +2,11 @@ import type { RemovableRef } from '@vueuse/core' import { - Alert, ProviderBaseUrlInput, ProviderBasicSettings, ProviderSettingsContainer, ProviderSettingsLayout, + ProviderValidationAlerts, } from '@proj-airi/stage-ui/components' import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation' import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers' @@ -38,6 +38,11 @@ const { validationMessage, handleResetSettings, forceValid, + hasManualValidators, + isManualTesting, + manualTestPassed, + manualTestMessage, + runManualTest, } = useProviderValidation(providerId) @@ -60,30 +65,18 @@ const { - - - - - - - + diff --git a/packages/stage-pages/src/pages/settings/providers/chat/ollama.vue b/packages/stage-pages/src/pages/settings/providers/chat/ollama.vue index 3a800296d..2c4303efc 100644 --- a/packages/stage-pages/src/pages/settings/providers/chat/ollama.vue +++ b/packages/stage-pages/src/pages/settings/providers/chat/ollama.vue @@ -2,12 +2,12 @@ import type { RemovableRef } from '@vueuse/core' import { - Alert, ProviderAdvancedSettings, ProviderBaseUrlInput, ProviderBasicSettings, ProviderSettingsContainer, ProviderSettingsLayout, + ProviderValidationAlerts, } from '@proj-airi/stage-ui/components' import { useProviderValidation } from '@proj-airi/stage-ui/composables/use-provider-validation' import { useProvidersStore } from '@proj-airi/stage-ui/stores/providers' @@ -39,6 +39,11 @@ const { validationMessage, handleResetSettings, forceValid, + hasManualValidators, + isManualTesting, + manualTestPassed, + manualTestMessage, + runManualTest, } = useProviderValidation(providerId) const headers = ref<{ key: string, value: string }[]>(Object.entries(providers.value[providerId]?.headers || {}).map(([key, value]) => ({ key, value } as { key: string, value: string })) || [{ key: '', value: '' }]) @@ -176,30 +181,18 @@ onMounted(() => { - - - - - - - + diff --git a/packages/stage-ui/src/components/scenarios/providers/index.ts b/packages/stage-ui/src/components/scenarios/providers/index.ts index 45d9d7757..a0785398a 100644 --- a/packages/stage-ui/src/components/scenarios/providers/index.ts +++ b/packages/stage-ui/src/components/scenarios/providers/index.ts @@ -5,6 +5,7 @@ export { default as ProviderBaseUrlInput } from './provider-base-url-input.vue' export { default as ProviderBasicSettings } from './provider-basic-settings.vue' export { default as ProviderSettingsContainer } from './provider-settings-container.vue' export { default as ProviderSettingsLayout } from './provider-settings-layout.vue' +export { default as ProviderValidationAlerts } from './provider-validation-alerts.vue' export { default as SpeechPlaygroundOpenAICompatible } from './speech-playground-openai-compatible.vue' export { default as SpeechPlayground } from './speech-playground.vue' diff --git a/packages/stage-ui/src/components/scenarios/providers/provider-validation-alerts.vue b/packages/stage-ui/src/components/scenarios/providers/provider-validation-alerts.vue new file mode 100644 index 000000000..4a2db7750 --- /dev/null +++ b/packages/stage-ui/src/components/scenarios/providers/provider-validation-alerts.vue @@ -0,0 +1,103 @@ + + + diff --git a/packages/stage-ui/src/composables/use-provider-validation.ts b/packages/stage-ui/src/composables/use-provider-validation.ts index 24e9e3a4b..bb4c34a80 100644 --- a/packages/stage-ui/src/composables/use-provider-validation.ts +++ b/packages/stage-ui/src/composables/use-provider-validation.ts @@ -1,5 +1,6 @@ import type { RemovableRef } from '@vueuse/core' +import { errorMessageFrom } from '@moeru/std' import { useDebounceFn } from '@vueuse/core' import { storeToRefs } from 'pinia' import { computed, onMounted, ref, watch } from 'vue' @@ -52,6 +53,11 @@ export function useProviderValidation(providerId: string) { const isValid = ref(false) const validationMessage = ref('') + const hasManualValidators = computed(() => !!providerMetadata.value?.validators.runManualValidation) + const isManualTesting = ref(false) + const manualTestPassed = ref(false) + const manualTestMessage = ref('') + async function validateConfiguration() { if (!providerMetadata.value) return @@ -85,7 +91,7 @@ export function useProviderValidation(providerId: string) { catch (error) { isValid.value = false finalValidationMessage = t('settings.dialogs.onboarding.validationError', { - error: error instanceof Error ? error.message : String(error), + error: errorMessageFrom(error), }) } finally { @@ -120,19 +126,54 @@ export function useProviderValidation(providerId: string) { watch(credentials, () => { debouncedValidateConfiguration() + // Reset manual test state when credentials change + manualTestPassed.value = false + manualTestMessage.value = '' }, { deep: true }) + async function runManualTest() { + if (!providerMetadata.value?.validators.runManualValidation) + return + + isManualTesting.value = true + manualTestMessage.value = '' + + try { + const config = { ...credentials.value } + if (config?.apiKey) + config.apiKey = config.apiKey.trim() + if (config?.baseUrl) + config.baseUrl = config.baseUrl.trim() + + const result = await providerMetadata.value.validators.runManualValidation(config) + manualTestPassed.value = result.valid + if (!result.valid) + manualTestMessage.value = result.reason + } + catch (error) { + manualTestPassed.value = false + manualTestMessage.value = errorMessageFrom(error) + } + finally { + isManualTesting.value = false + } + } + function handleResetSettings() { const defaultOptions = providerMetadata.value?.defaultOptions ? providerMetadata.value.defaultOptions() : {} providers.value[providerId] = { ...defaultOptions } isValid.value = false validationMessage.value = '' isValidating.value = 0 + manualTestPassed.value = false + manualTestMessage.value = '' } function forceValid() { isValid.value = true validationMessage.value = '' + manualTestPassed.value = true + manualTestMessage.value = '' providersStore.forceProviderConfigured(providerId) } @@ -148,5 +189,11 @@ export function useProviderValidation(providerId: string) { validationMessage, handleResetSettings, forceValid, + // Manual test generation + hasManualValidators, + isManualTesting, + manualTestPassed, + manualTestMessage, + runManualTest, } } diff --git a/packages/stage-ui/src/libs/providers/types.ts b/packages/stage-ui/src/libs/providers/types.ts index 3ff0ad50d..47d320212 100644 --- a/packages/stage-ui/src/libs/providers/types.ts +++ b/packages/stage-ui/src/libs/providers/types.ts @@ -65,6 +65,12 @@ export interface ProviderRuntimeValidator { name: string validator: (config: TConfig, provider: ProviderInstance, providerExtra: ProviderExtraMethods, contextOptions: { t: ComposerTranslation }) => MaybePromise schedule?: ProviderValidatorSchedule + /** + * When true the validator is excluded from automatic validation runs. + * It must be triggered explicitly by the user (e.g. a "Test Generation" button). + * This is used for costly checks like chat-completion probes that bill per request. + */ + manualOnly?: boolean } export interface ModelInfo { diff --git a/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts b/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts index c33ad9e30..c56747564 100644 --- a/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts +++ b/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts @@ -291,6 +291,7 @@ export function createOpenAICompatibleValidators { const errors: Array<{ error: unknown }> = [] const result = await getChatCheckResult( diff --git a/packages/stage-ui/src/libs/providers/validators/run.ts b/packages/stage-ui/src/libs/providers/validators/run.ts index 17f0bae95..a304db5f0 100644 --- a/packages/stage-ui/src/libs/providers/validators/run.ts +++ b/packages/stage-ui/src/libs/providers/validators/run.ts @@ -26,6 +26,8 @@ export interface ProviderValidationPlan { definition: ProviderDefinition configValidators: ProviderConfigValidator>[] providerValidators: ProviderRuntimeValidator>[] + /** Provider validators that require explicit user action (e.g. "Test Generation" button). */ + manualProviderValidators: ProviderRuntimeValidator>[] providerExtra: ProviderExtraMethods> | undefined shouldValidate: boolean } @@ -83,7 +85,12 @@ export function getValidatorsOfProvider(options: { const { definition } = options const configValidators = (definition.validators?.validateConfig || []).map(creator => creator(options.contextOptions)) - const providerValidators = (definition.validators?.validateProvider || []).map(creator => creator(options.contextOptions)) + const allProviderValidators = (definition.validators?.validateProvider || []).map(creator => creator(options.contextOptions)) + + // Separate automatic validators from manual-only ones (e.g. chat completion probes) + const providerValidators = allProviderValidators.filter(v => !v.manualOnly) + const manualProviderValidators = allProviderValidators.filter(v => v.manualOnly) + const steps: ProviderValidationStep[] = [ ...createConfigValidationSteps(configValidators), ...createProviderValidationSteps(providerValidators), @@ -99,6 +106,7 @@ export function getValidatorsOfProvider(options: { definition, configValidators: configValidators as ProviderValidationPlan['configValidators'], providerValidators: providerValidators as ProviderValidationPlan['providerValidators'], + manualProviderValidators: manualProviderValidators as ProviderValidationPlan['manualProviderValidators'], providerExtra: definition.extraMethods as ProviderValidationPlan['providerExtra'], shouldValidate, } @@ -156,7 +164,7 @@ export async function validateProvider( for (let i = 0; i < providerValidators.length; i++) { const step = steps[providerStepOffset + i] step.status = 'invalid' - step.reason = error instanceof Error ? error.message : String(error) + step.reason = errorMessageFrom(error) } return steps } @@ -174,7 +182,70 @@ export async function validateProvider( } catch (error) { step.status = 'invalid' - step.reason = error instanceof Error ? error.message : String(error) + step.reason = errorMessageFrom(error) + onValidatorError?.({ kind: 'provider', index, step, error }) + } + })) + + return steps +} + +/** + * Run only the manual-only validators (e.g. chat completion probes). + * These are excluded from automatic validation to avoid costly API calls. + * Must be triggered explicitly by the user. + */ +export async function validateProviderManual( + plan: ProviderValidationPlan, + contextOptions: { t: ComposerTranslation }, + callbacks: ProviderValidationCallbacks = {}, +) { + const { manualProviderValidators, config, definition, providerExtra } = plan + if (manualProviderValidators.length === 0) + return [] + + const runContext = { + ...contextOptions, + validationCache: new Map(), + } + const { onValidatorError, onValidatorStart, onValidatorSuccess } = callbacks + + let providerInstance: ProviderInstance + try { + providerInstance = await definition.createProvider(config as any) + } + catch (error) { + return manualProviderValidators.map(v => ({ + id: v.id, + label: v.name, + status: 'invalid' as ProviderValidationStepStatus, + reason: errorMessageFrom(error), + kind: 'provider' as ProviderValidationStepKind, + })) + } + + const steps: ProviderValidationStep[] = manualProviderValidators.map(v => ({ + id: v.id, + label: v.name, + status: 'idle' as ProviderValidationStepStatus, + reason: '', + kind: 'provider' as ProviderValidationStepKind, + })) + + await Promise.all(manualProviderValidators.map(async (validatorDefinition, index) => { + const step = steps[index] + step.status = 'validating' + step.reason = '' + onValidatorStart?.({ kind: 'provider', index, step }) + try { + const result = await validatorDefinition.validator(config, providerInstance as any, providerExtra as any, runContext) + step.status = result.valid ? 'valid' : 'invalid' + step.reason = result.valid ? '' : result.reason + onValidatorSuccess?.({ kind: 'provider', index, step, result }) + } + catch (error) { + step.status = 'invalid' + step.reason = errorMessageFrom(error) onValidatorError?.({ kind: 'provider', index, step, error }) } })) diff --git a/packages/stage-ui/src/stores/providers.ts b/packages/stage-ui/src/stores/providers.ts index 0a666f2fe..9aa3437c3 100644 --- a/packages/stage-ui/src/stores/providers.ts +++ b/packages/stage-ui/src/stores/providers.ts @@ -147,6 +147,15 @@ export interface ProviderMetadata { reason: string valid: boolean } + /** + * Run only the manual-only validators. Returns validation result. + * Only available when the provider has manual validators. + */ + runManualValidation?: (config: Record) => Promise<{ + errors: unknown[] + reason: string + valid: boolean + }> } transcriptionFeatures?: { supportsGenerate: boolean diff --git a/packages/stage-ui/src/stores/providers/converters.ts b/packages/stage-ui/src/stores/providers/converters.ts index e893b543f..56f1f91a1 100644 --- a/packages/stage-ui/src/stores/providers/converters.ts +++ b/packages/stage-ui/src/stores/providers/converters.ts @@ -7,7 +7,7 @@ import type { ProviderMetadata } from '../providers' import { listModels } from '@xsai/model' import { isModelProvider } from '../../libs/providers/types' -import { getValidatorsOfProvider, validateProvider } from '../../libs/providers/validators/run' +import { getValidatorsOfProvider, validateProvider, validateProviderManual } from '../../libs/providers/validators/run' function getCategoryFromTasks(tasks: string[]): ProviderMetadata['category'] { if (tasks.some(task => ['speech-to-text', 'automatic-speech-recognition', 'asr', 'stt'].includes(task.toLowerCase()))) { @@ -100,6 +100,8 @@ export function convertProviderDefinitionToMetadata( const keyExtractor = (input: string): string => input const category = getCategoryFromTasks(definition.tasks) const schemaDefaults = extractSchemaDefaults(definition, t) + const allValidators = (definition.validators?.validateProvider || []).map(creator => creator({ t })) + const hasManualValidators = allValidators.some(v => v.manualOnly) return { id: definition.id, @@ -194,6 +196,8 @@ export function convertProviderDefinitionToMetadata( // Run full validation pipeline (config + provider validators) only when required. // This preserves strict config checks while avoiding unnecessary network checks. + // NOTICE: manualOnly validators (e.g. chat completion probes) are already excluded + // from the plan's providerValidators by getValidatorsOfProvider. if (plan.shouldValidate) { await validateProvider(plan, { t }) const invalidSteps = plan.steps.filter(step => step.status === 'invalid') @@ -230,6 +234,26 @@ export function convertProviderDefinitionToMetadata( await validateProvider(plan, { t }) return buildConfigValidationResult(plan) }, + runManualValidation: hasManualValidators + ? async (config) => { + const plan = getValidatorsOfProvider({ + definition, + config, + schemaDefaults, + contextOptions: { t }, + }) + const steps = await validateProviderManual(plan, { t }) + const invalidSteps = steps.filter(step => step.status === 'invalid') + if (invalidSteps.length === 0) { + return { errors: [], reason: '', valid: true } + } + return { + errors: invalidSteps.map(step => new Error(step.reason || `${step.id} is invalid`)), + reason: invalidSteps.map(step => step.reason).filter(Boolean).join('; '), + valid: false, + } + } + : undefined, }, transcriptionFeatures: definition.capabilities?.transcription ? {