From c4da49e1264208248a6341e531a44c61cba5dfa8 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 19 Feb 2026 00:42:24 +0800 Subject: [PATCH] fix(ai): adapt huanke and volcano providers to sdk v2 --- lib/ai/providers/huanke-gemini.ts | 338 +++++++++++++++++-------- lib/ai/providers/volcano-multimodal.ts | 142 +++++++++-- 2 files changed, 357 insertions(+), 123 deletions(-) diff --git a/lib/ai/providers/huanke-gemini.ts b/lib/ai/providers/huanke-gemini.ts index 185a7a00f..49232da44 100644 --- a/lib/ai/providers/huanke-gemini.ts +++ b/lib/ai/providers/huanke-gemini.ts @@ -567,129 +567,253 @@ async function sendHuankeGeminiRequest( * 使用 Gemini 原生 API 格式 */ export function createHuankeGemini({ apiKey, baseURL }: HuankeGeminiConfig) { + const getMessagesFromOptions = (options: any): any[] => { + if (Array.isArray(options?.messages)) { + return options.messages; + } + + if (!Array.isArray(options?.prompt)) { + return []; + } + + return options.prompt + .map((message: any) => { + if (!message || typeof message !== "object") return null; + + if (typeof message.content === "string") { + return { + role: message.role, + content: message.content, + }; + } + + if (!Array.isArray(message.content)) { + return null; + } + + const content = message.content + .map((part: any) => { + if (!part || typeof part !== "object") return null; + + if (part.type === "text") { + return { type: "text", text: part.text || "" }; + } + + if (part.type === "file") { + const data = + typeof part.data === "string" + ? part.data + : part.data instanceof URL + ? part.data.toString() + : null; + + if (!data) return null; + + return { + type: "file", + data, + mediaType: part.mediaType, + filename: part.filename, + }; + } + + return null; + }) + .filter(Boolean); + + return { + role: message.role, + content, + }; + }) + .filter(Boolean); + }; + + const getImageParamsFromOptions = (options: any) => { + const providerOptions = + options?.providerOptions?.["huanke-gemini"] || + options?.providerOptions?.huankeGemini || + {}; + + const imageParams = options?.imageParams || {}; + return { + aspectRatio: imageParams.aspectRatio ?? providerOptions.aspectRatio ?? "auto", + imageSize: imageParams.imageSize ?? providerOptions.imageSize ?? "2K", + imageCount: imageParams.imageCount ?? providerOptions.imageCount ?? 1, + }; + }; + + const toV2Usage = (text: string) => { + const outputTokens = text.length; + return { + inputTokens: 0, + outputTokens, + totalTokens: outputTokens, + }; + }; + + const createV2TextStream = (run: Promise<{ text: string; finishReason: "stop" | "error" }>) => + new ReadableStream({ + start(controller) { + controller.enqueue({ type: "stream-start", warnings: [] }); + + void run + .then((result) => { + const usage = toV2Usage(result.text); + if (result.text) { + const textId = `txt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + controller.enqueue({ type: "text-start", id: textId }); + controller.enqueue({ + type: "text-delta", + id: textId, + delta: result.text, + }); + controller.enqueue({ type: "text-end", id: textId }); + } + + controller.enqueue({ + type: "finish", + finishReason: result.finishReason, + usage, + }); + }) + .catch((error) => { + controller.enqueue({ type: "error", error }); + controller.enqueue({ + type: "finish", + finishReason: "error", + usage: { + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + }, + }); + }) + .finally(() => { + controller.close(); + }); + }, + }); + 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; + + const messages = getMessagesFromOptions(options); + console.log( + "[Huanke Gemini] Messages received:", + JSON.stringify(messages).substring(0, 500) + "..." + ); + + const effectiveModelId = options?.overrideModelId || modelId; + const imageParams = getImageParamsFromOptions(options); + const imageCount = Math.min(Math.max(Number(imageParams.imageCount || 1), 1), 2); + console.log("[Huanke Gemini] Using modelId:", effectiveModelId); - console.log("[Huanke Gemini] Image params from options:", options.imageParams); + console.log("[Huanke Gemini] Final imageParams:", imageParams); + console.log(`[Huanke Gemini] Image count: ${imageCount}`); - 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] Starting ${imageCount} concurrent request(s)...`); + const startTime = Date.now(); - console.log( - `[Huanke Gemini] Generated ${allImageUrls.length} images from ${imageCount} concurrent request(s)` - ); + const requestPromises = Array.from({ length: imageCount }, () => + sendHuankeGeminiRequest(baseURL, apiKey, 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)` + ); + + const isV2Call = Array.isArray(options?.prompt); + if (isV2Call) { return { - text: markdownText, + content: [{ type: "text", 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 }, + usage: toV2Usage(markdownText), + warnings: [], }; } + + return { + text: markdownText, + finishReason: "stop" as const, + usage: { + promptTokens: 0, + completionTokens: markdownText.length, + }, + }; }, + doStream: function (this: any, options: any) { + const isV2Call = Array.isArray(options?.prompt); + + if (!isV2Call) { + const legacyStream = async function* (this: any) { + try { + const result = await (this as any).doGenerate(options); + yield { type: "text-delta" as const, textDelta: result.text }; + yield { + type: "finish" as const, + finishReason: "stop" as const, + usage: { + promptTokens: 0, + completionTokens: result.text?.length || 0, + }, + }; + } catch (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 }, + }; + } + }; + + return legacyStream.call(this); + } + + const run = (async () => { + try { + const result = await this.doGenerate(options); + const content = Array.isArray(result?.content) ? result.content : []; + const text = content + .filter((part: any) => part?.type === "text" && typeof part.text === "string") + .map((part: any) => part.text) + .join(""); + return { text, finishReason: "stop" as const }; + } catch (error) { + console.error("[Huanke Gemini Stream] Generation failed:", error); + const text = + error instanceof Error + ? `图片生成失败: ${error.message}` + : "图片生成失败,请稍后重试"; + return { text, finishReason: "error" as const }; + } + })(); + + return Promise.resolve({ + stream: createV2TextStream(run), + }); + }, + + supportedUrls: {}, modelId, provider: "huanke-gemini", - specificationVersion: "v1" as const, + specificationVersion: "v2" as const, defaultObjectGenerationMode: "json" as const, }), }; diff --git a/lib/ai/providers/volcano-multimodal.ts b/lib/ai/providers/volcano-multimodal.ts index e51a1e76c..acde7b20f 100644 --- a/lib/ai/providers/volcano-multimodal.ts +++ b/lib/ai/providers/volcano-multimodal.ts @@ -22,8 +22,14 @@ function parseMessageParts(content: unknown) { const imageUrls: string[] = []; const pushImageUrl = (value: unknown) => { - if (typeof value !== "string") return; - const url = value.trim(); + const raw = + typeof value === "string" + ? value + : value instanceof URL + ? value.toString() + : ""; + if (!raw) return; + const url = raw.trim(); if (!url) return; imageUrls.push(url); }; @@ -73,7 +79,11 @@ function extractPromptAndImages(options: any) { return { text: options.prompt.trim(), imageUrls: [] as string[] }; } - const messages = Array.isArray(options?.messages) ? options.messages : []; + const messages = Array.isArray(options?.messages) + ? options.messages + : Array.isArray(options?.prompt) + ? options.prompt + : []; for (let i = messages.length - 1; i >= 0; i -= 1) { const message = messages[i]; if (message?.role !== "user") continue; @@ -181,13 +191,65 @@ async function callResponsesApi(params: { export function createVolcanoMultimodal(config: VolcanoMultimodalConfig) { const endpoint = normalizeResponsesEndpoint(config.baseURL); const timeout = config.timeout ?? 60000; + const toV2Usage = (usage: { promptTokens?: number; completionTokens?: number }) => { + const inputTokens = usage.promptTokens ?? 0; + const outputTokens = usage.completionTokens ?? 0; + return { + inputTokens, + outputTokens, + totalTokens: inputTokens + outputTokens, + }; + }; + + const createV2TextStream = ( + run: Promise<{ text: string; finishReason: "stop" | "error"; usage: { promptTokens?: number; completionTokens?: number } }> + ) => + new ReadableStream({ + start(controller) { + controller.enqueue({ type: "stream-start", warnings: [] }); + + void run + .then((result) => { + if (result.text) { + const textId = `txt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + controller.enqueue({ type: "text-start", id: textId }); + controller.enqueue({ + type: "text-delta", + id: textId, + delta: result.text, + }); + controller.enqueue({ type: "text-end", id: textId }); + } + + controller.enqueue({ + type: "finish", + finishReason: result.finishReason, + usage: toV2Usage(result.usage), + }); + }) + .catch((error) => { + controller.enqueue({ type: "error", error }); + controller.enqueue({ + type: "finish", + finishReason: "error", + usage: { + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + }, + }); + }) + .finally(() => controller.close()); + }, + }); return { provider: "volcano-multimodal", chat: (modelId: string) => ({ provider: "volcano-multimodal", modelId, - specificationVersion: "v1" as const, + specificationVersion: "v2" as const, + supportedUrls: {}, defaultObjectGenerationMode: "json" as const, doGenerate: async (options: any) => { const { text, imageUrls } = extractPromptAndImages(options); @@ -199,23 +261,71 @@ export function createVolcanoMultimodal(config: VolcanoMultimodalConfig) { imageUrls, timeout, }); - return { + const legacyResult = { text: result.text, finishReason: "stop" as const, usage: result.usage, }; + + const isV2Call = Array.isArray(options?.prompt); + if (isV2Call) { + return { + content: [{ type: "text", text: result.text }], + finishReason: "stop" as const, + usage: toV2Usage(result.usage), + warnings: [], + }; + } + + return legacyResult; }, - doStream: async function* (options: any) { - const result = await this.doGenerate(options); - yield { - type: "text-delta" as const, - textDelta: result.text, - }; - yield { - type: "finish" as const, - finishReason: "stop" as const, - usage: result.usage, - }; + doStream: function (this: any, options: any) { + const isV2Call = Array.isArray(options?.prompt); + + if (!isV2Call) { + const legacyStream = async function* (this: any) { + const result = await this.doGenerate(options); + yield { + type: "text-delta" as const, + textDelta: result.text, + }; + yield { + type: "finish" as const, + finishReason: "stop" as const, + usage: result.usage, + }; + }; + return legacyStream.call(this); + } + + const run = (async () => { + try { + const result = await this.doGenerate(options); + const content = Array.isArray(result?.content) ? result.content : []; + const text = content + .filter((part: any) => part?.type === "text" && typeof part.text === "string") + .map((part: any) => part.text) + .join(""); + return { + text, + finishReason: "stop" as const, + usage: { + promptTokens: result?.usage?.inputTokens ?? 0, + completionTokens: result?.usage?.outputTokens ?? 0, + }, + }; + } catch (error) { + return { + text: error instanceof Error ? error.message : "请求失败", + finishReason: "error" as const, + usage: { promptTokens: 0, completionTokens: 0 }, + }; + } + })(); + + return Promise.resolve({ + stream: createV2TextStream(run), + }); }, }), };