diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 567823a35..42e1e689c 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -869,6 +869,7 @@ type AiModel = { fallbackModelId1?: string; fallbackModelId2?: string; excludeFromFailover?: boolean; + excludeFromHealthBan?: boolean; healthStatus?: "healthy" | "degraded" | "auto_disabled"; healthConsecutiveFailures?: number; healthLastEventType?: string | null; @@ -3608,6 +3609,7 @@ export function AdminSettingsClient() { fallbackModelId1: "", // 备用模型 ID 1 fallbackModelId2: "", // 备用模型 ID 2 excludeFromFailover: false, + excludeFromHealthBan: false, capabilities: {} as Record, // 🆕 图片生成模型特性配置 imageGenConfig: createDefaultImageGenConfig(), @@ -6892,6 +6894,7 @@ export function AdminSettingsClient() { fallbackModelId1: "", fallbackModelId2: "", excludeFromFailover: false, + excludeFromHealthBan: false, capabilities: {}, imageGenConfig: createDefaultImageGenConfig(), }); @@ -14978,19 +14981,44 @@ export function AdminSettingsClient() { 多模态模型(Agent 识图与分析使用同一模型) -
- - setModelForm({ - ...modelForm, - excludeFromFailover: checked, - }) - } - /> - +
+
+ 故障保护策略 +
+
+ + setModelForm({ + ...modelForm, + excludeFromFailover: checked, + }) + } + /> + +
+

+ 开启后,该模型不会作为其他模型请求失败时的自动备用模型。 +

+
+ + setModelForm({ + ...modelForm, + excludeFromHealthBan: checked, + }) + } + /> + +
+

+ 开启后,健康检查仍会记录故障日志,但不会因为连续失败自动禁用该模型。 +

