fix(ruxa-image): normalize runtime model id and enforce nano-banana-2 endpoint

This commit is contained in:
Your Name 2026-06-09 13:06:05 +08:00
parent 609700ab54
commit cd817a6336
3 changed files with 81 additions and 8 deletions

View File

@ -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,

View File

@ -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;
}

View File

@ -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<string, unknown>) {
const endpoint = getConfigValue(defaultParams, "endpoint");
if (endpoint) return endpoint;
return row.modelId;
}
function getConfigValue(config: Record<string, unknown>, 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);