fix(models,settings,conversation): scope model_id uniqueness per

provider and harden model reference resolution
This commit is contained in:
ringotypowriter
2026-02-21 22:31:32 +08:00
parent 9461f923df
commit 50bdbd519c
25 changed files with 376 additions and 107 deletions
@@ -229,12 +229,15 @@ import { inject, computed, watch, nextTick, type Ref, ref } from 'vue'
import { toTypedSchema } from '@vee-validate/zod'
import z from 'zod'
import { useMutation, useQueryCache } from '@pinia/colada'
import { postModels, putModelsModelByModelId } from '@memoh/sdk'
import { postModels, putModelsById, putModelsModelByModelId } from '@memoh/sdk'
import type { ModelsGetResponse } from '@memoh/sdk'
import { CLIENT_TYPE_LIST, CLIENT_TYPE_META } from '@/constants/client-types'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
const availableInputModalities = ['text', 'image', 'audio', 'video', 'file'] as const
const selectedModalities = ref<string[]>(['text'])
const { t } = useI18n()
const formSchema = toTypedSchema(z.object({
type: z.string().min(1),
@@ -313,6 +316,17 @@ const { mutateAsync: createModel, isLoading: createLoading } = useMutation({
onSettled: () => queryCache.invalidateQueries({ key: ['provider-models'] }),
})
const { mutateAsync: updateModel, isLoading: updateLoading } = useMutation({
mutation: async ({ id, data }: { id: string; data: Record<string, unknown> }) => {
const { data: result } = await putModelsById({
path: { id },
body: data as any,
throwOnError: true,
})
return result
},
onSettled: () => queryCache.invalidateQueries({ key: ['provider-models'] }),
})
const { mutateAsync: updateModelByLegacyModelID, isLoading: updateLegacyLoading } = useMutation({
mutation: async ({ modelId, data }: { modelId: string; data: Record<string, unknown> }) => {
const { data: result } = await putModelsModelByModelId({
path: { modelId },
@@ -323,7 +337,7 @@ const { mutateAsync: updateModel, isLoading: updateLoading } = useMutation({
},
onSettled: () => queryCache.invalidateQueries({ key: ['provider-models'] }),
})
const isLoading = computed(() => createLoading.value || updateLoading.value)
const isLoading = computed(() => createLoading.value || updateLoading.value || updateLegacyLoading.value)
async function addModel(e: Event) {
e.preventDefault()
@@ -366,16 +380,31 @@ async function addModel(e: Event) {
}
if (isEdit) {
await updateModel({ modelId: fallback!.model_id, data: payload as any })
const modelUUID = fallback?.id
if (modelUUID) {
await updateModel({ id: modelUUID, data: payload as any })
} else {
await updateModelByLegacyModelID({ modelId: fallback!.model_id, data: payload as any })
}
} else {
await createModel(payload as any)
}
open.value = false
} catch {
} catch (error) {
toast.error(resolveErrorMessage(error, t('common.saveFailed')))
return
}
}
function resolveErrorMessage(error: unknown, fallback: string): string {
if (error instanceof Error && error.message.trim()) return error.message
if (error && typeof error === 'object' && 'message' in error) {
const msg = (error as { message?: string }).message
if (msg && msg.trim()) return msg
}
return fallback
}
watch(open, async () => {
if (!open.value) {
title.value = 'title'
@@ -52,13 +52,13 @@
</div>
<button
v-for="model in group.models"
:key="model.model_id"
:key="model.id || `${model.llm_provider_id}:${model.model_id}`"
class="relative flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground"
:class="{ 'bg-accent': selected === model.model_id }"
@click="selectModel(model.model_id)"
:class="{ 'bg-accent': selected === model.id }"
@click="selectModel(model.id)"
>
<FontAwesomeIcon
v-if="selected === model.model_id"
v-if="selected === model.id"
:icon="['fas', 'check']"
class="size-3.5"
/>
@@ -145,11 +145,12 @@ const filteredGroups = computed(() => {
// 显示选中模型的名称
const displayLabel = computed(() => {
if (!selected.value) return ''
const model = typeFilteredModels.value.find((m) => m.model_id === selected.value)
const model = typeFilteredModels.value.find((m) => m.id === selected.value)
return model?.name || model?.model_id || selected.value
})
function selectModel(modelId: string) {
function selectModel(modelId?: string) {
if (!modelId) return
selected.value = modelId
open.value = false
}
@@ -1,7 +1,7 @@
<template>
<Item variant="outline">
<ItemContent>
<ItemTitle>{{ model.name }}</ItemTitle>
<ItemTitle>{{ model.name || model.model_id }}</ItemTitle>
<ItemDescription class="gap-2 flex flex-wrap items-center mt-3">
<Badge variant="outline">
{{ model.type }}
@@ -26,7 +26,7 @@
<ConfirmPopover
:message="$t('models.deleteModelConfirm')"
:loading="deleteLoading"
@confirm="$emit('delete', model.name)"
@confirm="$emit('delete', model.id ?? '')"
>
<template #trigger>
<Button variant="outline">
@@ -58,6 +58,6 @@ defineProps<{
defineEmits<{
edit: [model: ModelsGetResponse]
delete: [name: string]
delete: [id: string]
}>()
</script>
@@ -16,11 +16,11 @@
>
<ModelItem
v-for="model in models"
:key="model.model_id"
:key="model.id || `${model.llm_provider_id}:${model.model_id}`"
:model="model"
:delete-loading="deleteModelLoading"
@edit="(model) => $emit('edit', model)"
@delete="(name) => $emit('delete', name)"
@delete="(id) => $emit('delete', id)"
/>
</section>
@@ -61,6 +61,6 @@ defineProps<{
defineEmits<{
edit: [model: ModelsGetResponse]
delete: [name: string]
delete: [id: string]
}>()
</script>
@@ -33,7 +33,7 @@ import ProviderForm from './components/provider-form.vue'
import ModelList from './components/model-list.vue'
import { computed, inject, provide, reactive, ref, toRef, watch } from 'vue'
import { useQuery, useMutation, useQueryCache } from '@pinia/colada'
import { putProvidersById, deleteProvidersById, getProvidersByIdModels, deleteModelsModelByModelId } from '@memoh/sdk'
import { putProvidersById, deleteProvidersById, getProvidersByIdModels, deleteModelsById } from '@memoh/sdk'
import type { ModelsGetResponse, ProvidersGetResponse } from '@memoh/sdk'
// ---- Model provide CreateModel ----
@@ -86,8 +86,9 @@ const { mutate: changeProvider, isLoading: editLoading } = useMutation({
})
const { mutate: deleteModel, isLoading: deleteModelLoading } = useMutation({
mutation: async (modelName: string) => {
await deleteModelsModelByModelId({ path: { modelId: modelName }, throwOnError: true })
mutation: async (modelID: string) => {
if (!modelID) return
await deleteModelsById({ path: { id: modelID }, throwOnError: true })
},
onSettled: () => queryCache.invalidateQueries({ key: ['provider-models'] }),
})