882 lines
No EOL
31 KiB
JavaScript
882 lines
No EOL
31 KiB
JavaScript
/**
|
||
* Gemini转换器
|
||
* 处理Gemini(Google)协议与其他协议之间的转换
|
||
*/
|
||
|
||
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; |