fix(ai): adapt huanke and volcano providers to sdk v2
This commit is contained in:
parent
801d61f9a6
commit
c4da49e126
@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
@ -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),
|
||||
});
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user