fix ruxa image resolution mapping
This commit is contained in:
parent
e2111add87
commit
663ff7b41c
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -587,6 +587,7 @@ export async function generateCanvasImage(
|
||||
...imageParams,
|
||||
imageCount: batchImageCount,
|
||||
},
|
||||
imageGenConfig: selectedModel.model.imageGenConfig || null,
|
||||
});
|
||||
return result.text || "";
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user