feat(catalog): 统一模型调用身份并梳理生命周期

新增官方调用名、供应商真实名、显示名和兼容别名的独立契约,调整路由、目录聚合、管理端与 OpenAPI。

增加 Gemini、Qwen、DeepSeek、Claude 和 MiniMax 生命周期迁移、别名观测及引用保护,并补充数据库与聚合测试。
This commit is contained in:
2026-07-22 20:23:36 +08:00
parent e3f6c0fa3e
commit 8d86c4b3b3
20 changed files with 1282 additions and 284 deletions
+3 -2
View File
@@ -31,6 +31,7 @@ export function ModelsPage(props: { data: ConsoleData }) {
model.modelName,
model.description,
...model.providers.map((item) => item.name),
...(model.sources?.flatMap((item) => [item.providerModelName, ...(item.legacyAliases ?? [])]) ?? []),
...model.capabilityTags,
]
.filter(Boolean)
@@ -137,9 +138,9 @@ function ModelCard(props: { model: ModelCatalogItem }) {
<Card className="modelCard">
<CardContent>
<div className="modelCardTop">
<ModelIcon iconPath={props.model.iconPath} label={props.model.displayName || props.model.alias} />
<ModelIcon iconPath={props.model.iconPath} label={props.model.displayName || props.model.modelName} />
<div className="modelCardHeaderText">
<strong>{props.model.displayName || props.model.alias}</strong>
<strong>{props.model.displayName || props.model.modelName}</strong>
<p className="modelCardDescription text-xs" title={description}>{description}</p>
</div>
</div>
+5 -5
View File
@@ -1001,7 +1001,7 @@ function filterModelsByType(models: PlatformModel[], modelTypes: string[]) {
function buildModelOptions(models: PlatformModel[]): ModelOption[] {
const grouped = new Map<string, ModelOption>();
models.forEach((model) => {
const value = model.modelAlias || model.modelName;
const value = model.modelName;
if (!value) return;
const current = grouped.get(value);
if (current) {
@@ -1014,7 +1014,7 @@ function buildModelOptions(models: PlatformModel[]): ModelOption[] {
}
grouped.set(value, {
count: 1,
label: model.modelAlias || model.displayName || model.modelName,
label: model.displayName || model.modelName,
models: [model],
provider: model.provider || model.platformName || '',
value,
@@ -1044,9 +1044,9 @@ function resolveModelOptionValue(value: unknown, modelOptions: ModelOption[]) {
const direct = modelOptions.find((item) => item.value === raw);
if (direct) return direct.value;
const matched = modelOptions.find((item) => item.models.some((model) => (
model.modelAlias === raw
|| model.modelName === raw
|| model.displayName === raw
model.modelName === raw
|| model.modelAlias === raw
|| model.legacyAliases?.includes(raw)
)));
return matched?.value ?? '';
}
@@ -25,8 +25,17 @@ import { ModelCatalogCard } from './ModelCatalogCard';
type ModelForm = {
providerKey: string;
canonicalModelKey: string;
invocationName: string;
providerModelName: string;
modelAlias: string;
displayName: string;
legacyAliasesText: string;
officialModelId: string;
officialDisplayName: string;
lifecycleStage: string;
replacementModel: string;
retireAt: string;
sourceUrl: string;
verifiedAt: string;
status: string;
pricingRuleSetId: string;
runtimePolicySetId: string;
@@ -45,8 +54,17 @@ function createEmptyForm(modelType = 'text_generate'): ModelForm {
return {
providerKey: '',
canonicalModelKey: '',
invocationName: '',
providerModelName: '',
modelAlias: '',
displayName: '',
legacyAliasesText: '',
officialModelId: '',
officialDisplayName: '',
lifecycleStage: 'stable',
replacementModel: '',
retireAt: '',
sourceUrl: '',
verifiedAt: '',
status: 'active',
pricingRuleSetId: '',
runtimePolicySetId: '',
@@ -78,6 +96,7 @@ export function BaseModelCatalogPanel(props: {
const [query, setQuery] = useState('');
const [providerFilter, setProviderFilter] = useState('all');
const [typeFilter, setTypeFilter] = useState('all');
const [statusFilter, setStatusFilter] = useState('active');
const [pendingDeleteModel, setPendingDeleteModel] = useState<BaseModelCatalogItem | null>(null);
const [pendingResetModel, setPendingResetModel] = useState<BaseModelCatalogItem | null>(null);
const [pendingResetAll, setPendingResetAll] = useState(false);
@@ -106,10 +125,11 @@ export function BaseModelCatalogPanel(props: {
return props.baseModels.filter((item) => {
const matchesProvider = providerFilter === 'all' || item.providerKey === providerFilter;
const matchesType = typeFilter === 'all' || readModelTypes(item).includes(typeFilter);
const text = `${baseModelAlias(item)} ${item.providerModelName} ${item.canonicalModelKey}`.toLowerCase();
return matchesProvider && matchesType && (!keyword || text.includes(keyword));
const matchesStatus = statusFilter === 'all' || item.status === statusFilter;
const text = `${item.displayName} ${item.invocationName} ${item.providerModelName} ${item.canonicalModelKey} ${(item.legacyAliases ?? []).join(' ')}`.toLowerCase();
return matchesProvider && matchesType && matchesStatus && (!keyword || text.includes(keyword));
});
}, [props.baseModels, providerFilter, query, typeFilter]);
}, [props.baseModels, providerFilter, query, statusFilter, typeFilter]);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
@@ -213,6 +233,12 @@ export function BaseModelCatalogPanel(props: {
<option value="all"></option>
{typeOptions.map((item) => <option value={item} key={item}>{item}</option>)}
</Select>
<Select value={statusFilter} onChange={(event) => setStatusFilter(event.target.value)}>
<option value="active"> Active</option>
<option value="deprecated"> Deprecated</option>
<option value="hidden"> Hidden</option>
<option value="all"></option>
</Select>
</div>
{(props.message || localError) && <p className="formMessage">{localError || props.message}</p>}
</CardContent>
@@ -268,14 +294,17 @@ export function BaseModelCatalogPanel(props: {
{providerOptions.map((item) => <option value={item} key={item}>{providerNames.get(item) ?? item}</option>)}
</Select>
</FormItem>
<FormItem label="模型名">
<FormItem label="供应商真实名" description="实际发给上游平台的 providerModelName,可包含供应商前缀。">
<Input value={form.providerModelName} onChange={(event) => setForm({ ...form, providerModelName: event.target.value })} />
</FormItem>
<FormItem
description="作为调用和展示的模型名;多个不同平台模型设置同一别名后,可按该别名自动负载。"
label="模型别名"
>
<Input value={form.modelAlias} onChange={(event) => setForm({ ...form, modelAlias: event.target.value })} />
<FormItem description="API Token 用户推荐使用的官方模型 ID,保持官方大小写。" label="官方调用名">
<Input value={form.invocationName} onChange={(event) => setForm({ ...form, invocationName: event.target.value })} />
</FormItem>
<FormItem description="只用于模型卡片标题,不参与路由。" label="显示名">
<Input value={form.displayName} onChange={(event) => setForm({ ...form, displayName: event.target.value })} />
</FormItem>
<FormItem className="spanTwo" description="每行一个历史调用名;仅在 14 天兼容期内参与路由。" label="兼容别名">
<Textarea value={form.legacyAliasesText} onChange={(event) => setForm({ ...form, legacyAliasesText: event.target.value })} rows={3} />
</FormItem>
<FormItem className="spanTwo" label="Canonical Key">
<Input value={form.canonicalModelKey} onChange={(event) => setForm({ ...form, canonicalModelKey: event.target.value })} />
@@ -285,6 +314,29 @@ export function BaseModelCatalogPanel(props: {
{statuses.map((item) => <option value={item} key={item}>{item}</option>)}
</Select>
</FormItem>
<FormItem label="生命周期">
<Select value={form.lifecycleStage} onChange={(event) => setForm({ ...form, lifecycleStage: event.target.value })}>
{['stable', 'preview', 'deprecated', 'provider', 'internal'].map((item) => <option value={item} key={item}>{item}</option>)}
</Select>
</FormItem>
<FormItem label="替代模型">
<Input value={form.replacementModel} onChange={(event) => setForm({ ...form, replacementModel: event.target.value })} />
</FormItem>
<FormItem label="官方模型 ID">
<Input value={form.officialModelId} onChange={(event) => setForm({ ...form, officialModelId: event.target.value })} />
</FormItem>
<FormItem label="官方显示名">
<Input value={form.officialDisplayName} onChange={(event) => setForm({ ...form, officialDisplayName: event.target.value })} />
</FormItem>
<FormItem label="退役时间">
<Input value={form.retireAt} onChange={(event) => setForm({ ...form, retireAt: event.target.value })} placeholder="YYYY-MM-DD" />
</FormItem>
<FormItem label="核验时间">
<Input value={form.verifiedAt} onChange={(event) => setForm({ ...form, verifiedAt: event.target.value })} placeholder="YYYY-MM-DD" />
</FormItem>
<FormItem className="spanTwo" label="官方依据链接">
<Input value={form.sourceUrl} onChange={(event) => setForm({ ...form, sourceUrl: event.target.value })} />
</FormItem>
<FormItem label="计价规则">
<Select value={form.pricingRuleSetId || defaultPricingRuleSetId} onChange={(event) => setForm({ ...form, pricingRuleSetId: event.target.value })}>
{!props.pricingRuleSets.length && <option value=""></option>}
@@ -368,7 +420,7 @@ function ModelCard(props: {
<Pencil size={14} />
</Button>
<Button type="button" variant="destructive" size="sm" onClick={props.onDelete}>
<Button type="button" variant="destructive" size="sm" disabled={props.model.referenceCount > 0} onClick={props.onDelete}>
<Trash2 size={14} />
</Button>
@@ -378,13 +430,14 @@ function ModelCard(props: {
<Badge key="status" variant={props.model.status === 'active' ? 'success' : 'secondary'}>{props.model.status}</Badge>,
<Badge key="catalog-type" variant={isSystem ? 'secondary' : 'outline'}>{baseModelCatalogTypeLabel(props.model)}</Badge>,
...(isSystem && props.model.customizedAt ? [<Badge key="customized" variant="warning"></Badge>] : []),
<Badge key="references" variant={props.model.referenceCount ? 'warning' : 'outline'}>{props.model.referenceCount} </Badge>,
]}
chips={chips}
iconPath={readModelIconPath(props.model)}
iconText={props.model.providerKey}
meta={[props.providerName ?? props.model.providerKey, props.pricingRuleSetName, props.runtimePolicySetName, props.model.canonicalModelKey]}
subtitle={props.model.providerModelName}
title={baseModelAlias(props.model)}
subtitle={`${props.model.invocationName} · 上游 ${props.model.providerModelName}`}
title={props.model.displayName || props.model.invocationName}
/>
);
}
@@ -406,8 +459,17 @@ function modelToForm(model: BaseModelCatalogItem): ModelForm {
return {
providerKey: model.providerKey,
canonicalModelKey: model.canonicalModelKey,
invocationName: model.invocationName || model.modelAlias || model.providerModelName,
providerModelName: model.providerModelName,
modelAlias: baseModelAlias(model),
displayName: model.displayName || model.invocationName || model.providerModelName,
legacyAliasesText: (model.legacyAliases ?? []).join('\n'),
officialModelId: readString(model.metadata?.officialModelId),
officialDisplayName: readString(model.metadata?.officialDisplayName),
lifecycleStage: readString(model.metadata?.lifecycleStage) || 'stable',
replacementModel: readString(model.metadata?.replacementModel),
retireAt: readString(model.metadata?.retireAt),
sourceUrl: readString(model.metadata?.sourceUrl),
verifiedAt: readString(model.metadata?.verifiedAt),
status: model.status,
pricingRuleSetId: model.pricingRuleSetId ?? '',
runtimePolicySetId: model.runtimePolicySetId ?? '',
@@ -425,7 +487,7 @@ function readModelIconPath(model: BaseModelCatalogItem) {
}
function baseModelAlias(model?: BaseModelCatalogItem | null) {
return model?.modelAlias || model?.displayName || model?.providerModelName || '';
return model?.invocationName || model?.modelAlias || model?.providerModelName || '';
}
function primaryCapabilityType(capabilities: CapabilityEditorState) {
@@ -445,21 +507,36 @@ function formToPayload(form: ModelForm): BaseModelUpsertRequest {
const modelTypes = uniqueStrings(form.capabilities.types).filter(Boolean);
const primaryType = modelTypes[0] ?? 'text_generate';
const capabilities = syncCapabilityTypes(form.capabilities, modelTypes);
const modelAlias = form.modelAlias.trim() || form.providerModelName.trim();
const invocationName = form.invocationName.trim() || form.providerModelName.trim();
const displayName = form.displayName.trim() || invocationName;
const metadata = parseJsonObject(form.metadataJson, '元数据 JSON');
return {
providerKey: form.providerKey.trim(),
canonicalModelKey: form.canonicalModelKey.trim() || undefined,
invocationName,
providerModelName: form.providerModelName.trim(),
modelType: modelTypes.length ? modelTypes : [primaryType],
modelAlias,
modelAlias: invocationName,
displayName,
legacyAliases: uniqueStrings(form.legacyAliasesText.split(/[\n,]/)),
capabilities: capabilitiesFromForm(syncPrimaryCapability(capabilities, primaryType)),
baseBillingConfig: parseJsonObject(form.billingJson, '基准计费 JSON'),
defaultRateLimitPolicy: parseJsonObject(form.rateLimitJson, '限流 JSON'),
pricingRuleSetId: form.pricingRuleSetId.trim() || undefined,
runtimePolicySetId: form.runtimePolicySetId.trim() || undefined,
runtimePolicyOverride: parseJsonObject(form.runtimePolicyOverrideJson, '运行策略覆盖 JSON'),
metadata: { ...metadata, alias: modelAlias, originalTypes: modelTypes },
metadata: {
...metadata,
alias: invocationName,
officialModelId: form.officialModelId.trim() || invocationName,
officialDisplayName: form.officialDisplayName.trim() || displayName,
lifecycleStage: form.lifecycleStage,
replacementModel: form.replacementModel.trim() || undefined,
retireAt: form.retireAt.trim() || undefined,
sourceUrl: form.sourceUrl.trim() || undefined,
verifiedAt: form.verifiedAt.trim() || undefined,
originalTypes: modelTypes,
},
pricingVersion: Number.parseInt(form.pricingVersion, 10) || 1,
status: form.status,
};
+4 -3
View File
@@ -172,12 +172,13 @@ export function platformModelPayloads(models: BaseModelCatalogItem[], form: Plat
return {
baseModelId: model.id,
canonicalModelKey: model.canonicalModelKey,
modelName: model.providerModelName,
modelName: model.invocationName || model.providerModelName,
providerModelName,
modelAlias: preset?.modelAlias || stableModelAlias(model),
modelAlias: model.invocationName || preset?.modelAlias || stableModelAlias(model),
modelType: baseModelTypesForProvider(model, form.provider),
displayName:
preset?.displayName ||
model.displayName ||
stableModelAlias(model) ||
providerModelName ||
model.providerModelName,
@@ -199,8 +200,8 @@ export function platformModelPayloads(models: BaseModelCatalogItem[], form: Plat
export function stableModelAlias(model: BaseModelCatalogItem) {
return stripProviderPrefix(
readString(model.invocationName) ||
readString(model.modelAlias) ||
readString(model.displayName) ||
readString(model.metadata?.alias) ||
readString(readRecord(model.metadata?.rawModel)?.alias) ||
model.providerModelName ||