diff --git a/lib/ai/providers/ruxa-image.ts b/lib/ai/providers/ruxa-image.ts index e3e1da6aa..be0c9400f 100644 --- a/lib/ai/providers/ruxa-image.ts +++ b/lib/ai/providers/ruxa-image.ts @@ -2,6 +2,10 @@ import "server-only"; import sharp from "sharp"; import { uploadFile } from "@/lib/storage/service"; +import { + RUXA_NANO_BANANA_2_RUNTIME_MODEL_ID, + resolveRuxaImageRuntimeModelId, +} from "@/lib/ai/ruxa-image-models"; interface RuxaImageConfig { apiKey: string; @@ -41,6 +45,11 @@ function normalizeRuxaModelId(modelId?: string) { return raw; } +function resolveRuxaRuntimeModelId(modelId: string) { + const normalized = modelId.trim(); + return resolveRuxaImageRuntimeModelId(normalized); +} + function normalizeUrlForCompare(url: string) { try { const parsed = new URL(url); @@ -118,7 +127,7 @@ function resolveRuxaTaskInputMode({ return "resolution-based"; } - return activeModelId === "gemini-3.1-flash-image-preview" + return activeModelId === RUXA_NANO_BANANA_2_RUNTIME_MODEL_ID ? "size-image" : "resolution-based"; } @@ -469,7 +478,18 @@ export function createRuxaImage({ apiKey, baseURL }: RuxaImageConfig) { const aspectRatio = normalizeAspectRatio(options.imageParams?.aspectRatio); const resolution = normalizeResolution(options.imageParams?.imageSize); const requestedModelId = options.overrideModelId || modelId; - const activeModelId = normalizeRuxaModelId(requestedModelId); + const normalizedRequestedModelId = normalizeRuxaModelId(requestedModelId); + const activeModelId = resolveRuxaRuntimeModelId(normalizedRequestedModelId); + if (normalizedRequestedModelId !== activeModelId) { + console.warn("[Ruxa Image] 运行时模型已映射", { + requestedModelId: normalizedRequestedModelId, + runtimeModelId: activeModelId, + }); + console.warn("[Ruxa Image] 请确认数据库中已同步更新该模型的运行时 endpoint", { + requestedModelId: normalizedRequestedModelId, + runtimeModelId: activeModelId, + }); + } const taskInputMode = resolveRuxaTaskInputMode({ activeModelId, modelConfig: options.imageGenConfig, diff --git a/lib/ai/ruxa-image-models.ts b/lib/ai/ruxa-image-models.ts index 2dfd0fe30..6e0e417cd 100644 --- a/lib/ai/ruxa-image-models.ts +++ b/lib/ai/ruxa-image-models.ts @@ -74,3 +74,14 @@ export function getRuxaImageModelPresetById(modelId: string) { (preset) => preset.modelId === String(modelId || "").trim() ); } + +export function resolveRuxaImageRuntimeModelId(modelId: string) { + const preset = getRuxaImageModelPresetById(modelId); + const endpoint = preset?.defaultParams?.endpoint; + + if (typeof endpoint === "string" && endpoint.trim()) { + return endpoint.trim(); + } + + return modelId; +} diff --git a/scripts/verify-ruxa-image-models.ts b/scripts/verify-ruxa-image-models.ts index ca4bae38f..fb2ef5c5a 100644 --- a/scripts/verify-ruxa-image-models.ts +++ b/scripts/verify-ruxa-image-models.ts @@ -3,17 +3,25 @@ import { and, eq, sql } from "drizzle-orm"; import { db } from "../lib/db"; import { aiModel, aiProvider } from "../lib/db/schema"; +import { RUXA_NANO_BANANA_2_RUNTIME_MODEL_ID } from "../lib/ai/ruxa-image-models"; loadEnv({ path: ".env.local" }); type ModelExpectation = { modelId: string; displayName: string; + expectedRuntimeModelId?: string; + expectedTaskMode?: "size-image" | "resolution-based"; }; const EXPECTED_MODELS: ModelExpectation[] = [ - { modelId: "google/nano-banana-2", displayName: "nano-banana-2「专线」" }, - { modelId: "google/nano-banana-pro", displayName: "nano-banana-pro「专线」" }, + { + modelId: "google/nano-banana-2", + displayName: "nano-banana-2「专线」", + expectedRuntimeModelId: RUXA_NANO_BANANA_2_RUNTIME_MODEL_ID, + expectedTaskMode: "size-image", + }, + { modelId: "google/nano-banana-pro", displayName: "nano-banana-pro「专线」", expectedTaskMode: "resolution-based" }, ]; type Row = { @@ -28,6 +36,7 @@ type Row = { isHidden: boolean | null; healthStatus: string | null; imageGenConfig: unknown; + defaultParams: unknown; }; function getConfigFlag( @@ -61,6 +70,7 @@ async function main() { supportsVision: aiModel.supportsVision, isEnabled: aiModel.isEnabled, isHidden: aiModel.isHidden, + defaultParams: aiModel.defaultParams, healthStatus: aiModel.healthStatus, imageGenConfig: aiModel.imageGenConfig, }) @@ -91,7 +101,7 @@ async function main() { const failures: string[] = []; const expectByModelId = new Map( - EXPECTED_MODELS.map((entry) => [entry.modelId, entry.displayName]) + EXPECTED_MODELS.map((entry) => [entry.modelId, entry]) ); const expectedSet = new Set(expectByModelId.keys()); @@ -101,10 +111,15 @@ async function main() { continue; } - const expectedDisplayName = expectByModelId.get(row.modelId); - if (expectedDisplayName && row.displayName !== expectedDisplayName) { + const expected = expectByModelId.get(row.modelId); + if (!expected) { + failures.push(`Unexpected model present in RUXA image provider: ${row.modelId}`); + continue; + } + + if (row.displayName !== expected.displayName) { failures.push( - `${row.providerName} ${row.modelId}: displayName mismatch (expected "${expectedDisplayName}", got "${row.displayName}")` + `${row.providerName} ${row.modelId}: displayName mismatch (expected "${expected.displayName}", got "${row.displayName}")` ); } @@ -146,6 +161,22 @@ async function main() { `${row.providerName} ${row.modelId}: imageGenConfig.supports4K should be true` ); } + + const defaultParams = getImageGenConfig(row.defaultParams) ?? {}; + const runtimeModelFromDb = getRuntimeModelId(row, defaultParams); + const expectedRuntimeModelId = expected.expectedRuntimeModelId; + if (expectedRuntimeModelId && runtimeModelFromDb !== expectedRuntimeModelId) { + failures.push( + `${row.providerName} ${row.modelId}: default runtime model should be ${expectedRuntimeModelId}, got ${runtimeModelFromDb || "null"}` + ); + } + + const runtimeMode = config?.ruxaTaskInputMode; + if (runtimeMode !== expected.expectedTaskMode) { + failures.push( + `${row.providerName} ${row.modelId}: imageGenConfig.ruxaTaskInputMode should be ${expected.expectedTaskMode}, got ${runtimeMode}` + ); + } } for (const provider of providers) { @@ -204,6 +235,17 @@ async function main() { console.log("RUXA image model check passed."); } +function getRuntimeModelId(row: Row, defaultParams: Record) { + const endpoint = getConfigValue(defaultParams, "endpoint"); + if (endpoint) return endpoint; + return row.modelId; +} + +function getConfigValue(config: Record, key: string): string | null { + const raw = config?.[key]; + return typeof raw === "string" && raw.trim() ? raw.trim() : null; +} + main().catch((error) => { console.error("RUXA image model verification failed to execute:", error); process.exit(1);