fix(style-pro): block incompatible image models

This commit is contained in:
134 2026-08-10 22:38:32 +08:00
parent a3e3938f7b
commit 04ea2da58f
4 changed files with 134 additions and 3 deletions

View File

@ -0,0 +1,51 @@
import type { AiModel, AiProvider } from "@/lib/db/schema";
import {
getAiModelFrontendAvailability,
type ModelFrontendAvailability,
} from "@/lib/ai/model-frontend-availability";
import { modelSupportsReferenceImageInput } from "@/lib/ai/reference-image-capability";
type StyleProAvailabilityEntry = {
model: Pick<
AiModel,
| "displayName"
| "modelId"
| "supportsVision"
| "capabilities"
| "defaultParams"
| "isEnabled"
| "healthStatus"
| "healthAutoDisabledAt"
| "healthObservationStartedAt"
>;
provider: Pick<AiProvider, "type">;
};
export function getStyleProModelAvailability(
entry: StyleProAvailabilityEntry
): ModelFrontendAvailability {
if (!modelSupportsReferenceImageInput(entry.model, entry.provider)) {
return {
availabilityStatus: "maintenance",
availabilityLabel: "仅文生图",
selectable: false,
disabledReason: "风格化需要参考图,该模型仅支持文生图",
};
}
const availability = getAiModelFrontendAvailability(entry.model);
if (availability.availabilityStatus === "testing") {
return {
availabilityStatus: "testing",
availabilityLabel: "暂不可用",
selectable: false,
disabledReason: "模型近期生成异常,恢复健康后会自动开放",
};
}
return availability;
}
export function isStyleProModelRunnable(entry: StyleProAvailabilityEntry) {
return getStyleProModelAvailability(entry).selectable;
}

View File

@ -6,7 +6,6 @@ import {
getImageModelCreditDiscountLabel,
} from "@/lib/ai/image-model-pricing";
import {
getAiModelFrontendAvailability,
type ModelFrontendAvailabilityStatus,
} from "@/lib/ai/model-frontend-availability";
import { isStillImageGenerationModelDescriptor } from "@/lib/ai/model-classification";
@ -14,6 +13,10 @@ import {
getEnabledAiModelsWithProvider,
getFrontendDisplayAiModelsWithProvider,
} from "@/lib/db/queries";
import {
getStyleProModelAvailability,
isStyleProModelRunnable,
} from "./model-availability";
export type StyleProModelEntry = Awaited<
ReturnType<typeof getEnabledAiModelsWithProvider>
@ -56,7 +59,9 @@ export async function getStyleProImageModelEntries(options?: {
false,
options?.includeHidden === true
);
return models.filter(isStyleProImageModel);
return models
.filter(isStyleProImageModel)
.filter(isStyleProModelRunnable);
}
export async function getStyleProImageModelDisplayEntries(options?: {
@ -105,7 +110,7 @@ export function toPublicStyleProModelOption(
): PublicStyleProModelOption {
const imageGenConfig = (entry.model.imageGenConfig ||
null) as DiscountableImageGenConfigLike | null;
const availability = getAiModelFrontendAvailability(entry.model);
const availability = getStyleProModelAvailability(entry);
return {
id: entry.model.id,
modelId: entry.model.modelId,

View File

@ -4,6 +4,35 @@ import {
isStillImageGenerationModelDescriptor,
isVideoModelDescriptor,
} from "@/lib/ai/model-classification";
import { getStyleProModelAvailability } from "@/lib/style-pro/model-availability";
import type { AiProvider } from "@/lib/db/schema";
function styleProEntry(overrides?: {
providerType?: AiProvider["type"];
modelId?: string;
supportsVision?: boolean;
healthStatus?: "healthy" | "degraded" | "auto_disabled";
healthAutoDisabledAt?: Date | null;
healthObservationStartedAt?: Date | null;
}) {
return {
model: {
displayName: "Test Image Model",
modelId: overrides?.modelId || "test-image-model",
supportsVision: overrides?.supportsVision ?? true,
capabilities: null,
defaultParams: null,
isEnabled: true,
healthStatus: overrides?.healthStatus || "healthy",
healthAutoDisabledAt: overrides?.healthAutoDisabledAt || null,
healthObservationStartedAt:
overrides?.healthObservationStartedAt || null,
},
provider: {
type: overrides?.providerType || "openai-image",
},
};
}
test.describe("model classification", () => {
test("keeps multi-reference BananaImage models classified as image models", () => {
@ -51,4 +80,37 @@ test.describe("model classification", () => {
})
).toBe(false);
});
test("prevents reference-incompatible models from being selected in Style Pro", () => {
const availability = getStyleProModelAvailability(
styleProEntry({ supportsVision: false })
);
expect(availability.selectable).toBe(false);
expect(availability.availabilityLabel).toBe("仅文生图");
});
test("temporarily disables degraded models in Style Pro", () => {
const availability = getStyleProModelAvailability(
styleProEntry({
healthStatus: "degraded",
healthObservationStartedAt: new Date(),
})
);
expect(availability.selectable).toBe(false);
expect(availability.availabilityLabel).toBe("暂不可用");
});
test("keeps known reference-capable BFL models selectable", () => {
const availability = getStyleProModelAvailability(
styleProEntry({
providerType: "bfl-image",
modelId: "flux-2-klein-4b",
supportsVision: false,
})
);
expect(availability.selectable).toBe(true);
});
});

View File

@ -77,6 +77,19 @@ test.describe("3D workspace and Style Pro regressions", () => {
expect(workspace).toContain("revalidateOnReconnect: true");
});
test("enforces Style Pro model availability on both display and execution", () => {
const options = readSource("lib/style-pro/model-options.ts");
const availability = readSource(
"lib/style-pro/model-availability.ts"
);
expect(options).toContain(".filter(isStyleProModelRunnable)");
expect(options).toContain("getStyleProModelAvailability(entry)");
expect(availability).toContain("modelSupportsReferenceImageInput");
expect(availability).toContain('availabilityLabel: "仅文生图"');
expect(availability).toContain('availabilityLabel: "暂不可用"');
});
test("opens the canvas conversation panel by default", () => {
const page = readSource(
"app/canvas/[id]/CanvasEditorLegacyPageClient.tsx"