9router/open-sse/translator/response/gemini-to-openai.js

207 lines
6.1 KiB
JavaScript

import { register } from "../index.js";
import { FORMATS } from "../formats.js";
// Convert Gemini response chunk to OpenAI format
function geminiToOpenAIResponse(chunk, state) {
if (!chunk) return null;
// Handle Antigravity wrapper
const response = chunk.response || chunk;
if (!response || !response.candidates?.[0]) return null;
const results = [];
const candidate = response.candidates[0];
const content = candidate.content;
// Initialize state
if (!state.messageId) {
state.messageId = response.responseId || `msg_${Date.now()}`;
state.model = response.modelVersion || "gemini";
state.functionIndex = 0;
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: { role: "assistant" },
finish_reason: null
}]
});
}
// Process parts
if (content?.parts) {
for (const part of content.parts) {
const hasThoughtSig = part.thoughtSignature || part.thought_signature;
const isThought = part.thought === true;
// Handle thought signature (thinking mode)
if (hasThoughtSig) {
const hasTextContent = part.text !== undefined && part.text !== "";
const hasFunctionCall = !!part.functionCall;
if (hasTextContent) {
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: isThought
? { reasoning_content: part.text }
: { content: part.text },
finish_reason: null
}]
});
}
if (hasFunctionCall) {
const fcName = part.functionCall.name;
const fcArgs = part.functionCall.args || {};
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
index: toolCallIndex,
type: "function",
function: {
name: fcName,
arguments: JSON.stringify(fcArgs)
}
};
state.toolCalls.set(toolCallIndex, toolCall);
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: { tool_calls: [toolCall] },
finish_reason: null
}]
});
}
continue;
}
// Text content (non-thinking)
if (part.text !== undefined && part.text !== "") {
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: { content: part.text },
finish_reason: null
}]
});
}
// Function call
if (part.functionCall) {
const fcName = part.functionCall.name;
const fcArgs = part.functionCall.args || {};
const toolCallIndex = state.functionIndex++;
const toolCall = {
id: `${fcName}-${Date.now()}-${toolCallIndex}`,
index: toolCallIndex,
type: "function",
function: {
name: fcName,
arguments: JSON.stringify(fcArgs)
}
};
state.toolCalls.set(toolCallIndex, toolCall);
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: { tool_calls: [toolCall] },
finish_reason: null
}]
});
}
// Inline data (images)
const inlineData = part.inlineData || part.inline_data;
if (inlineData?.data) {
const mimeType = inlineData.mimeType || inlineData.mime_type || "image/png";
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: {
images: [{
type: "image_url",
image_url: { url: `data:${mimeType};base64,${inlineData.data}` }
}]
},
finish_reason: null
}]
});
}
}
}
// Finish reason
if (candidate.finishReason) {
let finishReason = candidate.finishReason.toLowerCase();
if (finishReason === "stop" && state.toolCalls.size > 0) {
finishReason = "tool_calls";
}
results.push({
id: `chatcmpl-${state.messageId}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: state.model,
choices: [{
index: 0,
delta: {},
finish_reason: finishReason
}]
});
state.finishReason = finishReason;
}
// Usage metadata
const usage = response.usageMetadata || chunk.usageMetadata;
if (usage) {
const promptTokens = (usage.promptTokenCount || 0) + (usage.thoughtsTokenCount || 0);
state.usage = {
prompt_tokens: promptTokens,
completion_tokens: usage.candidatesTokenCount || 0,
total_tokens: usage.totalTokenCount || 0
};
if (usage.thoughtsTokenCount > 0) {
state.usage.completion_tokens_details = {
reasoning_tokens: usage.thoughtsTokenCount
};
}
}
return results.length > 0 ? results : null;
}
// Register
register(FORMATS.GEMINI, FORMATS.OPENAI, null, geminiToOpenAIResponse);
register(FORMATS.GEMINI_CLI, FORMATS.OPENAI, null, geminiToOpenAIResponse);
register(FORMATS.ANTIGRAVITY, FORMATS.OPENAI, null, geminiToOpenAIResponse);