refactor: replace status check string literal with enum; add chat pin… (#1486)
* refactor: replace status check string literal with enum; add chat ping check option in provider configurations for anthropic, and if it's good, generalize to all openai based provider * fix: merging conflicts * refactor: remove manual validation logic and deprecated ProviderValidationAlerts component * docs: mark 'Health' enum as deprecated with additional usage context * feat: add manual test validation flow back * refactor: reintroduce and integrate `ProviderValidationAlerts` component for streamlined validation logic * chore: mark error msg * refactor: extract `CHAT_COMPLETIONS_VALIDATOR_ID` constant; enforce skipping chat ping check in background validations * chore: comments
This commit is contained in:
@@ -124,3 +124,4 @@ plugins-development
|
||||
apps/stage-pocket/ios/buildServer.json
|
||||
.ghfs/
|
||||
.docs/superpowers
|
||||
apps/stage-tamagotchi/electron.vite.config.*.mjs
|
||||
|
||||
@@ -19,13 +19,31 @@ dialogs:
|
||||
baseUrl: Base URL
|
||||
baseUrlHelp: API endpoint URL (use default if unsure)
|
||||
accountId: Account ID
|
||||
validationRunning: Running tests
|
||||
validationSuccess: Configuration validation success
|
||||
validationPartial: Configuration partially validated
|
||||
validationFailed: Configuration validation failed
|
||||
validationError: 'Validation error: {error}'
|
||||
testGeneration: Ping API
|
||||
testGenerationRunning: Ping...
|
||||
testGenerationFailed: Ping test failed
|
||||
retryPingCheck: Retry
|
||||
skipForNow: Skip for now
|
||||
saveAndContinue: Save and Continue
|
||||
next: Next
|
||||
retry: Retry
|
||||
credentialsSafeLabel: Keep your API keys and credentials safe!
|
||||
credentialsSafeLocal: >-
|
||||
AIRI is running pure locally in your PC, and we will never steal
|
||||
your credentials for AI / LLM providers. But keep in mind that your API
|
||||
keys are sensitive information. Make sure to keep them safe and do not
|
||||
share them with anyone.
|
||||
credentialsSafeOpenSource: >-
|
||||
AIRI is open sourced at {github}, if you want to check how we handle
|
||||
your credentials, feel free to inspect our code.
|
||||
enableChatCheck: >-
|
||||
Allow airi to send a "ping" message to the first available chat model to
|
||||
check availability
|
||||
start: Let's do it!
|
||||
loginPrompt: Sign in to use the official AIRI provider for the best experience.
|
||||
loginAction: Sign in
|
||||
|
||||
@@ -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: Ping API
|
||||
testGenerationRunning: Ping...
|
||||
testGenerationFailed: Ping falló
|
||||
skipForNow: Omitir por ahora
|
||||
saveAndContinue: Guardar y Continuar
|
||||
next: Siguiente
|
||||
|
||||
@@ -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: Ping API
|
||||
testGenerationRunning: Ping...
|
||||
testGenerationFailed: Ping échoué
|
||||
skipForNow: Passer pour l'instant
|
||||
saveAndContinue: Sauvegarder et Continuer
|
||||
next: Suivant
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: APIエンドポイントURL(不明な場合はデフォルトを使用)
|
||||
accountId: アカウントID
|
||||
validationSuccess: 設定検証に成功
|
||||
validationPartial: 設定が部分的に検証されました
|
||||
validationFailed: 設定検証に失敗
|
||||
validationError: '検証エラー: {error}'
|
||||
testGeneration: Ping API
|
||||
testGenerationRunning: Ping...
|
||||
testGenerationFailed: Ping テストに失敗
|
||||
skipForNow: 今はスキップ
|
||||
saveAndContinue: 保存して続行
|
||||
next: 次へ
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: API 엔드포인트 URL(확실하지 않은 경우 기본값 사용)
|
||||
accountId: 계정 ID
|
||||
validationSuccess: 구성 검증 성공
|
||||
validationPartial: 구성이 부분적으로 검증됨
|
||||
validationFailed: 구성 검증 실패
|
||||
validationError: '검증 오류: {error}'
|
||||
testGeneration: Ping API
|
||||
testGenerationRunning: Ping...
|
||||
testGenerationFailed: Ping 테스트 실패
|
||||
skipForNow: 지금은 건너뛰기
|
||||
saveAndContinue: 저장 및 계속
|
||||
next: 다음
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: URL конечной точки API (оставьте по умолчанию, если не уверены)
|
||||
accountId: ID аккаунта
|
||||
validationSuccess: Успешная проверка конфигурации
|
||||
validationPartial: Конфигурация частично проверена
|
||||
validationFailed: Проверка конфигурации не удалась
|
||||
validationError: 'Ошибка проверки: {error}'
|
||||
testGeneration: Ping API
|
||||
testGenerationRunning: Ping...
|
||||
testGenerationFailed: Ping не удался
|
||||
skipForNow: Пропустить на сейчас
|
||||
saveAndContinue: Сохранить и продолжить
|
||||
next: Далее
|
||||
|
||||
@@ -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: Ping API
|
||||
testGenerationRunning: Ping...
|
||||
testGenerationFailed: Ping thất bại
|
||||
skipForNow: Bỏ qua
|
||||
saveAndContinue: Lưu và tiếp tục
|
||||
next: Tiếp
|
||||
|
||||
@@ -18,13 +18,26 @@ dialogs:
|
||||
baseUrl: 基础 URL
|
||||
baseUrlHelp: API 端点 URL(如果不确定请使用默认值)
|
||||
accountId: 账户 ID
|
||||
validationRunning: 正在验证配置
|
||||
validationSuccess: 配置验证成功
|
||||
validationPartial: 配置部分验证通过
|
||||
validationFailed: 配置验证失败
|
||||
validationError: '验证错误:{error}'
|
||||
testGeneration: Ping API
|
||||
testGenerationRunning: Ping...
|
||||
testGenerationFailed: Ping 测试失败
|
||||
retryPingCheck: 再试一次
|
||||
skipForNow: 暂时跳过
|
||||
saveAndContinue: 保存并继续
|
||||
next: 下一步
|
||||
retry: 重试
|
||||
credentialsSafeLabel: 请妥善保管你的 API 密钥和凭据!
|
||||
credentialsSafeLocal: >-
|
||||
AIRI 完全在你的计算机中本地运行,我们绝不会窃取你的 AI / LLM
|
||||
服务商凭据。但请注意,你的 API 密钥是敏感信息,请妥善保管,不要与任何人分享。
|
||||
credentialsSafeOpenSource: >-
|
||||
AIRI 已在 {github} 开源,如果你想了解我们如何处理你的凭据,欢迎查阅我们的代码。
|
||||
enableChatCheck: 允许 airi 向第一个可用的聊天模型发送 "ping" 消息来检查可用性
|
||||
start: 开始吧!
|
||||
loginPrompt: Sign in to use the official AIRI provider for the best experience.
|
||||
loginAction: Sign in
|
||||
|
||||
@@ -19,8 +19,12 @@ dialogs:
|
||||
baseUrlHelp: API 端點 URL(如果不確定請使用預設值)
|
||||
accountId: 帳號 ID
|
||||
validationSuccess: 設定驗證成功
|
||||
validationPartial: 設定部分驗證通過
|
||||
validationFailed: 設定驗證失敗
|
||||
validationError: '驗證錯誤:{error}'
|
||||
testGeneration: Ping API
|
||||
testGenerationRunning: Ping...
|
||||
testGenerationFailed: Ping 測試失敗
|
||||
skipForNow: 先略過
|
||||
saveAndContinue: 儲存並繼續
|
||||
next: 下一步
|
||||
|
||||
@@ -109,7 +109,6 @@ function goToModelSelection() {
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<ProviderValidationAlerts
|
||||
:is-valid="isValid"
|
||||
:is-validating="isValidating"
|
||||
|
||||
@@ -117,7 +117,6 @@ const {
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<ProviderValidationAlerts
|
||||
:is-valid="isValid"
|
||||
:is-validating="isValidating"
|
||||
|
||||
@@ -82,7 +82,6 @@ const {
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<ProviderValidationAlerts
|
||||
:is-valid="isValid"
|
||||
:is-validating="isValidating"
|
||||
|
||||
@@ -64,7 +64,6 @@ const {
|
||||
/>
|
||||
</ProviderBasicSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<ProviderValidationAlerts
|
||||
:is-valid="isValid"
|
||||
:is-validating="isValidating"
|
||||
|
||||
@@ -180,7 +180,6 @@ onMounted(() => {
|
||||
/>
|
||||
</ProviderAdvancedSettings>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<ProviderValidationAlerts
|
||||
:is-valid="isValid"
|
||||
:is-validating="isValidating"
|
||||
|
||||
+30
-10
@@ -2,7 +2,8 @@
|
||||
import type { ProviderMetadata } from '../../../../stores/providers'
|
||||
import type { OnboardingStepNextHandler, OnboardingStepPrevHandler } from './types'
|
||||
|
||||
import { Button, Callout, FieldInput } from '@proj-airi/ui'
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { Button, Callout, FieldCheckbox, FieldInput } from '@proj-airi/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -24,6 +25,7 @@ const providersStore = useProvidersStore()
|
||||
const apiKey = ref('')
|
||||
const baseUrl = ref('')
|
||||
const accountId = ref('')
|
||||
const enableChatCheck = ref(true)
|
||||
|
||||
const validation = ref<'unchecked' | 'pending' | 'succeed' | 'failed'>('unchecked')
|
||||
const validationError = ref<any>()
|
||||
@@ -39,9 +41,10 @@ function initializeForm() {
|
||||
apiKey.value = ''
|
||||
accountId.value = ''
|
||||
|
||||
// Reset validation
|
||||
// Reset validation and chat check
|
||||
validation.value = 'unchecked'
|
||||
validationError.value = undefined
|
||||
enableChatCheck.value = true
|
||||
}
|
||||
|
||||
// Watch for provider changes
|
||||
@@ -67,6 +70,10 @@ const needsBaseUrl = computed(() => {
|
||||
return props.selectedProvider.id !== 'cloudflare-workers-ai'
|
||||
})
|
||||
|
||||
const showChatCheckOption = computed(() => {
|
||||
return props.selectedProvider?.validators.chatPingCheckAvailable
|
||||
})
|
||||
|
||||
const canProceed = computed(() => {
|
||||
if (!props.selectedProviderId)
|
||||
return false
|
||||
@@ -103,7 +110,9 @@ async function validateConfiguration() {
|
||||
|
||||
// Validate using provider's validator
|
||||
const metadata = providersStore.getProviderMetadata(props.selectedProvider.id)
|
||||
const validationResult = await metadata.validators.validateProviderConfig(config)
|
||||
const validationResult = await metadata.validators.validateProviderConfig(config, {
|
||||
skipChatPingCheck: !enableChatCheck.value,
|
||||
})
|
||||
validation.value = validationResult.valid ? 'succeed' : 'failed'
|
||||
if (validation.value === 'failed') {
|
||||
validationError.value = validationResult.reason
|
||||
@@ -112,11 +121,10 @@ async function validateConfiguration() {
|
||||
catch (error) {
|
||||
validation.value = 'failed'
|
||||
validationError.value = t('settings.dialogs.onboarding.validationError', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
error: errorMessageFrom(error) ?? 'Unknown error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleNext() {
|
||||
await validateConfiguration()
|
||||
if (validation.value === 'succeed') {
|
||||
@@ -184,15 +192,19 @@ initializeForm()
|
||||
<div h-5 w-5 />
|
||||
</div>
|
||||
<div v-if="props.selectedProvider" flex-1 overflow-y-auto space-y-4>
|
||||
<Callout label="Keep your API keys and credentials safe!" theme="violet">
|
||||
<Callout :label="t('settings.dialogs.onboarding.credentialsSafeLabel')" theme="violet">
|
||||
<div>
|
||||
<div>
|
||||
AIRI is running pure locally in your browser, and we will never steal your credentials for AI / LLM providers. But keep in mind that your API keys are sensitive information. Make sure to keep them safe and do not share them with anyone.
|
||||
{{ t('settings.dialogs.onboarding.credentialsSafeLocal') }}
|
||||
</div>
|
||||
<div>
|
||||
AIRI is open sourced at <div inline-flex translate-y-1 items-center gap-1>
|
||||
<div i-simple-icons:github inline-block /><a decoration-underline decoration-dashed href="https://github.com/moeru-ai/airi" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||
</div>, if you want to check how we handle your credentials, feel free to inspect our code.
|
||||
<i18n-t keypath="settings.dialogs.onboarding.credentialsSafeOpenSource" tag="span">
|
||||
<template #github>
|
||||
<span inline-flex translate-y-1 items-center gap-1>
|
||||
<span i-simple-icons:github inline-block /><a decoration-underline decoration-dashed href="https://github.com/moeru-ai/airi" target="_blank" rel="noopener noreferrer">GitHub</a>
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</Callout>
|
||||
@@ -226,6 +238,14 @@ initializeForm()
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Ping Check Option -->
|
||||
<FieldCheckbox
|
||||
v-if="showChatCheckOption"
|
||||
v-model="enableChatCheck"
|
||||
:label="t('settings.dialogs.onboarding.enableChatCheck')"
|
||||
placement="left"
|
||||
/>
|
||||
|
||||
<!-- Validation Status -->
|
||||
<Alert v-if="validation === 'failed'" type="error">
|
||||
<template #title>
|
||||
|
||||
+38
-23
@@ -20,14 +20,20 @@ const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Validation Error -->
|
||||
<Alert v-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<!-- Validation Running -->
|
||||
<Alert v-if="isValidating > 0" type="loading">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
{{ t('settings.dialogs.onboarding.validationRunning') }}
|
||||
</template>
|
||||
</Alert>
|
||||
<!-- Validation Error -->
|
||||
<Alert v-else-if="!isValid && isValidating === 0 && validationMessage" type="error">
|
||||
<template #title>
|
||||
<div :class="['w-full flex items-center justify-between']">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationFailed') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 font-medium transition-colors dark:bg-red-800/30 hover:bg-red-200 dark:text-red-300 dark:hover:bg-red-700/40"
|
||||
:class="['ml-2 rounded px-2 py-0.5 text-xs font-medium transition-colors', 'bg-red-100 text-red-600 hover:bg-red-200', 'dark:bg-red-800/30 dark:text-red-300 dark:hover:bg-red-700/40']"
|
||||
@click="props.onForceValid"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.continueAnyway') }}
|
||||
@@ -35,28 +41,28 @@ const { t } = useI18n()
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="validationMessage" #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
<div :class="['whitespace-pre-wrap break-all']">
|
||||
{{ validationMessage }}
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<!-- Partial Validation: manual validators exist, no test attempted yet -->
|
||||
<!-- Partial: auto validation passed, manual test not yet attempted -->
|
||||
<Alert v-else-if="isValid && isValidating === 0 && hasManualValidators && !manualTestPassed && !manualTestMessage" type="info">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<div :class="['w-full flex items-center justify-between']">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationPartial') }}</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div :class="['flex items-center gap-2']">
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isManualTesting"
|
||||
:class="['rounded px-2 py-0.5 text-xs font-medium transition-colors', isManualTesting ? 'opacity-50 cursor-not-allowed' : '', 'bg-blue-100 text-blue-600 hover:bg-blue-200', 'dark:bg-blue-800/30 dark:text-blue-300 dark:hover:bg-blue-700/40']"
|
||||
:class="['rounded px-2 py-0.5 text-xs font-medium transition-colors', 'bg-blue-100 text-blue-600 hover:bg-blue-200', 'dark:bg-blue-800/30 dark:text-blue-300 dark:hover:bg-blue-700/40', isManualTesting ? 'cursor-not-allowed opacity-50' : '']"
|
||||
@click="props.onRunTest"
|
||||
>
|
||||
{{ isManualTesting ? t('settings.dialogs.onboarding.testGenerationRunning') : t('settings.dialogs.onboarding.testGeneration') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded bg-blue-100 px-2 py-0.5 text-xs text-blue-600 font-medium transition-colors dark:bg-blue-800/30 hover:bg-blue-200 dark:text-blue-300 dark:hover:bg-blue-700/40"
|
||||
:class="['rounded px-2 py-0.5 text-xs font-medium transition-colors', 'bg-blue-100 text-blue-600 hover:bg-blue-200', 'dark:bg-blue-800/30 dark:text-blue-300 dark:hover:bg-blue-700/40']"
|
||||
@click="props.onGoToModelSelection"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.goToModelSelection') }}
|
||||
@@ -65,14 +71,14 @@ const { t } = useI18n()
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<!-- Full Validation Success -->
|
||||
<!-- Full success -->
|
||||
<Alert v-else-if="isValid && isValidating === 0 && (!hasManualValidators || manualTestPassed)" type="success">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<div :class="['w-full flex items-center justify-between']">
|
||||
<span>{{ t('settings.dialogs.onboarding.validationSuccess') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-green-100 px-2 py-0.5 text-xs text-green-600 font-medium transition-colors dark:bg-green-800/30 hover:bg-green-200 dark:text-green-300 dark:hover:bg-green-700/40"
|
||||
:class="['ml-2 rounded px-2 py-0.5 text-xs font-medium transition-colors', 'bg-green-100 text-green-600 hover:bg-green-200', 'dark:bg-green-800/30 dark:text-green-300 dark:hover:bg-green-700/40']"
|
||||
@click="props.onGoToModelSelection"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.goToModelSelection') }}
|
||||
@@ -80,22 +86,31 @@ const { t } = useI18n()
|
||||
</div>
|
||||
</template>
|
||||
</Alert>
|
||||
<!-- Manual Test Failed -->
|
||||
<!-- Manual test failed -->
|
||||
<Alert v-else-if="hasManualValidators && !manualTestPassed && manualTestMessage && !isManualTesting" type="error">
|
||||
<template #title>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<div :class="['w-full flex items-center justify-between']">
|
||||
<span>{{ t('settings.dialogs.onboarding.testGenerationFailed') }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-2 rounded bg-red-100 px-2 py-0.5 text-xs text-red-600 font-medium transition-colors dark:bg-red-800/30 hover:bg-red-200 dark:text-red-300 dark:hover:bg-red-700/40"
|
||||
@click="props.onForceValid"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.continueAnyway') }}
|
||||
</button>
|
||||
<div :class="['flex items-center gap-2']">
|
||||
<button
|
||||
type="button"
|
||||
:class="['rounded px-2 py-0.5 text-xs font-medium transition-colors', 'bg-red-100 text-red-600 hover:bg-red-200', 'dark:bg-red-800/30 dark:text-red-300 dark:hover:bg-red-700/40']"
|
||||
@click="props.onRunTest"
|
||||
>
|
||||
{{ t('settings.dialogs.onboarding.retryPingCheck') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['rounded px-2 py-0.5 text-xs font-medium transition-colors', 'bg-red-100 text-red-600 hover:bg-red-200', 'dark:bg-red-800/30 dark:text-red-300 dark:hover:bg-red-700/40']"
|
||||
@click="props.onForceValid"
|
||||
>
|
||||
{{ t('settings.pages.providers.common.continueAnyway') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="whitespace-pre-wrap break-all">
|
||||
<div :class="['whitespace-pre-wrap break-all']">
|
||||
{{ manualTestMessage }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -53,7 +53,8 @@ export function useProviderValidation(providerId: string) {
|
||||
const isValid = ref(false)
|
||||
const validationMessage = ref('')
|
||||
|
||||
const hasManualValidators = computed(() => !!providerMetadata.value?.validators.runManualValidation)
|
||||
// Manual chat ping check state (settings pages only)
|
||||
const hasManualValidators = computed(() => !!providerMetadata.value?.validators.chatPingCheckAvailable)
|
||||
const isManualTesting = ref(false)
|
||||
const manualTestPassed = ref(false)
|
||||
const manualTestMessage = ref('')
|
||||
@@ -74,7 +75,11 @@ export function useProviderValidation(providerId: string) {
|
||||
if (config.baseUrl)
|
||||
config.baseUrl = config.baseUrl.trim()
|
||||
|
||||
const validationResult = await providerMetadata.value.validators.validateProviderConfig(config)
|
||||
// Settings pages always skip chat ping check during automatic validation
|
||||
// to avoid unexpected API billing. Users can trigger it manually.
|
||||
const validationResult = await providerMetadata.value.validators.validateProviderConfig(config, {
|
||||
skipChatPingCheck: true,
|
||||
})
|
||||
isValid.value = validationResult.valid
|
||||
|
||||
if (!isValid.value)
|
||||
@@ -91,7 +96,7 @@ export function useProviderValidation(providerId: string) {
|
||||
catch (error) {
|
||||
isValid.value = false
|
||||
finalValidationMessage = t('settings.dialogs.onboarding.validationError', {
|
||||
error: errorMessageFrom(error),
|
||||
error: errorMessageFrom(error) ?? 'Generic error (993b5ad7)',
|
||||
})
|
||||
}
|
||||
finally {
|
||||
@@ -102,6 +107,36 @@ export function useProviderValidation(providerId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function runManualTest() {
|
||||
if (!providerMetadata.value)
|
||||
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.validateProviderConfig(config, {
|
||||
onlyChatPingCheck: true,
|
||||
})
|
||||
manualTestPassed.value = result.valid
|
||||
if (!result.valid)
|
||||
manualTestMessage.value = result.reason
|
||||
}
|
||||
catch (error) {
|
||||
manualTestPassed.value = false
|
||||
manualTestMessage.value = errorMessageFrom(error) ?? 'Generic error (e56ae24f)'
|
||||
}
|
||||
finally {
|
||||
isManualTesting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const debouncedValidateConfiguration = useDebounceFn(() => {
|
||||
const config = credentials.value
|
||||
const hasApiKey = 'apiKey' in config && !!config.apiKey?.trim()
|
||||
@@ -131,34 +166,6 @@ export function useProviderValidation(providerId: string) {
|
||||
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) ?? 'Unknown error'
|
||||
}
|
||||
finally {
|
||||
isManualTesting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleResetSettings() {
|
||||
const defaultOptions = providerMetadata.value?.defaultOptions ? providerMetadata.value.defaultOptions() : {}
|
||||
providers.value[providerId] = { ...defaultOptions }
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createChatProvider, createEmbedProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const ai302ConfigSchema = z.object({
|
||||
@@ -52,7 +53,7 @@ export const provider302AI = defineProvider<AI302Config>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['model_list'],
|
||||
checks: [ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createChatProvider, createEmbedProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const aihubmixConfigSchema = z.object({
|
||||
@@ -52,7 +53,7 @@ export const providerAIHubMix = defineProvider<AIHubMixConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['model_list'],
|
||||
checks: [ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { ModelInfo } from '../../types'
|
||||
import { createChatProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const anthropicConfigSchema = z.object({
|
||||
@@ -91,7 +92,7 @@ export const providerAnthropic = defineProvider<AnthropicConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ChatCompletions],
|
||||
additionalHeaders: {
|
||||
'anthropic-dangerous-direct-browser-access': 'true',
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createCerebras } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const cerebrasConfigSchema = z.object({
|
||||
@@ -47,7 +48,7 @@ export const providerCerebrasAI = defineProvider<CerebrasConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createChatProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const cometApiConfigSchema = z.object({
|
||||
@@ -50,7 +51,7 @@ export const providerCometAPI = defineProvider<CometApiConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['model_list'],
|
||||
checks: [ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createDeepSeek } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const deepSeekConfigSchema = z.object({
|
||||
@@ -48,7 +49,7 @@ export const providerDeepSeek = defineProvider<DeepSeekConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const featherlessConfigSchema = z.object({
|
||||
@@ -46,7 +47,7 @@ export const providerFeatherlessAI = defineProvider<FeatherlessConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createFireworks } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const fireworksConfigSchema = z.object({
|
||||
@@ -47,7 +48,7 @@ export const providerFireworksAI = defineProvider<FireworksConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createGoogleGenerativeAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const googleGenerativeConfigSchema = z.object({
|
||||
@@ -48,7 +49,7 @@ export const providerGoogleGenerativeAI = defineProvider<GoogleGenerativeConfig>
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const groqConfigSchema = z.object({
|
||||
@@ -46,7 +47,7 @@ export const providerGroq = defineProvider<GroqConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['model_list'],
|
||||
checks: [ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createChatProvider, createEmbedProvider, createModelProvider, merge } from '@xsai-ext/providers/utils'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const lmStudioConfigSchema = z.object({
|
||||
@@ -53,7 +54,7 @@ export const providerLmStudio = defineProvider<LMStudioConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
skipApiKeyCheck: true,
|
||||
schedule: {
|
||||
mode: 'interval',
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { ModelInfo } from '../../types'
|
||||
import { createMinimax, createMinimaxCn } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const minimaxCnConfigSchema = z.object({
|
||||
@@ -114,7 +115,7 @@ export const providerMinimax = defineProvider<MinimaxCnConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity'],
|
||||
checks: [ProviderValidationCheck.Connectivity],
|
||||
}),
|
||||
},
|
||||
})
|
||||
@@ -154,7 +155,7 @@ export const providerMinimaxGlobal = defineProvider<MinimaxGlobalConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity'],
|
||||
checks: [ProviderValidationCheck.Connectivity],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createMistral } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const mistralConfigSchema = z.object({
|
||||
@@ -47,7 +48,7 @@ export const providerMistralAI = defineProvider<MistralConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const modelscopeConfigSchema = z.object({
|
||||
@@ -47,7 +48,7 @@ export const providerModelScope = defineProvider<ModelscopeConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createMoonshotai } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const moonshotConfigSchema = z.object({
|
||||
@@ -46,7 +47,7 @@ export const providerMoonshotAI = defineProvider<MoonshotConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const n1nConfigSchema = z.object({
|
||||
@@ -48,7 +49,7 @@ export const providerN1N = defineProvider<N1NConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list', 'chat_completions'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList, ProviderValidationCheck.ChatCompletions],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createNovita } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const novitaConfigSchema = z.object({
|
||||
@@ -47,7 +48,7 @@ export const providerNovitaAI = defineProvider<NovitaConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,7 +2,8 @@ import { isStageTamagotchi } from '@proj-airi/stage-shared'
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const nvidiaConfigSchema = z.object({
|
||||
@@ -48,7 +49,7 @@ export const providerNvidia = defineProvider<NvidiaConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createOllama } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
@@ -166,7 +167,7 @@ export const providerOllama = defineProvider<OllamaConfig>({
|
||||
}),
|
||||
],
|
||||
validateProvider: createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
schedule: {
|
||||
mode: 'interval',
|
||||
intervalMs: 15_000,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const openAICompatibleConfigSchema = z.object({
|
||||
@@ -48,7 +49,7 @@ export const providerOpenAICompatible = defineProvider<OpenAICompatibleConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list', 'chat_completions'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList, ProviderValidationCheck.ChatCompletions],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createOpenAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const openAICompatibleConfigSchema = z.object({
|
||||
@@ -47,7 +48,7 @@ export const providerOpenAI = defineProvider<OpenAICompatibleConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list', 'chat_completions'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList, ProviderValidationCheck.ChatCompletions],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createOpenRouter } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
export const OPENROUTER_ATTRIBUTION_HEADERS: Record<string, string> = {
|
||||
@@ -64,7 +65,7 @@ export const providerOpenRouterAI = defineProvider<OpenRouterConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
additionalHeaders: OPENROUTER_ATTRIBUTION_HEADERS,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -3,7 +3,8 @@ import type { ModelInfo } from '../../types'
|
||||
import { createPerplexity } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const perplexityConfigSchema = z.object({
|
||||
@@ -83,7 +84,7 @@ export const providerPerplexityAI = defineProvider<PerplexityConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity'],
|
||||
checks: [ProviderValidationCheck.Connectivity],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createTogetherAI } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const togetherConfigSchema = z.object({
|
||||
@@ -47,7 +48,7 @@ export const providerTogetherAI = defineProvider<TogetherConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createXai } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const xaiConfigSchema = z.object({
|
||||
@@ -46,7 +47,7 @@ export const providerXAI = defineProvider<XAIConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createZai } from '@xsai-ext/providers/create'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { createOpenAICompatibleValidators } from '../../validators/openai-compatible'
|
||||
import { ProviderValidationCheck } from '../../types'
|
||||
import { createOpenAICompatibleValidators } from '../../validators'
|
||||
import { defineProvider } from '../registry'
|
||||
|
||||
const zaiConfigSchema = z.object({
|
||||
@@ -46,7 +47,7 @@ export const providerZai = defineProvider<ZaiConfig>({
|
||||
},
|
||||
validators: {
|
||||
...createOpenAICompatibleValidators({
|
||||
checks: ['connectivity', 'model_list'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -48,6 +48,29 @@ export interface ProviderValidationResult {
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Validator ID fragment for the chat completions probe.
|
||||
* Matched via `.includes()` against validator instance ids
|
||||
* (e.g. `openai-compatible:check-chat-completions`).
|
||||
*/
|
||||
export const CHAT_COMPLETIONS_VALIDATOR_ID = 'check-chat-completions'
|
||||
|
||||
export enum ProviderValidationCheck {
|
||||
/** Lightweight GET to /models endpoint to check reachability (definition system) */
|
||||
Connectivity = 'connectivity',
|
||||
/** Fetch model list and verify non-empty */
|
||||
ModelList = 'model_list',
|
||||
/** Send generateText ping with fine-grained error handling and caching (definition system) */
|
||||
ChatCompletions = 'chat_completions',
|
||||
/**
|
||||
* @deprecated
|
||||
* Being used in builder system (a deprecated provider creation protocol),
|
||||
* currently used by only OpenAI TTS && OpenAI Transcription.
|
||||
* Send generateText ping with simple pass/fail, fallback to 'test' model (builder system)
|
||||
*/
|
||||
Health = 'health',
|
||||
}
|
||||
|
||||
export interface ProviderValidatorSchedule {
|
||||
mode: 'once' | 'interval'
|
||||
intervalMs?: number
|
||||
@@ -65,12 +88,6 @@ export interface ProviderRuntimeValidator<TConfig> {
|
||||
name: string
|
||||
validator: (config: TConfig, provider: ProviderInstance, providerExtra: ProviderExtraMethods<TConfig>, contextOptions: { t: ComposerTranslation }) => MaybePromise<ProviderValidationResult>
|
||||
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 {
|
||||
@@ -163,6 +180,16 @@ export interface ProviderDefinition<TConfig extends any = any> {
|
||||
streamInput: boolean
|
||||
}
|
||||
}
|
||||
/**
|
||||
* When true, hides the "skip chat ping check" checkbox in the UI even
|
||||
* when the provider defines a ChatCompletions validator.
|
||||
*
|
||||
* By default, the checkbox is shown automatically whenever a provider
|
||||
* includes a ChatCompletions runtime validator. Set this to `true` for
|
||||
* providers where skipping that check is not meaningful or has not been
|
||||
* verified yet.
|
||||
*/
|
||||
disableChatPingCheckUI?: boolean
|
||||
business?: (contextOptions: { t: ComposerTranslation }) => {
|
||||
troubleshooting?: {
|
||||
validators?: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ProviderExtraMethods, ProviderInstance } from '../types'
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { ProviderValidationCheck } from '../types'
|
||||
import { createOpenAICompatibleValidators } from './openai-compatible'
|
||||
|
||||
const {
|
||||
@@ -59,7 +60,7 @@ describe('createOpenAICompatibleValidators', () => {
|
||||
|
||||
it('connectivity check uses lightweight fetch instead of generateText', async () => {
|
||||
const [connectivityValidator] = getProviderValidators({
|
||||
checks: ['connectivity'],
|
||||
checks: [ProviderValidationCheck.Connectivity],
|
||||
})
|
||||
|
||||
const result = await connectivityValidator.validator(config, provider, providerExtra, { t: mockT })
|
||||
@@ -76,7 +77,7 @@ describe('createOpenAICompatibleValidators', () => {
|
||||
fetchMock.mockRejectedValue(new TypeError('fetch failed'))
|
||||
|
||||
const [connectivityValidator] = getProviderValidators({
|
||||
checks: ['connectivity'],
|
||||
checks: [ProviderValidationCheck.Connectivity],
|
||||
})
|
||||
|
||||
const result = await connectivityValidator.validator(config, provider, providerExtra, { t: mockT })
|
||||
@@ -90,7 +91,7 @@ describe('createOpenAICompatibleValidators', () => {
|
||||
listModelsMock.mockResolvedValue([])
|
||||
|
||||
const [connectivityValidator, chatValidator] = getProviderValidators({
|
||||
checks: ['connectivity', 'chat_completions'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ChatCompletions],
|
||||
})
|
||||
|
||||
const connectivityResult = await connectivityValidator.validator(config, provider, providerExtra, { t: mockT })
|
||||
@@ -106,7 +107,7 @@ describe('createOpenAICompatibleValidators', () => {
|
||||
listModelsMock.mockResolvedValue([])
|
||||
|
||||
const [connectivityValidator, chatValidator] = getProviderValidators({
|
||||
checks: ['connectivity', 'chat_completions'],
|
||||
checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ChatCompletions],
|
||||
allowValidationWithoutModel: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -8,12 +8,10 @@ import { listModels } from '@xsai/model'
|
||||
import { message } from '@xsai/utils-chat'
|
||||
import { Mutex } from 'es-toolkit'
|
||||
|
||||
import { isModelProvider } from '../types'
|
||||
|
||||
type OpenAICompatibleValidationCheck = 'connectivity' | 'model_list' | 'chat_completions'
|
||||
import { isModelProvider, ProviderValidationCheck } from '../types'
|
||||
|
||||
interface OpenAICompatibleValidationOptions<TConfig extends { apiKey?: string, baseUrl?: string }> {
|
||||
checks?: OpenAICompatibleValidationCheck[]
|
||||
checks?: ProviderValidationCheck[]
|
||||
additionalHeaders?: Record<string, string>
|
||||
allowValidationWithoutModel?: boolean
|
||||
schedule?: {
|
||||
@@ -103,7 +101,7 @@ async function pickValidationModel<TConfig extends { apiKey?: string | null, bas
|
||||
export function createOpenAICompatibleValidators<TConfig extends { apiKey?: string, baseUrl?: string }>(
|
||||
options?: OpenAICompatibleValidationOptions<TConfig>,
|
||||
): ProviderDefinition<TConfig>['validators'] {
|
||||
const checks = options?.checks ?? ['connectivity', 'model_list']
|
||||
const checks = options?.checks ?? [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList]
|
||||
const additionalHeaders = options?.additionalHeaders
|
||||
const missingValidationModelReason = 'No model available for validation. Configure a model manually and try again.'
|
||||
|
||||
@@ -235,7 +233,7 @@ export function createOpenAICompatibleValidators<TConfig extends { apiKey?: stri
|
||||
},
|
||||
}))
|
||||
|
||||
if (checks.includes('connectivity')) {
|
||||
if (checks.includes(ProviderValidationCheck.Connectivity)) {
|
||||
validatorConfig.validateProvider?.push(({ t }) => ({
|
||||
id: 'openai-compatible:check-connectivity',
|
||||
name: t('settings.pages.providers.catalog.edit.validators.openai-compatible.check-connectivity.title'),
|
||||
@@ -286,12 +284,11 @@ export function createOpenAICompatibleValidators<TConfig extends { apiKey?: stri
|
||||
}))
|
||||
}
|
||||
|
||||
if (checks.includes('chat_completions')) {
|
||||
if (checks.includes(ProviderValidationCheck.ChatCompletions)) {
|
||||
validatorConfig.validateProvider?.push(({ t }) => ({
|
||||
id: 'openai-compatible:check-chat-completions',
|
||||
name: t('settings.pages.providers.catalog.edit.validators.openai-compatible.check-supports-chat-completion.title'),
|
||||
schedule: options?.schedule,
|
||||
manualOnly: true,
|
||||
validator: async (config, provider, providerExtra, contextOptions) => {
|
||||
const errors: Array<{ error: unknown }> = []
|
||||
const result = await getChatCheckResult(
|
||||
@@ -314,7 +311,7 @@ export function createOpenAICompatibleValidators<TConfig extends { apiKey?: stri
|
||||
}))
|
||||
}
|
||||
|
||||
if (checks.includes('model_list')) {
|
||||
if (checks.includes(ProviderValidationCheck.ModelList)) {
|
||||
validatorConfig.validateProvider?.push(({ t }) => ({
|
||||
id: 'openai-compatible:check-model-list',
|
||||
name: t('settings.pages.providers.catalog.edit.validators.openai-compatible.check-supports-model-listing.title'),
|
||||
|
||||
@@ -26,8 +26,6 @@ export interface ProviderValidationPlan {
|
||||
definition: ProviderDefinition
|
||||
configValidators: ProviderConfigValidator<Record<string, unknown>>[]
|
||||
providerValidators: ProviderRuntimeValidator<Record<string, unknown>>[]
|
||||
/** Provider validators that require explicit user action (e.g. "Test Generation" button). */
|
||||
manualProviderValidators: ProviderRuntimeValidator<Record<string, unknown>>[]
|
||||
providerExtra: ProviderExtraMethods<Record<string, unknown>> | undefined
|
||||
shouldValidate: boolean
|
||||
}
|
||||
@@ -87,9 +85,7 @@ export function getValidatorsOfProvider(options: {
|
||||
const configValidators = (definition.validators?.validateConfig || []).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 providerValidators = allProviderValidators
|
||||
|
||||
const steps: ProviderValidationStep[] = [
|
||||
...createConfigValidationSteps(configValidators),
|
||||
@@ -106,7 +102,6 @@ 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,
|
||||
}
|
||||
@@ -189,66 +184,3 @@ export async function validateProvider(
|
||||
|
||||
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<string, unknown>(),
|
||||
}
|
||||
const { onValidatorError, onValidatorStart, onValidatorSuccess } = callbacks
|
||||
|
||||
let providerInstance: ProviderInstance
|
||||
try {
|
||||
providerInstance = await definition.createProvider(config)
|
||||
}
|
||||
catch (error) {
|
||||
return manualProviderValidators.map(v => ({
|
||||
id: v.id,
|
||||
label: v.name,
|
||||
status: 'invalid' as ProviderValidationStepStatus,
|
||||
reason: errorMessageFrom(error) ?? 'Unknown 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, 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) ?? 'Unknown error'
|
||||
onValidatorError?.({ kind: 'provider', index, step, error })
|
||||
}
|
||||
}))
|
||||
|
||||
return steps
|
||||
}
|
||||
|
||||
@@ -47,8 +47,7 @@ import {
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { listProviders as listDefinedProviders } from '../libs/providers'
|
||||
import { getProviderValidationIntervalMs } from '../libs/providers/validators/run'
|
||||
import { getProviderValidationIntervalMs, listProviders as listDefinedProviders, ProviderValidationCheck } from '../libs/providers'
|
||||
import { getKokoroWorker } from '../workers/kokoro'
|
||||
import { getDefaultKokoroModel, KOKORO_MODELS, kokoroModelsToModelInfo } from '../workers/kokoro/constants'
|
||||
import { useAuthStore } from './auth'
|
||||
@@ -140,7 +139,14 @@ export interface ProviderMetadata {
|
||||
loadModel?: (config: Record<string, unknown>, hooks?: { onProgress?: (progress: ProgressInfo) => Promise<void> | void }) => Promise<void>
|
||||
}
|
||||
validators: {
|
||||
validateProviderConfig: (config: Record<string, unknown>) => Promise<{
|
||||
/**
|
||||
* Validate a provider's configuration.
|
||||
*
|
||||
* PITFALL: When `skipChatPingCheck` is not set, the ChatCompletions validator
|
||||
* (if present) may send a real `generateText("ping")` request that consumes
|
||||
* API tokens. All automatic/background callers may consider pass `skipChatPingCheck: true`.
|
||||
*/
|
||||
validateProviderConfig: (config: Record<string, unknown>, options?: { skipChatPingCheck?: boolean, onlyChatPingCheck?: boolean }) => Promise<{
|
||||
errors: unknown[]
|
||||
reason: string
|
||||
valid: boolean
|
||||
@@ -150,14 +156,12 @@ export interface ProviderMetadata {
|
||||
valid: boolean
|
||||
}
|
||||
/**
|
||||
* Run only the manual-only validators. Returns validation result.
|
||||
* Only available when the provider has manual validators.
|
||||
* Whether the "skip chat ping check" checkbox should be shown in the UI.
|
||||
*
|
||||
* Automatically derived: `true` when the provider has a ChatCompletions
|
||||
* runtime validator AND `disableChatPingCheckUI` is not set on the definition.
|
||||
*/
|
||||
runManualValidation?: (config: Record<string, unknown>) => Promise<{
|
||||
errors: unknown[]
|
||||
reason: string
|
||||
valid: boolean
|
||||
}>
|
||||
chatPingCheckAvailable: boolean
|
||||
}
|
||||
/**
|
||||
* If true, the provider does not require user-provided credentials (e.g. API keys).
|
||||
@@ -277,6 +281,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
listVoices: async () => [],
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: () => ({
|
||||
errors: [],
|
||||
reason: '',
|
||||
@@ -297,6 +302,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
creator: createOpenAI,
|
||||
validation: [],
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
if (!config.baseUrl) {
|
||||
return {
|
||||
@@ -327,6 +333,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
creator: createOpenAI,
|
||||
validation: [],
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
if (!config.baseUrl) {
|
||||
return {
|
||||
@@ -357,6 +364,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
creator: createOpenAI,
|
||||
validation: [],
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
if (!config.baseUrl) {
|
||||
return {
|
||||
@@ -387,6 +395,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
creator: createOpenAI,
|
||||
validation: [],
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
if (!config.baseUrl) {
|
||||
return {
|
||||
@@ -415,7 +424,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
tasks: ['text-to-speech'],
|
||||
defaultBaseUrl: 'https://api.openai.com/v1/',
|
||||
creator: createOpenAI,
|
||||
validation: ['health'],
|
||||
validation: [ProviderValidationCheck.Health],
|
||||
capabilities: {
|
||||
// NOTE: OpenAI does not provide an API endpoint to retrieve available voices.
|
||||
// Voices are hardcoded here - this is a provider limitation, not an application limitation.
|
||||
@@ -560,6 +569,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
const errors = [
|
||||
!config.apiKey && new Error('API Key is required'),
|
||||
@@ -646,7 +656,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
tasks: ['speech-to-text', 'automatic-speech-recognition', 'asr', 'stt'],
|
||||
defaultBaseUrl: 'https://api.openai.com/v1/',
|
||||
creator: createOpenAI,
|
||||
validation: ['health'],
|
||||
validation: [ProviderValidationCheck.Health],
|
||||
capabilities: {
|
||||
listModels: async () => {
|
||||
// OpenAI transcription models are hardcoded (no API endpoint to list them)
|
||||
@@ -695,6 +705,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
const errors = [
|
||||
!config.apiKey && new Error('API Key is required'),
|
||||
@@ -796,6 +807,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
const errors: Error[] = []
|
||||
const toString = (value: unknown) => typeof value === 'string' ? value.trim() : ''
|
||||
@@ -875,6 +887,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: () => {
|
||||
// Web Speech API requires no configuration, just browser support
|
||||
// Always return valid if browser supports it, so it auto-configures
|
||||
@@ -968,6 +981,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
const errors = [
|
||||
!config.apiKey && new Error('API key is required.'),
|
||||
@@ -1052,6 +1066,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
const errors: Error[] = []
|
||||
if (!config.apiKey) {
|
||||
@@ -1117,6 +1132,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
const errors = [
|
||||
!config.apiKey && new Error('API key is required.'),
|
||||
@@ -1193,6 +1209,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: async (config) => {
|
||||
const errors = [
|
||||
!config.baseUrl && new Error('Base URL is required. Default to http://localhost:11996/tts/ for Index-TTS.'),
|
||||
@@ -1282,6 +1299,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
const errors = [
|
||||
!config.apiKey && new Error('API key is required.'),
|
||||
@@ -1347,6 +1365,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
const errors = [
|
||||
!config.apiKey && new Error('API key is required.'),
|
||||
@@ -1382,7 +1401,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
createModelProvider({ apiKey, baseURL }),
|
||||
createSpeechProvider({ apiKey, baseURL }),
|
||||
),
|
||||
validation: ['model_list'],
|
||||
validation: [ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
'comet-api-transcription': buildOpenAICompatibleProvider({
|
||||
id: 'comet-api-transcription',
|
||||
@@ -1398,7 +1417,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
createModelProvider({ apiKey, baseURL }),
|
||||
createTranscriptionProvider({ apiKey, baseURL }),
|
||||
),
|
||||
validation: ['model_list'],
|
||||
validation: [ProviderValidationCheck.ModelList],
|
||||
}),
|
||||
'player2-speech': {
|
||||
id: 'player2-speech',
|
||||
@@ -1481,6 +1500,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: async (config) => {
|
||||
const errors = [
|
||||
!config.baseUrl && new Error('Base URL is required. Default to http://localhost:4315/v1/'),
|
||||
@@ -1681,6 +1701,7 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
},
|
||||
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: async (config: any) => {
|
||||
const model = config.model as string
|
||||
|
||||
@@ -1803,7 +1824,12 @@ export const useProvidersStore = defineStore('providers', () => {
|
||||
}
|
||||
|
||||
const runValidation = async () => {
|
||||
const validationResult = await metadata.validators.validateProviderConfig(config || {})
|
||||
// PITFALL: Please consider skip chat ping check during automatic/background validation,
|
||||
// since this can consume API tokens and may only be triggered
|
||||
// by user action (e.g. "Ping API" button on settings pages) or other user intentions.
|
||||
const validationResult = await metadata.validators.validateProviderConfig(config || {}, {
|
||||
skipChatPingCheck: true,
|
||||
})
|
||||
|
||||
if (providerRuntimeState.value[providerId]) {
|
||||
providerRuntimeState.value[providerId].isConfigured = validationResult.valid
|
||||
|
||||
@@ -99,7 +99,7 @@ describe('providers converters', () => {
|
||||
} as any
|
||||
|
||||
const metadata = convertProviderDefinitionToMetadata(definition, ((key: string) => key) as any)
|
||||
const result = await metadata.validators.validateProviderConfig({ apiKey: 'k' })
|
||||
const result = await metadata.validators.validateProviderConfig({ apiKey: 'k' }, { skipChatPingCheck: true })
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.reason).toContain('Base URL is required.')
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { ComposerTranslation } from 'vue-i18n'
|
||||
|
||||
import type { ProviderDefinition } from '../../libs/providers/types'
|
||||
import type { ProviderValidationPlan } from '../../libs/providers/validators/run'
|
||||
import type { ProviderDefinition, ProviderValidationPlan } from '../../libs'
|
||||
import type { ProviderMetadata } from '../providers'
|
||||
|
||||
import { listModels } from '@xsai/model'
|
||||
|
||||
import { isModelProvider } from '../../libs/providers/types'
|
||||
import { getValidatorsOfProvider, validateProvider, validateProviderManual } from '../../libs/providers/validators/run'
|
||||
import { CHAT_COMPLETIONS_VALIDATOR_ID, isModelProvider } from '../../libs/providers/types'
|
||||
import { getValidatorsOfProvider, validateProvider } 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,9 +99,6 @@ 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,
|
||||
order: definition.order,
|
||||
@@ -187,7 +183,36 @@ export function convertProviderDefinitionToMetadata(
|
||||
: undefined,
|
||||
},
|
||||
validators: {
|
||||
validateProviderConfig: async (config) => {
|
||||
chatPingCheckAvailable: !definition.disableChatPingCheckUI
|
||||
&& (definition.validators?.validateProvider || [])
|
||||
.some(creator => creator({ t }).id.includes(CHAT_COMPLETIONS_VALIDATOR_ID)),
|
||||
validateProviderConfig: async (config, options) => {
|
||||
// onlyChatPingCheck: skip all validators except chat completions.
|
||||
// Used by the manual "Test Generation" button on settings pages.
|
||||
if (options?.onlyChatPingCheck) {
|
||||
const plan = getValidatorsOfProvider({
|
||||
definition,
|
||||
config,
|
||||
schemaDefaults,
|
||||
contextOptions: { t },
|
||||
})
|
||||
plan.configValidators = []
|
||||
plan.providerValidators = plan.providerValidators.filter(v => v.id.includes(CHAT_COMPLETIONS_VALIDATOR_ID))
|
||||
plan.steps = plan.steps.filter(s => s.id.includes(CHAT_COMPLETIONS_VALIDATOR_ID))
|
||||
|
||||
if (plan.providerValidators.length === 0) {
|
||||
return { errors: [], reason: '', valid: true }
|
||||
}
|
||||
|
||||
await validateProvider(plan, { t })
|
||||
const invalidSteps = plan.steps.filter(step => step.status === 'invalid')
|
||||
return {
|
||||
errors: invalidSteps.map(step => new Error(step.reason || `${step.id} is invalid`)),
|
||||
reason: invalidSteps.map(step => step.reason).filter(Boolean).join('; '),
|
||||
valid: invalidSteps.length === 0,
|
||||
}
|
||||
}
|
||||
|
||||
const plan = getValidatorsOfProvider({
|
||||
definition,
|
||||
config,
|
||||
@@ -195,10 +220,13 @@ export function convertProviderDefinitionToMetadata(
|
||||
contextOptions: { t },
|
||||
})
|
||||
|
||||
if (options?.skipChatPingCheck) {
|
||||
plan.providerValidators = plan.providerValidators.filter(v => !v.id.includes(CHAT_COMPLETIONS_VALIDATOR_ID))
|
||||
plan.steps = plan.steps.filter(s => !s.id.includes(CHAT_COMPLETIONS_VALIDATOR_ID))
|
||||
}
|
||||
|
||||
// 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')
|
||||
@@ -235,26 +263,6 @@ 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
|
||||
? {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { generateText } from '@xsai/generate-text'
|
||||
import { listModels } from '@xsai/model'
|
||||
import { message } from '@xsai/utils-chat'
|
||||
|
||||
import { ProviderValidationCheck } from '../../libs/providers'
|
||||
|
||||
type ProviderCreator = (apiKey: string, baseUrl: string) => any
|
||||
|
||||
// Lightweight normalization utilities and conditional logging
|
||||
@@ -47,7 +49,7 @@ export function buildOpenAICompatibleProvider(
|
||||
creator: ProviderCreator
|
||||
capabilities?: ProviderMetadata['capabilities']
|
||||
validators?: ProviderMetadata['validators']
|
||||
validation?: ('health' | 'model_list' | 'chat_completions')[]
|
||||
validation?: ProviderValidationCheck[]
|
||||
additionalHeaders?: Record<string, string>
|
||||
transcriptionFeatures?: ProviderMetadata['transcriptionFeatures']
|
||||
},
|
||||
@@ -114,6 +116,7 @@ export function buildOpenAICompatibleProvider(
|
||||
}
|
||||
|
||||
const finalValidators = validators || {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: async (config: Record<string, unknown>) => {
|
||||
const errors: Error[] = []
|
||||
let baseUrl = normalizeString(config.baseUrl)
|
||||
@@ -191,7 +194,7 @@ export function buildOpenAICompatibleProvider(
|
||||
|
||||
// Health check = try generating text (was: fetch(`${baseUrl}chat/completions`))
|
||||
const asyncChecks: Promise<Error | null>[] = []
|
||||
if (validationChecks.includes('health') && hasApiKey) {
|
||||
if (validationChecks.includes(ProviderValidationCheck.Health) && hasApiKey) {
|
||||
asyncChecks.push((async () => {
|
||||
try {
|
||||
const model = await modelPromise
|
||||
@@ -212,7 +215,7 @@ export function buildOpenAICompatibleProvider(
|
||||
}
|
||||
|
||||
// Model list validation (was: fetch(`${baseUrl}models`))
|
||||
if (validationChecks.includes('model_list') && hasApiKey) {
|
||||
if (validationChecks.includes(ProviderValidationCheck.ModelList) && hasApiKey) {
|
||||
asyncChecks.push((async () => {
|
||||
try {
|
||||
const models = await listModels({
|
||||
|
||||
@@ -250,6 +250,7 @@ export function buildOpenRouterAudioSpeechProvider(
|
||||
listVoices: async () => listVoices(),
|
||||
},
|
||||
validators: {
|
||||
chatPingCheckAvailable: false,
|
||||
validateProviderConfig: (config) => {
|
||||
const errors: Error[] = []
|
||||
if (!config.apiKey)
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { Checkbox } from '../checkbox'
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
label?: string
|
||||
description?: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
/** Controls whether the switch is placed on the left or right side of the label. */
|
||||
placement?: 'left' | 'right'
|
||||
}>(), {
|
||||
placement: 'right',
|
||||
})
|
||||
|
||||
const modelValue = defineModel<boolean>({ required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="flex flex-col gap-4">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div :class="['flex items-center gap-2', props.placement === 'left' ? 'flex-row-reverse' : 'flex-row']">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-1 text-sm font-medium">
|
||||
<slot name="label">
|
||||
|
||||
Reference in New Issue
Block a user