+
{{ manualTestMessage }}
diff --git a/packages/stage-ui/src/composables/use-provider-validation.ts b/packages/stage-ui/src/composables/use-provider-validation.ts
index 0fe1f49f2..5f013c5fe 100644
--- a/packages/stage-ui/src/composables/use-provider-validation.ts
+++ b/packages/stage-ui/src/composables/use-provider-validation.ts
@@ -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 }
diff --git a/packages/stage-ui/src/libs/providers/providers/302-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/302-ai/index.ts
index 6fcf9af69..cc458e69e 100644
--- a/packages/stage-ui/src/libs/providers/providers/302-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/302-ai/index.ts
@@ -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
({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['model_list'],
+ checks: [ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/aihubmix/index.ts b/packages/stage-ui/src/libs/providers/providers/aihubmix/index.ts
index 1584ab0cf..d5bd38150 100644
--- a/packages/stage-ui/src/libs/providers/providers/aihubmix/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/aihubmix/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['model_list'],
+ checks: [ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/anthropic/index.ts b/packages/stage-ui/src/libs/providers/providers/anthropic/index.ts
index ec0d9992f..032f92b81 100644
--- a/packages/stage-ui/src/libs/providers/providers/anthropic/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/anthropic/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ChatCompletions],
additionalHeaders: {
'anthropic-dangerous-direct-browser-access': 'true',
},
diff --git a/packages/stage-ui/src/libs/providers/providers/cerebras-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/cerebras-ai/index.ts
index aa0dd970f..bf541a4de 100644
--- a/packages/stage-ui/src/libs/providers/providers/cerebras-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/cerebras-ai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/comet-api/index.ts b/packages/stage-ui/src/libs/providers/providers/comet-api/index.ts
index 795c708c5..de9ba380b 100644
--- a/packages/stage-ui/src/libs/providers/providers/comet-api/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/comet-api/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['model_list'],
+ checks: [ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/deepseek/index.ts b/packages/stage-ui/src/libs/providers/providers/deepseek/index.ts
index f15076efc..cd48ce389 100644
--- a/packages/stage-ui/src/libs/providers/providers/deepseek/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/deepseek/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/featherless-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/featherless-ai/index.ts
index 8841d7023..9eec89474 100644
--- a/packages/stage-ui/src/libs/providers/providers/featherless-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/featherless-ai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/fireworks-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/fireworks-ai/index.ts
index 08131392e..8b69b5cf1 100644
--- a/packages/stage-ui/src/libs/providers/providers/fireworks-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/fireworks-ai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts
index 956853a99..f931095bc 100644
--- a/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/google-generative-ai/index.ts
@@ -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
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/groq/index.ts b/packages/stage-ui/src/libs/providers/providers/groq/index.ts
index 692d55b10..d0c362bd3 100644
--- a/packages/stage-ui/src/libs/providers/providers/groq/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/groq/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['model_list'],
+ checks: [ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/lm-studio/index.ts b/packages/stage-ui/src/libs/providers/providers/lm-studio/index.ts
index 776d0ccd4..f6f758790 100644
--- a/packages/stage-ui/src/libs/providers/providers/lm-studio/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/lm-studio/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
skipApiKeyCheck: true,
schedule: {
mode: 'interval',
diff --git a/packages/stage-ui/src/libs/providers/providers/minimax/index.ts b/packages/stage-ui/src/libs/providers/providers/minimax/index.ts
index aef84c793..11b03ab85 100644
--- a/packages/stage-ui/src/libs/providers/providers/minimax/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/minimax/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity'],
+ checks: [ProviderValidationCheck.Connectivity],
}),
},
})
@@ -154,7 +155,7 @@ export const providerMinimaxGlobal = defineProvider({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity'],
+ checks: [ProviderValidationCheck.Connectivity],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/mistral-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/mistral-ai/index.ts
index 947e44600..4d2518cae 100644
--- a/packages/stage-ui/src/libs/providers/providers/mistral-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/mistral-ai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/modelscope/index.ts b/packages/stage-ui/src/libs/providers/providers/modelscope/index.ts
index 138de26c2..d559f98ad 100644
--- a/packages/stage-ui/src/libs/providers/providers/modelscope/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/modelscope/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/moonshot-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/moonshot-ai/index.ts
index a2e5903fc..a3ee32e4c 100644
--- a/packages/stage-ui/src/libs/providers/providers/moonshot-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/moonshot-ai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/n1n/index.ts b/packages/stage-ui/src/libs/providers/providers/n1n/index.ts
index c6c74c936..3d0d6b7cd 100644
--- a/packages/stage-ui/src/libs/providers/providers/n1n/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/n1n/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list', 'chat_completions'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList, ProviderValidationCheck.ChatCompletions],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/novita-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/novita-ai/index.ts
index c04bf5305..6749ccd37 100644
--- a/packages/stage-ui/src/libs/providers/providers/novita-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/novita-ai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts b/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts
index e53ad5afd..2583f20dd 100644
--- a/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/nvidia/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/ollama/index.ts b/packages/stage-ui/src/libs/providers/providers/ollama/index.ts
index d5478da4b..bbc32312d 100644
--- a/packages/stage-ui/src/libs/providers/providers/ollama/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/ollama/index.ts
@@ -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({
}),
],
validateProvider: createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
schedule: {
mode: 'interval',
intervalMs: 15_000,
diff --git a/packages/stage-ui/src/libs/providers/providers/openai-compatible/index.ts b/packages/stage-ui/src/libs/providers/providers/openai-compatible/index.ts
index 1a7a80e2b..a5b68493c 100644
--- a/packages/stage-ui/src/libs/providers/providers/openai-compatible/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/openai-compatible/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list', 'chat_completions'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList, ProviderValidationCheck.ChatCompletions],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/openai/index.ts b/packages/stage-ui/src/libs/providers/providers/openai/index.ts
index 02b0b3539..2f073530d 100644
--- a/packages/stage-ui/src/libs/providers/providers/openai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/openai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list', 'chat_completions'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList, ProviderValidationCheck.ChatCompletions],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.ts
index de7847405..0233bf45b 100644
--- a/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/openrouter-ai/index.ts
@@ -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 = {
@@ -64,7 +65,7 @@ export const providerOpenRouterAI = defineProvider({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
additionalHeaders: OPENROUTER_ATTRIBUTION_HEADERS,
}),
},
diff --git a/packages/stage-ui/src/libs/providers/providers/perplexity-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/perplexity-ai/index.ts
index 84e741dbf..f9307ec2a 100644
--- a/packages/stage-ui/src/libs/providers/providers/perplexity-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/perplexity-ai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity'],
+ checks: [ProviderValidationCheck.Connectivity],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/together-ai/index.ts b/packages/stage-ui/src/libs/providers/providers/together-ai/index.ts
index a28992c26..6ef1a3f7f 100644
--- a/packages/stage-ui/src/libs/providers/providers/together-ai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/together-ai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/xai/index.ts b/packages/stage-ui/src/libs/providers/providers/xai/index.ts
index 42c3f69a0..2fcdaca23 100644
--- a/packages/stage-ui/src/libs/providers/providers/xai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/xai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/providers/zai/index.ts b/packages/stage-ui/src/libs/providers/providers/zai/index.ts
index 1b8b06e78..0e1cdfb82 100644
--- a/packages/stage-ui/src/libs/providers/providers/zai/index.ts
+++ b/packages/stage-ui/src/libs/providers/providers/zai/index.ts
@@ -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({
},
validators: {
...createOpenAICompatibleValidators({
- checks: ['connectivity', 'model_list'],
+ checks: [ProviderValidationCheck.Connectivity, ProviderValidationCheck.ModelList],
}),
},
})
diff --git a/packages/stage-ui/src/libs/providers/types.ts b/packages/stage-ui/src/libs/providers/types.ts
index d7897981e..ee93c6b58 100644
--- a/packages/stage-ui/src/libs/providers/types.ts
+++ b/packages/stage-ui/src/libs/providers/types.ts
@@ -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 {
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 {
@@ -163,6 +180,16 @@ export interface ProviderDefinition {
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?: {
diff --git a/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts b/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts
index b1fa16d55..6fa70c0e3 100644
--- a/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts
+++ b/packages/stage-ui/src/libs/providers/validators/openai-compatible.test.ts
@@ -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,
})
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 c56747564..9a953fd27 100644
--- a/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts
+++ b/packages/stage-ui/src/libs/providers/validators/openai-compatible.ts
@@ -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 {
- checks?: OpenAICompatibleValidationCheck[]
+ checks?: ProviderValidationCheck[]
additionalHeaders?: Record
allowValidationWithoutModel?: boolean
schedule?: {
@@ -103,7 +101,7 @@ async function pickValidationModel(
options?: OpenAICompatibleValidationOptions,
): ProviderDefinition['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 ({
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 ({
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 ({
id: 'openai-compatible:check-model-list',
name: t('settings.pages.providers.catalog.edit.validators.openai-compatible.check-supports-model-listing.title'),
diff --git a/packages/stage-ui/src/libs/providers/validators/run.ts b/packages/stage-ui/src/libs/providers/validators/run.ts
index 37b62ee92..f084f6d44 100644
--- a/packages/stage-ui/src/libs/providers/validators/run.ts
+++ b/packages/stage-ui/src/libs/providers/validators/run.ts
@@ -26,8 +26,6 @@ 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
}
@@ -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(),
- }
- 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
-}
diff --git a/packages/stage-ui/src/stores/providers.ts b/packages/stage-ui/src/stores/providers.ts
index ce14431e6..52012fcb6 100644
--- a/packages/stage-ui/src/stores/providers.ts
+++ b/packages/stage-ui/src/stores/providers.ts
@@ -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, hooks?: { onProgress?: (progress: ProgressInfo) => Promise | void }) => Promise
}
validators: {
- validateProviderConfig: (config: Record) => 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, 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) => 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
diff --git a/packages/stage-ui/src/stores/providers/converters.test.ts b/packages/stage-ui/src/stores/providers/converters.test.ts
index 448a0dc9f..cb262c62d 100644
--- a/packages/stage-ui/src/stores/providers/converters.test.ts
+++ b/packages/stage-ui/src/stores/providers/converters.test.ts
@@ -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.')
diff --git a/packages/stage-ui/src/stores/providers/converters.ts b/packages/stage-ui/src/stores/providers/converters.ts
index 07140c105..3ef4d067b 100644
--- a/packages/stage-ui/src/stores/providers/converters.ts
+++ b/packages/stage-ui/src/stores/providers/converters.ts
@@ -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
? {
diff --git a/packages/stage-ui/src/stores/providers/openai-compatible-builder.ts b/packages/stage-ui/src/stores/providers/openai-compatible-builder.ts
index 1dbbf373f..838426a85 100644
--- a/packages/stage-ui/src/stores/providers/openai-compatible-builder.ts
+++ b/packages/stage-ui/src/stores/providers/openai-compatible-builder.ts
@@ -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
transcriptionFeatures?: ProviderMetadata['transcriptionFeatures']
},
@@ -114,6 +116,7 @@ export function buildOpenAICompatibleProvider(
}
const finalValidators = validators || {
+ chatPingCheckAvailable: false,
validateProviderConfig: async (config: Record) => {
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[] = []
- 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({
diff --git a/packages/stage-ui/src/stores/providers/openrouter/audio-speech.ts b/packages/stage-ui/src/stores/providers/openrouter/audio-speech.ts
index 188d2f37f..ad1b7f22b 100644
--- a/packages/stage-ui/src/stores/providers/openrouter/audio-speech.ts
+++ b/packages/stage-ui/src/stores/providers/openrouter/audio-speech.ts
@@ -250,6 +250,7 @@ export function buildOpenRouterAudioSpeechProvider(
listVoices: async () => listVoices(),
},
validators: {
+ chatPingCheckAvailable: false,
validateProviderConfig: (config) => {
const errors: Error[] = []
if (!config.apiKey)
diff --git a/packages/ui/src/components/form/field/field-checkbox.vue b/packages/ui/src/components/form/field/field-checkbox.vue
index d7d8a2009..e8143b373 100644
--- a/packages/ui/src/components/form/field/field-checkbox.vue
+++ b/packages/ui/src/components/form/field/field-checkbox.vue
@@ -1,18 +1,22 @@
-