diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts index d4ad386a4a..fe5d9f18ad 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIApiClient.ts @@ -21,6 +21,7 @@ import { isSupportedThinkingTokenZhipuModel, isVisionModel } from '@renderer/config/models' +import { isSupportDeveloperRoleProvider } from '@renderer/config/providers' import { processPostsuffixQwen3Model, processReqMessages } from '@renderer/services/ModelMessageService' import { estimateTextTokens } from '@renderer/services/TokenService' // For Copilot token @@ -62,7 +63,6 @@ import { ChatCompletionContentPart, ChatCompletionContentPartRefusal, ChatComple import { GenericChunk } from '../../middleware/schemas' import { RequestTransformer, ResponseChunkTransformer, ResponseChunkTransformerContext } from '../types' import { OpenAIBaseClient } from './OpenAIBaseClient' -import { isSupportDeveloperRoleProvider } from '@renderer/config/providers' const logger = loggerService.withContext('OpenAIApiClient') diff --git a/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts b/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts index cc9dba7095..970dd1399f 100644 --- a/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts +++ b/src/renderer/src/aiCore/clients/openai/OpenAIResponseAPIClient.ts @@ -6,6 +6,7 @@ import { isSupportedReasoningEffortOpenAIModel, isVisionModel } from '@renderer/config/models' +import { isSupportDeveloperRoleProvider } from '@renderer/config/providers' import { estimateTextTokens } from '@renderer/services/TokenService' import { FileMetadata, @@ -44,7 +45,6 @@ import { ResponseInput } from 'openai/resources/responses/responses' import { RequestTransformer, ResponseChunkTransformer } from '../types' import { OpenAIAPIClient } from './OpenAIApiClient' import { OpenAIBaseClient } from './OpenAIBaseClient' -import { isSupportDeveloperRoleProvider } from '@renderer/config/providers' export class OpenAIResponseAPIClient extends OpenAIBaseClient< OpenAI, diff --git a/src/renderer/src/components/ModelList/ManageModelsPopup.tsx b/src/renderer/src/components/ModelList/ManageModelsPopup.tsx index acaf0a3a00..5346f341e9 100644 --- a/src/renderer/src/components/ModelList/ManageModelsPopup.tsx +++ b/src/renderer/src/components/ModelList/ManageModelsPopup.tsx @@ -18,40 +18,35 @@ import { import { useProvider } from '@renderer/hooks/useProvider' import { fetchModels } from '@renderer/services/ApiService' import { Model, Provider } from '@renderer/types' -import { - filterModelsByKeywords, - getDefaultGroupName, - getFancyProviderName, - isFreeModel, - runAsyncFunction -} from '@renderer/utils' +import { filterModelsByKeywords, getDefaultGroupName, getFancyProviderName, isFreeModel } from '@renderer/utils' import { Button, Empty, Flex, Modal, Spin, Tabs, Tooltip } from 'antd' import Input from 'antd/es/input/Input' import { groupBy, isEmpty, uniqBy } from 'lodash' import { debounce } from 'lodash' -import { Search } from 'lucide-react' +import { RefreshCcw, Search } from 'lucide-react' import { useCallback, useEffect, useMemo, useOptimistic, useRef, useState, useTransition } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import { HStack } from '../Layout' import ManageModelsList from './ManageModelsList' import { isModelInProvider, isValidNewApiModel } from './utils' const logger = loggerService.withContext('ManageModelsPopup') interface ShowParams { - provider: Provider + providerId: string } interface Props extends ShowParams { resolve: (data: any) => void } -const PopupContainer: React.FC = ({ provider: _provider, resolve }) => { +const PopupContainer: React.FC = ({ providerId, resolve }) => { const [open, setOpen] = useState(true) - const { provider, models, addModel, removeModel } = useProvider(_provider.id) + const { provider, models, addModel, removeModel } = useProvider(providerId) const [listModels, setListModels] = useState([]) - const [loading, setLoading] = useState(false) + const [loadingModels, setLoadingModels] = useState(false) const [searchText, setSearchText] = useState('') const [filterSearchText, setFilterSearchText] = useState('') const debouncedSetFilterText = useMemo( @@ -78,9 +73,14 @@ const PopupContainer: React.FC = ({ provider: _provider, resolve }) => { const { t, i18n } = useTranslation() const searchInputRef = useRef(null) - const systemModels = SYSTEM_MODELS[_provider.id] || [] + const systemModels = SYSTEM_MODELS[provider.id] || [] const allModels = uniqBy([...systemModels, ...listModels, ...models], 'id') + const isLoading = useMemo( + () => loadingModels || isFilterTypePending || isSearchPending, + [loadingModels, isFilterTypePending, isSearchPending] + ) + const list = useMemo( () => filterModelsByKeywords(filterSearchText, allModels).filter((model) => { @@ -149,48 +149,66 @@ const PopupContainer: React.FC = ({ provider: _provider, resolve }) => { const onRemoveModel = useCallback((model: Model) => removeModel(model), [removeModel]) - useEffect(() => { - let timer: NodeJS.Timeout - let mounted = true + const onRemoveAll = useCallback(() => { + list.filter((model) => isModelInProvider(provider, model.id)).forEach(onRemoveModel) + }, [list, onRemoveModel, provider]) - runAsyncFunction(async () => { - try { - setLoading(true) - const models = await fetchModels(_provider) - setListModels( - models - .map((model) => ({ - // @ts-ignore modelId - id: model?.id || model?.name, - // @ts-ignore name - name: model?.display_name || model?.displayName || model?.name || model?.id, - provider: _provider.id, - // @ts-ignore group - group: getDefaultGroupName(model?.id || model?.name, _provider.id), - // @ts-ignore description - description: model?.description || '', - // @ts-ignore owned_by - owned_by: model?.owned_by || '', - // @ts-ignore supported_endpoint_types - supported_endpoint_types: model?.supported_endpoint_types - })) - .filter((model) => !isEmpty(model.name)) - ) - } catch (error) { - logger.error('Failed to fetch models', error as Error) - } finally { - if (mounted) { - timer = setTimeout(() => setLoading(false), 300) + const onAddAll = useCallback(() => { + const wouldAddModel = list.filter((model) => !isModelInProvider(provider, model.id)) + window.modal.confirm({ + title: t('settings.models.manage.add_listed.label'), + content: t('settings.models.manage.add_listed.confirm'), + centered: true, + onOk: () => { + if (provider.id === 'new-api') { + if (models.every(isValidNewApiModel)) { + wouldAddModel.forEach(onAddModel) + } else { + NewApiBatchAddModelPopup.show({ + title: t('settings.models.add.batch_add_models'), + batchModels: wouldAddModel, + provider + }) + } + } else { + wouldAddModel.forEach(onAddModel) } } }) + }, [list, models, onAddModel, provider, t]) - return () => { - mounted = false - if (timer) { - clearTimeout(timer) - } + const loadModels = useCallback(async (provider: Provider) => { + setLoadingModels(true) + try { + const models = await fetchModels(provider) + const filteredModels = models + .map((model) => ({ + // @ts-ignore modelId + id: model?.id || model?.name, + // @ts-ignore name + name: model?.display_name || model?.displayName || model?.name || model?.id, + provider: provider.id, + // @ts-ignore group + group: getDefaultGroupName(model?.id || model?.name, provider.id), + // @ts-ignore description + description: model?.description || '', + // @ts-ignore owned_by + owned_by: model?.owned_by || '', + // @ts-ignore supported_endpoint_types + supported_endpoint_types: model?.supported_endpoint_types + })) + .filter((model) => !isEmpty(model.name)) + + setListModels(filteredModels) + } catch (error) { + logger.error(`Failed to load models for provider ${getFancyProviderName(provider)}`, error as Error) + } finally { + setLoadingModels(false) } + }, []) + + useEffect(() => { + loadModels(provider) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -222,57 +240,39 @@ const PopupContainer: React.FC = ({ provider: _provider, resolve }) => { const renderTopTools = useCallback(() => { const isAllFilteredInProvider = list.length > 0 && list.every((model) => isModelInProvider(provider, model.id)) - const onRemoveAll = () => { - list.filter((model) => isModelInProvider(provider, model.id)).forEach(onRemoveModel) - } - - const onAddAll = () => { - const wouldAddModel = list.filter((model) => !isModelInProvider(provider, model.id)) - window.modal.confirm({ - title: t('settings.models.manage.add_listed.label'), - content: t('settings.models.manage.add_listed.confirm'), - centered: true, - onOk: () => { - if (provider.id === 'new-api') { - if (models.every(isValidNewApiModel)) { - wouldAddModel.forEach(onAddModel) - } else { - NewApiBatchAddModelPopup.show({ - title: t('settings.models.add.batch_add_models'), - batchModels: wouldAddModel, - provider - }) - } - } else { - wouldAddModel.forEach(onAddModel) - } - } - }) - } - return ( - -