feat: 添加环科Gemini生图API支持(禁用Base64,使用URL格式)

This commit is contained in:
Your Name 2026-02-01 23:08:59 +08:00
parent 7680cbe02e
commit 9d4579eb53
4 changed files with 677 additions and 3 deletions

View File

@ -279,6 +279,7 @@ const PROVIDER_TYPES = [
{ value: "custom", label: "自定义 OpenAI 兼容" },
{ value: "local", label: "本地模型" },
{ value: "gemini", label: "Gemini中转" },
{ value: "huanke-gemini", label: "环科Gemini生图" }, // 🆕 环科 Gemini 原生格式 API
{ value: "qianwen-video", label: "千问视频生成" },
{ value: "volcano-vision", label: "火山引擎视觉" },
{ value: "volcano-video", label: "火山引擎视频" },

View File

@ -14,6 +14,7 @@ import {
import type { AiProvider } from "@/lib/db/schema";
import { createLocalQwen } from "./providers/local-qwen";
import { createGeminiImage } from "./providers/gemini-image";
import { createHuankeGemini } from "./providers/huanke-gemini";
import { createQianwenVideo } from "./providers/qianwen-video";
import { createVolcanoVision } from "./providers/volcano-vision";
import { createAlibabaImage } from "./providers/alibaba-image";
@ -87,6 +88,17 @@ function getOrCreateProvider(provider: Pick<AiProvider, "id" | "type" | "baseUrl
return geminiProvider as any;
}
case "huanke-gemini": {
// 环科 Gemini 原生格式 API
// 使用 /v1beta/models/{model}:generateContent 端点
const huankeGeminiProvider = createHuankeGemini({
baseURL: provider.baseUrl,
apiKey: provider.apiKey,
});
console.log(`[Provider] Created Huanke Gemini image generation provider: ${provider.baseUrl}`);
return huankeGeminiProvider as any;
}
case "qianwen-video": {
// 千问视频生成服务(通义万相 I2V)
// 支持图片转视频功能,异步任务轮询
@ -202,6 +214,11 @@ export async function getDynamicModel(
console.log("[getDynamicModel] Getting Alibaba Image chat model..."); // 调试日志
baseModel = providerInstance.chat(model.modelId);
console.log("[getDynamicModel] Alibaba Image model retrieved:", baseModel ? "success" : "failed"); // 调试日志
} else if (provider.type === "huanke-gemini") {
// 环科 Gemini 原生格式使用 chat() 方法
console.log("[getDynamicModel] Getting Huanke Gemini chat model..."); // 调试日志
baseModel = providerInstance.chat(model.modelId);
console.log("[getDynamicModel] Huanke Gemini model retrieved:", baseModel ? "success" : "failed"); // 调试日志
} else {
// OpenAI-compatible providers
console.log("[getDynamicModel] Getting OpenAI-compatible model..."); // 调试日志

View File

@ -30,7 +30,7 @@ const INTENT_MODEL_MAP: Record<
> = {
// 图片编辑:优先 alibaba-image(灵魂4.5,支持提示词扩展)
image_edit: {
providerTypes: ["alibaba-image", "gemini"],
providerTypes: ["alibaba-image", "gemini", "huanke-gemini"],
requireVision: true,
requireImageGen: true,
description: "图片编辑",
@ -38,14 +38,14 @@ const INTENT_MODEL_MAP: Record<
// 视觉理解:优先千问3 VL(成本低效果好)
vision: {
providerTypes: ["alibaba", "gemini"],
providerTypes: ["alibaba", "gemini", "huanke-gemini"],
requireVision: true,
description: "图片理解",
},
// 图片生成:优先灵魂4.5
image_gen: {
providerTypes: ["alibaba-image", "gemini", "volcano-vision"],
providerTypes: ["alibaba-image", "gemini", "huanke-gemini", "volcano-vision"],
requireImageGen: true,
description: "图片生成",
},

View File

@ -0,0 +1,656 @@
import "server-only";
import { uploadFile } from "@/lib/storage/service";
interface HuankeGeminiConfig {
apiKey: string;
baseURL: string;
}
/**
* 压缩图片生成缩略图(使用 sharp)
*/
async function createThumbnail(buffer: Buffer, mimeType: string): Promise<Buffer> {
try {
const sharp = await import("sharp");
// 创建缩略图(宽度 500px,高度按比例,质量 80%)
const thumbnailBuffer = await sharp.default(buffer)
.resize(500, null, {
fit: "inside",
withoutEnlargement: true,
})
.jpeg({ quality: 80 })
.toBuffer();
console.log(`[Huanke Gemini] Thumbnail created, size: ${thumbnailBuffer.length} bytes (original: ${buffer.length} bytes)`);
return thumbnailBuffer;
} catch (error) {
console.error("[Huanke Gemini] Failed to create thumbnail:", error);
// 如果失败,返回原图
return buffer;
}
}
/**
* 将 base64 图片上传到 S3,返回缩略图和原图的 URL
*/
async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/jpeg"): Promise<{ thumbnail: string; original: string }> {
try {
// 解析 base64 数据
const buffer = Buffer.from(base64Data, "base64");
// 生成文件名
const ext = mimeType.split("/")[1] || "jpg";
const timestamp = Date.now();
const randomStr = Math.random().toString(36).slice(2, 8);
const thumbnailFilename = `huanke-thumb-${timestamp}-${randomStr}.${ext}`;
const originalFilename = `huanke-orig-${timestamp}-${randomStr}.${ext}`;
console.log(`[Huanke Gemini] Original size: ${buffer.length} bytes`);
// 生成缩略图
const thumbnailBuffer = await createThumbnail(buffer, mimeType);
// 转换为 ArrayBuffer
const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice(
thumbnailBuffer.byteOffset,
thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength
) as ArrayBuffer;
const originalArrayBuffer = buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
) as ArrayBuffer;
// 并行上传缩略图和原图到 S3
console.log(`[Huanke Gemini] Uploading thumbnail and original to S3...`);
const [thumbnailResult, originalResult] = await Promise.all([
uploadFile(thumbnailFilename, thumbnailArrayBuffer),
uploadFile(originalFilename, originalArrayBuffer),
]);
console.log(`[Huanke Gemini] Uploaded to S3:`);
console.log(` - Thumbnail: ${thumbnailResult.url}`);
console.log(` - Original: ${originalResult.url}`);
return {
thumbnail: thumbnailResult.url,
original: originalResult.url,
};
} catch (error) {
console.error("[Huanke Gemini] Failed to upload to S3:", error);
throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`);
}
}
/**
* 下载远程URL图片并上传到 S3
*/
async function downloadAndUploadToS3(imageUrl: string, retries = 3): Promise<{ thumbnail: string; original: string }> {
for (let attempt = 0; attempt < retries; attempt++) {
try {
console.log(`[Huanke Gemini] Downloading image from URL (attempt ${attempt + 1}/${retries}): ${imageUrl.substring(0, 100)}...`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
const response = await fetch(imageUrl, {
signal: controller.signal,
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
},
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
if (buffer.length === 0) {
throw new Error("下载的图片数据为空");
}
const contentType = response.headers.get("content-type") || "image/jpeg";
const ext = contentType.split("/")[1] || "jpg";
const timestamp = Date.now();
const randomStr = Math.random().toString(36).slice(2, 8);
const thumbnailFilename = `huanke-thumb-${timestamp}-${randomStr}.${ext}`;
const originalFilename = `huanke-orig-${timestamp}-${randomStr}.${ext}`;
console.log(`[Huanke Gemini] Downloaded size: ${buffer.length} bytes`);
const thumbnailBuffer = await createThumbnail(buffer, contentType);
const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice(
thumbnailBuffer.byteOffset,
thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength
) as ArrayBuffer;
const originalArrayBuffer = buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
) as ArrayBuffer;
console.log(`[Huanke Gemini] Uploading downloaded image to S3...`);
const [thumbnailResult, originalResult] = await Promise.all([
uploadFile(thumbnailFilename, thumbnailArrayBuffer),
uploadFile(originalFilename, originalArrayBuffer),
]);
console.log(`[Huanke Gemini] Uploaded to S3:`);
console.log(` - Thumbnail: ${thumbnailResult.url}`);
console.log(` - Original: ${originalResult.url}`);
return {
thumbnail: thumbnailResult.url,
original: originalResult.url,
};
} catch (error) {
const isLastAttempt = attempt === retries - 1;
console.error(
`[Huanke Gemini] Download attempt ${attempt + 1}/${retries} failed:`,
error instanceof Error ? error.message : String(error)
);
if (isLastAttempt) {
throw new Error(
`图片下载并上传失败(已重试 ${retries} 次): ${error instanceof Error ? error.message : "未知错误"}`
);
}
const delay = Math.pow(2, attempt) * 1000;
console.log(`[Huanke Gemini] Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error("下载失败:未知错误");
}
/**
* 确保图片是公网可访问的 URL(如果是 Base64,上传到 S3)
*/
async function ensureImageUrl(url: string): Promise<string> {
if (url.startsWith("http")) return url;
if (url.startsWith("data:")) {
try {
console.log("[Huanke Gemini] Converting Base64 reference image to S3 URL...");
const matches = url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
if (!matches) {
console.warn("[Huanke Gemini] Invalid Base64 format, skipping upload");
return url;
}
const mimeType = matches[1];
const base64Data = matches[2];
const buffer = Buffer.from(base64Data, "base64");
const ext = mimeType.split("/")[1] || "png";
const filename = `ref-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
const result = await uploadFile(filename, arrayBuffer);
console.log(`[Huanke Gemini] Uploaded reference image to: ${result.url}`);
return result.url;
} catch (error) {
console.error("[Huanke Gemini] Failed to upload reference image to S3:", error);
return url;
}
}
return url;
}
// 环科 Gemini API 响应格式
interface HuankeGeminiResponse {
candidates?: Array<{
content: {
parts: Array<{
text?: string;
inlineData?: {
mimeType: string;
data: string;
};
inline_data?: {
mime_type: string;
data: string;
};
}>;
};
finishReason?: string;
}>;
// OpenAI 兼容格式(可能的返回格式)
choices?: Array<{
message: {
content: string;
};
}>;
error?: {
message: string;
code?: string;
};
}
/**
* 从 API 响应中提取图片并上传到 S3
*/
async function extractAndUploadImages(response: HuankeGeminiResponse): Promise<Array<{ thumbnail: string; original: string }>> {
console.log("[Huanke Gemini] Extracting images from response:", JSON.stringify(response).substring(0, 500));
const images: Array<{ thumbnail: string; original: string }> = [];
const uploadPromises: Promise<void>[] = [];
// 1. 尝试 Gemini 原生格式 (candidates[0].content.parts)
if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0];
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
// inline_data 格式(小写下划线)
if (part.inline_data && part.inline_data.data) {
const mimeType = part.inline_data.mime_type || "image/png";
console.log(`[Huanke Gemini] Found inline_data image, mimeType: ${mimeType}`);
const uploadPromise = uploadBase64ToS3(part.inline_data.data, mimeType)
.then(urls => { images.push(urls); });
uploadPromises.push(uploadPromise);
}
// inlineData 格式(驼峰命名)
else if (part.inlineData && part.inlineData.data) {
const mimeType = part.inlineData.mimeType || "image/png";
console.log(`[Huanke Gemini] Found inlineData image, mimeType: ${mimeType}`);
const uploadPromise = uploadBase64ToS3(part.inlineData.data, mimeType)
.then(urls => { images.push(urls); });
uploadPromises.push(uploadPromise);
}
// 文本中的 Markdown 图片
else if (part.text) {
const allImageMatches = part.text.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g);
for (const match of allImageMatches) {
const url = match[1];
const base64Match = url.match(/^data:image\/([^;]+);base64,(.+)$/);
if (base64Match) {
const uploadPromise = uploadBase64ToS3(base64Match[2], `image/${base64Match[1]}`)
.then(urls => { images.push(urls); });
uploadPromises.push(uploadPromise);
} else if (url.startsWith("http")) {
const uploadPromise = downloadAndUploadToS3(url)
.then(urls => { images.push(urls); })
.catch(error => {
console.error("[Huanke Gemini] Failed to download image:", error);
throw error;
});
uploadPromises.push(uploadPromise);
}
}
}
}
}
}
// 2. 尝试 OpenAI 兼容格式 (choices[0].message.content)
if (response.choices && response.choices.length > 0) {
const content = response.choices[0]?.message?.content;
if (content && typeof content === "string") {
console.log("[Huanke Gemini] Parsing OpenAI format, content:", content.substring(0, 100) + "...");
const allImageMatches = content.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g);
for (const match of allImageMatches) {
const url = match[1];
const base64Match = url.match(/^data:image\/([^;]+);base64,(.+)$/);
if (base64Match) {
const uploadPromise = uploadBase64ToS3(base64Match[2], `image/${base64Match[1]}`)
.then(urls => { images.push(urls); });
uploadPromises.push(uploadPromise);
} else if (url.startsWith("http")) {
const uploadPromise = downloadAndUploadToS3(url)
.then(urls => { images.push(urls); })
.catch(error => {
console.error("[Huanke Gemini] Failed to download image:", error);
throw error;
});
uploadPromises.push(uploadPromise);
}
}
}
}
// 等待所有上传完成
await Promise.all(uploadPromises);
console.log("[Huanke Gemini] Extracted and uploaded", images.length, "images");
return images;
}
/**
* 将图片数据转换为 Markdown 格式
*/
function formatImageResponse(
images: Array<{ thumbnail: string; original: string }>,
imageSize: string = "2K"
): string {
if (images.length === 0) {
return "生成图片失败,未找到图片数据";
}
return images.map((img) => `![thumb|||${img.original}|||${imageSize}](${img.thumbnail})`).join("\n\n");
}
/**
* 将 Base64 图片上传到 S3 并返回 URL
* 环科 API 禁止使用 Base64 格式,必须使用 URL
*/
async function convertBase64ToUrl(base64Url: string): Promise<string> {
const matches = base64Url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
if (!matches) {
console.warn("[Huanke Gemini] Invalid Base64 format");
return base64Url;
}
const mimeType = matches[1];
const base64Data = matches[2];
const buffer = Buffer.from(base64Data, "base64");
const ext = mimeType.split("/")[1] || "png";
const filename = `huanke-ref-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`;
const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer;
const result = await uploadFile(filename, arrayBuffer);
console.log(`[Huanke Gemini] Converted Base64 to URL: ${result.url}`);
return result.url;
}
/**
* 发送环科 Gemini 原生格式请求
* 端点格式: /v1beta/models/{model}:generateContent
*
* 重要:环科 API 禁止使用 Base64 格式,所有图片必须使用 URL 格式传输
*/
async function sendHuankeGeminiRequest(
baseURL: string,
apiKey: string,
messages: any[],
modelId: string,
imageParams?: {
aspectRatio?: string;
imageSize?: string;
}
): Promise<HuankeGeminiResponse> {
// 构建 Gemini 原生格式的端点
// 例如: https://api.example.com/v1beta/models/gemini-3-pro-image-preview-4k:generateContent
const url = `${baseURL}/v1beta/models/${modelId}:generateContent?key=${apiKey}`;
console.log("[Huanke Gemini] Sending request to:", url.replace(apiKey, "***"));
console.log("[Huanke Gemini] Using model:", modelId);
console.log("[Huanke Gemini] Image params:", imageParams);
// 构建 Gemini 原生格式的请求体
const contents: any[] = [];
for (const msg of messages) {
const parts: any[] = [];
if (typeof msg.content === "string") {
parts.push({ text: msg.content });
} else if (Array.isArray(msg.content)) {
for (const contentPart of msg.content) {
if (contentPart.type === "text") {
parts.push({ text: contentPart.text });
} else if (contentPart.type === "image_url" && contentPart.image_url?.url) {
// 处理图片 URL - 禁止使用 Base64,必须转换为 URL
let imageUrl = contentPart.image_url.url;
if (imageUrl.startsWith("data:")) {
// Base64 图片 -> 上传到 S3 获取 URL
console.log("[Huanke Gemini] Converting Base64 image to URL (Base64 forbidden)...");
try {
imageUrl = await convertBase64ToUrl(imageUrl);
} catch (error) {
console.error("[Huanke Gemini] Failed to convert Base64 to URL:", error);
continue; // 跳过这张图片
}
}
// 使用 fileData 格式传递图片 URL(不使用 inlineData/Base64)
parts.push({
fileData: {
fileUri: imageUrl,
mimeType: "image/jpeg", // 默认 MIME 类型
}
});
console.log("[Huanke Gemini] Added image URL:", imageUrl.substring(0, 100));
} else if (contentPart.type === "image" && contentPart.image) {
// CoreMessage 的 image 类型
let imageUrl = typeof contentPart.image === "string"
? contentPart.image
: contentPart.image.toString();
if (imageUrl.startsWith("data:")) {
// Base64 图片 -> 上传到 S3 获取 URL
console.log("[Huanke Gemini] Converting Base64 image to URL (Base64 forbidden)...");
try {
imageUrl = await convertBase64ToUrl(imageUrl);
} catch (error) {
console.error("[Huanke Gemini] Failed to convert Base64 to URL:", error);
continue;
}
}
// 使用 fileData 格式传递图片 URL
parts.push({
fileData: {
fileUri: imageUrl,
mimeType: "image/jpeg",
}
});
console.log("[Huanke Gemini] Added image URL:", imageUrl.substring(0, 100));
}
}
}
if (parts.length > 0) {
contents.push({
role: msg.role === "assistant" ? "model" : "user",
parts,
});
}
}
// 构建请求体(Gemini 原生格式)
const requestBody: any = {
contents,
generationConfig: {
responseModalities: ["TEXT", "IMAGE"],
},
};
// 添加图片配置
if (imageParams) {
requestBody.generationConfig.imageConfig = {};
if (imageParams.aspectRatio && imageParams.aspectRatio !== "auto") {
requestBody.generationConfig.imageConfig.aspectRatio = imageParams.aspectRatio;
}
if (imageParams.imageSize) {
requestBody.generationConfig.imageConfig.imageSize = imageParams.imageSize;
}
}
console.log("[Huanke Gemini] Request body (truncated):", JSON.stringify(requestBody).substring(0, 500) + "...");
// 根据分辨率动态设置超时时间
const timeout = imageParams?.imageSize === "4K" ? 300000 : 120000;
console.log(`[Huanke Gemini] Timeout set to ${timeout/1000} seconds for ${imageParams?.imageSize || "default"}`);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
console.error("[Huanke Gemini] Request failed:", response.status, errorText);
throw new Error(
`Huanke Gemini API error: ${response.status} ${response.statusText} - ${errorText}`
);
}
const data = await response.json();
console.log("[Huanke Gemini] Response received:", JSON.stringify(data).substring(0, 500));
// 检查是否有错误
if (data.error) {
throw new Error(`API Error: ${data.error.message || JSON.stringify(data.error)}`);
}
return data as HuankeGeminiResponse;
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
console.error("[Huanke Gemini] Request timeout");
throw new Error("环科 Gemini 请求超时,请稍后重试");
}
throw error;
}
}
/**
* 创建环科 Gemini 图像生成 Provider
* 使用 Gemini 原生 API 格式
*/
export function createHuankeGemini({ apiKey, baseURL }: HuankeGeminiConfig) {
return {
chat: (modelId: string) => ({
doGenerate: async (options: any) => {
console.log("[Huanke Gemini] doGenerate called!");
console.log("[Huanke Gemini] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "...");
const effectiveModelId = options.overrideModelId || modelId;
console.log("[Huanke Gemini] Using modelId:", effectiveModelId);
console.log("[Huanke Gemini] Image params from options:", options.imageParams);
try {
const imageParams = {
aspectRatio: options.imageParams?.aspectRatio || "auto",
imageSize: options.imageParams?.imageSize || "2K"
};
const imageCount = Math.min(Math.max(options.imageParams?.imageCount || 1, 1), 2);
console.log("[Huanke Gemini] Final imageParams:", imageParams);
console.log(`[Huanke Gemini] Image count: ${imageCount}`);
console.log(`[Huanke Gemini] Starting ${imageCount} concurrent request(s)...`);
const startTime = Date.now();
const requestPromises = Array.from({ length: imageCount }, () =>
sendHuankeGeminiRequest(baseURL, apiKey, options.messages, effectiveModelId, imageParams)
);
const responses = await Promise.all(requestPromises);
console.log(`[Huanke Gemini] All ${imageCount} request(s) completed in ${Date.now() - startTime}ms`);
const extractPromises = responses.map(response => extractAndUploadImages(response));
const imageUrlsArrays = await Promise.all(extractPromises);
const allImageUrls = imageUrlsArrays.flat();
const markdownText = formatImageResponse(allImageUrls, imageParams.imageSize);
console.log(
`[Huanke Gemini] Generated ${allImageUrls.length} images from ${imageCount} concurrent request(s)`
);
return {
text: markdownText,
finishReason: "stop" as const,
usage: {
promptTokens: 0,
completionTokens: markdownText.length,
},
};
} catch (error) {
console.error("[Huanke Gemini] Generation failed:", error);
throw error;
}
},
doStream: async function* (options: any) {
console.log("[Huanke Gemini] ===== doStream ENTERED! =====");
console.log("[Huanke Gemini] Messages received:", JSON.stringify(options.messages).substring(0, 500) + "...");
const effectiveModelId = options.overrideModelId || modelId;
console.log("[Huanke Gemini] Using modelId:", effectiveModelId);
console.log("[Huanke Gemini] Image params from options:", options.imageParams);
try {
const imageParams = {
aspectRatio: options.imageParams?.aspectRatio || "auto",
imageSize: options.imageParams?.imageSize || "2K"
};
const imageCount = Math.min(Math.max(options.imageParams?.imageCount || 1, 1), 2);
console.log("[Huanke Gemini] Final imageParams:", imageParams);
console.log(`[Huanke Gemini Stream] Image count: ${imageCount}`);
console.log(`[Huanke Gemini Stream] Starting ${imageCount} concurrent request(s)...`);
const startTime = Date.now();
const requestPromises = Array.from({ length: imageCount }, () =>
sendHuankeGeminiRequest(baseURL, apiKey, options.messages, effectiveModelId, imageParams)
);
const responses = await Promise.all(requestPromises);
console.log(`[Huanke Gemini Stream] All ${imageCount} request(s) completed in ${Date.now() - startTime}ms`);
const extractPromises = responses.map(response => extractAndUploadImages(response));
const imageUrlsArrays = await Promise.all(extractPromises);
const allImageUrls = imageUrlsArrays.flat();
const markdownText = formatImageResponse(allImageUrls, imageParams.imageSize);
console.log(
`[Huanke Gemini Stream] Generated ${allImageUrls.length} images from ${imageCount} concurrent request(s)`
);
yield { type: "text-delta" as const, textDelta: markdownText };
console.log("[Huanke Gemini Stream] Sent complete markdown");
yield {
type: "finish" as const,
finishReason: "stop" as const,
usage: {
promptTokens: 0,
completionTokens: markdownText.length,
},
};
} catch (error) {
console.error("[Huanke Gemini Stream] Generation failed:", error);
const errorMessage =
error instanceof Error
? `图片生成失败: ${error.message}`
: "图片生成失败,请稍后重试";
yield { type: "text-delta" as const, textDelta: errorMessage };
yield {
type: "finish" as const,
finishReason: "error" as const,
usage: { promptTokens: 0, completionTokens: 0 },
};
}
},
modelId,
provider: "huanke-gemini",
specificationVersion: "v1" as const,
defaultObjectGenerationMode: "json" as const,
}),
};
}