fix: complete channel model batch testing (#5532)

This commit is contained in:
yyhhyyyyyy
2026-06-17 20:04:58 +08:00
committed by GitHub
parent 502858d35d
commit a2f3ac02e4
7 changed files with 300 additions and 51 deletions
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { type ChangeEvent, useCallback, useMemo, useState } from 'react'
import { type ChangeEvent, useCallback, useMemo, useRef, useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import {
type ColumnDef,
@@ -32,6 +32,7 @@ import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Progress } from '@/components/ui/progress'
import {
Select,
SelectContent,
@@ -103,6 +104,13 @@ type TestResult = {
errorCode?: string
}
type BatchProgress = {
total: number
completed: number
success: number
failed: number
}
type ChannelTestCachePatch = {
responseTime: number
testTime: number
@@ -181,6 +189,8 @@ const STREAM_INCOMPATIBLE_ENDPOINTS = new Set([
const MODEL_PRICE_ERROR_CODE = 'model_price_error'
const FAILURE_SUMMARY_MAX_LENGTH = 96
const BATCH_TEST_CONCURRENCY = 5
const BATCH_TEST_DELAY_MS = 100
type FailureStatusDisplay = {
summary: string
@@ -193,6 +203,10 @@ type FailureDetailsState = {
details: string
}
function sleep(ms: number) {
return new Promise<void>((resolve) => window.setTimeout(resolve, ms))
}
function normalizeInlineError(errorText: string) {
return errorText.replace(/\s+/g, ' ').trim()
}
@@ -289,6 +303,7 @@ function ChannelTestDialogContent({
const { t } = useTranslation()
const queryClient = useQueryClient()
const currentChannelId = currentRow.id
const batchStopRequestedRef = useRef(false)
const [endpointType, setEndpointType] = useState('auto')
const [isStreamTest, setIsStreamTest] = useState(false)
const [searchTerm, setSearchTerm] = useState('')
@@ -298,6 +313,8 @@ function ChannelTestDialogContent({
() => new Set()
)
const [isBatchTesting, setIsBatchTesting] = useState(false)
const [isBatchStopRequested, setIsBatchStopRequested] = useState(false)
const [batchProgress, setBatchProgress] = useState<BatchProgress | null>(null)
const [failureDetails, setFailureDetails] =
useState<FailureDetailsState | null>(null)
const [pagination, setPagination] = useState({
@@ -314,6 +331,7 @@ function ChannelTestDialogContent({
)
const resetState = useCallback(() => {
batchStopRequestedRef.current = true
setEndpointType('auto')
setIsStreamTest(false)
setSearchTerm('')
@@ -321,6 +339,8 @@ function ChannelTestDialogContent({
setRowSelection({})
setTestingModels(() => new Set())
setIsBatchTesting(false)
setIsBatchStopRequested(false)
setBatchProgress(null)
setFailureDetails(null)
setPagination({ pageIndex: 0, pageSize: 10 })
}, [])
@@ -501,29 +521,118 @@ function ChannelTestDialogContent({
]
)
const handleStopBatchTest = useCallback(() => {
if (!isBatchTesting || isBatchStopRequested) return
batchStopRequestedRef.current = true
setIsBatchStopRequested(true)
}, [isBatchStopRequested, isBatchTesting])
const handleBatchTest = useCallback(
async (modelsToTest: string[]) => {
if (!modelsToTest.length) return
const uniqueModels = Array.from(
new Set(modelsToTest.map((model) => model.trim()).filter(Boolean))
)
if (!uniqueModels.length) return
batchStopRequestedRef.current = false
setIsBatchTesting(true)
setIsBatchStopRequested(false)
setBatchProgress({
total: uniqueModels.length,
completed: 0,
success: 0,
failed: 0,
})
let resultPatch: ChannelTestCachePatch | undefined
const results: TestResult[] = []
let completedCount = 0
let successCount = 0
let failedCount = 0
try {
const settled = await Promise.allSettled(
modelsToTest.map((modelName) =>
testSingleModel(modelName, true, false)
const createFallbackResult = (error?: unknown): TestResult => ({
status: 'error',
completedAt: Date.now(),
error: error instanceof Error ? error.message : t('Test failed'),
})
const recordBatchResult = (result: TestResult) => {
results.push(result)
completedCount += 1
if (result.status === 'success') {
successCount += 1
}
failedCount = completedCount - successCount
setBatchProgress({
total: uniqueModels.length,
completed: completedCount,
success: successCount,
failed: failedCount,
})
}
for (
let startIndex = 0;
startIndex < uniqueModels.length;
startIndex += BATCH_TEST_CONCURRENCY
) {
if (batchStopRequestedRef.current) {
break
}
const batch = uniqueModels.slice(
startIndex,
startIndex + BATCH_TEST_CONCURRENCY
)
)
const results = settled
.map((result) =>
result.status === 'fulfilled' ? result.value : undefined
)
.filter((result): result is TestResult => Boolean(result))
const batchPromises = batch.map(async (modelName) => {
try {
const result = await testSingleModel(modelName, true, false)
const finalResult = result ?? createFallbackResult()
if (!result) {
updateTestResult(modelName, finalResult)
}
recordBatchResult(finalResult)
return finalResult
} catch (error: unknown) {
const fallbackResult = createFallbackResult(error)
updateTestResult(modelName, fallbackResult)
recordBatchResult(fallbackResult)
return fallbackResult
}
})
await Promise.allSettled(batchPromises)
if (
batchStopRequestedRef.current ||
startIndex + BATCH_TEST_CONCURRENCY >= uniqueModels.length
) {
break
}
await sleep(BATCH_TEST_DELAY_MS)
}
resultPatch = getLatestChannelTestCachePatch(results)
const successCount = results.filter(
(result) => result.status === 'success'
).length
const failedCount = modelsToTest.length - successCount
if (failedCount > 0) {
const stopped =
batchStopRequestedRef.current && completedCount < uniqueModels.length
if (stopped) {
toast.info(
t(
'Batch test stopped: {{completed}}/{{total}} completed, {{success}} succeeded, {{failed}} failed',
{
completed: completedCount,
total: uniqueModels.length,
success: successCount,
failed: failedCount,
}
)
)
} else if (failedCount > 0) {
toast.error(
t(
'Batch test completed: {{success}} succeeded, {{failed}} failed',
@@ -541,12 +650,15 @@ function ChannelTestDialogContent({
)
}
} finally {
batchStopRequestedRef.current = false
setIsBatchTesting(false)
setIsBatchStopRequested(false)
setBatchProgress(null)
setRowSelection({})
refreshChannelLists(resultPatch)
}
},
[refreshChannelLists, t, testSingleModel]
[refreshChannelLists, t, testSingleModel, updateTestResult]
)
const handleClose = useCallback(() => {
@@ -564,6 +676,10 @@ function ChannelTestDialogContent({
)
const isAnyTesting = testingModels.size > 0 || isBatchTesting
const isFilteringModels = searchTerm.trim().length > 0
const testAllButtonLabel = isFilteringModels
? t('Test {{count}} matching models', { count: filteredModels.length })
: t('Test all {{count}} models', { count: filteredModels.length })
const columns = useMemo<ColumnDef<ModelRow>[]>(
() => [
@@ -571,11 +687,11 @@ function ChannelTestDialogContent({
id: 'select',
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
indeterminate={table.getIsSomePageRowsSelected()}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
checked={table.getIsAllRowsSelected()}
indeterminate={
table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()
}
onCheckedChange={(value) => table.toggleAllRowsSelected(!!value)}
aria-label={t('Select all models')}
/>
),
@@ -674,6 +790,7 @@ function ChannelTestDialogContent({
rowSelection,
pagination,
enableRowSelection: true,
getRowId: (row) => row.model,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
withFilteredRowModel: false,
@@ -759,14 +876,41 @@ function ChannelTestDialogContent({
{t('Select models to run batch tests.')}
</p>
</div>
<Input
placeholder={t('Filter models...')}
value={searchTerm}
onChange={handleSearchTermChange}
className='sm:w-64'
/>
<div className='flex flex-col gap-2 sm:flex-row sm:items-center'>
<Input
placeholder={t('Filter models...')}
value={searchTerm}
onChange={handleSearchTermChange}
className='sm:w-64'
/>
{isBatchTesting ? (
<Button
variant='outline'
onClick={handleStopBatchTest}
disabled={isBatchStopRequested}
>
{isBatchStopRequested
? t('Stopping...')
: t('Stop testing')}
</Button>
) : (
<Button
onClick={() => handleBatchTest(filteredModels)}
disabled={isAnyTesting || filteredModels.length === 0}
>
{testAllButtonLabel}
</Button>
)}
</div>
</div>
{batchProgress && (
<BatchProgressSummary
progress={batchProgress}
isStopping={isBatchStopRequested}
/>
)}
<div className='space-y-3'>
<DataTableView
table={table}
@@ -827,6 +971,45 @@ function ChannelTestDialogContent({
)
}
function BatchProgressSummary({
progress,
isStopping,
}: {
progress: BatchProgress
isStopping: boolean
}) {
const { t } = useTranslation()
const progressValue =
progress.total > 0
? Math.min(100, Math.round((progress.completed / progress.total) * 100))
: 0
return (
<div className='bg-muted/30 flex flex-col gap-2 rounded-md border p-3'>
<div className='flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between'>
<p className='text-sm font-medium'>
{isStopping
? t('Stopping batch test...')
: t('Batch testing models...')}
</p>
<p className='text-muted-foreground text-xs tabular-nums'>
{t('{{completed}}/{{total}} completed', {
completed: progress.completed,
total: progress.total,
})}
</p>
</div>
<Progress value={progressValue} />
<p className='text-muted-foreground text-xs'>
{t('{{success}} succeeded, {{failed}} failed', {
success: progress.success,
failed: progress.failed,
})}
</p>
</div>
)
}
function TestStatusCell({
result,
model,