mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-12 00:49:14 +08:00
refactor: health check timeout (#8837)
* refactor: enable model edit/remove on checking * refactor: add timeout setting to healthcheck popup * fix: type
This commit is contained in:
parent
aac4adea1a
commit
63b126b530
@ -2990,6 +2990,7 @@
|
||||
"select_api_key": "Select the API key to use:",
|
||||
"single": "Single",
|
||||
"start": "Start",
|
||||
"timeout": "Timeout",
|
||||
"title": "Model health check",
|
||||
"use_all_keys": "Key(s)"
|
||||
},
|
||||
|
||||
@ -2990,6 +2990,7 @@
|
||||
"select_api_key": "使用するAPIキーを選択:",
|
||||
"single": "単一",
|
||||
"start": "開始",
|
||||
"timeout": "タイムアウト",
|
||||
"title": "モデル健康チェック",
|
||||
"use_all_keys": "キー"
|
||||
},
|
||||
|
||||
@ -2990,6 +2990,7 @@
|
||||
"select_api_key": "Выберите API ключ для использования:",
|
||||
"single": "Один",
|
||||
"start": "Начать",
|
||||
"timeout": "Тайм-аут",
|
||||
"title": "Проверка состояния моделей",
|
||||
"use_all_keys": "Использовать все ключи"
|
||||
},
|
||||
|
||||
@ -2990,6 +2990,7 @@
|
||||
"select_api_key": "选择要使用的 API 密钥:",
|
||||
"single": "单个",
|
||||
"start": "开始",
|
||||
"timeout": "超时",
|
||||
"title": "模型健康检测",
|
||||
"use_all_keys": "使用密钥"
|
||||
},
|
||||
|
||||
@ -2990,6 +2990,7 @@
|
||||
"select_api_key": "選擇要使用的 API 密鑰:",
|
||||
"single": "單個",
|
||||
"start": "開始",
|
||||
"timeout": "超時",
|
||||
"title": "模型健康檢查",
|
||||
"use_all_keys": "使用密鑰"
|
||||
},
|
||||
|
||||
@ -2,7 +2,7 @@ import { Box } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { maskApiKey } from '@renderer/utils/api'
|
||||
import { Button, Modal, Radio, Segmented, Space, Typography } from 'antd'
|
||||
import { Flex, InputNumber, Modal, Radio, Segmented, Typography } from 'antd'
|
||||
import { Alert } from 'antd'
|
||||
import { useCallback, useMemo, useReducer } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -17,6 +17,7 @@ interface ResolveData {
|
||||
apiKeys: string[]
|
||||
isConcurrent: boolean
|
||||
cancelled?: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
@ -31,6 +32,7 @@ type State = {
|
||||
selectedKeyIndex: number
|
||||
keyCheckMode: 'single' | 'all' // Whether to check with single key or all keys
|
||||
isConcurrent: boolean
|
||||
timeoutSeconds: number // Timeout in seconds
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,6 +43,7 @@ type Action =
|
||||
| { type: 'SET_KEY_INDEX'; payload: number }
|
||||
| { type: 'SET_KEY_CHECK_MODE'; payload: 'single' | 'all' }
|
||||
| { type: 'SET_CONCURRENT'; payload: boolean }
|
||||
| { type: 'SET_TIMEOUT_SECONDS'; payload: number }
|
||||
|
||||
/**
|
||||
* Reducer function to handle state updates
|
||||
@ -55,6 +58,8 @@ function reducer(state: State, action: Action): State {
|
||||
return { ...state, keyCheckMode: action.payload }
|
||||
case 'SET_CONCURRENT':
|
||||
return { ...state, isConcurrent: action.payload }
|
||||
case 'SET_TIMEOUT_SECONDS':
|
||||
return { ...state, timeoutSeconds: action.payload }
|
||||
default:
|
||||
return state
|
||||
}
|
||||
@ -69,6 +74,7 @@ function useModalActions(
|
||||
selectedKeyIndex: number,
|
||||
keyCheckMode: 'single' | 'all',
|
||||
isConcurrent: boolean,
|
||||
timeoutSeconds: number,
|
||||
dispatch: React.Dispatch<Action>
|
||||
) {
|
||||
const onStart = useCallback(() => {
|
||||
@ -78,11 +84,12 @@ function useModalActions(
|
||||
// Return config data
|
||||
resolve({
|
||||
apiKeys: keysToUse,
|
||||
isConcurrent
|
||||
isConcurrent,
|
||||
timeout: timeoutSeconds * 1000 // Convert seconds to milliseconds
|
||||
})
|
||||
|
||||
dispatch({ type: 'SET_OPEN', payload: false })
|
||||
}, [apiKeys, selectedKeyIndex, keyCheckMode, isConcurrent, resolve, dispatch])
|
||||
}, [apiKeys, selectedKeyIndex, keyCheckMode, isConcurrent, timeoutSeconds, resolve, dispatch])
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
dispatch({ type: 'SET_OPEN', payload: false })
|
||||
@ -106,10 +113,11 @@ const PopupContainer: React.FC<Props> = ({ title, apiKeys, resolve }) => {
|
||||
open: true,
|
||||
selectedKeyIndex: 0,
|
||||
keyCheckMode: 'all',
|
||||
isConcurrent: true
|
||||
isConcurrent: true,
|
||||
timeoutSeconds: 15
|
||||
})
|
||||
|
||||
const { open, selectedKeyIndex, keyCheckMode, isConcurrent } = state
|
||||
const { open, selectedKeyIndex, keyCheckMode, isConcurrent, timeoutSeconds } = state
|
||||
|
||||
// Use custom hooks
|
||||
const { onStart, onCancel, onClose } = useModalActions(
|
||||
@ -118,60 +126,92 @@ const PopupContainer: React.FC<Props> = ({ title, apiKeys, resolve }) => {
|
||||
selectedKeyIndex,
|
||||
keyCheckMode,
|
||||
isConcurrent,
|
||||
timeoutSeconds,
|
||||
dispatch
|
||||
)
|
||||
|
||||
// Check if we have multiple API keys
|
||||
const hasMultipleKeys = useMemo(() => apiKeys.length > 1, [apiKeys.length])
|
||||
|
||||
const renderFooter = useMemo(() => {
|
||||
return (
|
||||
<Flex vertical gap={10}>
|
||||
<Flex align="center" justify="space-between" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>{t('settings.models.check.use_all_keys')}:</Typography.Text>
|
||||
<Segmented
|
||||
value={keyCheckMode}
|
||||
onChange={(value) => dispatch({ type: 'SET_KEY_CHECK_MODE', payload: value as 'single' | 'all' })}
|
||||
size="small"
|
||||
options={[
|
||||
{ value: 'single', label: t('settings.models.check.single') },
|
||||
{ value: 'all', label: t('settings.models.check.all') }
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex align="center" justify="space-between" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>{t('settings.models.check.enable_concurrent')}:</Typography.Text>
|
||||
<Segmented
|
||||
value={isConcurrent ? 'enabled' : 'disabled'}
|
||||
onChange={(value) => dispatch({ type: 'SET_CONCURRENT', payload: value === 'enabled' })}
|
||||
size="small"
|
||||
options={[
|
||||
{ value: 'disabled', label: t('settings.models.check.disabled') },
|
||||
{ value: 'enabled', label: t('settings.models.check.enabled') }
|
||||
]}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex align="center" justify="space-between" style={{ width: '100%' }}>
|
||||
<Typography.Text strong>{t('settings.models.check.timeout')}:</Typography.Text>
|
||||
<InputNumber
|
||||
value={timeoutSeconds}
|
||||
onChange={(value) => dispatch({ type: 'SET_TIMEOUT_SECONDS', payload: value || 15 })}
|
||||
min={5}
|
||||
max={60}
|
||||
size="small"
|
||||
style={{ width: 90 }}
|
||||
addonAfter="s"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}, [isConcurrent, keyCheckMode, timeoutSeconds, t])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={title}
|
||||
open={open}
|
||||
onOk={onStart}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
okText={t('settings.models.check.start')}
|
||||
cancelText={t('common.cancel')}
|
||||
centered
|
||||
maskClosable={false}
|
||||
width={500}
|
||||
transitionName="animation-move-down"
|
||||
footer={
|
||||
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Space>
|
||||
<Space align="center">
|
||||
<Typography.Text strong>{t('settings.models.check.use_all_keys')}</Typography.Text>
|
||||
<Segmented
|
||||
value={keyCheckMode}
|
||||
onChange={(value) => dispatch({ type: 'SET_KEY_CHECK_MODE', payload: value as 'single' | 'all' })}
|
||||
size="small"
|
||||
options={[
|
||||
{ value: 'single', label: t('settings.models.check.single') },
|
||||
{ value: 'all', label: t('settings.models.check.all') }
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
<Space align="center">
|
||||
<Typography.Text strong>{t('settings.models.check.enable_concurrent')}</Typography.Text>
|
||||
<Segmented
|
||||
value={isConcurrent ? 'enabled' : 'disabled'}
|
||||
onChange={(value) => dispatch({ type: 'SET_CONCURRENT', payload: value === 'enabled' })}
|
||||
size="small"
|
||||
options={[
|
||||
{ value: 'disabled', label: t('settings.models.check.disabled') },
|
||||
{ value: 'enabled', label: t('settings.models.check.enabled') }
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
</Space>
|
||||
<Button key="start" type="primary" onClick={onStart} size="small">
|
||||
{t('settings.models.check.start')}
|
||||
</Button>
|
||||
</Space>
|
||||
}>
|
||||
<Alert message={t('settings.models.check.disclaimer')} type="warning" showIcon style={{ fontSize: 12 }} />
|
||||
footer={(_, { OkBtn, CancelBtn }) => (
|
||||
<>
|
||||
{renderFooter}
|
||||
<Flex justify="space-between" style={{ marginTop: 16 }}>
|
||||
<div /> {/* Empty div for spacing */}
|
||||
<Flex gap={8}>
|
||||
<CancelBtn />
|
||||
<OkBtn />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
)}>
|
||||
<Alert
|
||||
message={t('common.warning')}
|
||||
description={t('settings.models.check.disclaimer')}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ fontSize: 12 }}
|
||||
/>
|
||||
|
||||
{/* API key selection section - only shown for 'single' mode and multiple keys */}
|
||||
{keyCheckMode === 'single' && hasMultipleKeys && (
|
||||
<Box style={{ marginBottom: 16 }}>
|
||||
<Box style={{ marginTop: 10, marginBottom: 10 }}>
|
||||
<strong>{t('settings.models.check.select_api_key')}</strong>
|
||||
<Radio.Group
|
||||
value={selectedKeyIndex}
|
||||
|
||||
@ -129,7 +129,6 @@ const ModelList: React.FC<ModelListProps> = ({ providerId }) => {
|
||||
models={displayedModelGroups[group]}
|
||||
modelStatuses={modelStatuses}
|
||||
defaultOpen={i <= 5}
|
||||
disabled={isHealthChecking}
|
||||
onEditModel={(model) => EditModelPopup.show({ provider, model })}
|
||||
onRemoveModel={removeModel}
|
||||
onRemoveGroup={() => displayedModelGroups[group].forEach((model) => removeModel(model))}
|
||||
|
||||
@ -62,7 +62,8 @@ export const useHealthCheck = (provider: Provider, models: Model[]) => {
|
||||
provider,
|
||||
models: modelsToCheck,
|
||||
apiKeys: result.apiKeys,
|
||||
isConcurrent: result.isConcurrent
|
||||
isConcurrent: result.isConcurrent,
|
||||
timeout: result.timeout
|
||||
},
|
||||
(checkResult, index) => {
|
||||
setModelStatuses((current) => {
|
||||
|
||||
@ -849,10 +849,9 @@ export function checkApiProvider(provider: Provider): void {
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkApi(provider: Provider, model: Model): Promise<void> {
|
||||
export async function checkApi(provider: Provider, model: Model, timeout = 15000): Promise<void> {
|
||||
checkApiProvider(provider)
|
||||
|
||||
const timeout = 15000
|
||||
const controller = new AbortController()
|
||||
const abortFn = () => controller.abort()
|
||||
const taskId = uuid()
|
||||
@ -930,3 +929,9 @@ export async function checkApi(provider: Provider, model: Model): Promise<void>
|
||||
removeAbortController(taskId, abortFn)
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkModel(provider: Provider, model: Model, timeout = 15000): Promise<{ latency: number }> {
|
||||
const startTime = performance.now()
|
||||
await checkApi(provider, model, timeout)
|
||||
return { latency: performance.now() - startTime }
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { ApiKeyWithStatus, HealthStatus, ModelCheckOptions, ModelWithStatus } fr
|
||||
import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import { aggregateApiKeyResults } from '@renderer/utils/healthCheck'
|
||||
|
||||
import { checkModel } from './ModelService'
|
||||
import { checkModel } from './ApiService'
|
||||
|
||||
const logger = loggerService.withContext('HealthCheckService')
|
||||
|
||||
@ -14,12 +14,13 @@ const logger = loggerService.withContext('HealthCheckService')
|
||||
export async function checkModelWithMultipleKeys(
|
||||
provider: Provider,
|
||||
model: Model,
|
||||
apiKeys: string[]
|
||||
apiKeys: string[],
|
||||
timeout?: number
|
||||
): Promise<ApiKeyWithStatus[]> {
|
||||
const checkPromises = apiKeys.map(async (key) => {
|
||||
const startTime = Date.now()
|
||||
// 如果 checkModel 抛出错误,让这个 promise 失败
|
||||
await checkModel({ ...provider, apiKey: key }, model)
|
||||
await checkModel({ ...provider, apiKey: key }, model, timeout)
|
||||
const latency = Date.now() - startTime
|
||||
|
||||
return {
|
||||
@ -51,12 +52,12 @@ export async function checkModelsHealth(
|
||||
options: ModelCheckOptions,
|
||||
onModelChecked?: (result: ModelWithStatus, index: number) => void
|
||||
): Promise<ModelWithStatus[]> {
|
||||
const { provider, models, apiKeys, isConcurrent } = options
|
||||
const { provider, models, apiKeys, isConcurrent, timeout } = options
|
||||
const results: ModelWithStatus[] = []
|
||||
|
||||
try {
|
||||
const modelPromises = models.map(async (model, index) => {
|
||||
const keyResults = await checkModelWithMultipleKeys(provider, model, apiKeys)
|
||||
const keyResults = await checkModelWithMultipleKeys(provider, model, apiKeys, timeout)
|
||||
const analysis = aggregateApiKeyResults(keyResults)
|
||||
|
||||
const result: ModelWithStatus = {
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import store from '@renderer/store'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import { Model } from '@renderer/types'
|
||||
import { getFancyProviderName } from '@renderer/utils'
|
||||
import { pick } from 'lodash'
|
||||
|
||||
import { checkApi } from './ApiService'
|
||||
|
||||
export const getModelUniqId = (m?: Model) => {
|
||||
return m?.id ? JSON.stringify(pick(m, ['id', 'provider'])) : ''
|
||||
}
|
||||
@ -30,24 +28,3 @@ export function getModelName(model?: Model) {
|
||||
|
||||
return modelName
|
||||
}
|
||||
|
||||
// Generic function to perform model checks with exception handling
|
||||
async function performModelCheck<T>(
|
||||
provider: Provider,
|
||||
model: Model,
|
||||
checkFn: (provider: Provider, model: Model) => Promise<T>
|
||||
): Promise<{ latency: number }> {
|
||||
const startTime = performance.now()
|
||||
await checkFn(provider, model)
|
||||
const latency = performance.now() - startTime
|
||||
|
||||
return { latency }
|
||||
}
|
||||
|
||||
// Unified model check function
|
||||
// Automatically selects appropriate check method based on model type
|
||||
export async function checkModel(provider: Provider, model: Model): Promise<{ latency: number }> {
|
||||
return performModelCheck(provider, model, async (provider, model) => {
|
||||
await checkApi(provider, model)
|
||||
})
|
||||
}
|
||||
|
||||
@ -49,4 +49,5 @@ export interface ModelCheckOptions {
|
||||
models: Model[]
|
||||
apiKeys: string[]
|
||||
isConcurrent: boolean
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user