Fix RUXA nano-banana model input mode and runtime failover IDs

This commit is contained in:
Your Name 2026-06-09 11:47:42 +08:00
parent 56e68ff5d0
commit 609700ab54
5 changed files with 81 additions and 16 deletions

View File

@ -25,6 +25,29 @@ export const IMAGE_FAILOVER_PROVIDER_ALLOWLIST = new Set([
const IMAGE_FAILOVER_MODEL_COOLDOWN_MS = 30 * 60 * 1000;
const imageFailoverModelCooldown = new Map<string, { until: number; reason: string }>();
export function resolveImageModelRuntimeId(
model: { defaultParams?: unknown; modelId?: unknown; id?: unknown } | null | undefined,
) {
const defaultParams = model?.defaultParams;
const defaultEndpoint =
typeof defaultParams === "object" &&
defaultParams !== null &&
"endpoint" in defaultParams
? String((defaultParams as Record<string, unknown>).endpoint || "").trim()
: "";
if (defaultEndpoint) {
return defaultEndpoint;
}
const modelId = String(model?.modelId || "").trim();
if (modelId) {
return modelId;
}
return String(model?.id || "").trim();
}
export function normalizeFailoverErrorMessage(error: unknown): string {
if (!error) return "";
if (typeof error === "string") return error;
@ -186,14 +209,17 @@ export async function generateImageWithFailoverCandidate(params: {
}) {
const { candidate, messages, imageParams, fallbackImageParams } = params;
const providerType = String(candidate.provider?.type || "");
const candidateRuntimeModelId = String(candidate.model?.modelId || candidate.model?.id || "");
const candidateRuntimeModelId = resolveImageModelRuntimeId(candidate.model);
if (
providerType === "fal-image" &&
isFalGptImage2EditModelId(candidateRuntimeModelId)
) {
throw new Error("FAL GPT Image 2 Edit 仅支持异步队列调用,已跳过同步故障迁移");
}
const candidateLanguageModel = await getLanguageModelById(candidate.model.id);
const candidateLanguageModel = await getLanguageModelById(
candidate.model.id,
candidateRuntimeModelId
);
if (!candidateLanguageModel || typeof (candidateLanguageModel as any).doGenerate !== "function") {
throw new Error(`备用模型不支持当前生图故障迁移调用:${candidate.model.displayName || candidate.model.id}`);
}
@ -207,7 +233,7 @@ export async function generateImageWithFailoverCandidate(params: {
imageParams: imageParams || fallbackImageParams,
imageGenConfig: candidate.model.imageGenConfig || null,
modelDisplayName: candidate.model.displayName,
overrideModelId: candidate.model.modelId,
overrideModelId: candidateRuntimeModelId || candidate.model.modelId,
});
if (candidateResult?.finishReason === "error") {

View File

@ -261,6 +261,7 @@ import {
generateImageWithFailoverCandidate,
getImageCrossProviderCandidates,
IMAGE_FAILOVER_PROVIDER_ALLOWLIST,
resolveImageModelRuntimeId,
normalizeFailoverErrorMessage,
setImageFallbackModelCooldown,
shouldCooldownImageFallbackModel,
@ -7211,13 +7212,14 @@ export async function POST(request: Request) {
messagesForAlibaba,
convertProvider
);
const candidateRuntimeModelId = resolveImageModelRuntimeId(candidate.model);
const candidateResult = await (candidateLanguageModel as any).doGenerate({
messages: candidateMessages,
imageParams: effectiveImageGenerationParams,
imageGenConfig: candidate.model.imageGenConfig || null,
modelDisplayName: candidate.model.displayName,
overrideModelId: candidate.model.modelId,
overrideModelId: candidateRuntimeModelId || candidate.model.modelId,
});
if (candidateResult?.finishReason === "error") {
@ -7623,13 +7625,14 @@ export async function POST(request: Request) {
messagesForVolcano,
convertProvider
);
const candidateRuntimeModelId = resolveImageModelRuntimeId(candidate.model);
const candidateResult = await (candidateLanguageModel as any).doGenerate({
messages: candidateMessages,
imageParams: effectiveImageGenerationParams,
imageGenConfig: candidate.model.imageGenConfig || null,
modelDisplayName: candidate.model.displayName,
overrideModelId: candidate.model.modelId,
overrideModelId: candidateRuntimeModelId || candidate.model.modelId,
});
if (candidateResult?.finishReason === "error") {
@ -8478,13 +8481,14 @@ export async function POST(request: Request) {
messagesForCrossProvider,
convertProvider
);
const candidateRuntimeModelId = resolveImageModelRuntimeId(candidate.model);
const candidateResult = await (candidateLanguageModel as any).doGenerate({
messages: candidateMessages,
imageParams: effectiveImageGenerationParams,
imageGenConfig: candidate.model.imageGenConfig || null,
modelDisplayName: candidate.model.displayName,
overrideModelId: candidate.model.modelId,
overrideModelId: candidateRuntimeModelId || candidate.model.modelId,
});
if (candidateResult?.finishReason === "error") {

View File

@ -33,6 +33,9 @@ const imageGenConfigSchema = z.object({
supportsNegativePrompt: z.boolean().optional(),
supportsWatermark: z.boolean().optional(),
supportsSeed: z.boolean().optional(),
ruxaTaskInputMode: z
.enum(["size-image", "resolution-based"])
.optional(),
creditCostPerImage: z.number().min(0).max(100000).optional(),
creditCostMultiplierBySize: z.record(z.string(), z.number().min(0.01).max(100)).optional(),
creditDiscountRate: z.number().min(0.1).max(10).optional(),

View File

@ -20,6 +20,7 @@ interface RuxaImageModelConfig {
supports1K?: boolean;
supports2K?: boolean;
supports4K?: boolean;
ruxaTaskInputMode?: "size-image" | "resolution-based";
}
interface UploadedImage {
@ -102,6 +103,26 @@ function isResolutionSupported(resolution: string, config?: RuxaImageModelConfig
return true;
}
function resolveRuxaTaskInputMode({
activeModelId,
modelConfig,
}: {
activeModelId: string;
modelConfig?: RuxaImageModelConfig | null;
}) {
if (modelConfig?.ruxaTaskInputMode === "size-image") {
return "size-image";
}
if (modelConfig?.ruxaTaskInputMode === "resolution-based") {
return "resolution-based";
}
return activeModelId === "gemini-3.1-flash-image-preview"
? "size-image"
: "resolution-based";
}
function buildRuxaTaskInput({
prompt,
imageUrls,
@ -109,6 +130,7 @@ function buildRuxaTaskInput({
resolution,
outputFormat,
modelConfig,
taskInputMode,
}: {
prompt: string;
imageUrls: string[];
@ -116,6 +138,7 @@ function buildRuxaTaskInput({
resolution: string;
outputFormat: string;
modelConfig?: RuxaImageModelConfig | null;
taskInputMode: "size-image" | "resolution-based";
}) {
const input: Record<string, unknown> = {
prompt,
@ -124,16 +147,18 @@ function buildRuxaTaskInput({
const ratio = aspectRatio || "1:1";
input.size = ratio;
input.aspect_ratio = ratio;
if (isResolutionSupported(resolution, modelConfig)) {
input.resolution = resolution;
}
if (imageUrls[0]) {
input.image = imageUrls[0];
}
if (imageUrls.length > 0) {
input.image_input = imageUrls.slice(0, 8);
if (taskInputMode === "size-image") {
if (imageUrls[0]) {
input.image = imageUrls[0];
}
} else {
input.aspect_ratio = ratio;
if (isResolutionSupported(resolution, modelConfig)) {
input.resolution = resolution;
}
if (imageUrls.length > 0) {
input.image_input = imageUrls.slice(0, 8);
}
}
return input;
@ -445,6 +470,10 @@ export function createRuxaImage({ apiKey, baseURL }: RuxaImageConfig) {
const resolution = normalizeResolution(options.imageParams?.imageSize);
const requestedModelId = options.overrideModelId || modelId;
const activeModelId = normalizeRuxaModelId(requestedModelId);
const taskInputMode = resolveRuxaTaskInputMode({
activeModelId,
modelConfig: options.imageGenConfig,
});
const outputFormat = normalizeOutputFormat(options.imageParams?.outputFormat);
const createEndpoint = joinApiPath(baseURL, "/api/v1/tasks/create");
@ -456,6 +485,7 @@ export function createRuxaImage({ apiKey, baseURL }: RuxaImageConfig) {
resolution,
outputFormat,
modelConfig: options.imageGenConfig,
taskInputMode,
});
const requestBody = {
input,

View File

@ -45,6 +45,7 @@ export const RUXA_IMAGE_MODEL_PRESETS: RuxaImageModelPreset[] = [
},
imageGenConfig: {
...RUXA_IMAGE_CONFIG,
ruxaTaskInputMode: "size-image",
},
sortOrder: 10,
},
@ -62,6 +63,7 @@ export const RUXA_IMAGE_MODEL_PRESETS: RuxaImageModelPreset[] = [
},
imageGenConfig: {
...RUXA_IMAGE_CONFIG,
ruxaTaskInputMode: "resolution-based",
},
sortOrder: 20,
},