### What problem does this PR solve? Add edit button for local LLM models <img width="1531" height="1428" alt="image" src="https://github.com/user-attachments/assets/19d62255-59a6-4a7e-9772-8b8743101f78" /> <img width="1531" height="1428" alt="image" src="https://github.com/user-attachments/assets/c3a0f77e-cc6b-4190-95a6-13835463428b" /> ### Type of change - [ ] Bug Fix (non-breaking change which fixes an issue) - [x] New Feature (non-breaking change which adds functionality) - [ ] Documentation Update - [ ] Refactoring - [ ] Performance Improvement - [ ] Other (please describe): --------- Co-authored-by: Liu An <asiro@qq.com>tags/v0.20.0
| @login_required | @login_required | ||||
| def my_llms(): | def my_llms(): | ||||
| try: | try: | ||||
| res = {} | |||||
| for o in TenantLLMService.get_my_llms(current_user.id): | |||||
| if o["llm_factory"] not in res: | |||||
| res[o["llm_factory"]] = { | |||||
| "tags": o["tags"], | |||||
| "llm": [] | |||||
| } | |||||
| res[o["llm_factory"]]["llm"].append({ | |||||
| "type": o["model_type"], | |||||
| "name": o["llm_name"], | |||||
| "used_token": o["used_tokens"] | |||||
| }) | |||||
| include_details = request.args.get('include_details', 'false').lower() == 'true' | |||||
| if include_details: | |||||
| res = {} | |||||
| objs = TenantLLMService.query(tenant_id=current_user.id) | |||||
| factories = LLMFactoriesService.query(status=StatusEnum.VALID.value) | |||||
| for o in objs: | |||||
| o_dict = o.to_dict() | |||||
| factory_tags = None | |||||
| for f in factories: | |||||
| if f.name == o_dict["llm_factory"]: | |||||
| factory_tags = f.tags | |||||
| break | |||||
| if o_dict["llm_factory"] not in res: | |||||
| res[o_dict["llm_factory"]] = { | |||||
| "tags": factory_tags, | |||||
| "llm": [] | |||||
| } | |||||
| res[o_dict["llm_factory"]]["llm"].append({ | |||||
| "type": o_dict["model_type"], | |||||
| "name": o_dict["llm_name"], | |||||
| "used_token": o_dict["used_tokens"], | |||||
| "api_base": o_dict["api_base"] or "", | |||||
| "max_tokens": o_dict["max_tokens"] or 8192 | |||||
| }) | |||||
| else: | |||||
| res = {} | |||||
| for o in TenantLLMService.get_my_llms(current_user.id): | |||||
| if o["llm_factory"] not in res: | |||||
| res[o["llm_factory"]] = { | |||||
| "tags": o["tags"], | |||||
| "llm": [] | |||||
| } | |||||
| res[o["llm_factory"]]["llm"].append({ | |||||
| "type": o["model_type"], | |||||
| "name": o["llm_name"], | |||||
| "used_token": o["used_tokens"] | |||||
| }) | |||||
| return get_json_result(data=res) | return get_json_result(data=res) | ||||
| except Exception as e: | except Exception as e: | ||||
| return server_error_response(e) | return server_error_response(e) | ||||
| @manager.route('/list', methods=['GET']) # noqa: F821 | @manager.route('/list', methods=['GET']) # noqa: F821 | ||||
| @login_required | @login_required | ||||
| def list_app(): | def list_app(): |
| return { data, loading }; | return { data, loading }; | ||||
| }; | }; | ||||
| export const useFetchMyLlmListDetailed = (): ResponseGetType< | |||||
| Record<string, any> | |||||
| > => { | |||||
| const { data, isFetching: loading } = useQuery({ | |||||
| queryKey: ['myLlmListDetailed'], | |||||
| initialData: {}, | |||||
| gcTime: 0, | |||||
| queryFn: async () => { | |||||
| const { data } = await userService.my_llm({ include_details: true }); | |||||
| return data?.data ?? {}; | |||||
| }, | |||||
| }); | |||||
| return { data, loading }; | |||||
| }; | |||||
| export const useSelectLlmList = () => { | export const useSelectLlmList = () => { | ||||
| const { data: myLlmList, loading: myLlmListLoading } = useFetchMyLlmList(); | const { data: myLlmList, loading: myLlmListLoading } = useFetchMyLlmList(); | ||||
| const { data: factoryList, loading: factoryListLoading } = | const { data: factoryList, loading: factoryListLoading } = | ||||
| if (data.code === 0) { | if (data.code === 0) { | ||||
| message.success(t('message.modified')); | message.success(t('message.modified')); | ||||
| queryClient.invalidateQueries({ queryKey: ['myLlmList'] }); | queryClient.invalidateQueries({ queryKey: ['myLlmList'] }); | ||||
| queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] }); | |||||
| queryClient.invalidateQueries({ queryKey: ['factoryList'] }); | queryClient.invalidateQueries({ queryKey: ['factoryList'] }); | ||||
| } | } | ||||
| return data.code; | return data.code; | ||||
| const { data } = await userService.add_llm(params); | const { data } = await userService.add_llm(params); | ||||
| if (data.code === 0) { | if (data.code === 0) { | ||||
| queryClient.invalidateQueries({ queryKey: ['myLlmList'] }); | queryClient.invalidateQueries({ queryKey: ['myLlmList'] }); | ||||
| queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] }); | |||||
| queryClient.invalidateQueries({ queryKey: ['factoryList'] }); | queryClient.invalidateQueries({ queryKey: ['factoryList'] }); | ||||
| message.success(t('message.modified')); | message.success(t('message.modified')); | ||||
| } | } | ||||
| const { data } = await userService.delete_llm(params); | const { data } = await userService.delete_llm(params); | ||||
| if (data.code === 0) { | if (data.code === 0) { | ||||
| queryClient.invalidateQueries({ queryKey: ['myLlmList'] }); | queryClient.invalidateQueries({ queryKey: ['myLlmList'] }); | ||||
| queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] }); | |||||
| queryClient.invalidateQueries({ queryKey: ['factoryList'] }); | queryClient.invalidateQueries({ queryKey: ['factoryList'] }); | ||||
| message.success(t('message.deleted')); | message.success(t('message.deleted')); | ||||
| } | } | ||||
| const { data } = await userService.deleteFactory(params); | const { data } = await userService.deleteFactory(params); | ||||
| if (data.code === 0) { | if (data.code === 0) { | ||||
| queryClient.invalidateQueries({ queryKey: ['myLlmList'] }); | queryClient.invalidateQueries({ queryKey: ['myLlmList'] }); | ||||
| queryClient.invalidateQueries({ queryKey: ['myLlmListDetailed'] }); | |||||
| queryClient.invalidateQueries({ queryKey: ['factoryList'] }); | queryClient.invalidateQueries({ queryKey: ['factoryList'] }); | ||||
| message.success(t('message.deleted')); | message.success(t('message.deleted')); | ||||
| } | } |
| apiKeyTip: | apiKeyTip: | ||||
| 'Der API-Schlüssel kann durch Registrierung beim entsprechenden LLM-Anbieter erhalten werden.', | 'Der API-Schlüssel kann durch Registrierung beim entsprechenden LLM-Anbieter erhalten werden.', | ||||
| showMoreModels: 'Mehr Modelle anzeigen', | showMoreModels: 'Mehr Modelle anzeigen', | ||||
| hideModels: 'Modelle ausblenden', | |||||
| baseUrl: 'Basis-URL', | baseUrl: 'Basis-URL', | ||||
| baseUrlTip: | baseUrlTip: | ||||
| 'Wenn Ihr API-Schlüssel von OpenAI stammt, ignorieren Sie dies. Andere Zwischenanbieter geben diese Basis-URL mit dem API-Schlüssel an.', | 'Wenn Ihr API-Schlüssel von OpenAI stammt, ignorieren Sie dies. Andere Zwischenanbieter geben diese Basis-URL mit dem API-Schlüssel an.', |
| cancel: 'Cancel', | cancel: 'Cancel', | ||||
| addedModels: 'Added models', | addedModels: 'Added models', | ||||
| modelsToBeAdded: 'Models to be added', | modelsToBeAdded: 'Models to be added', | ||||
| addTheModel: 'Add the model', | |||||
| addTheModel: 'Add Model', | |||||
| apiKey: 'API-Key', | apiKey: 'API-Key', | ||||
| apiKeyMessage: | apiKeyMessage: | ||||
| 'Please enter the API key (for locally deployed model,ignore this).', | 'Please enter the API key (for locally deployed model,ignore this).', | ||||
| apiKeyTip: | apiKeyTip: | ||||
| 'The API key can be obtained by registering the corresponding LLM supplier.', | 'The API key can be obtained by registering the corresponding LLM supplier.', | ||||
| showMoreModels: 'Show more models', | |||||
| showMoreModels: 'View Models', | |||||
| hideModels: 'Hide Models', | |||||
| baseUrl: 'Base-Url', | baseUrl: 'Base-Url', | ||||
| baseUrlTip: | baseUrlTip: | ||||
| 'If your API key is from OpenAI, just ignore it. Any other intermediate providers will give this base url with the API key.', | 'If your API key is from OpenAI, just ignore it. Any other intermediate providers will give this base url with the API key.', | ||||
| workspace: 'Workspace', | workspace: 'Workspace', | ||||
| upgrade: 'Upgrade', | upgrade: 'Upgrade', | ||||
| addLlmTitle: 'Add LLM', | addLlmTitle: 'Add LLM', | ||||
| editLlmTitle: 'Edit {{name}} Model', | |||||
| editModel: 'Edit Model', | |||||
| modelName: 'Model name', | modelName: 'Model name', | ||||
| modelID: 'Model ID', | modelID: 'Model ID', | ||||
| modelUid: 'Model UID', | modelUid: 'Model UID', |
| apiKeyTip: | apiKeyTip: | ||||
| 'La clave API puede obtenerse registrándose con el proveedor correspondiente de LLM.', | 'La clave API puede obtenerse registrándose con el proveedor correspondiente de LLM.', | ||||
| showMoreModels: 'Mostrar más modelos', | showMoreModels: 'Mostrar más modelos', | ||||
| hideModels: 'Ocultar modelos', | |||||
| baseUrl: 'URL base', | baseUrl: 'URL base', | ||||
| baseUrlTip: | baseUrlTip: | ||||
| 'Si tu clave API es de OpenAI, ignora esto. Cualquier otro proveedor intermedio proporcionará esta URL base junto con la clave API.', | 'Si tu clave API es de OpenAI, ignora esto. Cualquier otro proveedor intermedio proporcionará esta URL base junto con la clave API.', |
| apiKeyTip: | apiKeyTip: | ||||
| 'Kunci API dapat diperoleh dengan mendaftar ke penyedia LLM yang sesuai.', | 'Kunci API dapat diperoleh dengan mendaftar ke penyedia LLM yang sesuai.', | ||||
| showMoreModels: 'Tampilkan lebih banyak model', | showMoreModels: 'Tampilkan lebih banyak model', | ||||
| hideModels: 'Sembunyikan model', | |||||
| baseUrl: 'Base-Url', | baseUrl: 'Base-Url', | ||||
| baseUrlTip: | baseUrlTip: | ||||
| 'Jika kunci API Anda berasal dari OpenAI, abaikan saja. Penyedia perantara lainnya akan memberikan base url ini dengan kunci API.', | 'Jika kunci API Anda berasal dari OpenAI, abaikan saja. Penyedia perantara lainnya akan memberikan base url ini dengan kunci API.', |
| apiKeyTip: | apiKeyTip: | ||||
| 'APIキーは、対応するLLMサプライヤーに登録することで取得できます。', | 'APIキーは、対応するLLMサプライヤーに登録することで取得できます。', | ||||
| showMoreModels: 'さらにモデルを表示', | showMoreModels: 'さらにモデルを表示', | ||||
| hideModels: 'モデルを隠す', | |||||
| baseUrl: 'ベースURL', | baseUrl: 'ベースURL', | ||||
| baseUrlTip: | baseUrlTip: | ||||
| 'APIキーがOpenAIからのものであれば無視してください。他の中間プロバイダーはAPIキーと共にこのベースURLを提供します。', | 'APIキーがOpenAIからのものであれば無視してください。他の中間プロバイダーはAPIキーと共にこのベースURLを提供します。', | ||||
| workspace: 'ワークスペース', | workspace: 'ワークスペース', | ||||
| upgrade: 'アップグレード', | upgrade: 'アップグレード', | ||||
| addLlmTitle: 'LLMを追加', | addLlmTitle: 'LLMを追加', | ||||
| editLlmTitle: '{{name}}モデルを編集', | |||||
| editModel: 'モデルを編集', | |||||
| modelName: 'モデル名', | modelName: 'モデル名', | ||||
| modelID: 'モデルID', | modelID: 'モデルID', | ||||
| modelUid: 'モデルUID', | modelUid: 'モデルUID', |
| apiKeyTip: | apiKeyTip: | ||||
| 'A chave da API pode ser obtida registrando-se no fornecedor correspondente do LLM.', | 'A chave da API pode ser obtida registrando-se no fornecedor correspondente do LLM.', | ||||
| showMoreModels: 'Mostrar mais modelos', | showMoreModels: 'Mostrar mais modelos', | ||||
| hideModels: 'Ocultar modelos', | |||||
| baseUrl: 'URL Base', | baseUrl: 'URL Base', | ||||
| baseUrlTip: | baseUrlTip: | ||||
| 'Se sua chave da API for do OpenAI, ignore isso. Outros provedores intermediários fornecerão essa URL base com a chave da API.', | 'Se sua chave da API for do OpenAI, ignore isso. Outros provedores intermediários fornecerão essa URL base com a chave da API.', |
| apiKeyTip: | apiKeyTip: | ||||
| 'Khóa API có thể được lấy bằng cách đăng ký nhà cung cấp LLM tương ứng.', | 'Khóa API có thể được lấy bằng cách đăng ký nhà cung cấp LLM tương ứng.', | ||||
| showMoreModels: 'Hiển thị thêm mô hình', | showMoreModels: 'Hiển thị thêm mô hình', | ||||
| hideModels: 'Ẩn mô hình', | |||||
| baseUrl: 'Base-Url', | baseUrl: 'Base-Url', | ||||
| baseUrlTip: | baseUrlTip: | ||||
| 'Nếu khóa API của bạn từ OpenAI, chỉ cần bỏ qua nó. Bất kỳ nhà cung cấp trung gian nào khác sẽ cung cấp URL cơ sở này với khóa API.', | 'Nếu khóa API của bạn từ OpenAI, chỉ cần bỏ qua nó. Bất kỳ nhà cung cấp trung gian nào khác sẽ cung cấp URL cơ sở này với khóa API.', |
| apiKeyMessage: '請輸入api key(如果是本地部署的模型,請忽略它)', | apiKeyMessage: '請輸入api key(如果是本地部署的模型,請忽略它)', | ||||
| apiKeyTip: 'API key可以通過註冊相應的LLM供應商來獲取。', | apiKeyTip: 'API key可以通過註冊相應的LLM供應商來獲取。', | ||||
| showMoreModels: '展示更多模型', | showMoreModels: '展示更多模型', | ||||
| hideModels: '隱藏模型', | |||||
| baseUrl: 'base-url', | baseUrl: 'base-url', | ||||
| baseUrlTip: | baseUrlTip: | ||||
| '如果您的 API 密鑰來自 OpenAI,請忽略它。任何其他中間提供商都會提供帶有 API 密鑰的基本 URL。', | '如果您的 API 密鑰來自 OpenAI,請忽略它。任何其他中間提供商都會提供帶有 API 密鑰的基本 URL。', |
| apiKeyMessage: '请输入api key(如果是本地部署的模型,请忽略它)', | apiKeyMessage: '请输入api key(如果是本地部署的模型,请忽略它)', | ||||
| apiKeyTip: 'API key可以通过注册相应的LLM供应商来获取。', | apiKeyTip: 'API key可以通过注册相应的LLM供应商来获取。', | ||||
| showMoreModels: '展示更多模型', | showMoreModels: '展示更多模型', | ||||
| hideModels: '隐藏模型', | |||||
| baseUrl: 'Base-Url', | baseUrl: 'Base-Url', | ||||
| baseUrlTip: | baseUrlTip: | ||||
| '如果您的 API 密钥来自 OpenAI,请忽略它。 任何其他中间提供商都会提供带有 API 密钥的基本 URL。', | '如果您的 API 密钥来自 OpenAI,请忽略它。 任何其他中间提供商都会提供带有 API 密钥的基本 URL。', | ||||
| workspace: '工作空间', | workspace: '工作空间', | ||||
| upgrade: '升级', | upgrade: '升级', | ||||
| addLlmTitle: '添加 LLM', | addLlmTitle: '添加 LLM', | ||||
| editLlmTitle: '编辑 {{name}} 模型', | |||||
| editModel: '编辑模型', | |||||
| modelName: '模型名称', | modelName: '模型名称', | ||||
| modelID: '模型ID', | modelID: '模型ID', | ||||
| modelUid: '模型UID', | modelUid: '模型UID', |
| loading: boolean; | loading: boolean; | ||||
| initialValue: string; | initialValue: string; | ||||
| llmFactory: string; | llmFactory: string; | ||||
| editMode?: boolean; | |||||
| onOk: (postBody: ApiKeyPostBody) => void; | onOk: (postBody: ApiKeyPostBody) => void; | ||||
| showModal?(): void; | showModal?(): void; | ||||
| } | } | ||||
| llmFactory, | llmFactory, | ||||
| loading, | loading, | ||||
| initialValue, | initialValue, | ||||
| editMode = false, | |||||
| onOk, | onOk, | ||||
| }: IProps) => { | }: IProps) => { | ||||
| const [form] = Form.useForm(); | const [form] = Form.useForm(); | ||||
| return ( | return ( | ||||
| <Modal | <Modal | ||||
| title={t('modify')} | |||||
| title={editMode ? t('editModel') : t('modify')} | |||||
| open={visible} | open={visible} | ||||
| onOk={handleOk} | onOk={handleOk} | ||||
| onCancel={hideModal} | onCancel={hideModal} |
| } from '@/hooks/llm-hooks'; | } from '@/hooks/llm-hooks'; | ||||
| import { useFetchTenantInfo } from '@/hooks/user-setting-hooks'; | import { useFetchTenantInfo } from '@/hooks/user-setting-hooks'; | ||||
| import { IAddLlmRequestBody } from '@/interfaces/request/llm'; | import { IAddLlmRequestBody } from '@/interfaces/request/llm'; | ||||
| import { getRealModelName } from '@/utils/llm-util'; | |||||
| import { useCallback, useState } from 'react'; | import { useCallback, useState } from 'react'; | ||||
| import { ApiKeyPostBody } from '../interface'; | import { ApiKeyPostBody } from '../interface'; | ||||
| const [savingParams, setSavingParams] = useState<SavingParamsState>( | const [savingParams, setSavingParams] = useState<SavingParamsState>( | ||||
| {} as SavingParamsState, | {} as SavingParamsState, | ||||
| ); | ); | ||||
| const [editMode, setEditMode] = useState(false); | |||||
| const { saveApiKey, loading } = useSaveApiKey(); | const { saveApiKey, loading } = useSaveApiKey(); | ||||
| const { | const { | ||||
| visible: apiKeyVisible, | visible: apiKeyVisible, | ||||
| if (ret === 0) { | if (ret === 0) { | ||||
| hideApiKeyModal(); | hideApiKeyModal(); | ||||
| setEditMode(false); | |||||
| } | } | ||||
| }, | }, | ||||
| [hideApiKeyModal, saveApiKey, savingParams], | [hideApiKeyModal, saveApiKey, savingParams], | ||||
| ); | ); | ||||
| const onShowApiKeyModal = useCallback( | const onShowApiKeyModal = useCallback( | ||||
| (savingParams: SavingParamsState) => { | |||||
| (savingParams: SavingParamsState, isEdit = false) => { | |||||
| setSavingParams(savingParams); | setSavingParams(savingParams); | ||||
| setEditMode(isEdit); | |||||
| showApiKeyModal(); | showApiKeyModal(); | ||||
| }, | }, | ||||
| [showApiKeyModal, setSavingParams], | [showApiKeyModal, setSavingParams], | ||||
| saveApiKeyLoading: loading, | saveApiKeyLoading: loading, | ||||
| initialApiKey: '', | initialApiKey: '', | ||||
| llmFactory: savingParams.llm_factory, | llmFactory: savingParams.llm_factory, | ||||
| editMode, | |||||
| onApiKeySavingOk, | onApiKeySavingOk, | ||||
| apiKeyVisible, | apiKeyVisible, | ||||
| hideApiKeyModal, | hideApiKeyModal, | ||||
| export const useSubmitOllama = () => { | export const useSubmitOllama = () => { | ||||
| const [selectedLlmFactory, setSelectedLlmFactory] = useState<string>(''); | const [selectedLlmFactory, setSelectedLlmFactory] = useState<string>(''); | ||||
| const [editMode, setEditMode] = useState(false); | |||||
| const [initialValues, setInitialValues] = useState<Partial<IAddLlmRequestBody> | undefined>(); | |||||
| const [originalModelName, setOriginalModelName] = useState<string>(''); | |||||
| const { addLlm, loading } = useAddLlm(); | const { addLlm, loading } = useAddLlm(); | ||||
| const { | const { | ||||
| visible: llmAddingVisible, | visible: llmAddingVisible, | ||||
| const onLlmAddingOk = useCallback( | const onLlmAddingOk = useCallback( | ||||
| async (payload: IAddLlmRequestBody) => { | async (payload: IAddLlmRequestBody) => { | ||||
| const ret = await addLlm(payload); | |||||
| const cleanedPayload = { ...payload }; | |||||
| if (!cleanedPayload.api_key || cleanedPayload.api_key.trim() === '') { | |||||
| delete cleanedPayload.api_key; | |||||
| } | |||||
| const ret = await addLlm(cleanedPayload); | |||||
| if (ret === 0) { | if (ret === 0) { | ||||
| hideLlmAddingModal(); | hideLlmAddingModal(); | ||||
| setEditMode(false); | |||||
| setInitialValues(undefined); | |||||
| } | } | ||||
| }, | }, | ||||
| [hideLlmAddingModal, addLlm], | [hideLlmAddingModal, addLlm], | ||||
| ); | ); | ||||
| const handleShowLlmAddingModal = (llmFactory: string) => { | |||||
| const handleShowLlmAddingModal = (llmFactory: string, isEdit = false, modelData?: any, detailedData?: any) => { | |||||
| setSelectedLlmFactory(llmFactory); | setSelectedLlmFactory(llmFactory); | ||||
| setEditMode(isEdit); | |||||
| if (isEdit && detailedData) { | |||||
| const initialVals = { | |||||
| llm_name: getRealModelName(detailedData.name), | |||||
| model_type: detailedData.type, | |||||
| api_base: detailedData.api_base || '', | |||||
| max_tokens: detailedData.max_tokens || 8192, | |||||
| api_key: '', | |||||
| }; | |||||
| setInitialValues(initialVals); | |||||
| } else { | |||||
| setInitialValues(undefined); | |||||
| } | |||||
| showLlmAddingModal(); | showLlmAddingModal(); | ||||
| }; | }; | ||||
| return { | return { | ||||
| llmAddingLoading: loading, | llmAddingLoading: loading, | ||||
| editMode, | |||||
| initialValues, | |||||
| onLlmAddingOk, | onLlmAddingOk, | ||||
| llmAddingVisible, | llmAddingVisible, | ||||
| hideLlmAddingModal, | hideLlmAddingModal, |
| import { useTheme } from '@/components/theme-provider'; | import { useTheme } from '@/components/theme-provider'; | ||||
| import { LLMFactory } from '@/constants/llm'; | import { LLMFactory } from '@/constants/llm'; | ||||
| import { useSetModalState, useTranslate } from '@/hooks/common-hooks'; | import { useSetModalState, useTranslate } from '@/hooks/common-hooks'; | ||||
| import { LlmItem, useSelectLlmList } from '@/hooks/llm-hooks'; | |||||
| import { LlmItem, useSelectLlmList, useFetchMyLlmListDetailed } from '@/hooks/llm-hooks'; | |||||
| import { getRealModelName } from '@/utils/llm-util'; | import { getRealModelName } from '@/utils/llm-util'; | ||||
| import { CloseCircleOutlined, SettingOutlined } from '@ant-design/icons'; | |||||
| import { CloseCircleOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons'; | |||||
| import { | import { | ||||
| Button, | Button, | ||||
| Card, | Card, | ||||
| interface IModelCardProps { | interface IModelCardProps { | ||||
| item: LlmItem; | item: LlmItem; | ||||
| clickApiKey: (llmFactory: string) => void; | clickApiKey: (llmFactory: string) => void; | ||||
| handleEditModel: (model: any, factory: LlmItem) => void; | |||||
| } | } | ||||
| const ModelCard = ({ item, clickApiKey }: IModelCardProps) => { | |||||
| const ModelCard = ({ item, clickApiKey, handleEditModel }: IModelCardProps) => { | |||||
| const { visible, switchVisible } = useSetModalState(); | const { visible, switchVisible } = useSetModalState(); | ||||
| const { t } = useTranslate('setting'); | const { t } = useTranslate('setting'); | ||||
| const { theme } = useTheme(); | const { theme } = useTheme(); | ||||
| </Button> | </Button> | ||||
| <Button onClick={handleShowMoreClick}> | <Button onClick={handleShowMoreClick}> | ||||
| <Flex align="center" gap={4}> | <Flex align="center" gap={4}> | ||||
| {t('showMoreModels')} | |||||
| {visible ? t('hideModels') : t('showMoreModels')} | |||||
| <MoreModelIcon /> | <MoreModelIcon /> | ||||
| </Flex> | </Flex> | ||||
| </Button> | </Button> | ||||
| size="small" | size="small" | ||||
| dataSource={item.llm} | dataSource={item.llm} | ||||
| className={styles.llmList} | className={styles.llmList} | ||||
| renderItem={(item) => ( | |||||
| renderItem={(model) => ( | |||||
| <List.Item> | <List.Item> | ||||
| <Space> | <Space> | ||||
| {getRealModelName(item.name)} | |||||
| <Tag color="#b8b8b8">{item.type}</Tag> | |||||
| {getRealModelName(model.name)} | |||||
| <Tag color="#b8b8b8">{model.type}</Tag> | |||||
| {isLocalLlmFactory(item.name) && ( | |||||
| <Tooltip title={t('edit', { keyPrefix: 'common' })}> | |||||
| <Button type={'text'} onClick={() => handleEditModel(model, item)}> | |||||
| <EditOutlined style={{ color: '#1890ff' }} /> | |||||
| </Button> | |||||
| </Tooltip> | |||||
| )} | |||||
| <Tooltip title={t('delete', { keyPrefix: 'common' })}> | <Tooltip title={t('delete', { keyPrefix: 'common' })}> | ||||
| <Button type={'text'} onClick={handleDeleteLlm(item.name)}> | |||||
| <Button type={'text'} onClick={handleDeleteLlm(model.name)}> | |||||
| <CloseCircleOutlined style={{ color: '#D92D20' }} /> | <CloseCircleOutlined style={{ color: '#D92D20' }} /> | ||||
| </Button> | </Button> | ||||
| </Tooltip> | </Tooltip> | ||||
| const UserSettingModel = () => { | const UserSettingModel = () => { | ||||
| const { factoryList, myLlmList: llmList, loading } = useSelectLlmList(); | const { factoryList, myLlmList: llmList, loading } = useSelectLlmList(); | ||||
| const { data: detailedLlmList } = useFetchMyLlmListDetailed(); | |||||
| const { theme } = useTheme(); | const { theme } = useTheme(); | ||||
| const { | const { | ||||
| saveApiKeyLoading, | saveApiKeyLoading, | ||||
| initialApiKey, | initialApiKey, | ||||
| llmFactory, | llmFactory, | ||||
| editMode, | |||||
| onApiKeySavingOk, | onApiKeySavingOk, | ||||
| apiKeyVisible, | apiKeyVisible, | ||||
| hideApiKeyModal, | hideApiKeyModal, | ||||
| showLlmAddingModal, | showLlmAddingModal, | ||||
| onLlmAddingOk, | onLlmAddingOk, | ||||
| llmAddingLoading, | llmAddingLoading, | ||||
| editMode: llmEditMode, | |||||
| initialValues: llmInitialValues, | |||||
| selectedLlmFactory, | selectedLlmFactory, | ||||
| } = useSubmitOllama(); | } = useSubmitOllama(); | ||||
| [showApiKeyModal, showLlmAddingModal, ModalMap], | [showApiKeyModal, showLlmAddingModal, ModalMap], | ||||
| ); | ); | ||||
| const handleEditModel = useCallback( | |||||
| (model: any, factory: LlmItem) => { | |||||
| if (factory) { | |||||
| const detailedFactory = detailedLlmList[factory.name]; | |||||
| const detailedModel = detailedFactory?.llm?.find((m: any) => m.name === model.name); | |||||
| const editData = { | |||||
| llm_factory: factory.name, | |||||
| llm_name: model.name, | |||||
| model_type: model.type | |||||
| }; | |||||
| if (isLocalLlmFactory(factory.name)) { | |||||
| showLlmAddingModal(factory.name, true, editData, detailedModel); | |||||
| } else if (factory.name in ModalMap) { | |||||
| ModalMap[factory.name as keyof typeof ModalMap](); | |||||
| } else { | |||||
| showApiKeyModal(editData, true); | |||||
| } | |||||
| } | |||||
| }, | |||||
| [showApiKeyModal, showLlmAddingModal, ModalMap, detailedLlmList], | |||||
| ); | |||||
| const items: CollapseProps['items'] = [ | const items: CollapseProps['items'] = [ | ||||
| { | { | ||||
| key: '1', | key: '1', | ||||
| grid={{ gutter: 16, column: 1 }} | grid={{ gutter: 16, column: 1 }} | ||||
| dataSource={llmList} | dataSource={llmList} | ||||
| renderItem={(item) => ( | renderItem={(item) => ( | ||||
| <ModelCard item={item} clickApiKey={handleAddModel}></ModelCard> | |||||
| <ModelCard item={item} clickApiKey={handleAddModel} handleEditModel={handleEditModel}></ModelCard> | |||||
| )} | )} | ||||
| /> | /> | ||||
| ), | ), | ||||
| hideModal={hideApiKeyModal} | hideModal={hideApiKeyModal} | ||||
| loading={saveApiKeyLoading} | loading={saveApiKeyLoading} | ||||
| initialValue={initialApiKey} | initialValue={initialApiKey} | ||||
| editMode={editMode} | |||||
| onOk={onApiKeySavingOk} | onOk={onApiKeySavingOk} | ||||
| llmFactory={llmFactory} | llmFactory={llmFactory} | ||||
| ></ApiKeyModal> | ></ApiKeyModal> | ||||
| hideModal={hideLlmAddingModal} | hideModal={hideLlmAddingModal} | ||||
| onOk={onLlmAddingOk} | onOk={onLlmAddingOk} | ||||
| loading={llmAddingLoading} | loading={llmAddingLoading} | ||||
| editMode={llmEditMode} | |||||
| initialValues={llmInitialValues} | |||||
| llmFactory={selectedLlmFactory} | llmFactory={selectedLlmFactory} | ||||
| ></OllamaModal> | ></OllamaModal> | ||||
| <VolcEngineModal | <VolcEngineModal |
| Switch, | Switch, | ||||
| } from 'antd'; | } from 'antd'; | ||||
| import omit from 'lodash/omit'; | import omit from 'lodash/omit'; | ||||
| import { useEffect } from 'react'; | |||||
| type FieldType = IAddLlmRequestBody & { vision: boolean }; | type FieldType = IAddLlmRequestBody & { vision: boolean }; | ||||
| onOk, | onOk, | ||||
| loading, | loading, | ||||
| llmFactory, | llmFactory, | ||||
| }: IModalProps<IAddLlmRequestBody> & { llmFactory: string }) => { | |||||
| editMode = false, | |||||
| initialValues, | |||||
| }: IModalProps<IAddLlmRequestBody> & { | |||||
| llmFactory: string; | |||||
| editMode?: boolean; | |||||
| initialValues?: Partial<IAddLlmRequestBody>; | |||||
| }) => { | |||||
| const [form] = Form.useForm<FieldType>(); | const [form] = Form.useForm<FieldType>(); | ||||
| const { t } = useTranslate('setting'); | const { t } = useTranslate('setting'); | ||||
| await handleOk(); | await handleOk(); | ||||
| } | } | ||||
| }; | }; | ||||
| useEffect(() => { | |||||
| if (visible && editMode && initialValues) { | |||||
| const formValues = { | |||||
| llm_name: initialValues.llm_name, | |||||
| model_type: initialValues.model_type, | |||||
| api_base: initialValues.api_base, | |||||
| max_tokens: initialValues.max_tokens || 8192, | |||||
| api_key: '', | |||||
| ...initialValues, | |||||
| }; | |||||
| form.setFieldsValue(formValues); | |||||
| } else if (visible && !editMode) { | |||||
| form.resetFields(); | |||||
| } | |||||
| }, [visible, editMode, initialValues, form]); | |||||
| const url = | const url = | ||||
| llmFactoryToUrlMap[llmFactory as LlmFactory] || | llmFactoryToUrlMap[llmFactory as LlmFactory] || | ||||
| }; | }; | ||||
| return ( | return ( | ||||
| <Modal | <Modal | ||||
| title={t('addLlmTitle', { name: llmFactory })} | |||||
| title={editMode ? t('editLlmTitle', { name: llmFactory }) : t('addLlmTitle', { name: llmFactory })} | |||||
| open={visible} | open={visible} | ||||
| onOk={handleOk} | onOk={handleOk} | ||||
| onCancel={hideModal} | onCancel={hideModal} | ||||
| name="api_key" | name="api_key" | ||||
| rules={[{ required: false, message: t('apiKeyMessage') }]} | rules={[{ required: false, message: t('apiKeyMessage') }]} | ||||
| > | > | ||||
| <Input placeholder={t('apiKeyMessage')} onKeyDown={handleKeyDown} /> | |||||
| <Input | |||||
| placeholder={t('apiKeyMessage')} | |||||
| onKeyDown={handleKeyDown} | |||||
| /> | |||||
| </Form.Item> | </Form.Item> | ||||
| <Form.Item<FieldType> | <Form.Item<FieldType> | ||||
| label={t('maxTokens')} | label={t('maxTokens')} |