{/* 🆕 图片生成模型特性配置 */} @@ -15911,7 +15939,12 @@ export function AdminSettingsClient() { )} {(model as any).excludeFromFailover && ( - 不参与故障迁移 + 不参与迁移池 + + )} + {(model as any).excludeFromHealthBan && ( + + 不参与封禁池 )} {healthStatus === "auto_disabled" && ( @@ -16033,6 +16066,8 @@ export function AdminSettingsClient() { (model as any).fallbackModelId2 || "", excludeFromFailover: (model as any).excludeFromFailover || false, + excludeFromHealthBan: + (model as any).excludeFromHealthBan || false, capabilities: (model as any).capabilities && typeof (model as any).capabilities === "object" && diff --git a/app/api/admin/ai-models/route.ts b/app/api/admin/ai-models/route.ts index 6706e3ba1..33c58c429 100644 --- a/app/api/admin/ai-models/route.ts +++ b/app/api/admin/ai-models/route.ts @@ -82,6 +82,7 @@ const createModelSchema = z.object({ fallbackModelId1: z.string().max(128).optional(), fallbackModelId2: z.string().max(128).optional(), excludeFromFailover: z.boolean().optional(), + excludeFromHealthBan: z.boolean().optional(), isAgentModel: z.boolean().optional(), agentCodingModelId: z.string().max(128).optional(), isMultimodal: z.boolean().optional(), @@ -106,6 +107,7 @@ const updateModelSchema = z.object({ fallbackModelId1: z.string().max(128).optional(), fallbackModelId2: z.string().max(128).optional(), excludeFromFailover: z.boolean().optional(), + excludeFromHealthBan: z.boolean().optional(), isAgentModel: z.boolean().optional(), agentCodingModelId: z.string().max(128).optional(), isMultimodal: z.boolean().optional(), diff --git a/lib/ai/runtime/model-health.ts b/lib/ai/runtime/model-health.ts index fdd778bea..73bb58feb 100644 --- a/lib/ai/runtime/model-health.ts +++ b/lib/ai/runtime/model-health.ts @@ -211,6 +211,7 @@ export async function reviveAiModelsFromCooldown(params?: { modelId?: string }) const conditions = [ eq(aiModel.isEnabled, false), eq(aiModel.excludeFromFailover, false), + eq(aiModel.excludeFromHealthBan, false), ]; if (params?.modelId) { @@ -392,6 +393,7 @@ export async function recordAiModelHealthFailure(params: { displayName: aiModel.displayName, isEnabled: aiModel.isEnabled, excludeFromFailover: aiModel.excludeFromFailover, + excludeFromHealthBan: aiModel.excludeFromHealthBan, healthStatus: aiModel.healthStatus, healthConsecutiveFailures: aiModel.healthConsecutiveFailures, healthRecoverySuccessStreak: aiModel.healthRecoverySuccessStreak, @@ -414,6 +416,7 @@ export async function recordAiModelHealthFailure(params: { const shouldAutoDisable = classification.counted && !existing.excludeFromFailover && + !existing.excludeFromHealthBan && ((isObservation && existing.isEnabled) || (existing.healthStatus !== "auto_disabled" && nextConsecutiveFailures >= AUTO_DISABLE_FAILURE_THRESHOLD)); diff --git a/lib/db/migrate.ts b/lib/db/migrate.ts index d4bcbb7bc..37abedf6e 100644 --- a/lib/db/migrate.ts +++ b/lib/db/migrate.ts @@ -106,6 +106,10 @@ const runMigrate = async () => { ALTER TABLE "AiModel" 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` ALTER TABLE "AiModel" ADD COLUMN IF NOT EXISTS "isDefaultTextEditModel" boolean NOT NULL DEFAULT false; diff --git a/lib/db/migrations/0083_add_ai_model_health_ban_exclusion.sql b/lib/db/migrations/0083_add_ai_model_health_ban_exclusion.sql new file mode 100644 index 000000000..a843056a2 --- /dev/null +++ b/lib/db/migrations/0083_add_ai_model_health_ban_exclusion.sql @@ -0,0 +1,2 @@ +ALTER TABLE "AiModel" + ADD COLUMN IF NOT EXISTS "excludeFromHealthBan" boolean NOT NULL DEFAULT false; diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index 0ffa0cf27..b97e11452 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -484,6 +484,13 @@ "when": 1779267679691, "tag": "0082_add_client_performance_events", "breakpoints": true + }, + { + "idx": 69, + "version": "7", + "when": 1779849000000, + "tag": "0083_add_ai_model_health_ban_exclusion", + "breakpoints": true } ] } diff --git a/lib/db/queries.ts b/lib/db/queries.ts index f0b48c81b..aa86b26ae 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -4068,6 +4068,7 @@ export async function createAiModel({ fallbackModelId1, fallbackModelId2, excludeFromFailover, + excludeFromHealthBan, imageGenConfig, capabilities, isAgentModel, @@ -4089,6 +4090,7 @@ export async function createAiModel({ fallbackModelId1?: string; fallbackModelId2?: string; excludeFromFailover?: boolean; + excludeFromHealthBan?: boolean; imageGenConfig?: ImageGenConfig; capabilities?: Record | null; isAgentModel?: boolean; @@ -4139,6 +4141,7 @@ export async function createAiModel({ fallbackModelId1: fallbackModelId1 || null, fallbackModelId2: fallbackModelId2 || null, excludeFromFailover: excludeFromFailover ?? false, + excludeFromHealthBan: excludeFromHealthBan ?? false, imageGenConfig: imageGenConfig || null, defaultParams, capabilities: @@ -4172,6 +4175,7 @@ export async function updateAiModel({ fallbackModelId1, fallbackModelId2, excludeFromFailover, + excludeFromHealthBan, imageGenConfig, capabilities, isAgentModel, @@ -4194,6 +4198,7 @@ export async function updateAiModel({ fallbackModelId1?: string; fallbackModelId2?: string; excludeFromFailover?: boolean; + excludeFromHealthBan?: boolean; imageGenConfig?: ImageGenConfig; capabilities?: Record | null; isAgentModel?: boolean; @@ -4261,6 +4266,21 @@ export async function updateAiModel({ if (fallbackModelId1 !== undefined) updateData.fallbackModelId1 = fallbackModelId1 || null; if (fallbackModelId2 !== undefined) updateData.fallbackModelId2 = fallbackModelId2 || null; 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 ( diff --git a/lib/db/schema.ts b/lib/db/schema.ts index ef2063801..94c88e72b 100644 --- a/lib/db/schema.ts +++ b/lib/db/schema.ts @@ -335,6 +335,7 @@ export const aiModel = pgTable("AiModel", { fallbackModelId1: varchar("fallbackModelId1", { length: 128 }), // 备用模型 ID 1 fallbackModelId2: varchar("fallbackModelId2", { length: 128 }), // 备用模型 ID 2 excludeFromFailover: boolean("excludeFromFailover").notNull().default(false), // 不参与故障迁移 + excludeFromHealthBan: boolean("excludeFromHealthBan").notNull().default(false), // 不参与故障自动封禁池 healthStatus: varchar("healthStatus", { enum: ["healthy", "degraded", "auto_disabled"], })