diff --git a/web/src/hooks/use-llm-request.tsx b/web/src/hooks/use-llm-request.tsx index 5566e5e93d..73370d625b 100644 --- a/web/src/hooks/use-llm-request.tsx +++ b/web/src/hooks/use-llm-request.tsx @@ -13,6 +13,7 @@ import { IAddProviderInstanceRequestBody, IAddProviderRequestBody, IDeleteProviderInstanceRequestBody, + IEditInstanceModelRequestBody, IListAllModelsRequestParams, IListProviderModelsRequestBody, IListProvidersRequestParams, @@ -37,6 +38,7 @@ export const enum LLMApiAction { VerifyProviderConnection = 'verifyProviderConnection', ListProviderModels = 'listProviderModels', AddInstanceModel = 'addInstanceModel', + EditInstanceModel = 'editInstanceModel', DeleteProviderInstance = 'deleteProviderInstance', ListDefaultModels = 'listDefaultModels', SetDefaultModel = 'setDefaultModel', @@ -354,6 +356,44 @@ export const useAddInstanceModel = () => { return { data, loading, addInstanceModel: mutateAsync }; }; +export const useEditInstanceModel = () => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + const { + data, + isPending: loading, + mutateAsync, + } = useMutation({ + mutationKey: [LLMApiAction.EditInstanceModel], + mutationFn: async ( + params: { + provider_name: string; + instance_name: string; + } & IEditInstanceModelRequestBody, + ) => { + const { data } = await llmService.editInstanceModel(params); + if (data.code === 0) { + message.success(t('message.modified')); + queryClient.invalidateQueries({ + queryKey: LlmKeys.instanceModels( + params.provider_name, + params.instance_name, + ), + }); + queryClient.invalidateQueries({ + queryKey: LlmKeys.allModels(), + }); + queryClient.invalidateQueries({ + queryKey: LlmKeys.defaultModels(), + }); + } + return data; + }, + }); + + return { data, loading, editInstanceModel: mutateAsync }; +}; + export const useDeleteProviderInstance = () => { const queryClient = useQueryClient(); const { t } = useTranslation(); diff --git a/web/src/interfaces/request/llm.ts b/web/src/interfaces/request/llm.ts index f17a656c61..939bdfe9d7 100644 --- a/web/src/interfaces/request/llm.ts +++ b/web/src/interfaces/request/llm.ts @@ -57,6 +57,11 @@ export interface IAddInstanceModelRequestBody { extra?: Record; } +export interface IEditInstanceModelRequestBody { + model_name: string[]; + model_type: string[]; +} + export interface IListAllModelsRequestParams { type?: string; } diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 47290e15b4..df72c2d321 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1824,6 +1824,7 @@ Example: Virtual Hosted Style`, 'Please select at least one model before verification.', addCustomModel: 'Add custom model', addCustomModelTitle: 'Add custom model', + editCustomModelTitle: 'Edit model', modelMaxTokens: 'Max tokens', modelFeatures: 'Model features', modelFeatureToolCall: 'Tool call', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 66e92eb1ad..016478d9fc 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1510,6 +1510,7 @@ NER:使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系 selectModelBeforeVerify: '请至少选择一个模型后再验证。', addCustomModel: '添加自定义模型', addCustomModelTitle: '添加自定义模型', + editCustomModelTitle: '编辑模型', modelMaxTokens: '最大 Token 数', modelTypes: { chat: 'Chat', diff --git a/web/src/pages/user-setting/setting-model/components/used-model.tsx b/web/src/pages/user-setting/setting-model/components/used-model.tsx index fbc13ac1a6..b50d2eef37 100644 --- a/web/src/pages/user-setting/setting-model/components/used-model.tsx +++ b/web/src/pages/user-setting/setting-model/components/used-model.tsx @@ -10,6 +10,7 @@ import { Switch } from '@/components/ui/switch'; import { ModelStatus } from '@/constants/llm'; import { useDeleteProviderInstance, + useEditInstanceModel, useFetchAddedProviders, useFetchInstanceModels, useFetchProviderInstance, @@ -21,9 +22,19 @@ import { IInstanceModel, IProviderInstance, } from '@/interfaces/database/llm'; -import { ChevronsDown, ChevronsUp, Settings, Trash2 } from 'lucide-react'; +import { IProviderModelItem } from '@/interfaces/request/llm'; +import { cn } from '@/lib/utils'; +import { + ChevronsDown, + ChevronsUp, + Pencil, + Settings, + Trash2, +} from 'lucide-react'; import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { AddCustomModelDialog } from '../modal/provider-modal/components/add-custom-model-dialog'; +import { useCustomModelFields } from '../modal/provider-modal/components/use-custom-model-fields'; import { mapModelKey } from './un-add-model'; export function UsedModel({ @@ -287,7 +298,11 @@ function ModelListItem({ providerName: string; instanceName: string; }) { + const { t } = useTranslation(); const { updateModelStatus } = useUpdateModelStatus(); + const { editInstanceModel, loading: editLoading } = useEditInstanceModel(); + const customModelFields = useCustomModelFields(); + const [editOpen, setEditOpen] = useState(false); const handleStatusChange = (checked: boolean) => { updateModelStatus({ @@ -298,11 +313,45 @@ function ModelListItem({ }); }; + // Only show name / model_types / max_tokens; only model_types is editable. + const editFields = useMemo( + () => + customModelFields + .filter((f) => ['name', 'model_types', 'max_tokens'].includes(f.name)) + .map((f) => ({ + ...f, + disabled: f.name !== 'model_types', + })), + [customModelFields], + ); + + const editDefaultValues = useMemo( + () => ({ + name: model.name, + model_types: model.model_type ?? [], + max_tokens: model.max_tokens ?? 0, + }), + [model], + ); + + const handleEditSubmit = useCallback( + async (item: IProviderModelItem) => { + await editInstanceModel({ + provider_name: providerName, + instance_name: instanceName, + model_name: [model.name], + model_type: item.model_types ?? [], + }); + setEditOpen(false); + }, + [editInstanceModel, providerName, instanceName, model.name], + ); + return ( -
  • +
  • {model.name} - {model.model_type.map((modelType) => ( + {model.model_type.slice(0, 3).map((modelType) => ( ))} + {model.model_type.length > 3 && ( + + ... + + )} +
    +
  • ); } diff --git a/web/src/pages/user-setting/setting-model/modal/provider-modal/components/add-custom-model-dialog.tsx b/web/src/pages/user-setting/setting-model/modal/provider-modal/components/add-custom-model-dialog.tsx index 00ce90788b..654f246cad 100644 --- a/web/src/pages/user-setting/setting-model/modal/provider-modal/components/add-custom-model-dialog.tsx +++ b/web/src/pages/user-setting/setting-model/modal/provider-modal/components/add-custom-model-dialog.tsx @@ -37,6 +37,8 @@ export interface AddCustomModelDialogFields { defaultValue?: unknown; /** Minimum value for number type */ min?: number; + /** Whether this field is disabled (non-editable) */ + disabled?: boolean; } interface AddCustomModelDialogProps { @@ -56,6 +58,8 @@ interface AddCustomModelDialogProps { loading?: boolean; /** Existing model names for uniqueness validation */ existingNames: string[]; + /** Initial form values (overrides field-level defaults). Useful for edit mode. */ + defaultValues?: Record; } type FormValues = Record; @@ -74,8 +78,10 @@ export const AddCustomModelDialog = ({ cancelText, loading = false, existingNames, + defaultValues, }: AddCustomModelDialogProps) => { const { t } = useTranslate('setting'); + const { t: commonT } = useTranslate('common'); const formRef = useRef(null); // Translate AddCustomModelDialogFields -> FormFieldConfig for DynamicForm. @@ -96,6 +102,7 @@ export const AddCustomModelDialog = ({ type: FormFieldType.Custom, required: field.required, defaultValue, + disabled: field.disabled, schema: field.required ? z.array(z.string()).min(1, t('modelTypeRequired')) : z.array(z.string()).optional(), @@ -120,7 +127,9 @@ export const AddCustomModelDialog = ({ { + if (field.disabled) return; const next = checked ? [...currentValues, opt.value] : currentValues.filter((v) => v !== opt.value); @@ -148,6 +157,7 @@ export const AddCustomModelDialog = ({ type: typeMap[field.type as 'text' | 'number' | 'multi-select'], required: field.required, defaultValue, + disabled: field.disabled, options: field.options, placeholder: field.label, ...(field.min !== undefined @@ -193,12 +203,14 @@ export const AddCustomModelDialog = ({ [onSubmit], ); - // Reset form whenever the dialog closes, so the next open starts fresh. + // Reset form whenever the dialog opens/closes, applying defaultValues for edit mode. useEffect(() => { if (!open) { formRef.current?.reset(); + } else if (defaultValues) { + formRef.current?.reset(defaultValues); } - }, [open]); + }, [open, defaultValues]); return ( @@ -211,8 +223,9 @@ export const AddCustomModelDialog = ({ ref={formRef} fields={dynamicFields} onSubmit={handleSubmit} + defaultValues={defaultValues} > - + diff --git a/web/src/services/llm-service.ts b/web/src/services/llm-service.ts index 0d46a7cad3..4866975f16 100644 --- a/web/src/services/llm-service.ts +++ b/web/src/services/llm-service.ts @@ -13,6 +13,7 @@ const { listInstanceModels, showProviderInstance, addInstanceModel, + editInstanceModel, deleteProviderInstance, updateModelStatus, } = api; @@ -66,6 +67,10 @@ const methods = { url: addInstanceModel, method: 'post', }, + editInstanceModel: { + url: editInstanceModel, + method: 'put', + }, deleteProviderInstance: { url: deleteProviderInstance, method: 'delete', diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 22211531bb..4132249a07 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -59,6 +59,14 @@ export default { instance_name: string; }) => `${restAPIv1}/providers/${provider_name}/instances/${instance_name}/models`, + editInstanceModel: ({ + provider_name, + instance_name, + }: { + provider_name: string; + instance_name: string; + }) => + `${restAPIv1}/providers/${provider_name}/instances/${instance_name}/models`, deleteProviderInstance: ({ provider_name }: { provider_name: string }) => `${restAPIv1}/providers/${provider_name}/instances`, updateModelStatus: ({