AIClient-2-API/src/converters/strategies/GeminiConverter.js

882 lines
No EOL
31 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Gemini转换器
* 处理GeminiGoogle协议与其他协议之间的转换
*/
import { v4 as uuidv4 } from 'uuid';
import { BaseConverter } from '../BaseConverter.js';
import {
checkAndAssignOrDefault,
OPENAI_DEFAULT_MAX_TOKENS,
OPENAI_DEFAULT_TEMPERATURE,
OPENAI_DEFAULT_TOP_P,
CLAUDE_DEFAULT_MAX_TOKENS,
CLAUDE_DEFAULT_TEMPERATURE,
CLAUDE_DEFAULT_TOP_P
} from '../utils.js';
import { MODEL_PROTOCOL_PREFIX } from '../../common.js';
import {
generateResponseCreated,
generateResponseInProgress,
generateOutputItemAdded,
generateContentPartAdded,
generateOutputTextDone,
generateContentPartDone,
generateOutputItemDone,
generateResponseCompleted
} from '../../openai/openai-responses-core.mjs';
/**
* Gemini转换器类
* 实现Gemini协议到其他协议的转换
*/
export class GeminiConverter extends BaseConverter {
constructor() {
super('gemini');
}
/**
* 转换请求
*/
convertRequest(data, targetProtocol) {
switch (targetProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
return this.toOpenAIRequest(data);
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return this.toClaudeRequest(data);
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
return this.toOpenAIResponsesRequest(data);
default:
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
}
}
/**
* 转换响应
*/
convertResponse(data, targetProtocol, model) {
switch (targetProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
return this.toOpenAIResponse(data, model);
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return this.toClaudeResponse(data, model);
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
return this.toOpenAIResponsesResponse(data, model);
default:
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
}
}
/**
* 转换流式响应块
*/
convertStreamChunk(chunk, targetProtocol, model) {
switch (targetProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
return this.toOpenAIStreamChunk(chunk, model);
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return this.toClaudeStreamChunk(chunk, model);
case MODEL_PROTOCOL_PREFIX.OPENAI_RESPONSES:
return this.toOpenAIResponsesStreamChunk(chunk, model);
default:
throw new Error(`Unsupported target protocol: ${targetProtocol}`);
}
}
/**
* 转换模型列表
*/
convertModelList(data, targetProtocol) {
switch (targetProtocol) {
case MODEL_PROTOCOL_PREFIX.OPENAI:
return this.toOpenAIModelList(data);
case MODEL_PROTOCOL_PREFIX.CLAUDE:
return this.toClaudeModelList(data);
default:
return data;
}
}
// =========================================================================
// Gemini -> OpenAI 转换
// =========================================================================
/**
* Gemini请求 -> OpenAI请求
*/
toOpenAIRequest(geminiRequest) {
const openaiRequest = {
messages: [],
model: geminiRequest.model,
max_tokens: checkAndAssignOrDefault(geminiRequest.max_tokens, OPENAI_DEFAULT_MAX_TOKENS),
temperature: checkAndAssignOrDefault(geminiRequest.temperature, OPENAI_DEFAULT_TEMPERATURE),
top_p: checkAndAssignOrDefault(geminiRequest.top_p, OPENAI_DEFAULT_TOP_P),
};
// 处理系统指令
if (geminiRequest.systemInstruction && Array.isArray(geminiRequest.systemInstruction.parts)) {
const systemContent = this.processGeminiPartsToOpenAIContent(geminiRequest.systemInstruction.parts);
if (systemContent) {
openaiRequest.messages.push({
role: 'system',
content: systemContent
});
}
}
// 处理内容
if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
geminiRequest.contents.forEach(content => {
if (content && Array.isArray(content.parts)) {
const openaiContent = this.processGeminiPartsToOpenAIContent(content.parts);
if (openaiContent && openaiContent.length > 0) {
const openaiRole = content.role === 'model' ? 'assistant' : content.role;
openaiRequest.messages.push({
role: openaiRole,
content: openaiContent
});
}
}
});
}
return openaiRequest;
}
/**
* Gemini响应 -> OpenAI响应
*/
toOpenAIResponse(geminiResponse, model) {
const content = this.processGeminiResponseContent(geminiResponse);
return {
id: `chatcmpl-${uuidv4()}`,
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: model,
choices: [{
index: 0,
message: {
role: "assistant",
content: content
},
finish_reason: "stop",
}],
usage: geminiResponse.usageMetadata ? {
prompt_tokens: geminiResponse.usageMetadata.promptTokenCount || 0,
completion_tokens: geminiResponse.usageMetadata.candidatesTokenCount || 0,
total_tokens: geminiResponse.usageMetadata.totalTokenCount || 0,
cached_tokens: geminiResponse.usageMetadata.cachedContentTokenCount || 0,
prompt_tokens_details: {
cached_tokens: geminiResponse.usageMetadata.cachedContentTokenCount || 0
},
completion_tokens_details: {
reasoning_tokens: geminiResponse.usageMetadata.thoughtsTokenCount || 0
}
} : {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
cached_tokens: 0,
prompt_tokens_details: {
cached_tokens: 0
},
completion_tokens_details: {
reasoning_tokens: 0
}
},
};
}
/**
* Gemini流式响应 -> OpenAI流式响应
*/
toOpenAIStreamChunk(geminiChunk, model) {
if (!geminiChunk) return null;
const candidate = geminiChunk.candidates?.[0];
if (!candidate) return null;
let content = '';
const toolCalls = [];
// 从parts中提取文本和tool calls
const parts = candidate.content?.parts;
if (parts && Array.isArray(parts)) {
for (const part of parts) {
if (part.text) {
content += part.text;
}
if (part.functionCall) {
toolCalls.push({
index: toolCalls.length,
id: part.functionCall.id || `call_${uuidv4()}`,
type: 'function',
function: {
name: part.functionCall.name,
arguments: typeof part.functionCall.args === 'string'
? part.functionCall.args
: JSON.stringify(part.functionCall.args)
}
});
}
// thoughtSignature is ignored (internal Gemini data)
}
}
// 处理finishReason
let finishReason = null;
if (candidate.finishReason) {
finishReason = candidate.finishReason === 'STOP' ? 'stop' :
candidate.finishReason === 'MAX_TOKENS' ? 'length' :
candidate.finishReason.toLowerCase();
}
// 如果包含工具调用,且完成原因为 stop则将完成原因修改为 tool_calls
if (toolCalls.length > 0 && finishReason === 'stop') {
finishReason = 'tool_calls';
}
// 构建delta对象
const delta = {};
if (content) delta.content = content;
if (toolCalls.length > 0) delta.tool_calls = toolCalls;
// Don't return empty delta chunks
if (Object.keys(delta).length === 0 && !finishReason) {
return null;
}
return {
id: `chatcmpl-${uuidv4()}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: model,
choices: [{
index: 0,
delta: delta,
finish_reason: finishReason,
}],
usage: geminiChunk.usageMetadata ? {
prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0,
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
prompt_tokens_details: {
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
},
completion_tokens_details: {
reasoning_tokens: geminiChunk.usageMetadata.thoughtsTokenCount || 0
}
} : {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
cached_tokens: 0,
prompt_tokens_details: {
cached_tokens: 0
},
completion_tokens_details: {
reasoning_tokens: 0
}
},
};
}
/**
* Gemini模型列表 -> OpenAI模型列表
*/
toOpenAIModelList(geminiModels) {
return {
object: "list",
data: geminiModels.models.map(m => ({
id: m.name.startsWith('models/') ? m.name.substring(7) : m.name,
object: "model",
created: Math.floor(Date.now() / 1000),
owned_by: "google",
})),
};
}
/**
* 处理Gemini parts到OpenAI内容
*/
processGeminiPartsToOpenAIContent(parts) {
if (!parts || !Array.isArray(parts)) return '';
const contentArray = [];
parts.forEach(part => {
if (!part) return;
if (typeof part.text === 'string') {
contentArray.push({
type: 'text',
text: part.text
});
}
if (part.inlineData) {
const { mimeType, data } = part.inlineData;
if (mimeType && data) {
contentArray.push({
type: 'image_url',
image_url: {
url: `data:${mimeType};base64,${data}`
}
});
}
}
if (part.fileData) {
const { mimeType, fileUri } = part.fileData;
if (mimeType && fileUri) {
if (mimeType.startsWith('image/')) {
contentArray.push({
type: 'image_url',
image_url: {
url: fileUri
}
});
} else if (mimeType.startsWith('audio/')) {
contentArray.push({
type: 'text',
text: `[Audio file: ${fileUri}]`
});
}
}
}
});
return contentArray.length === 1 && contentArray[0].type === 'text'
? contentArray[0].text
: contentArray;
}
/**
* 处理Gemini响应内容
*/
processGeminiResponseContent(geminiResponse) {
if (!geminiResponse || !geminiResponse.candidates) return '';
const contents = [];
geminiResponse.candidates.forEach(candidate => {
if (candidate.content && candidate.content.parts) {
candidate.content.parts.forEach(part => {
if (part.text) {
contents.push(part.text);
}
});
}
});
return contents.join('\n');
}
// =========================================================================
// Gemini -> Claude 转换
// =========================================================================
/**
* Gemini请求 -> Claude请求
*/
toClaudeRequest(geminiRequest) {
const claudeRequest = {
model: geminiRequest.model || 'claude-3-opus',
messages: [],
max_tokens: checkAndAssignOrDefault(geminiRequest.generationConfig?.maxOutputTokens, CLAUDE_DEFAULT_MAX_TOKENS),
temperature: checkAndAssignOrDefault(geminiRequest.generationConfig?.temperature, CLAUDE_DEFAULT_TEMPERATURE),
top_p: checkAndAssignOrDefault(geminiRequest.generationConfig?.topP, CLAUDE_DEFAULT_TOP_P),
};
// 处理系统指令
if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) {
const systemText = geminiRequest.systemInstruction.parts
.filter(p => p.text)
.map(p => p.text)
.join('\n');
if (systemText) {
claudeRequest.system = systemText;
}
}
// 处理内容
if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
geminiRequest.contents.forEach(content => {
if (!content || !content.parts) return;
const role = content.role === 'model' ? 'assistant' : 'user';
const claudeContent = this.processGeminiPartsToClaudeContent(content.parts);
if (claudeContent.length > 0) {
claudeRequest.messages.push({
role: role,
content: claudeContent
});
}
});
}
// 处理工具
if (geminiRequest.tools && geminiRequest.tools[0]?.functionDeclarations) {
claudeRequest.tools = geminiRequest.tools[0].functionDeclarations.map(func => ({
name: func.name,
description: func.description || '',
input_schema: func.parameters || { type: 'object', properties: {} }
}));
}
return claudeRequest;
}
/**
* Gemini响应 -> Claude响应
*/
toClaudeResponse(geminiResponse, model) {
if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) {
return {
id: `msg_${uuidv4()}`,
type: "message",
role: "assistant",
content: [],
model: model,
stop_reason: "end_turn",
stop_sequence: null,
usage: {
input_tokens: geminiResponse?.usageMetadata?.promptTokenCount || 0,
output_tokens: geminiResponse?.usageMetadata?.candidatesTokenCount || 0
}
};
}
const candidate = geminiResponse.candidates[0];
const content = this.processGeminiResponseToClaudeContent(geminiResponse);
const finishReason = candidate.finishReason;
let stopReason = "end_turn";
if (finishReason) {
switch (finishReason) {
case 'STOP':
stopReason = 'end_turn';
break;
case 'MAX_TOKENS':
stopReason = 'max_tokens';
break;
case 'SAFETY':
stopReason = 'safety';
break;
case 'RECITATION':
stopReason = 'recitation';
break;
case 'OTHER':
stopReason = 'other';
break;
default:
stopReason = 'end_turn';
}
}
return {
id: `msg_${uuidv4()}`,
type: "message",
role: "assistant",
content: content,
model: model,
stop_reason: stopReason,
stop_sequence: null,
usage: {
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: geminiResponse.usageMetadata?.cachedContentTokenCount || 0,
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0
}
};
}
/**
* Gemini流式响应 -> Claude流式响应
*/
toClaudeStreamChunk(geminiChunk, model) {
if (!geminiChunk) return null;
// 处理完整的Gemini chunk对象
if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) {
const candidate = geminiChunk.candidates?.[0];
if (candidate) {
const parts = candidate.content?.parts;
// 提取文本内容
if (parts && Array.isArray(parts)) {
const textParts = parts.filter(part => part && typeof part.text === 'string');
if (textParts.length > 0) {
const text = textParts.map(part => part.text).join('');
return {
type: "content_block_delta",
index: 0,
delta: {
type: "text_delta",
text: text
}
};
}
}
// 处理finishReason
if (candidate.finishReason) {
const result = {
type: "message_delta",
delta: {
stop_reason: candidate.finishReason === 'STOP' ? 'end_turn' :
candidate.finishReason === 'MAX_TOKENS' ? 'max_tokens' :
candidate.finishReason.toLowerCase()
}
};
// 添加 usage 信息
if (geminiChunk.usageMetadata) {
result.usage = {
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
cache_creation_input_tokens: 0,
cache_read_input_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
prompt_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
completion_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0,
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
};
}
return result;
}
}
}
// 向后兼容:处理字符串格式
if (typeof geminiChunk === 'string') {
return {
type: "content_block_delta",
index: 0,
delta: {
type: "text_delta",
text: geminiChunk
}
};
}
return null;
}
/**
* Gemini模型列表 -> Claude模型列表
*/
toClaudeModelList(geminiModels) {
return {
models: geminiModels.models.map(m => ({
name: m.name.startsWith('models/') ? m.name.substring(7) : m.name,
description: "",
})),
};
}
/**
* 处理Gemini parts到Claude内容
*/
processGeminiPartsToClaudeContent(parts) {
if (!parts || !Array.isArray(parts)) return [];
const content = [];
parts.forEach(part => {
if (!part) return;
if (part.text) {
content.push({
type: 'text',
text: part.text
});
}
if (part.inlineData) {
content.push({
type: 'image',
source: {
type: 'base64',
media_type: part.inlineData.mimeType,
data: part.inlineData.data
}
});
}
if (part.functionCall) {
content.push({
type: 'tool_use',
id: uuidv4(),
name: part.functionCall.name,
input: part.functionCall.args || {}
});
}
if (part.functionResponse) {
content.push({
type: 'tool_result',
tool_use_id: part.functionResponse.name,
content: part.functionResponse.response
});
}
});
return content;
}
/**
* 处理Gemini响应到Claude内容
*/
processGeminiResponseToClaudeContent(geminiResponse) {
if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) return [];
const content = [];
for (const candidate of geminiResponse.candidates) {
if (candidate.finishReason && candidate.finishReason !== 'STOP') {
if (candidate.finishMessage) {
content.push({
type: 'text',
text: `Error: ${candidate.finishMessage}`
});
}
continue;
}
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
if (part.text) {
content.push({
type: 'text',
text: part.text
});
} else if (part.inlineData) {
content.push({
type: 'image',
source: {
type: 'base64',
media_type: part.inlineData.mimeType,
data: part.inlineData.data
}
});
} else if (part.functionCall) {
content.push({
type: 'tool_use',
id: uuidv4(),
name: part.functionCall.name,
input: part.functionCall.args || {}
});
}
}
}
}
return content;
}
// =========================================================================
// Gemini -> OpenAI Responses 转换
// =========================================================================
/**
* Gemini请求 -> OpenAI Responses请求
*/
toOpenAIResponsesRequest(geminiRequest) {
const responsesRequest = {
model: geminiRequest.model,
max_tokens: checkAndAssignOrDefault(geminiRequest.generationConfig?.maxOutputTokens, OPENAI_DEFAULT_MAX_TOKENS),
temperature: checkAndAssignOrDefault(geminiRequest.generationConfig?.temperature, OPENAI_DEFAULT_TEMPERATURE),
top_p: checkAndAssignOrDefault(geminiRequest.generationConfig?.topP, OPENAI_DEFAULT_TOP_P),
};
// 处理系统指令
if (geminiRequest.systemInstruction && geminiRequest.systemInstruction.parts) {
const instructionsText = geminiRequest.systemInstruction.parts
.filter(p => p.text)
.map(p => p.text)
.join('\n');
if (instructionsText) {
responsesRequest.instructions = instructionsText;
}
}
// 处理输入
if (geminiRequest.contents && Array.isArray(geminiRequest.contents)) {
const lastContent = geminiRequest.contents[geminiRequest.contents.length - 1];
if (lastContent && lastContent.parts) {
const inputText = lastContent.parts
.filter(p => p.text)
.map(p => p.text)
.join(' ');
if (inputText) {
responsesRequest.input = inputText;
}
}
}
return responsesRequest;
}
/**
* Gemini响应 -> OpenAI Responses响应
*/
toOpenAIResponsesResponse(geminiResponse, model) {
const content = this.processGeminiResponseContent(geminiResponse);
const textContent = typeof content === 'string' ? content : JSON.stringify(content);
let output = [];
output.push({
id: `msg_${uuidv4().replace(/-/g, '')}`,
summary: [],
type: "message",
role: "assistant",
status: "completed",
content: [{
annotations: [],
logprobs: [],
text: textContent,
type: "output_text"
}]
});
return {
background: false,
created_at: Math.floor(Date.now() / 1000),
error: null,
id: `resp_${uuidv4().replace(/-/g, '')}`,
incomplete_details: null,
max_output_tokens: null,
max_tool_calls: null,
metadata: {},
model: model,
object: "response",
output: output,
parallel_tool_calls: true,
previous_response_id: null,
prompt_cache_key: null,
reasoning: {},
safety_identifier: "user-" + uuidv4().replace(/-/g, ''),
service_tier: "default",
status: "completed",
store: false,
temperature: 1,
text: {
format: { type: "text" },
},
tool_choice: "auto",
tools: [],
top_logprobs: 0,
top_p: 1,
truncation: "disabled",
usage: {
input_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
input_tokens_details: {
cached_tokens: geminiResponse.usageMetadata?.cachedContentTokenCount || 0
},
output_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
output_tokens_details: {
reasoning_tokens: geminiResponse.usageMetadata?.thoughtsTokenCount || 0
},
total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0
},
user: null
};
}
/**
* Gemini流式响应 -> OpenAI Responses流式响应
*/
toOpenAIResponsesStreamChunk(geminiChunk, model, requestId = null) {
if (!geminiChunk) return [];
const responseId = requestId || `resp_${uuidv4().replace(/-/g, '')}`;
const events = [];
// 处理完整的Gemini chunk对象
if (typeof geminiChunk === 'object' && !Array.isArray(geminiChunk)) {
const candidate = geminiChunk.candidates?.[0];
if (candidate) {
const parts = candidate.content?.parts;
// 第一个chunk - 检测是否是开始有role
if (candidate.content?.role === 'model' && parts && parts.length > 0) {
// 只在第一次有内容时发送开始事件
const hasContent = parts.some(part => part && typeof part.text === 'string' && part.text.length > 0);
if (hasContent) {
events.push(
generateResponseCreated(responseId, model || 'unknown'),
generateResponseInProgress(responseId),
generateOutputItemAdded(responseId),
generateContentPartAdded(responseId)
);
}
}
// 提取文本内容
if (parts && Array.isArray(parts)) {
const textParts = parts.filter(part => part && typeof part.text === 'string');
if (textParts.length > 0) {
const text = textParts.map(part => part.text).join('');
events.push({
delta: text,
item_id: `msg_${uuidv4().replace(/-/g, '')}`,
output_index: 0,
sequence_number: 3,
type: "response.output_text.delta"
});
}
}
// 处理finishReason
if (candidate.finishReason) {
events.push(
generateOutputTextDone(responseId),
generateContentPartDone(responseId),
generateOutputItemDone(responseId),
generateResponseCompleted(responseId)
);
// 如果有 usage 信息,更新最后一个事件
if (geminiChunk.usageMetadata && events.length > 0) {
const lastEvent = events[events.length - 1];
if (lastEvent.response) {
lastEvent.response.usage = {
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
input_tokens_details: {
cached_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0
},
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0,
output_tokens_details: {
reasoning_tokens: geminiChunk.usageMetadata.thoughtsTokenCount || 0
},
total_tokens: geminiChunk.usageMetadata.totalTokenCount || 0
};
}
}
}
}
}
// 向后兼容:处理字符串格式
if (typeof geminiChunk === 'string') {
events.push({
delta: geminiChunk,
item_id: `msg_${uuidv4().replace(/-/g, '')}`,
output_index: 0,
sequence_number: 3,
type: "response.output_text.delta"
});
}
return events;
}
}
export default GeminiConverter;