feat: add AI model health ban exclusion
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-27 11:54:17 +08:00
parent 15a08e6d65
commit c7966298dc
8 changed files with 88 additions and 14 deletions

View File

@ -869,6 +869,7 @@ type AiModel = {
fallbackModelId1?: string; fallbackModelId1?: string;
fallbackModelId2?: string; fallbackModelId2?: string;
excludeFromFailover?: boolean; excludeFromFailover?: boolean;
excludeFromHealthBan?: boolean;
healthStatus?: "healthy" | "degraded" | "auto_disabled"; healthStatus?: "healthy" | "degraded" | "auto_disabled";
healthConsecutiveFailures?: number; healthConsecutiveFailures?: number;
healthLastEventType?: string | null; healthLastEventType?: string | null;
@ -3608,6 +3609,7 @@ export function AdminSettingsClient() {
fallbackModelId1: "", // 备用模型 ID 1 fallbackModelId1: "", // 备用模型 ID 1
fallbackModelId2: "", // 备用模型 ID 2 fallbackModelId2: "", // 备用模型 ID 2
excludeFromFailover: false, excludeFromFailover: false,
excludeFromHealthBan: false,
capabilities: {} as Record<string, unknown>, capabilities: {} as Record<string, unknown>,
// 🆕 图片生成模型特性配置 // 🆕 图片生成模型特性配置
imageGenConfig: createDefaultImageGenConfig(), imageGenConfig: createDefaultImageGenConfig(),
@ -6892,6 +6894,7 @@ export function AdminSettingsClient() {
fallbackModelId1: "", fallbackModelId1: "",
fallbackModelId2: "", fallbackModelId2: "",
excludeFromFailover: false, excludeFromFailover: false,
excludeFromHealthBan: false,
capabilities: {}, capabilities: {},
imageGenConfig: createDefaultImageGenConfig(), imageGenConfig: createDefaultImageGenConfig(),
}); });
@ -14978,6 +14981,10 @@ export function AdminSettingsClient() {
多模态模型(Agent 识图与分析使用同一模型) 多模态模型(Agent 识图与分析使用同一模型)
</Label> </Label>
</div> </div>
<div className="space-y-3 rounded-md border p-3">
<div className="text-sm font-medium">
故障保护策略
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Switch <Switch
checked={modelForm.excludeFromFailover || false} checked={modelForm.excludeFromFailover || false}
@ -14989,9 +14996,30 @@ export function AdminSettingsClient() {
} }
/> />
<Label> <Label>
禁止参与故障迁移(不作为其他模型的自动备用模型) 不参与故障迁移候选池
</Label> </Label>
</div> </div>
<p className="text-xs text-muted-foreground">
开启后,该模型不会作为其他模型请求失败时的自动备用模型。
</p>
<div className="flex items-center gap-2">
<Switch
checked={modelForm.excludeFromHealthBan || false}
onCheckedChange={(checked) =>
setModelForm({
...modelForm,
excludeFromHealthBan: checked,
})
}
/>
<Label>
不参与故障封禁池
</Label>
</div>
<p className="text-xs text-muted-foreground">
开启后,健康检查仍会记录故障日志,但不会因为连续失败自动禁用该模型。
</p>
</div>
{/* 🆕 图片生成模型特性配置 */} {/* 🆕 图片生成模型特性配置 */}
{(isImageProviderForModelConfig || {(isImageProviderForModelConfig ||
@ -15911,7 +15939,12 @@ export function AdminSettingsClient() {
)} )}
{(model as any).excludeFromFailover && ( {(model as any).excludeFromFailover && (
<span className="text-xs bg-rose-100 text-rose-700 px-2 py-0.5 rounded"> <span className="text-xs bg-rose-100 text-rose-700 px-2 py-0.5 rounded">
不参与故障迁移 不参与迁移池
</span>
)}
{(model as any).excludeFromHealthBan && (
<span className="text-xs bg-stone-100 text-stone-700 px-2 py-0.5 rounded">
不参与封禁池
</span> </span>
)} )}
{healthStatus === "auto_disabled" && ( {healthStatus === "auto_disabled" && (
@ -16033,6 +16066,8 @@ export function AdminSettingsClient() {
(model as any).fallbackModelId2 || "", (model as any).fallbackModelId2 || "",
excludeFromFailover: excludeFromFailover:
(model as any).excludeFromFailover || false, (model as any).excludeFromFailover || false,
excludeFromHealthBan:
(model as any).excludeFromHealthBan || false,
capabilities: capabilities:
(model as any).capabilities && (model as any).capabilities &&
typeof (model as any).capabilities === "object" && typeof (model as any).capabilities === "object" &&

View File

@ -82,6 +82,7 @@ const createModelSchema = z.object({
fallbackModelId1: z.string().max(128).optional(), fallbackModelId1: z.string().max(128).optional(),
fallbackModelId2: z.string().max(128).optional(), fallbackModelId2: z.string().max(128).optional(),
excludeFromFailover: z.boolean().optional(), excludeFromFailover: z.boolean().optional(),
excludeFromHealthBan: z.boolean().optional(),
isAgentModel: z.boolean().optional(), isAgentModel: z.boolean().optional(),
agentCodingModelId: z.string().max(128).optional(), agentCodingModelId: z.string().max(128).optional(),
isMultimodal: z.boolean().optional(), isMultimodal: z.boolean().optional(),
@ -106,6 +107,7 @@ const updateModelSchema = z.object({
fallbackModelId1: z.string().max(128).optional(), fallbackModelId1: z.string().max(128).optional(),
fallbackModelId2: z.string().max(128).optional(), fallbackModelId2: z.string().max(128).optional(),
excludeFromFailover: z.boolean().optional(), excludeFromFailover: z.boolean().optional(),
excludeFromHealthBan: z.boolean().optional(),
isAgentModel: z.boolean().optional(), isAgentModel: z.boolean().optional(),
agentCodingModelId: z.string().max(128).optional(), agentCodingModelId: z.string().max(128).optional(),
isMultimodal: z.boolean().optional(), isMultimodal: z.boolean().optional(),

View File

@ -211,6 +211,7 @@ export async function reviveAiModelsFromCooldown(params?: { modelId?: string })
const conditions = [ const conditions = [
eq(aiModel.isEnabled, false), eq(aiModel.isEnabled, false),
eq(aiModel.excludeFromFailover, false), eq(aiModel.excludeFromFailover, false),
eq(aiModel.excludeFromHealthBan, false),
]; ];
if (params?.modelId) { if (params?.modelId) {
@ -392,6 +393,7 @@ export async function recordAiModelHealthFailure(params: {
displayName: aiModel.displayName, displayName: aiModel.displayName,
isEnabled: aiModel.isEnabled, isEnabled: aiModel.isEnabled,
excludeFromFailover: aiModel.excludeFromFailover, excludeFromFailover: aiModel.excludeFromFailover,
excludeFromHealthBan: aiModel.excludeFromHealthBan,
healthStatus: aiModel.healthStatus, healthStatus: aiModel.healthStatus,
healthConsecutiveFailures: aiModel.healthConsecutiveFailures, healthConsecutiveFailures: aiModel.healthConsecutiveFailures,
healthRecoverySuccessStreak: aiModel.healthRecoverySuccessStreak, healthRecoverySuccessStreak: aiModel.healthRecoverySuccessStreak,
@ -414,6 +416,7 @@ export async function recordAiModelHealthFailure(params: {
const shouldAutoDisable = const shouldAutoDisable =
classification.counted && classification.counted &&
!existing.excludeFromFailover && !existing.excludeFromFailover &&
!existing.excludeFromHealthBan &&
((isObservation && existing.isEnabled) || ((isObservation && existing.isEnabled) ||
(existing.healthStatus !== "auto_disabled" && (existing.healthStatus !== "auto_disabled" &&
nextConsecutiveFailures >= AUTO_DISABLE_FAILURE_THRESHOLD)); nextConsecutiveFailures >= AUTO_DISABLE_FAILURE_THRESHOLD));

View File

@ -106,6 +106,10 @@ const runMigrate = async () => {
ALTER TABLE "AiModel" ALTER TABLE "AiModel"
ADD COLUMN IF NOT EXISTS "excludeFromFailover" boolean NOT NULL DEFAULT false; ADD COLUMN IF NOT EXISTS "excludeFromFailover" boolean NOT NULL DEFAULT false;
`); `);
await db.execute(sql`
ALTER TABLE "AiModel"
ADD COLUMN IF NOT EXISTS "excludeFromHealthBan" boolean NOT NULL DEFAULT false;
`);
await db.execute(sql` await db.execute(sql`
ALTER TABLE "AiModel" ALTER TABLE "AiModel"
ADD COLUMN IF NOT EXISTS "isDefaultTextEditModel" boolean NOT NULL DEFAULT false; ADD COLUMN IF NOT EXISTS "isDefaultTextEditModel" boolean NOT NULL DEFAULT false;

View File

@ -0,0 +1,2 @@
ALTER TABLE "AiModel"
ADD COLUMN IF NOT EXISTS "excludeFromHealthBan" boolean NOT NULL DEFAULT false;

View File

@ -484,6 +484,13 @@
"when": 1779267679691, "when": 1779267679691,
"tag": "0082_add_client_performance_events", "tag": "0082_add_client_performance_events",
"breakpoints": true "breakpoints": true
},
{
"idx": 69,
"version": "7",
"when": 1779849000000,
"tag": "0083_add_ai_model_health_ban_exclusion",
"breakpoints": true
} }
] ]
} }

View File

@ -4068,6 +4068,7 @@ export async function createAiModel({
fallbackModelId1, fallbackModelId1,
fallbackModelId2, fallbackModelId2,
excludeFromFailover, excludeFromFailover,
excludeFromHealthBan,
imageGenConfig, imageGenConfig,
capabilities, capabilities,
isAgentModel, isAgentModel,
@ -4089,6 +4090,7 @@ export async function createAiModel({
fallbackModelId1?: string; fallbackModelId1?: string;
fallbackModelId2?: string; fallbackModelId2?: string;
excludeFromFailover?: boolean; excludeFromFailover?: boolean;
excludeFromHealthBan?: boolean;
imageGenConfig?: ImageGenConfig; imageGenConfig?: ImageGenConfig;
capabilities?: Record<string, unknown> | null; capabilities?: Record<string, unknown> | null;
isAgentModel?: boolean; isAgentModel?: boolean;
@ -4139,6 +4141,7 @@ export async function createAiModel({
fallbackModelId1: fallbackModelId1 || null, fallbackModelId1: fallbackModelId1 || null,
fallbackModelId2: fallbackModelId2 || null, fallbackModelId2: fallbackModelId2 || null,
excludeFromFailover: excludeFromFailover ?? false, excludeFromFailover: excludeFromFailover ?? false,
excludeFromHealthBan: excludeFromHealthBan ?? false,
imageGenConfig: imageGenConfig || null, imageGenConfig: imageGenConfig || null,
defaultParams, defaultParams,
capabilities: capabilities:
@ -4172,6 +4175,7 @@ export async function updateAiModel({
fallbackModelId1, fallbackModelId1,
fallbackModelId2, fallbackModelId2,
excludeFromFailover, excludeFromFailover,
excludeFromHealthBan,
imageGenConfig, imageGenConfig,
capabilities, capabilities,
isAgentModel, isAgentModel,
@ -4194,6 +4198,7 @@ export async function updateAiModel({
fallbackModelId1?: string; fallbackModelId1?: string;
fallbackModelId2?: string; fallbackModelId2?: string;
excludeFromFailover?: boolean; excludeFromFailover?: boolean;
excludeFromHealthBan?: boolean;
imageGenConfig?: ImageGenConfig; imageGenConfig?: ImageGenConfig;
capabilities?: Record<string, unknown> | null; capabilities?: Record<string, unknown> | null;
isAgentModel?: boolean; isAgentModel?: boolean;
@ -4261,6 +4266,21 @@ export async function updateAiModel({
if (fallbackModelId1 !== undefined) updateData.fallbackModelId1 = fallbackModelId1 || null; if (fallbackModelId1 !== undefined) updateData.fallbackModelId1 = fallbackModelId1 || null;
if (fallbackModelId2 !== undefined) updateData.fallbackModelId2 = fallbackModelId2 || null; if (fallbackModelId2 !== undefined) updateData.fallbackModelId2 = fallbackModelId2 || null;
if (excludeFromFailover !== undefined) updateData.excludeFromFailover = excludeFromFailover; if (excludeFromFailover !== undefined) updateData.excludeFromFailover = excludeFromFailover;
if (excludeFromHealthBan !== undefined) {
updateData.excludeFromHealthBan = excludeFromHealthBan;
if (excludeFromHealthBan) {
updateData.healthStatus = "healthy";
updateData.healthConsecutiveFailures = 0;
updateData.healthLastEventType = null;
updateData.healthLastError = null;
updateData.healthLastFailureAt = null;
updateData.healthAutoDisabledAt = null;
updateData.healthAutoDisabledReason = null;
updateData.healthRecoverySuccessStreak = 0;
updateData.healthObservationStartedAt = null;
updateData.healthUpdatedAt = new Date();
}
}
// 🆕 图片生成配置 // 🆕 图片生成配置
if (imageGenConfig !== undefined) updateData.imageGenConfig = imageGenConfig || null; if (imageGenConfig !== undefined) updateData.imageGenConfig = imageGenConfig || null;
if ( if (

View File

@ -335,6 +335,7 @@ export const aiModel = pgTable("AiModel", {
fallbackModelId1: varchar("fallbackModelId1", { length: 128 }), // 备用模型 ID 1 fallbackModelId1: varchar("fallbackModelId1", { length: 128 }), // 备用模型 ID 1
fallbackModelId2: varchar("fallbackModelId2", { length: 128 }), // 备用模型 ID 2 fallbackModelId2: varchar("fallbackModelId2", { length: 128 }), // 备用模型 ID 2
excludeFromFailover: boolean("excludeFromFailover").notNull().default(false), // 不参与故障迁移 excludeFromFailover: boolean("excludeFromFailover").notNull().default(false), // 不参与故障迁移
excludeFromHealthBan: boolean("excludeFromHealthBan").notNull().default(false), // 不参与故障自动封禁池
healthStatus: varchar("healthStatus", { healthStatus: varchar("healthStatus", {
enum: ["healthy", "degraded", "auto_disabled"], enum: ["healthy", "degraded", "auto_disabled"],
}) })