Fix Gemini image reference forwarding for grsai

This commit is contained in:
Your Name 2026-06-09 10:46:36 +08:00
parent 0ba1ea521f
commit 37e48b7878
2 changed files with 84 additions and 30 deletions

View File

@ -581,45 +581,64 @@ export async function convertMessagesWithImages(
if (hasAnnotation) {
chatApiLogger.info("[Message Convert] 🎨 检测到标注图片,将自动添加系统提示");
}
// 对于 Gemini,需要特殊处理图片
const convertedMessages = [];
for (const message of preparedMessages) {
const content: any[] = [];
// 🆕 如果是用户消息且有标注图片,在文本前添加系统提示
let hasAddedAnnotationPrompt = false;
// 处理消息的各个部分
for (const part of message.parts) {
if (part.type === "text" && part.text) {
let textContent = part.text;
// 🆕 如果有标注图片且是用户消息,在用户文本前添加系统提示
if (hasAnnotation && message.role === "user" && !hasAddedAnnotationPrompt) {
textContent = ANNOTATION_SYSTEM_PROMPT + "\n\n用户请求:" + textContent;
hasAddedAnnotationPrompt = true;
chatApiLogger.info("[Message Convert] 🎨 已注入标注提示词");
}
content.push({
type: "text",
text: textContent,
});
} else if (part.type === "file" && part.url) {
// 直接使用 URL 格式(不再转换为 Base64)
// gemini-image 和 huanke-gemini provider 内部会处理图片上传
continue;
}
const candidateImageUrl =
part.type === "file"
? part.url || part.data
: part.type === "image"
? part.image || part.url
: part.type === "image_url"
? part.image_url?.url || part.url
: undefined;
if (typeof candidateImageUrl !== "string" || !candidateImageUrl.trim()) {
continue;
}
const normalizedImageUrl = candidateImageUrl.trim();
// 兼容 file/image/image_url 三类输入;Gemini 内部会将图片交给 S3 后再请求生图网关。
if (part.type === "file" || part.type === "image" || part.type === "image_url") {
content.push({
type: "image_url",
image_url: {
url: part.url,
url: normalizedImageUrl,
},
});
chatApiLogger.info("[Message Convert] Image kept as URL format:", part.url.substring(0, 80) + "...");
chatApiLogger.info(
"[Message Convert] Image kept as URL format:",
normalizedImageUrl.substring(0, 80) + "..."
);
}
}
// 🆕 如果有标注图片但消息中没有文本部分,添加一个纯提示文本
if (hasAnnotation && message.role === "user" && !hasAddedAnnotationPrompt && content.length > 0) {
content.unshift({
@ -628,12 +647,10 @@ export async function convertMessagesWithImages(
});
chatApiLogger.info("[Message Convert] 🎨 已在消息开头注入标注提示词");
}
convertedMessages.push({
role: message.role,
content: content.length === 1 && content[0].type === "text"
? content[0].text
: content,
content: content.length === 1 && content[0].type === "text" ? content[0].text : content,
});
}

View File

@ -715,6 +715,18 @@ async function sendGeminiRequest(
imageSize?: string;
}
): Promise<GeminiResponse> {
const normalizeAspectRatio = (aspectRatio: string) => {
const normalized = String(aspectRatio || "").trim();
if (!normalized || normalized.toLowerCase() === "auto") {
return undefined;
}
if (!/^\d+\s*:\s*\d+$/i.test(normalized)) {
return undefined;
}
const [widthRaw, heightRaw] = normalized.split(":").map((item) => item.trim());
return `${Number(widthRaw)}:${Number(heightRaw)}`;
};
const isGptImageModel = /^gpt-image/i.test(modelId);
const shouldUseGrsaiAsyncPolling =
!isGptImageModel &&
@ -739,18 +751,20 @@ async function sendGeminiRequest(
messages.forEach((msg) => {
if (msg.role === "user" && Array.isArray(msg.content)) {
msg.content.forEach((contentPart: any) => {
// 支持标准 image_url
if (contentPart.type === "image_url" && contentPart.image_url?.url) {
urls.push(contentPart.image_url.url);
}
// 🆕 支持 CoreMessage 的 image 类型 (可能来自 Attachment)
else if (contentPart.type === "image" && contentPart.image) {
if (typeof contentPart.image === 'string') {
urls.push(contentPart.image);
} else if (contentPart.image instanceof URL) {
urls.push(contentPart.image.toString());
}
const candidateImageUrl =
contentPart.type === "image_url"
? contentPart.image_url?.url
: contentPart.type === "file"
? contentPart.url || contentPart.data
: contentPart.type === "image"
? contentPart.image || contentPart.url
: undefined;
if (!candidateImageUrl || typeof candidateImageUrl !== "string") {
return;
}
urls.push(candidateImageUrl);
});
}
});
@ -789,16 +803,39 @@ async function sendGeminiRequest(
? await inferImageAspectRatioFromUrl(processedUrls[0])
: null;
const resolvedAspectRatio = inferredAspectRatio || requestedAspectRatio;
requestBody.size = resolveGrsaiGptImageSize(resolvedAspectRatio, imageParams?.imageSize);
const normalizedRequestedAspectRatio = normalizeAspectRatio(requestedAspectRatio);
const resolvedSize = resolveGrsaiGptImageSize(resolvedAspectRatio, imageParams?.imageSize);
// GPT-image(如 gpt-image-2-vip)在不同网关中参数差异较大:
// - 有的网关只识别 size;
// - 有的网关同时支持 aspectRatio/aspect_ratio;
// - 有的会读取 imageSize/image_size;
// 为避免接口兼容问题,保留核心 size 后补齐兼容字段。
requestBody.size = resolvedSize;
requestBody.imageSize = imageParams?.imageSize;
requestBody.image_size = imageParams?.imageSize;
if (normalizedRequestedAspectRatio) {
requestBody.aspectRatio = normalizedRequestedAspectRatio;
requestBody.aspect_ratio = normalizedRequestedAspectRatio;
requestBody.aspectRatioRaw = requestedAspectRatio;
}
requestBody.shutProgress = false;
if (processedUrls.length > 0) {
requestBody.images = processedUrls;
if (processedUrls.length === 1) {
requestBody.image = processedUrls[0];
}
requestBody.imageUrls = processedUrls;
}
logger.debug(
`${apiLogPrefix} GPT image payload mapping: requestedAspectRatio=${requestedAspectRatio}, resolvedAspectRatio=${resolvedAspectRatio}, imageSize=${imageParams?.imageSize || "2K"}, resolvedSize=${requestBody.size}`
`${apiLogPrefix} GPT image payload mapping: requestedAspectRatio=${requestedAspectRatio}, normalizedRequestedAspectRatio=${normalizedRequestedAspectRatio || "n/a"}, resolvedAspectRatio=${resolvedAspectRatio}, imageSize=${imageParams?.imageSize || "2K"}, resolvedSize=${resolvedSize}`
);
if (inferredAspectRatio) {
logger.debug(`${apiLogPrefix} Inferred reference aspect ratio: ${inferredAspectRatio}`);
}
logger.debug(
`${apiLogPrefix} Resolved size: aspectRatio=${resolvedAspectRatio}, imageSize=${imageParams?.imageSize || "2K"} => ${requestBody.size}`
`${apiLogPrefix} Resolved size: aspectRatio=${resolvedAspectRatio}, imageSize=${imageParams?.imageSize || "2K"} => ${resolvedSize}`
);
} else {
// 添加 Nano Banana 特定参数