fix ruxa image resolution mapping

This commit is contained in:
Your Name 2026-06-06 16:35:45 +08:00
parent e2111add87
commit 663ff7b41c
7 changed files with 178 additions and 138 deletions

View File

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

View File

@ -410,6 +410,7 @@ async function replayAssistantMessagesForDuplicateRequest({
}
type ChatImageParams = NonNullable<PostRequestBody["imageParams"]>;
type ChatImageSize = NonNullable<ChatImageParams["imageSize"]>;
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,
});

View File

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

View File

@ -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<string, number> = {
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<ImageSizeValue, number> = {
"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 };
}

View File

@ -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<string, string> = {
"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<string, unknown> = {
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<string, unknown> = {
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);

View File

@ -587,6 +587,7 @@ export async function generateCanvasImage(
...imageParams,
imageCount: batchImageCount,
},
imageGenConfig: selectedModel.model.imageGenConfig || null,
});
return result.text || "";
};

View File

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