From 609700ab5411345b006ef63244bebff6f690e85a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Jun 2026 11:47:42 +0800 Subject: [PATCH] Fix RUXA nano-banana model input mode and runtime failover IDs --- app/(chat)/api/chat/_lib/image-failover.ts | 32 ++++++++++++-- app/(chat)/api/chat/route.ts | 10 +++-- app/api/admin/ai-models/route.ts | 3 ++ lib/ai/providers/ruxa-image.ts | 50 +++++++++++++++++----- lib/ai/ruxa-image-models.ts | 2 + 5 files changed, 81 insertions(+), 16 deletions(-) diff --git a/app/(chat)/api/chat/_lib/image-failover.ts b/app/(chat)/api/chat/_lib/image-failover.ts index 665a61109..97d9be07c 100644 --- a/app/(chat)/api/chat/_lib/image-failover.ts +++ b/app/(chat)/api/chat/_lib/image-failover.ts @@ -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(); +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).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") { diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 660940100..47d7ec468 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -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") { diff --git a/app/api/admin/ai-models/route.ts b/app/api/admin/ai-models/route.ts index 33c58c429..48ac62f47 100644 --- a/app/api/admin/ai-models/route.ts +++ b/app/api/admin/ai-models/route.ts @@ -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(), diff --git a/lib/ai/providers/ruxa-image.ts b/lib/ai/providers/ruxa-image.ts index 00b03e2c4..e3e1da6aa 100644 --- a/lib/ai/providers/ruxa-image.ts +++ b/lib/ai/providers/ruxa-image.ts @@ -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 = { 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, diff --git a/lib/ai/ruxa-image-models.ts b/lib/ai/ruxa-image-models.ts index 0ed5bfe78..2dfd0fe30 100644 --- a/lib/ai/ruxa-image-models.ts +++ b/lib/ai/ruxa-image-models.ts @@ -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, },