fix(admin): repair model discovery and simplify model setup
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
export async function createDiscoveredModelId(providerId: string, providerModel: string) {
|
||||
const digest = await globalThis.crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode(`${providerId}\0${providerModel}`),
|
||||
);
|
||||
const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
return `m-${hex.slice(0, 62)}`;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
|
||||
import { createDiscoveredModelId } from "./model-id";
|
||||
import { formatAdminDate } from "./resource-table";
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -72,10 +73,7 @@ type ProviderForm = {
|
||||
apiKey?: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
type ModelForm = Omit<ModelVersion, "id" | "configId" | "version" | "providerCode" | "status" | "createdAt" | "publishedAt" | "settings"> & {
|
||||
versionId?: string;
|
||||
settingsJson: string;
|
||||
};
|
||||
type ModelForm = Pick<ModelVersion, "providerId" | "providerModel" | "creditCost" | "enabled" | "isDefault">;
|
||||
|
||||
type ModelsPayload = { data: ModelVersion[]; total: number; providers: Provider[] };
|
||||
type DiscoveredModel = { id: string; label?: string };
|
||||
@@ -106,7 +104,6 @@ export default function ModelManagement() {
|
||||
const [modelForm] = Form.useForm<ModelForm>();
|
||||
const [filterForm] = Form.useForm<ModelFilters>();
|
||||
const selectedProviderId = Form.useWatch("providerId", modelForm);
|
||||
const selectedProviderModel = Form.useWatch("providerModel", modelForm);
|
||||
const [models, setModels] = useState<ModelVersion[]>([]);
|
||||
const [providers, setProviders] = useState<Provider[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -167,38 +164,19 @@ export default function ModelManagement() {
|
||||
|
||||
function openModel(model?: ModelVersion, providerId?: string) {
|
||||
setEditingModel(model ?? null);
|
||||
setDiscoveredModels([]);
|
||||
setDiscoveredModels(model ? [{ id: model.providerModel, label: model.label }] : []);
|
||||
modelForm.setFieldsValue(model ? {
|
||||
modelId: model.modelId,
|
||||
versionId: model.id,
|
||||
providerId: model.providerId,
|
||||
label: model.label,
|
||||
description: model.description,
|
||||
providerModel: model.providerModel,
|
||||
modelTier: model.modelTier,
|
||||
creditCost: model.creditCost,
|
||||
contextWindow: model.contextWindow,
|
||||
inputCostMicrousdPerMillion: model.inputCostMicrousdPerMillion,
|
||||
outputCostMicrousdPerMillion: model.outputCostMicrousdPerMillion,
|
||||
enabled: model.enabled,
|
||||
isDefault: model.isDefault,
|
||||
fallbackModelId: model.fallbackModelId,
|
||||
settingsJson: JSON.stringify(model.settings, null, 2),
|
||||
} : {
|
||||
modelId: "",
|
||||
providerId: providerId ?? providers[0]?.id,
|
||||
label: "",
|
||||
description: "",
|
||||
providerModel: "",
|
||||
modelTier: "standard",
|
||||
creditCost: 1,
|
||||
contextWindow: null,
|
||||
inputCostMicrousdPerMillion: 0,
|
||||
outputCostMicrousdPerMillion: 0,
|
||||
enabled: false,
|
||||
isDefault: false,
|
||||
fallbackModelId: null,
|
||||
settingsJson: "{}",
|
||||
});
|
||||
setModelOpen(true);
|
||||
}
|
||||
@@ -239,7 +217,7 @@ export default function ModelManagement() {
|
||||
body: JSON.stringify({ providerId }),
|
||||
});
|
||||
setDiscoveredModels(payload.data);
|
||||
if (!payload.data.length) message.info("未发现可用模型,可继续手工输入");
|
||||
if (!payload.data.length) message.info("未发现可用模型,请检查供应商配置后重试");
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : "获取模型列表失败");
|
||||
} finally {
|
||||
@@ -247,46 +225,30 @@ export default function ModelManagement() {
|
||||
}
|
||||
}
|
||||
|
||||
function selectDiscoveredModel(providerModel: string) {
|
||||
const discovered = discoveredModels.find((item) => item.id === providerModel);
|
||||
const values = modelForm.getFieldsValue(["modelId", "label"]);
|
||||
modelForm.setFieldsValue({
|
||||
providerModel,
|
||||
...(!values.modelId ? {
|
||||
modelId: providerModel.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "model",
|
||||
} : {}),
|
||||
...(!values.label ? { label: discovered?.label?.trim() || providerModel } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
async function saveModel(values: ModelForm) {
|
||||
setSaving(true);
|
||||
try {
|
||||
let settings: unknown;
|
||||
try {
|
||||
settings = JSON.parse(values.settingsJson);
|
||||
} catch {
|
||||
throw new Error("设置 JSON 格式不正确");
|
||||
}
|
||||
const discovered = discoveredModels.find((item) => item.id === values.providerModel);
|
||||
const modelId = editingModel?.modelId ?? await createDiscoveredModelId(values.providerId, values.providerModel);
|
||||
await adminRequestJson("/api/admin/models", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
action: "saveDraft",
|
||||
modelId: values.modelId.trim(),
|
||||
modelId,
|
||||
versionId: editingModel?.id ?? null,
|
||||
providerId: values.providerId,
|
||||
label: values.label.trim(),
|
||||
description: values.description?.trim() ?? "",
|
||||
label: editingModel?.label ?? (discovered?.label?.trim() || values.providerModel).slice(0, 60),
|
||||
providerModel: values.providerModel.trim(),
|
||||
modelTier: values.modelTier,
|
||||
creditCost: values.creditCost,
|
||||
contextWindow: values.contextWindow ?? null,
|
||||
inputCostMicrousdPerMillion: values.inputCostMicrousdPerMillion,
|
||||
outputCostMicrousdPerMillion: values.outputCostMicrousdPerMillion,
|
||||
enabled: values.enabled,
|
||||
description: editingModel?.description ?? "",
|
||||
modelTier: editingModel?.modelTier ?? "standard",
|
||||
contextWindow: editingModel?.contextWindow ?? null,
|
||||
inputCostMicrousdPerMillion: editingModel?.inputCostMicrousdPerMillion ?? 0,
|
||||
outputCostMicrousdPerMillion: editingModel?.outputCostMicrousdPerMillion ?? 0,
|
||||
isDefault: values.isDefault,
|
||||
fallbackModelId: values.fallbackModelId?.trim() || null,
|
||||
settings,
|
||||
fallbackModelId: editingModel?.fallbackModelId ?? null,
|
||||
settings: editingModel?.settings ?? {},
|
||||
}),
|
||||
});
|
||||
message.success("模型草稿已保存");
|
||||
@@ -415,39 +377,40 @@ export default function ModelManagement() {
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal title={editingModel ? `编辑 ${editingModel.modelId}` : "新增模型草稿"} open={modelOpen} width={860} okText="保存草稿" cancelText="取消" confirmLoading={saving} onOk={() => modelForm.submit()} onCancel={() => setModelOpen(false)} destroyOnHidden>
|
||||
<Modal title={editingModel ? `编辑 ${editingModel.modelId}` : "新增模型草稿"} open={modelOpen} width={720} okText="保存草稿" cancelText="取消" confirmLoading={saving} onOk={() => modelForm.submit()} onCancel={() => setModelOpen(false)} destroyOnHidden>
|
||||
<Form<ModelForm> form={modelForm} layout="vertical" onFinish={saveModel}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={8}><Form.Item name="modelId" label="模型 ID" rules={[{ required: true }, { pattern: /^[a-z0-9][a-z0-9._-]{0,63}$/ }]}><Input disabled={Boolean(editingModel)} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name="label" label="显示名称" rules={[{ required: true }]}><Input /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item label="供应商" required><Space.Compact block><Form.Item name="providerId" noStyle rules={[{ required: true, message: "请选择供应商" }]}><Select onChange={() => setDiscoveredModels([])} options={providers.map((item) => ({ value: item.id, label: `${item.name} (${item.code})` }))} /></Form.Item><Button loading={discovering} disabled={!selectedProviderId} onClick={() => void discoverModels(selectedProviderId)}>获取模型列表</Button></Space.Compact></Form.Item></Col>
|
||||
</Row>
|
||||
<Form.Item label="发现的模型" extra="选择后会填入供应商模型名;模型 ID 和显示名称仅在为空时自动补全。">
|
||||
<Form.Item label="供应商" required>
|
||||
<Space.Compact block>
|
||||
<Form.Item name="providerId" noStyle rules={[{ required: true, message: "请选择供应商" }]}>
|
||||
<Select
|
||||
onChange={() => {
|
||||
setDiscoveredModels([]);
|
||||
modelForm.setFieldValue("providerModel", "");
|
||||
}}
|
||||
options={providers.map((item) => ({ value: item.id, label: `${item.name} (${item.code})` }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button loading={discovering} disabled={!selectedProviderId} onClick={() => void discoverModels(selectedProviderId)}>获取模型列表</Button>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
<Form.Item name="providerModel" label="模型" rules={[{ required: true, message: "请从供应商模型列表中选择" }]} extra="获取列表后选择供应商提供的模型。">
|
||||
<Select
|
||||
showSearch
|
||||
allowClear
|
||||
optionFilterProp="label"
|
||||
placeholder={selectedProviderId ? "先获取模型列表,或在下方手工输入" : "请先选择供应商"}
|
||||
placeholder={selectedProviderId ? "获取并选择模型" : "请先选择供应商"}
|
||||
disabled={!selectedProviderId || !discoveredModels.length}
|
||||
value={discoveredModels.some((item) => item.id === selectedProviderModel) ? selectedProviderModel : undefined}
|
||||
options={discoveredModels.map((item) => ({ value: item.id, label: item.label ? `${item.label} (${item.id})` : item.id }))}
|
||||
onSelect={selectDiscoveredModel}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}><Form.Item name="providerModel" label="供应商模型名(可手工输入)" rules={[{ required: true }]}><Input /></Form.Item></Col>
|
||||
<Col xs={24} md={6}><Form.Item name="modelTier" label="模型档位" rules={[{ required: true }]}><Select options={Object.entries(modelTierLabels).map(([value, label]) => ({ value, label }))} /></Form.Item></Col>
|
||||
<Col xs={24} md={6}><Form.Item name="creditCost" label="单次点数" rules={[{ required: true }]}><InputNumber min={1} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
|
||||
<Col xs={24} md={12}><Form.Item name="creditCost" label="单次点数" rules={[{ required: true }]}><InputNumber min={1} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
|
||||
<Col xs={24} md={12}>
|
||||
<Space size="large">
|
||||
<Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item>
|
||||
<Form.Item name="isDefault" label="默认模型" valuePropName="checked" dependencies={["enabled"]} rules={[({ getFieldValue }) => ({ validator(_, value) { return value && !getFieldValue("enabled") ? Promise.reject(new Error("默认模型必须启用")) : Promise.resolve(); } })]}><Switch /></Form.Item>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="description" label="说明"><Input.TextArea rows={2} /></Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={8}><Form.Item name="contextWindow" label="上下文窗口"><InputNumber min={1} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name="inputCostMicrousdPerMillion" label="输入成本(微美元/百万 Token)" rules={[{ required: true }]}><InputNumber min={0} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
|
||||
<Col xs={24} md={8}><Form.Item name="outputCostMicrousdPerMillion" label="输出成本(微美元/百万 Token)" rules={[{ required: true }]}><InputNumber min={0} precision={0} style={{ width: "100%" }} /></Form.Item></Col>
|
||||
</Row>
|
||||
<Form.Item name="fallbackModelId" label="回退模型 ID"><Input allowClear /></Form.Item>
|
||||
<Form.Item name="settingsJson" label="设置 JSON" rules={[{ required: true }]}><Input.TextArea rows={5} spellCheck={false} /></Form.Item>
|
||||
<Space size="large"><Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item><Form.Item name="isDefault" label="默认模型" valuePropName="checked" dependencies={["enabled"]} rules={[({ getFieldValue }) => ({ validator(_, value) { return value && !getFieldValue("enabled") ? Promise.reject(new Error("默认模型必须启用")) : Promise.resolve(); } })]}><Switch /></Form.Item></Space>
|
||||
</Form>
|
||||
</Modal>
|
||||
<Modal
|
||||
|
||||
Reference in New Issue
Block a user