From c82561839849f9b7908acf11ad04ef69d85244fd Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 6 Jun 2026 16:35:45 +0800 Subject: [PATCH] fix ruxa image resolution mapping --- app/(chat)/api/chat/_lib/image-failover.ts | 1 + app/(chat)/api/chat/route.ts | 133 ++++++++---------- app/(chat)/api/chat/schema.ts | 12 +- components/image-params-bar.tsx | 19 ++- lib/ai/providers/ruxa-image.ts | 148 +++++++++++++-------- lib/canvas/generate-image.ts | 1 + lib/observability/redaction.ts | 2 +- 7 files changed, 178 insertions(+), 138 deletions(-) diff --git a/app/(chat)/api/chat/_lib/image-failover.ts b/app/(chat)/api/chat/_lib/image-failover.ts index d52054805..665a61109 100644 --- a/app/(chat)/api/chat/_lib/image-failover.ts +++ b/app/(chat)/api/chat/_lib/image-failover.ts @@ -205,6 +205,7 @@ export async function generateImageWithFailoverCandidate(params: { const candidateResult = await (candidateLanguageModel as any).doGenerate({ messages: candidateMessages, imageParams: imageParams || fallbackImageParams, + imageGenConfig: candidate.model.imageGenConfig || null, modelDisplayName: candidate.model.displayName, overrideModelId: candidate.model.modelId, }); diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index cce83e7fc..660940100 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -410,6 +410,7 @@ async function replayAssistantMessagesForDuplicateRequest({ } type ChatImageParams = NonNullable; +type ChatImageSize = NonNullable; type AgentRecommendedImageParams = { aspectRatio?: string; imageSize?: string; @@ -444,6 +445,14 @@ type AgentVideoPlan = { recommendedVideoParams?: AgentRecommendedVideoParams; }; +function normalizeChatImageSize(value?: string | null): ChatImageSize | undefined { + const normalized = String(value || "").trim().toUpperCase(); + if (normalized === "1K" || normalized === "2K" || normalized === "4K") { + return normalized; + } + return undefined; +} + function hasUserCustomizedImageParams(params?: PostRequestBody["imageParams"]): boolean { if (!params) return false; const customizedAspectRatio = @@ -484,7 +493,8 @@ function mergeImageParamsWithAgentRecommendation(params: { if (!hasUserCustomSettings) { if (recommended.aspectRatio) merged.aspectRatio = recommended.aspectRatio; - if (recommended.imageSize) merged.imageSize = recommended.imageSize; + const recommendedImageSize = normalizeChatImageSize(recommended.imageSize); + if (recommendedImageSize) merged.imageSize = recommendedImageSize; if (typeof recommended.imageCount === "number") merged.imageCount = recommended.imageCount; } @@ -2038,12 +2048,19 @@ export async function POST(request: Request) { imageSize: smartAutoDetectedIntent === "image_edit" ? "4K" : "2K", imageCount: 1, } as const; + const normalizedEffectiveImageSize = normalizeChatImageSize(effectiveImageParams?.imageSize); + const effectiveImageGenerationParams = { + ...(effectiveImageParams || {}), + aspectRatio: effectiveImageParams?.aspectRatio || defaultImageQualityParams.aspectRatio, + imageSize: normalizedEffectiveImageSize || defaultImageQualityParams.imageSize, + imageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount, + }; if (shouldApplyAgentImageParams) { chatApiLogger.info("[Smart Auto Mode] 🎯 Effective image params:", { incoming: requestBody.imageParams, recommended: smartAutoRecommendedImageParams, - merged: effectiveImageParams, + merged: effectiveImageGenerationParams, hasUserCustomImageParams, }); } @@ -3551,7 +3568,7 @@ export async function POST(request: Request) { } let finalImage = firstImage; - let finalImageSizeLabel = resolvedImageSize; + let finalImageSizeLabel: string = resolvedImageSize; try { const normalizedImage = await normalizeReplacementResultImage({ generatedImageUrl: firstImage.originalUrl, @@ -4379,7 +4396,7 @@ export async function POST(request: Request) { throw new Error("未返回结果图片"); } let finalImage = firstImage; - let finalImageSizeLabel = resolvedImageSize; + let finalImageSizeLabel: string = resolvedImageSize; try { const normalizedImage = await normalizeReplacementResultImage({ generatedImageUrl: firstImage.originalUrl, @@ -6127,10 +6144,7 @@ export async function POST(request: Request) { : undefined ); const modelCreditCostPerImage = getModelCreditCost(currentModelConfig, "image", { - imageSize: - typeof effectiveImageParams?.imageSize === "string" - ? effectiveImageParams.imageSize - : defaultImageQualityParams.imageSize, + imageSize: effectiveImageGenerationParams.imageSize, }); const modelCreditCostPerVideo = getModelCreditCost(currentModelConfig, "video", { duration: Number(effectiveVideoParams?.duration) || undefined, @@ -6165,7 +6179,7 @@ export async function POST(request: Request) { 1, Math.round( Number( - effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1 + effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1 ) ) ); @@ -6245,7 +6259,7 @@ export async function POST(request: Request) { const isGeminiImageProvider = ["gemini-image", "huanke-gemini"].includes((languageModel as any).provider); if (isGeminiImageProvider && typeof (languageModel as any).doGenerate === "function") { chatApiLogger.info("[Chat API] Using Gemini image generation, provider:", (languageModel as any).provider); - chatApiLogger.info("[Chat API] Image params:", effectiveImageParams); + chatApiLogger.info("[Chat API] Image params:", effectiveImageGenerationParams); // 🆕 获取备用模型配置 const currentModelConfig = await getAiModelById({ id: finalModelId }); @@ -6332,11 +6346,7 @@ export async function POST(request: Request) { // 调用 doGenerate 获取图片(如果是备用模型,传入 overrideModelId) const result = await (languageModel as any).doGenerate({ messages: modelMessages, - imageParams: - effectiveImageParams || { - aspectRatio: defaultImageQualityParams.aspectRatio, - imageSize: defaultImageQualityParams.imageSize, - }, + imageParams: effectiveImageGenerationParams, overrideModelId: isRetry ? modelIdToUse : undefined, // 🆕 使用备用模型 ID }); @@ -6362,7 +6372,7 @@ export async function POST(request: Request) { phase: "processing", providerType: (languageModel as any).provider, modelName: currentModelConfig?.displayName || actualModelId, - imageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, + imageCount: effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, "gemini:image-status-start", ); @@ -6384,10 +6394,10 @@ export async function POST(request: Request) { runtimeModelId: actualModelId, finalModelId, providerType: (languageModel as any).provider, - aspectRatio: effectiveImageParams?.aspectRatio, - imageSize: effectiveImageParams?.imageSize, + aspectRatio: effectiveImageGenerationParams.aspectRatio, + imageSize: effectiveImageGenerationParams.imageSize, imageCount: - effectiveImageParams?.imageCount || + effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, @@ -6405,7 +6415,7 @@ export async function POST(request: Request) { const fallbackResult = await generateImageWithFailoverCandidate({ candidate, messages: buildMessagesForGemini(), - imageParams: effectiveImageParams, + imageParams: effectiveImageGenerationParams, fallbackImageParams: { aspectRatio: defaultImageQualityParams.aspectRatio, imageSize: defaultImageQualityParams.imageSize, @@ -6431,8 +6441,8 @@ export async function POST(request: Request) { primaryModelId: actualModelId, fallbackModelId: candidate.model.modelId, providerType: candidate.provider.type, - aspectRatio: effectiveImageParams?.aspectRatio, - imageSize: effectiveImageParams?.imageSize, + aspectRatio: effectiveImageGenerationParams.aspectRatio, + imageSize: effectiveImageGenerationParams.imageSize, }, }).catch((healthError) => { chatApiLogger.warn("[Chat API] Record Gemini fallback health failure failed:", healthError); @@ -6467,9 +6477,9 @@ export async function POST(request: Request) { triedFallbackModelIds.add(String(candidate.model.id || "")); chatApiLogger.info(`[Chat API] 🔁 Gemini cross-provider failover: ${candidate.model.displayName} (${candidate.provider.type})`); const fallbackResult = await generateImageWithFailoverCandidate({ - candidate, - messages: buildMessagesForGemini(), - imageParams: effectiveImageParams, + candidate, + messages: buildMessagesForGemini(), + imageParams: effectiveImageGenerationParams, fallbackImageParams: { aspectRatio: defaultImageQualityParams.aspectRatio, imageSize: defaultImageQualityParams.imageSize, @@ -6665,12 +6675,7 @@ export async function POST(request: Request) { const volcanoResult = await (volcanoLanguageModel as any).doGenerate({ messages: volcanoMessages, maxImages: 1, // 故障转移时只生成 1 张 - imageParams: - effectiveImageParams || { - aspectRatio: defaultImageQualityParams.aspectRatio, - imageSize: defaultImageQualityParams.imageSize, - imageCount: defaultImageQualityParams.imageCount, - }, + imageParams: effectiveImageGenerationParams, }); const imageMarkdown = volcanoResult.text || "图片生成失败"; @@ -6844,7 +6849,7 @@ export async function POST(request: Request) { ].includes((languageModel as any).provider); if (isAlibabaLikeImageProvider && typeof (languageModel as any).doGenerate === "function") { chatApiLogger.info("[Chat API] Using image generation provider:", (languageModel as any).provider); - chatApiLogger.info("[Chat API] Image params:", effectiveImageParams); + chatApiLogger.info("[Chat API] Image params:", effectiveImageGenerationParams); // 🆕 获取模型显示名称,用于错误提示 const alibabaModelConfig = await getAiModelById({ id: finalModelId }); @@ -6950,12 +6955,7 @@ export async function POST(request: Request) { apiKey: falApiKey, modelId: actualModelId, messages: falMessages, - imageParams: - effectiveImageParams || { - aspectRatio: defaultImageQualityParams.aspectRatio, - imageSize: defaultImageQualityParams.imageSize, - imageCount: defaultImageQualityParams.imageCount, - }, + imageParams: effectiveImageGenerationParams, }); const placeholderMessageId = generateUUID(); @@ -6982,9 +6982,9 @@ export async function POST(request: Request) { optimizedPrompt: smartAutoOptimizedPrompt || userPrompt || queuedTask.prompt, userPrompt: userPrompt || queuedTask.prompt, requestedImageCount: - effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, - aspectRatio: effectiveImageParams?.aspectRatio, - imageSize: effectiveImageParams?.imageSize, + effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, + aspectRatio: effectiveImageGenerationParams.aspectRatio, + imageSize: effectiveImageGenerationParams.imageSize, outputSizeLabel: queuedTask.outputSizeLabel, sourceImageUrls: requestTargetResolution?.targetImageUrls || queuedTask.sourceImageUrls, sourceAssetIds: requestTargetResolution?.targetAssetIds || [], @@ -7012,7 +7012,7 @@ export async function POST(request: Request) { providerType: "fal-image", modelName: modelDisplayName, imageCount: - effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, + effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, "alibaba:image-status-queued", ); @@ -7074,12 +7074,8 @@ export async function POST(request: Request) { // 调用 doGenerate 获取图片,传递模型显示名称 const result = await (languageModel as any).doGenerate({ messages: modelMessages, - imageParams: - effectiveImageParams || { - aspectRatio: defaultImageQualityParams.aspectRatio, - imageSize: defaultImageQualityParams.imageSize, - imageCount: defaultImageQualityParams.imageCount, - }, + imageParams: effectiveImageGenerationParams, + imageGenConfig: alibabaModelConfig?.imageGenConfig || null, modelDisplayName, // 🆕 传递模型显示名称 overrideModelId: isRetry ? modelIdToUse : undefined, }); @@ -7107,7 +7103,7 @@ export async function POST(request: Request) { phase: "processing", providerType: (languageModel as any).provider, modelName: alibabaModelConfig?.displayName || actualModelId, - imageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, + imageCount: effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, "alibaba:image-status-start", ); @@ -7126,7 +7122,7 @@ export async function POST(request: Request) { const fallbackResult = await generateImageWithFailoverCandidate({ candidate, messages: messagesForAlibaba, - imageParams: effectiveImageParams, + imageParams: effectiveImageGenerationParams, fallbackImageParams: { aspectRatio: defaultImageQualityParams.aspectRatio, imageSize: defaultImageQualityParams.imageSize, @@ -7218,12 +7214,8 @@ export async function POST(request: Request) { const candidateResult = await (candidateLanguageModel as any).doGenerate({ messages: candidateMessages, - imageParams: - effectiveImageParams || { - aspectRatio: defaultImageQualityParams.aspectRatio, - imageSize: defaultImageQualityParams.imageSize, - imageCount: defaultImageQualityParams.imageCount, - }, + imageParams: effectiveImageGenerationParams, + imageGenConfig: candidate.model.imageGenConfig || null, modelDisplayName: candidate.model.displayName, overrideModelId: candidate.model.modelId, }); @@ -7501,12 +7493,7 @@ export async function POST(request: Request) { const tryGenerateImage = async (modelIdToUse: string, isRetry: boolean) => { const result = await (languageModel as any).doGenerate({ messages: modelMessages, - imageParams: - effectiveImageParams || { - aspectRatio: defaultImageQualityParams.aspectRatio, - imageSize: defaultImageQualityParams.imageSize, - imageCount: defaultImageQualityParams.imageCount, - }, + imageParams: effectiveImageGenerationParams, overrideModelId: isRetry ? modelIdToUse : undefined, }); if (result?.finishReason === "error") { @@ -7531,7 +7518,7 @@ export async function POST(request: Request) { phase: "processing", providerType: "volcano-vision", modelName: volcanoModelConfig?.displayName || actualModelId, - imageCount: effectiveImageParams?.imageCount || defaultImageQualityParams.imageCount || 1, + imageCount: effectiveImageGenerationParams.imageCount || defaultImageQualityParams.imageCount || 1, }, "volcano:image-status-start", ); @@ -7548,7 +7535,7 @@ export async function POST(request: Request) { const fallbackResult = await generateImageWithFailoverCandidate({ candidate, messages: messagesForVolcano, - imageParams: effectiveImageParams, + imageParams: effectiveImageGenerationParams, fallbackImageParams: { aspectRatio: defaultImageQualityParams.aspectRatio, imageSize: defaultImageQualityParams.imageSize, @@ -7639,12 +7626,8 @@ export async function POST(request: Request) { const candidateResult = await (candidateLanguageModel as any).doGenerate({ messages: candidateMessages, - imageParams: - effectiveImageParams || { - aspectRatio: defaultImageQualityParams.aspectRatio, - imageSize: defaultImageQualityParams.imageSize, - imageCount: defaultImageQualityParams.imageCount, - }, + imageParams: effectiveImageGenerationParams, + imageGenConfig: candidate.model.imageGenConfig || null, modelDisplayName: candidate.model.displayName, overrideModelId: candidate.model.modelId, }); @@ -8402,7 +8385,7 @@ export async function POST(request: Request) { const fallbackResult = await generateImageWithFailoverCandidate({ candidate, messages: messagesForCrossProvider, - imageParams: effectiveImageParams, + imageParams: effectiveImageGenerationParams, fallbackImageParams: { aspectRatio: defaultImageQualityParams.aspectRatio, imageSize: defaultImageQualityParams.imageSize, @@ -8498,12 +8481,8 @@ export async function POST(request: Request) { const candidateResult = await (candidateLanguageModel as any).doGenerate({ messages: candidateMessages, - imageParams: - effectiveImageParams || { - aspectRatio: defaultImageQualityParams.aspectRatio, - imageSize: defaultImageQualityParams.imageSize, - imageCount: defaultImageQualityParams.imageCount, - }, + imageParams: effectiveImageGenerationParams, + imageGenConfig: candidate.model.imageGenConfig || null, modelDisplayName: candidate.model.displayName, overrideModelId: candidate.model.modelId, }); diff --git a/app/(chat)/api/chat/schema.ts b/app/(chat)/api/chat/schema.ts index 5d5005c09..c634f63e5 100644 --- a/app/(chat)/api/chat/schema.ts +++ b/app/(chat)/api/chat/schema.ts @@ -30,10 +30,20 @@ const videoParamsSchema = z.object({ returnLastFrame: z.boolean().optional(), }).optional(); +const imageSizeValues = ["1K", "2K", "4K"] as const; + +function normalizeImageSizeInput(value: unknown) { + if (typeof value !== "string") return undefined; + const normalized = value.trim().toUpperCase(); + return imageSizeValues.includes(normalized as (typeof imageSizeValues)[number]) + ? normalized + : undefined; +} + // 🆕 图片生成参数 schema const imageParamsSchema = z.object({ aspectRatio: z.string().optional(), - imageSize: z.string().optional(), + imageSize: z.preprocess(normalizeImageSizeInput, z.enum(imageSizeValues).optional()), imageCount: z.number().min(1).max(10).optional(), // 阿里生图专用参数 size: z.string().optional(), // 自定义分辨率,如 "1024*1024" diff --git a/components/image-params-bar.tsx b/components/image-params-bar.tsx index d3f28f606..8f85aa3ea 100644 --- a/components/image-params-bar.tsx +++ b/components/image-params-bar.tsx @@ -71,8 +71,17 @@ const IMAGE_SIZE_OPTIONS = [ { value: "2K", label: "2K", disabled: false }, { value: "4K", label: "4K", disabled: false }, ] as const; +type ImageSizeValue = (typeof IMAGE_SIZE_OPTIONS)[number]["value"]; -const IMAGE_SIZE_PIXEL_MAP: Record = { +function normalizeImageSizeValue(value?: string, fallback: ImageSizeValue = "2K"): ImageSizeValue { + const normalized = String(value || "").trim().toUpperCase(); + if (normalized === "1K" || normalized === "2K" || normalized === "4K") { + return normalized; + } + return fallback; +} + +const IMAGE_SIZE_PIXEL_MAP: Record = { "1K": 1024, "2K": 2048, "4K": 4096, @@ -143,16 +152,16 @@ export function ImageParamsBar({ const fallbackCount = options[0] ?? 1; const imageCount = options.includes(raw.imageCount || fallbackCount) ? (raw.imageCount || fallbackCount) : fallbackCount; // 根据模型配置校正分辨率:如果当前选择的档位被禁用,则降级到可用的档位 - let imageSize = raw.imageSize || "2K"; + let imageSize = normalizeImageSizeValue(raw.imageSize); if (imageGenConfig) { - const sizeAllowed = (s: string) => { + const sizeAllowed = (s: ImageSizeValue) => { if (s === "1K") return imageGenConfig.supports1K !== false; if (s === "2K") return imageGenConfig.supports2K !== false; if (s === "4K") return imageGenConfig.supports4K !== false; return true; }; if (!sizeAllowed(imageSize)) { - imageSize = ["2K", "1K", "4K"].find(sizeAllowed) || "2K"; + imageSize = (["2K", "1K", "4K"] as const).find(sizeAllowed) || "2K"; } } return { @@ -209,7 +218,7 @@ export function ImageParamsBar({ return getGrsaiGptImageOutputSize(aspectRatio, imageSize); } - const size = IMAGE_SIZE_PIXEL_MAP[imageSize] || IMAGE_SIZE_PIXEL_MAP["2K"]; + const size = IMAGE_SIZE_PIXEL_MAP[normalizeImageSizeValue(imageSize)]; if (aspectRatio === "auto" || aspectRatio === "1:1") { return { width: size, height: size }; } diff --git a/lib/ai/providers/ruxa-image.ts b/lib/ai/providers/ruxa-image.ts index 078179c5a..00b03e2c4 100644 --- a/lib/ai/providers/ruxa-image.ts +++ b/lib/ai/providers/ruxa-image.ts @@ -15,29 +15,29 @@ interface RuxaImageParams { outputFormat?: "png" | "jpg" | "jpeg" | "webp"; } +interface RuxaImageModelConfig { + supportsSize?: boolean; + supports1K?: boolean; + supports2K?: boolean; + supports4K?: boolean; +} + interface UploadedImage { thumbnail: string; original: string; } -const DEFAULT_RESOLUTION = "1k"; +const DEFAULT_RESOLUTION = "1K"; const DEFAULT_OUTPUT_FORMAT = "png"; const POLL_INTERVAL_MS = 2000; -const POLL_MAX_ATTEMPTS = 90; -const RUXA_NANO_BANANA_2_DOC_MODEL_ID = "gemini-3.1-flash-image-preview"; -const RUXA_MODEL_ID_ALIASES: Record = { - "google/nano-banana-2": RUXA_NANO_BANANA_2_DOC_MODEL_ID, -}; +const POLL_MAX_ATTEMPTS = 240; function normalizeRuxaModelId(modelId?: string) { const raw = String(modelId || "").trim(); - if (!raw) return "gpt-image-1.5"; - return RUXA_MODEL_ID_ALIASES[raw] || raw; -} - -function isRuxaNanoBanana2Model(modelId?: string) { - const normalized = normalizeRuxaModelId(modelId); - return normalized === RUXA_NANO_BANANA_2_DOC_MODEL_ID; + if (!raw) { + throw new Error("RUXA 模型 ID 未配置"); + } + return raw; } function normalizeUrlForCompare(url: string) { @@ -68,7 +68,9 @@ function normalizeAspectRatio(aspectRatio?: string) { const normalized = aspectRatio.trim().toLowerCase(); if (normalized === "auto") return undefined; const ratio = aspectRatio.trim(); - if (["1:1", "16:9", "9:16", "4:3", "3:4", "2:3", "3:2"].includes(ratio)) { + if ( + ["1:1", "16:9", "9:16", "4:3", "3:4", "2:3", "3:2", "5:4", "4:5", "21:9"].includes(ratio) + ) { return ratio; } return undefined; @@ -76,25 +78,65 @@ function normalizeAspectRatio(aspectRatio?: string) { function normalizeResolution(imageSize?: string) { if (!imageSize) return DEFAULT_RESOLUTION; - const normalized = imageSize.trim().toLowerCase(); - if (["1k", "2k", "4k"].includes(normalized)) return normalized; + const normalized = imageSize.trim().toUpperCase(); + if (["1K", "2K", "4K"].includes(normalized)) return normalized; return DEFAULT_RESOLUTION; } -function normalizeOutputFormat(outputFormat?: string): "png" | "jpg" | "webp" { +function normalizeOutputFormat(outputFormat?: string): "png" | "jpg" { if (!outputFormat) return DEFAULT_OUTPUT_FORMAT; const normalized = outputFormat.trim().toLowerCase(); if (normalized === "jpg" || normalized === "jpeg") return "jpg"; - if (normalized === "webp") return "webp"; return "png"; } -function normalizeRuxaOutputFormat(modelId: string | undefined, outputFormat?: string) { - const normalized = normalizeOutputFormat(outputFormat); - if (!isRuxaNanoBanana2Model(modelId)) { - return normalized; +function isResolutionSupported(resolution: string, config?: RuxaImageModelConfig | null) { + if (config?.supportsSize === false) { + return false; } - return normalized === "jpg" ? "jpeg" : normalized; + + const normalized = resolution.toLowerCase(); + if (normalized === "1k") return config?.supports1K !== false; + if (normalized === "2k") return config?.supports2K !== false; + if (normalized === "4k") return config?.supports4K !== false; + return true; +} + +function buildRuxaTaskInput({ + prompt, + imageUrls, + aspectRatio, + resolution, + outputFormat, + modelConfig, +}: { + prompt: string; + imageUrls: string[]; + aspectRatio?: string; + resolution: string; + outputFormat: string; + modelConfig?: RuxaImageModelConfig | null; +}) { + const input: Record = { + prompt, + output_format: outputFormat, + }; + 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); + } + + return input; } function extractTaskId(payload: any): string | undefined { @@ -132,9 +174,16 @@ function extractStatus(payload: any): string | undefined { function extractErrorMessage(payload: any): string { let parsedResultJson: any = null; - if (typeof payload?.resultJson === "string" && payload.resultJson.trim()) { + const rawResultJson = + typeof payload?.resultJson === "string" && payload.resultJson.trim() + ? payload.resultJson + : typeof payload?.data?.resultJson === "string" && payload.data.resultJson.trim() + ? payload.data.resultJson + : ""; + + if (rawResultJson) { try { - parsedResultJson = JSON.parse(payload.resultJson); + parsedResultJson = JSON.parse(rawResultJson); } catch { parsedResultJson = null; } @@ -148,7 +197,7 @@ function extractErrorMessage(payload: any): string { payload?.errorMessage || parsedResultJson?.failMsg || parsedResultJson?.error || - payload?.resultJson || + rawResultJson || payload?.data?.message || payload?.data?.error || "RUXA 任务执行失败" @@ -388,48 +437,38 @@ export function createRuxaImage({ apiKey, baseURL }: RuxaImageConfig) { doGenerate: async (options: { messages: Array<{ role: string; content: any }>; imageParams?: RuxaImageParams; + imageGenConfig?: RuxaImageModelConfig | null; overrideModelId?: string; }) => { const { prompt, imageUrls } = extractPromptAndImages(options.messages); const aspectRatio = normalizeAspectRatio(options.imageParams?.aspectRatio); const resolution = normalizeResolution(options.imageParams?.imageSize); - const requestedModelId = options.overrideModelId || modelId || "gpt-image-1.5"; + const requestedModelId = options.overrideModelId || modelId; const activeModelId = normalizeRuxaModelId(requestedModelId); - const outputFormat = normalizeRuxaOutputFormat(activeModelId, options.imageParams?.outputFormat); - const isNanoBanana2 = isRuxaNanoBanana2Model(activeModelId); + const outputFormat = normalizeOutputFormat(options.imageParams?.outputFormat); const createEndpoint = joinApiPath(baseURL, "/api/v1/tasks/create"); const callbackUrl = process.env.RUXA_CALLBACK_URL?.trim(); - const input: Record = { + const input = buildRuxaTaskInput({ prompt, - output_format: outputFormat, - }; - if (isNanoBanana2) { - input.size = aspectRatio || "1:1"; - } else { - input.resolution = resolution; - } - if (aspectRatio && !isNanoBanana2) { - input.size = aspectRatio; - input.aspect_ratio = aspectRatio; - } - if (imageUrls[0]) { - input.image = imageUrls[0]; - } - if (imageUrls.length > 1) { - input.image_input = imageUrls; - } + imageUrls, + aspectRatio, + resolution, + outputFormat, + modelConfig: options.imageGenConfig, + }); const requestBody = { input, model: activeModelId, - ...(callbackUrl ? { callBackUrl: callbackUrl } : {}), + ...(callbackUrl ? { callback_url: callbackUrl } : {}), }; - if (activeModelId !== requestedModelId) { - console.log("[Ruxa Image] Remapped modelId for provider compatibility:", { - requestedModelId, - activeModelId, - }); - } + console.log("[Ruxa Image] Creating task:", { + model: activeModelId, + resolution: input.resolution || null, + aspectRatio: input.aspect_ratio || input.size || null, + outputFormat, + imageInputCount: imageUrls.length, + }); const createResponse = await fetch(createEndpoint, { method: "POST", headers: { @@ -494,6 +533,7 @@ export function createRuxaImage({ apiKey, baseURL }: RuxaImageConfig) { doStream: async function* (options: { messages: Array<{ role: string; content: any }>; imageParams?: RuxaImageParams; + imageGenConfig?: RuxaImageModelConfig | null; overrideModelId?: string; }) { const result = await this.doGenerate(options); diff --git a/lib/canvas/generate-image.ts b/lib/canvas/generate-image.ts index d013aeb67..90e083bc9 100644 --- a/lib/canvas/generate-image.ts +++ b/lib/canvas/generate-image.ts @@ -587,6 +587,7 @@ export async function generateCanvasImage( ...imageParams, imageCount: batchImageCount, }, + imageGenConfig: selectedModel.model.imageGenConfig || null, }); return result.text || ""; }; diff --git a/lib/observability/redaction.ts b/lib/observability/redaction.ts index e39bdc828..4130179b2 100644 --- a/lib/observability/redaction.ts +++ b/lib/observability/redaction.ts @@ -3,7 +3,7 @@ const MAX_STRING_LENGTH = 240; const SENSITIVE_KEY_PATTERN = /(authorization|cookie|password|secret|token|apikey|api_key|accesskey|access_key|clientsecret|client_secret|credential|prompt|session|csrf|code)/i; -const URL_KEY_PATTERN = /(url|uri|endpoint|origin|href|src|image|thumbnail|avatar|file)/i; +const URL_KEY_PATTERN = /(url|uri|endpoint|origin|href|src|thumbnail|avatar|file)/i; const DATA_URL_PATTERN = /^data:[^;]+;base64,/i; const EMBEDDED_DATA_URL_PATTERN = /data:[^;,\s]+;base64,[A-Za-z0-9+/=_-]+/gi; const EMBEDDED_URL_PATTERN = /https?:\/\/[^\s"'<>),]+/gi;