fix(provider): support editing fetched model types before save (#17728)

### Summary

As title

now can edit
This commit is contained in:
Haruko386
2026-08-03 18:07:41 +08:00
committed by GitHub
parent f9fd764c4e
commit 9adcd59586
9 changed files with 125 additions and 242 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -345,9 +345,7 @@ func ParseListModel(modelList ModelList) []ListModelResponse {
if pm != nil {
modelEntity = pm.GetModelByNameOrAlias(modelName)
}
if model.OwnedBy != "" {
modelName = modelName + "@" + model.OwnedBy
}
modelResponse.Name = modelName
if modelEntity != nil {
modelResponse.MaxDimension = modelEntity.MaxDimension

View File

@@ -110,8 +110,8 @@ func TestFunASRListModelsWithoutAPIKey(t *testing.T) {
if err != nil {
t.Fatalf("ListModels: %v", err)
}
if len(models) != 1 || models[0].Name != "fun-asr-nano@funasr" {
t.Fatalf("models=%v, want fun-asr-nano@funasr", models)
if len(models) != 1 || models[0].Name != "fun-asr-nano" {
t.Fatalf("models=%v, want fun-asr-nano", models)
}
}

View File

@@ -189,7 +189,7 @@ func TestGiteeListModelsMapsAllDeepSeekAliasesToModelMetadata(t *testing.T) {
}
}
func TestGiteeListModelsKeepsOwnedBySuffixAfterAliasMetadataLookup(t *testing.T) {
func TestGiteeListModelsKeepsModelNameAfterAliasMetadataLookup(t *testing.T) {
withSSRFBypass(t)
ctx := t.Context()
initProviderManagerWithGiteeForTest(t)
@@ -207,8 +207,8 @@ func TestGiteeListModelsKeepsOwnedBySuffixAfterAliasMetadataLookup(t *testing.T)
t.Fatalf("len(models)=%d, want 1", len(models))
}
model := models[0]
if model.Name != "deepseek/deepseek-v4-pro@gitee" {
t.Fatalf("Name=%q, want deepseek/deepseek-v4-pro@gitee", model.Name)
if model.Name != "deepseek/deepseek-v4-pro" {
t.Fatalf("Name=%q, want deepseek/deepseek-v4-pro", model.Name)
}
if model.MaxTokens == nil || *model.MaxTokens != 1048576 {
t.Fatalf("MaxTokens=%v, want 1048576", model.MaxTokens)

View File

@@ -392,7 +392,7 @@ func TestCollectGoogleModelNamesPaginates(t *testing.T) {
t.Fatalf("expected no error, got %v", err)
}
expectedModels := []ListModelResponse{{Name: "Gemini 2.5 Flash@Google"}, {Name: "Gemini 2.5 Pro@Google"}}
expectedModels := []ListModelResponse{{Name: "Gemini 2.5 Flash"}, {Name: "Gemini 2.5 Pro"}}
if !reflect.DeepEqual(models, expectedModels) {
t.Fatalf("expected models %v, got %v", expectedModels, models)
}

View File

@@ -84,7 +84,7 @@ func TestVolcEngineListModelsHappyPath(t *testing.T) {
if err != nil {
t.Fatalf("ListModels: %v", err)
}
if joinModelNames(models, ",") != "doubao-seed-2-0-pro-260215@volcengine,doubao-embedding-vision-251215" {
if joinModelNames(models, ",") != "doubao-seed-2-0-pro-260215,doubao-embedding-vision-251215" {
t.Errorf("models=%v", models)
}
}

View File

@@ -154,7 +154,7 @@ func (h *ProviderHandler) ListModels(c *gin.Context) {
return
}
// 1. Get static models from config (may be nil when models list is empty)
// 1. Get static models from config (maybe nil when models list is empty)
staticModels, _ := dao.GetModelProviderManager().ListModels(providerName)
if staticModels == nil {
staticModels = []map[string]interface{}{}

View File

@@ -140,9 +140,59 @@ export function useModelsCatalog({
}: UseModelsCatalogArgs) {
const { listProviderModels } = useListProviderModels();
const [catalog, setCatalog] = useState<IProviderModelItem[]>([]);
const [catalogOverrides, setCatalogOverrides] = useState<
Record<string, IProviderModelItem>
>({});
const catalogOverridesRef = useRef(catalogOverrides);
const [manualListLoading, setManualListLoading] = useState(false);
const [hasFetched, setHasFetched] = useState(false);
const applyCatalogOverrides = useCallback((items: IProviderModelItem[]) => {
const overrides = catalogOverridesRef.current;
const names = new Set<string>();
const merged = items.map((item) => {
names.add(item.name);
const override = overrides[item.name];
return override ? { ...item, ...override, name: item.name } : item;
});
Object.entries(overrides).forEach(([name, override]) => {
if (!names.has(name)) {
merged.push(override);
}
});
return merged;
}, []);
const updateCatalogModel = useCallback(
(name: string, item: IProviderModelItem) => {
setCatalogOverrides((prev) => {
const next = {
...prev,
[name]: { ...(prev[name] ?? {}), ...item, name },
};
catalogOverridesRef.current = next;
return next;
});
setCatalog((prev) => {
if (!prev.some((m) => m.name === name)) {
return [...prev, { ...item, name }];
}
return prev.map((m) => (m.name === name ? { ...m, ...item, name } : m));
});
},
[],
);
const clearCatalogOverride = useCallback((name: string) => {
setCatalogOverrides((prev) => {
if (!prev[name]) return prev;
const next = { ...prev };
delete next[name];
catalogOverridesRef.current = next;
return next;
});
}, []);
// Manual "List models" handler — hits the upstream catalog endpoint.
// The result is merged into `catalog`; the displayed list then becomes
// the union of catalog + instance models.
@@ -160,7 +210,9 @@ export function useModelsCatalog({
base_url: baseUrl,
});
if (ret?.code === 0) {
setCatalog((ret.data as IProviderModelItem[]) ?? []);
setCatalog(
applyCatalogOverrides((ret.data as IProviderModelItem[]) ?? []),
);
}
setHasFetched(true);
} catch {
@@ -203,6 +255,8 @@ export function useModelsCatalog({
return {
catalog,
setCatalog,
updateCatalogModel,
clearCatalogOverride,
manualListLoading,
hasFetched,
handleListModels,
@@ -599,6 +653,7 @@ interface UseModelMutationsArgs {
filteredModels: IProviderModelItem[];
addedSet: Set<string>;
setCatalog: Dispatch<SetStateAction<IProviderModelItem[]>>;
clearCatalogOverride: (name: string) => void;
/**
* Local mutators for the draft instance's model list. Required when
* `isDraftInstance` is true so per-model add / remove / batch updates
@@ -621,6 +676,7 @@ export function useModelMutations({
filteredModels,
addedSet,
setCatalog,
clearCatalogOverride,
addDraftModel,
removeDraftModel,
setDraftModelsList,
@@ -644,6 +700,7 @@ export function useModelMutations({
// rides along with the instance save (model_info in the add body).
if (isDraftInstance) {
addDraftModel?.(model);
clearCatalogOverride(model.name);
return;
}
await addInstanceModel({
@@ -652,8 +709,12 @@ export function useModelMutations({
model_name: model.name,
model_type: model.model_types ?? [],
max_tokens: model.max_tokens ?? 0,
extra: { is_tools: hasToolFeature(model.features), ...(model.extra ?? {}) },
extra: {
is_tools: hasToolFeature(model.features),
...(model.extra ?? {}),
},
});
clearCatalogOverride(model.name);
};
const handleRemoveModel = async (model: IProviderModelItem) => {
@@ -683,6 +744,7 @@ export function useModelMutations({
// dropped on save.
if (isDraftInstance) {
addDraftModel?.(item);
clearCatalogOverride(item.name);
}
return;
}
@@ -694,6 +756,7 @@ export function useModelMutations({
max_tokens: item.max_tokens ?? 0,
extra: { is_tools: hasToolFeature(item.features), ...(item.extra ?? {}) },
});
clearCatalogOverride(item.name);
};
// Batch attach/detach the currently visible (filtered) models.
@@ -718,6 +781,11 @@ export function useModelMutations({
if (isDraftInstance) {
setDraftModelsList?.(nextModels);
filteredModels.forEach((m) => {
if (!addedSet.has(m.name)) {
clearCatalogOverride(m.name);
}
});
return;
}
@@ -731,6 +799,11 @@ export function useModelMutations({
region: instance?.region ?? 'default',
model_info: buildModelInfo(nextModels),
});
filteredModels.forEach((m) => {
if (!addedSet.has(m.name)) {
clearCatalogOverride(m.name);
}
});
};
return {
@@ -750,14 +823,20 @@ export function useModelMutations({
interface UseModelEditArgs {
providerName: string;
instanceName: string;
addedSet: Set<string>;
isDraftInstance?: boolean;
updateCatalogModel: (name: string, item: IProviderModelItem) => void;
clearCatalogOverride: (name: string) => void;
updateDraftModel?: (item: IProviderModelItem) => void;
}
export function useModelEdit({
providerName,
instanceName,
addedSet,
isDraftInstance,
updateCatalogModel,
clearCatalogOverride,
updateDraftModel,
}: UseModelEditArgs) {
const queryClient = useQueryClient();
@@ -807,10 +886,7 @@ export function useModelEdit({
// Build the features array from `extra` booleans whose keys match
// the standard feature (`is_tools`) or the provider-specific
// whitelist. Only `true` values become selected switch-group entries.
const featureKeySet = new Set<string>([
'is_tools',
...providerFeatureKeys,
]);
const featureKeySet = new Set<string>(['is_tools', ...providerFeatureKeys]);
const features: string[] = [];
const featureBooleans = new Set<string>();
for (const [key, value] of Object.entries(extra)) {
@@ -823,9 +899,7 @@ export function useModelEdit({
}
// Remaining extra fields (non-feature: element-format selects, etc.).
const remainingExtra = Object.fromEntries(
Object.entries(extra).filter(
([k]) => !featureBooleans.has(k),
),
Object.entries(extra).filter(([k]) => !featureBooleans.has(k)),
);
return {
name: editingModel.name,
@@ -845,12 +919,18 @@ export function useModelEdit({
if (!editingModel) return;
const targetName = editingModel.name;
if (isDraftInstance && updateDraftModel) {
if (isDraftInstance && updateDraftModel && addedSet.has(targetName)) {
updateDraftModel(item);
setEditingModel(null);
return;
}
if (!addedSet.has(targetName)) {
updateCatalogModel(targetName, item);
setEditingModel(null);
return;
}
queryClient.setQueryData<IInstanceModel[]>(
LlmKeys.instanceModels(providerName, instanceName),
(prev) => {
@@ -864,7 +944,10 @@ export function useModelEdit({
max_tokens: item.max_tokens ?? 0,
model_type: item.model_types ?? [],
is_tools: hasToolFeature(item.features),
extra: { is_tools: hasToolFeature(item.features), ...(item.extra ?? {}) },
extra: {
is_tools: hasToolFeature(item.features),
...(item.extra ?? {}),
},
};
return next;
},
@@ -878,6 +961,7 @@ export function useModelEdit({
model_type: item.model_types ?? [],
extra: { is_tools: hasToolFeature(item.features), ...(item.extra ?? {}) },
});
clearCatalogOverride(targetName);
setEditingModel(null);
};

View File

@@ -86,6 +86,8 @@ export function ModelsSection(props: ModelsSectionProps) {
const {
catalog,
setCatalog,
updateCatalogModel,
clearCatalogOverride,
manualListLoading,
hasFetched,
handleListModels,
@@ -237,6 +239,7 @@ export function ModelsSection(props: ModelsSectionProps) {
filteredModels,
addedSet,
setCatalog,
clearCatalogOverride,
addDraftModel,
removeDraftModel,
setDraftModelsList: setDraftModels,
@@ -255,7 +258,10 @@ export function ModelsSection(props: ModelsSectionProps) {
} = useModelEdit({
providerName,
instanceName,
addedSet,
isDraftInstance,
updateCatalogModel,
clearCatalogOverride,
updateDraftModel,
});