feat(models): secure catalog and pin model versions

This commit is contained in:
Jesse_Chen
2026-08-06 19:49:26 +08:00
parent 7040fd998e
commit 28e04857fa
13 changed files with 1426 additions and 150 deletions
@@ -0,0 +1,238 @@
import {
expectedModelProviderSecretRef,
modelConnectionTestSucceeded,
modelProviderModelsUrl,
modelProviderSecretValue,
type ModelProviderType,
} from "../model-provider-policy.ts";
type SaveProviderAction = {
action: "saveProvider";
id?: string | null;
code: string;
name: string;
providerType: ModelProviderType;
baseUrl?: string | null;
secretRef?: string;
enabled: boolean;
reason: string;
};
type SaveDraftAction = {
action: "saveDraft";
modelId: string;
versionId?: string | null;
providerId: string;
label: string;
description: string;
providerModel: string;
modelTier: "standard" | "premium" | "internal";
creditCost: number;
contextWindow?: number | null;
inputCostMicrousdPerMillion: number;
outputCostMicrousdPerMillion: number;
enabled: boolean;
isDefault: boolean;
fallbackModelId?: string | null;
settings: Record<string, unknown>;
reason: string;
};
export type AdminModelMutation = SaveProviderAction | SaveDraftAction | {
action: "test";
versionId: string;
} | {
action: "publish";
versionId: string;
reason: string;
} | {
action: "rollback";
configId: string;
targetVersion: number;
reason: string;
};
type ProviderRow = {
id: string;
code: string;
provider_type: ModelProviderType;
base_url: string | null;
secret_ref: string;
enabled: boolean;
version_id: string;
version_enabled: boolean;
};
type Dependencies = {
queryRows(sql: string, values?: readonly unknown[]): Promise<readonly Record<string, unknown>[]>;
assertAllowedUrl(value: string): Promise<unknown>;
probeAllowed(value: string, apiKey: string): Promise<number>;
invalidateCatalog(): void;
environment?: Readonly<Record<string, string | undefined>>;
};
function json(body: unknown, status = 200) {
return Response.json(body, { status });
}
function isImmutableProviderError(error: unknown) {
return error instanceof Error && error.message.includes("model_provider_runtime_immutable");
}
async function versionProvider(
dependencies: Dependencies,
where: string,
values: readonly unknown[],
) {
const rows = await dependencies.queryRows(`
select p.id,p.code,p.provider_type,p.base_url,p.secret_ref,p.enabled,
v.id version_id,v.enabled version_enabled
from public.model_config_versions v
join public.model_providers p on p.id=v.provider_id
where ${where}
`, values);
return (rows[0] as ProviderRow | undefined) ?? null;
}
async function runnableProvider(
provider: ProviderRow,
dependencies: Dependencies,
) {
if (!provider.enabled || !provider.version_enabled) return null;
const apiKey = modelProviderSecretValue({
code: provider.code,
providerType: provider.provider_type,
secretRef: provider.secret_ref,
}, dependencies.environment ?? process.env);
if (!apiKey) return null;
if (provider.provider_type === "openai-compatible") {
await dependencies.assertAllowedUrl(provider.base_url ?? "");
}
return apiKey;
}
async function probeAndRecord(
provider: ProviderRow,
actorUserId: string,
requestId: string,
dependencies: Dependencies,
) {
const apiKey = await runnableProvider(provider, dependencies);
if (!apiKey) return { recorded: false, status: 0, success: false };
let status = 0;
try {
status = await dependencies.probeAllowed(modelProviderModelsUrl({
providerType: provider.provider_type,
baseUrl: provider.base_url,
}), apiKey);
} catch {
status = 0;
}
await dependencies.queryRows(
"select public.admin_record_model_connection_test($1,$2,$3,$4) id",
[actorUserId, provider.version_id, status, requestId],
);
return { recorded: true, status, success: modelConnectionTestSucceeded(status) };
}
export async function handleAdminModelMutation(
action: AdminModelMutation,
context: Readonly<{ actorUserId: string; requestId: string }>,
dependencies: Dependencies,
) {
if (action.action === "saveProvider") {
const secretRef = expectedModelProviderSecretRef(action.code, action.providerType);
if (action.secretRef && action.secretRef !== secretRef) {
return json({ error: "模型供应商密钥引用不受允许" }, 400);
}
if (action.providerType === "openai-compatible") {
await dependencies.assertAllowedUrl(action.baseUrl ?? "");
}
if (action.enabled && !modelProviderSecretValue({
code: action.code,
providerType: action.providerType,
secretRef,
}, dependencies.environment ?? process.env)) {
return json({ error: "模型供应商密钥未配置" }, 409);
}
let rows: readonly Record<string, unknown>[];
try {
rows = await dependencies.queryRows(
"select public.admin_save_model_provider($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) id",
[context.actorUserId, action.id ?? null, action.code, action.name, action.providerType,
action.providerType === "openai" ? null : action.baseUrl ?? null,
secretRef, action.enabled, action.reason, context.requestId],
);
} catch (error) {
if (isImmutableProviderError(error)) {
return json({
error: "已发布或已退役版本使用的供应商连接配置不可修改,请新建供应商和模型版本后重新测试并发布",
code: "model_provider_runtime_immutable",
}, 409);
}
throw error;
}
return json({ data: { id: rows[0]!.id, requestId: context.requestId } });
}
if (action.action === "saveDraft") {
const rows = await dependencies.queryRows(
"select public.admin_save_model_draft($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16::jsonb,$17,$18) id",
[context.actorUserId, action.modelId, action.versionId ?? null, action.providerId, action.label,
action.description, action.providerModel, action.modelTier, action.creditCost, action.contextWindow ?? null,
action.inputCostMicrousdPerMillion, action.outputCostMicrousdPerMillion, action.enabled, action.isDefault,
action.fallbackModelId ?? null, JSON.stringify(action.settings), action.reason, context.requestId],
);
return json({ data: { id: rows[0]!.id, requestId: context.requestId } });
}
if (action.action === "test") {
const provider = await versionProvider(
dependencies,
"v.id=$1 and v.status in ('draft','published','retired')",
[action.versionId],
);
if (!provider) return json({ error: "模型版本不存在" }, 404);
const result = await probeAndRecord(provider, context.actorUserId, context.requestId, dependencies);
if (!result.recorded) return json({ error: "模型版本或供应商未启用,或部署密钥不可用" }, 409);
return json({
data: {
id: provider.version_id,
reachable: result.success,
status: result.status,
secretConfigured: true,
requestId: context.requestId,
},
}, result.success ? 200 : 409);
}
if (action.action === "publish") {
const rows = await dependencies.queryRows(
"select public.admin_publish_model($1,$2,$3,$4) id",
[context.actorUserId, action.versionId, action.reason, context.requestId],
);
dependencies.invalidateCatalog();
return json({ data: { id: rows[0]!.id, requestId: context.requestId } });
}
const provider = await versionProvider(
dependencies,
"v.config_id=$1 and v.version=$2 and v.status='retired'",
[action.configId, action.targetVersion],
);
if (!provider) return json({ error: "回滚版本不存在" }, 404);
const result = await probeAndRecord(provider, context.actorUserId, context.requestId, dependencies);
if (!result.recorded) return json({ error: "回滚版本或供应商未启用,或部署密钥不可用" }, 409);
if (!result.success) {
return json({
error: "回滚版本运行时连接测试失败",
data: { status: result.status, requestId: context.requestId },
}, 409);
}
const rows = await dependencies.queryRows(
"select public.admin_rollback_model($1,$2,$3,$4,$5) id",
[context.actorUserId, action.configId, action.targetVersion, action.reason, context.requestId],
);
dependencies.invalidateCatalog();
return json({ data: { id: rows[0]!.id, requestId: context.requestId } });
}