|
|
|
@@ -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,
|
|
|
|
|
};
|
|
|
|
|