Feat: Add edit model type function (#16029)

### What problem does this PR solve?

Feat: Add edit model type function

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
This commit is contained in:
chanx
2026-06-15 19:11:05 +08:00
committed by GitHub
parent 47495c1f6a
commit 7d94b0818e
8 changed files with 159 additions and 7 deletions

View File

@@ -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();

View File

@@ -57,6 +57,11 @@ export interface IAddInstanceModelRequestBody {
extra?: Record<string, any>;
}
export interface IEditInstanceModelRequestBody {
model_name: string[];
model_type: string[];
}
export interface IListAllModelsRequestParams {
type?: string;
}

View File

@@ -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',

View File

@@ -1510,6 +1510,7 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
selectModelBeforeVerify: '请至少选择一个模型后再验证。',
addCustomModel: '添加自定义模型',
addCustomModelTitle: '添加自定义模型',
editCustomModelTitle: '编辑模型',
modelMaxTokens: '最大 Token 数',
modelTypes: {
chat: 'Chat',

View File

@@ -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 (
<li className="flex items-center border-b-[0.5px] border-border-button justify-between p-3 hover:bg-bg-card transition-colors last:border-b-0">
<li className="flex items-center border-b-[0.5px] border-border-button justify-between p-3 hover:bg-bg-card transition-colors last:border-b-0 group">
<div className="flex items-center space-x-3">
<span className="font-medium text-text-primary">{model.name}</span>
{model.model_type.map((modelType) => (
{model.model_type.slice(0, 3).map((modelType) => (
<span
className="px-2 py-1 text-xs bg-bg-card text-text-secondary rounded-md"
key={modelType}
@@ -310,11 +359,41 @@ function ModelListItem({
{modelType}
</span>
))}
{model.model_type.length > 3 && (
<span
className="px-2 py-1 text-xs bg-bg-card text-text-secondary rounded-md"
key="ellipsis"
>
...
</span>
)}
<Button
size="icon"
variant="ghost"
className={cn('h-6 w-6 hidden', 'group-hover:flex')}
onClick={(e: any) => {
e.stopPropagation();
setEditOpen(true);
}}
aria-label={t('setting.editCustomModelTitle')}
>
<Pencil size={12} />
</Button>
</div>
<Switch
checked={model.status === ModelStatus.Active}
onCheckedChange={handleStatusChange}
/>
<AddCustomModelDialog
open={editOpen}
onOpenChange={setEditOpen}
title={t('setting.editCustomModelTitle')}
fields={editFields}
defaultValues={editDefaultValues}
onSubmit={handleEditSubmit}
loading={editLoading}
existingNames={[]}
/>
</li>
);
}

View File

@@ -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<string, unknown>;
}
type FormValues = Record<string, unknown>;
@@ -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<DynamicFormRef>(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 = ({
<Switch
id={switchId}
checked={isChecked}
disabled={field.disabled}
onCheckedChange={(checked) => {
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 (
<Dialog open={open} onOpenChange={onOpenChange}>
@@ -211,8 +223,9 @@ export const AddCustomModelDialog = ({
ref={formRef}
fields={dynamicFields}
onSubmit={handleSubmit}
defaultValues={defaultValues}
>
<DialogFooter>
<DialogFooter className="mb-0 pb-0">
<Button
type="button"
variant="outline"
@@ -223,7 +236,7 @@ export const AddCustomModelDialog = ({
</Button>
<Button type="submit" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{submitText ?? t('confirm')}
{submitText ?? commonT('confirm')}
</Button>
</DialogFooter>
</DynamicForm.Root>

View File

@@ -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',

View File

@@ -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: ({