From a8e62962b84dbadf12641f8845f7072021cfa51b Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 8 Jun 2026 18:22:41 +0800 Subject: [PATCH] chore: add ruxa image model verification script --- package.json | 1 + scripts/verify-ruxa-image-models.ts | 208 ++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 scripts/verify-ruxa-image-models.ts diff --git a/package.json b/package.json index bac85331e..8dd33e593 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "clean:artifacts": "node scripts/clean-local-artifacts.mjs", "smoke:routes": "node scripts/smoke-routes.mjs", "verify:release": "node scripts/verify-release.mjs", + "verify:ruxa-models": "pnpm exec tsx scripts/verify-ruxa-image-models.ts", "backfill:agent-preview-images": "pnpm exec tsx scripts/backfill-agent-workflow-preview-images.ts", "rewrite:agent-preview-image-urls": "pnpm exec tsx scripts/rewrite-agent-workflow-preview-image-urls.ts", "format": "pnpm exec ultracite fix", diff --git a/scripts/verify-ruxa-image-models.ts b/scripts/verify-ruxa-image-models.ts new file mode 100644 index 000000000..3db9bcbf3 --- /dev/null +++ b/scripts/verify-ruxa-image-models.ts @@ -0,0 +1,208 @@ +import { config as loadEnv } from "dotenv"; +import { and, eq, sql } from "drizzle-orm"; + +import { db } from "../lib/db"; +import { aiModel, aiProvider } from "../lib/db/schema"; + +loadEnv({ path: ".env.local" }); + +type ModelExpectation = { + modelId: string; + displayName: string; +}; + +const EXPECTED_MODELS: ModelExpectation[] = [ + { modelId: "google/nano-banana-2", displayName: "nano-banana-2「专线」" }, + { modelId: "google/nano-banana-pro", displayName: "nano-banana-pro「专线」" }, +]; + +type Row = { + providerId: string; + providerName: string; + providerType: string; + modelId: string; + displayName: string; + supportsImageGen: boolean | null; + supportsVision: boolean | null; + isEnabled: boolean | null; + isHidden: boolean | null; + healthStatus: string | null; + imageGenConfig: Record | null; +}; + +function getConfigFlag( + config: Record | null, + key: string +): boolean | null { + const raw = config?.[key]; + return typeof raw === "boolean" ? raw : null; +} + +async function main() { + const rows = await db + .select({ + providerId: aiProvider.id, + providerName: aiProvider.name, + providerType: aiProvider.type, + modelId: aiModel.modelId, + displayName: aiModel.displayName, + supportsImageGen: aiModel.supportsImageGen, + supportsVision: aiModel.supportsVision, + isEnabled: aiModel.isEnabled, + isHidden: aiModel.isHidden, + healthStatus: aiModel.healthStatus, + imageGenConfig: aiModel.imageGenConfig, + }) + .from(aiModel) + .innerJoin(aiProvider, eq(aiModel.providerId, aiProvider.id)) + .where( + and( + eq(aiProvider.type, "ruxa-image"), + sql`LOWER(${aiProvider.name}) LIKE ${"%ruxa%"}`, + sql`LOWER(${aiModel.modelId}) IN (${"google/nano-banana-2"}, ${"google/nano-banana-pro"})` + ) + ); + + const found = new Map(); + for (const row of rows) { + const bucket = found.get(row.providerId) ?? []; + bucket.push(row); + found.set(row.providerId, bucket); + } + + const providers = [...found.entries()].map(([providerId, models]) => ({ + providerId, + providerName: models[0]?.providerName ?? "", + providerType: models[0]?.providerType ?? "", + modelCount: models.length, + models, + })); + + const failures: string[] = []; + const expectByModelId = new Map( + EXPECTED_MODELS.map((entry) => [entry.modelId, entry.displayName]) + ); + const expectedSet = new Set(expectByModelId.keys()); + + for (const row of rows) { + if (!expectedSet.has(row.modelId)) { + failures.push(`Unexpected model present in RUXA image provider: ${row.modelId}`); + continue; + } + + const expectedDisplayName = expectByModelId.get(row.modelId); + if (expectedDisplayName && row.displayName !== expectedDisplayName) { + failures.push( + `${row.providerName} ${row.modelId}: displayName mismatch (expected "${expectedDisplayName}", got "${row.displayName}")` + ); + } + + const config = + row.imageGenConfig && + typeof row.imageGenConfig === "object" && + !Array.isArray(row.imageGenConfig) + ? (row.imageGenConfig as Record) + : null; + + if (row.supportsImageGen !== true) { + failures.push(`${row.providerName} ${row.modelId}: supportsImageGen is not true`); + } + if (row.supportsVision !== true) { + failures.push(`${row.providerName} ${row.modelId}: supportsVision is not true`); + } + if (row.isEnabled !== true) { + failures.push(`${row.providerName} ${row.modelId}: isEnabled is not true`); + } + if (row.isHidden === true) { + failures.push(`${row.providerName} ${row.modelId}: isHidden should be false`); + } + if (row.healthStatus !== "healthy") { + failures.push( + `${row.providerName} ${row.modelId}: healthStatus is ${row.healthStatus ?? "null"}, expected healthy` + ); + } + + if (getConfigFlag(config, "supportsSize") !== true) { + failures.push(`${row.providerName} ${row.modelId}: imageGenConfig.supportsSize should be true`); + } + if (getConfigFlag(config, "supports1K") !== true) { + failures.push( + `${row.providerName} ${row.modelId}: imageGenConfig.supports1K should be true` + ); + } + if (getConfigFlag(config, "supports2K") !== true) { + failures.push( + `${row.providerName} ${row.modelId}: imageGenConfig.supports2K should be true` + ); + } + if (getConfigFlag(config, "supports4K") !== true) { + failures.push( + `${row.providerName} ${row.modelId}: imageGenConfig.supports4K should be true` + ); + } + } + + for (const provider of providers) { + const providerModels = provider.models.map((item) => item.modelId); + for (const expected of EXPECTED_MODELS) { + if (!providerModels.includes(expected.modelId)) { + failures.push( + `${provider.providerName}: missing required model ${expected.modelId} (${expected.displayName})` + ); + } + } + } + + const providerSummary = providers + .sort((a, b) => a.providerName.localeCompare(b.providerName)) + .map((provider) => { + return { + providerName: provider.providerName, + providerType: provider.providerType, + providerId: provider.providerId, + hasAllTargets: EXPECTED_MODELS.every((expected) => + provider.models.some((row) => row.modelId === expected.modelId) + ), + modelRows: provider.models + .sort((a, b) => a.modelId.localeCompare(b.modelId)) + .map((row) => ({ + modelId: row.modelId, + displayName: row.displayName, + isEnabled: row.isEnabled, + healthStatus: row.healthStatus, + imageSize4K: getConfigFlag( + row.imageGenConfig && + typeof row.imageGenConfig === "object" && + !Array.isArray(row.imageGenConfig) + ? (row.imageGenConfig as Record) + : null, + "supports4K" + ), + })), + }; + }); + + console.log( + JSON.stringify( + { + expected: EXPECTED_MODELS, + providerSummary, + }, + null, + 2 + ) + ); + + if (failures.length > 0) { + console.error("RUXA image model check failed:"); + failures.forEach((item) => console.error(`- ${item}`)); + process.exit(1); + } + + console.log("RUXA image model check passed."); +} + +main().catch((error) => { + console.error("RUXA image model verification failed to execute:", error); + process.exit(1); +});