refactor: restructure translator from from-openai/to-openai to request/response folders
This commit is contained in:
232
open-sse/translator/request/claude-to-openai.js
Normal file
232
open-sse/translator/request/claude-to-openai.js
Normal file
@@ -0,0 +1,232 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
|
||||
// Convert Claude request to OpenAI format
|
||||
function claudeToOpenAIRequest(model, body, stream) {
|
||||
const result = {
|
||||
model: model,
|
||||
messages: [],
|
||||
stream: stream
|
||||
};
|
||||
|
||||
// Max tokens
|
||||
if (body.max_tokens) {
|
||||
result.max_tokens = adjustMaxTokens(body);
|
||||
}
|
||||
|
||||
// Temperature
|
||||
if (body.temperature !== undefined) {
|
||||
result.temperature = body.temperature;
|
||||
}
|
||||
|
||||
// System message
|
||||
if (body.system) {
|
||||
const systemContent = Array.isArray(body.system)
|
||||
? body.system.map(s => s.text || "").join("\n")
|
||||
: body.system;
|
||||
|
||||
if (systemContent) {
|
||||
result.messages.push({
|
||||
role: "system",
|
||||
content: systemContent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Convert messages
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
for (let i = 0; i < body.messages.length; i++) {
|
||||
const msg = body.messages[i];
|
||||
const converted = convertClaudeMessage(msg);
|
||||
if (converted) {
|
||||
// Handle array of messages (multiple tool results)
|
||||
if (Array.isArray(converted)) {
|
||||
result.messages.push(...converted);
|
||||
} else {
|
||||
result.messages.push(converted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix missing tool responses - OpenAI requires every tool_call to have a response
|
||||
fixMissingToolResponses(result.messages);
|
||||
|
||||
// Tools
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
result.tools = body.tools.map(tool => ({
|
||||
type: "function",
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.input_schema || { type: "object", properties: {} }
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Tool choice
|
||||
if (body.tool_choice) {
|
||||
result.tool_choice = convertToolChoice(body.tool_choice);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fix missing tool responses - add empty responses for tool_calls without responses
|
||||
function fixMissingToolResponses(messages) {
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
const msg = messages[i];
|
||||
if (msg.role === "assistant" && msg.tool_calls && msg.tool_calls.length > 0) {
|
||||
const toolCallIds = msg.tool_calls.map(tc => tc.id);
|
||||
|
||||
// Collect all tool response IDs that IMMEDIATELY follow this assistant message
|
||||
const respondedIds = new Set();
|
||||
let insertPosition = i + 1;
|
||||
for (let j = i + 1; j < messages.length; j++) {
|
||||
const nextMsg = messages[j];
|
||||
if (nextMsg.role === "tool" && nextMsg.tool_call_id) {
|
||||
respondedIds.add(nextMsg.tool_call_id);
|
||||
insertPosition = j + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find missing responses and insert them
|
||||
const missingIds = toolCallIds.filter(id => !respondedIds.has(id));
|
||||
|
||||
if (missingIds.length > 0) {
|
||||
const missingResponses = missingIds.map(id => ({
|
||||
role: "tool",
|
||||
tool_call_id: id,
|
||||
content: "[No response received]"
|
||||
}));
|
||||
messages.splice(insertPosition, 0, ...missingResponses);
|
||||
i = insertPosition + missingResponses.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert single Claude message - returns single message or array of messages
|
||||
function convertClaudeMessage(msg) {
|
||||
const role = msg.role === "user" || msg.role === "tool" ? "user" : "assistant";
|
||||
|
||||
// Simple string content
|
||||
if (typeof msg.content === "string") {
|
||||
return { role, content: msg.content };
|
||||
}
|
||||
|
||||
// Array content
|
||||
if (Array.isArray(msg.content)) {
|
||||
const parts = [];
|
||||
const toolCalls = [];
|
||||
const toolResults = [];
|
||||
|
||||
for (const block of msg.content) {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
parts.push({ type: "text", text: block.text });
|
||||
break;
|
||||
|
||||
case "image":
|
||||
if (block.source?.type === "base64") {
|
||||
parts.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: `data:${block.source.media_type};base64,${block.source.data}`
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "tool_use":
|
||||
toolCalls.push({
|
||||
id: block.id,
|
||||
type: "function",
|
||||
function: {
|
||||
name: block.name,
|
||||
arguments: JSON.stringify(block.input || {})
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case "tool_result":
|
||||
let resultContent = "";
|
||||
if (typeof block.content === "string") {
|
||||
resultContent = block.content;
|
||||
} else if (Array.isArray(block.content)) {
|
||||
resultContent = block.content
|
||||
.filter(c => c.type === "text")
|
||||
.map(c => c.text)
|
||||
.join("\n") || JSON.stringify(block.content);
|
||||
} else if (block.content) {
|
||||
resultContent = JSON.stringify(block.content);
|
||||
}
|
||||
|
||||
toolResults.push({
|
||||
role: "tool",
|
||||
tool_call_id: block.tool_use_id,
|
||||
content: resultContent
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If has tool results, return array of tool messages
|
||||
if (toolResults.length > 0) {
|
||||
if (parts.length > 0) {
|
||||
const textContent = parts.length === 1 && parts[0].type === "text"
|
||||
? parts[0].text
|
||||
: parts;
|
||||
return [...toolResults, { role: "user", content: textContent }];
|
||||
}
|
||||
return toolResults;
|
||||
}
|
||||
|
||||
// If has tool calls, return assistant message with tool_calls
|
||||
if (toolCalls.length > 0) {
|
||||
const result = { role: "assistant" };
|
||||
if (parts.length > 0) {
|
||||
result.content = parts.length === 1 && parts[0].type === "text"
|
||||
? parts[0].text
|
||||
: parts;
|
||||
}
|
||||
result.tool_calls = toolCalls;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Return content
|
||||
if (parts.length > 0) {
|
||||
return {
|
||||
role,
|
||||
content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts
|
||||
};
|
||||
}
|
||||
|
||||
// Empty content array
|
||||
if (msg.content.length === 0) {
|
||||
return { role, content: "" };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert tool choice
|
||||
function convertToolChoice(choice) {
|
||||
if (!choice) return "auto";
|
||||
if (typeof choice === "string") return choice;
|
||||
|
||||
switch (choice.type) {
|
||||
case "auto": return "auto";
|
||||
case "any": return "required";
|
||||
case "tool": return { type: "function", function: { name: choice.name } };
|
||||
default: return "auto";
|
||||
}
|
||||
}
|
||||
|
||||
// Register
|
||||
register(FORMATS.CLAUDE, FORMATS.OPENAI, claudeToOpenAIRequest, null);
|
||||
|
||||
147
open-sse/translator/request/gemini-to-openai.js
Normal file
147
open-sse/translator/request/gemini-to-openai.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
|
||||
// Convert Gemini request to OpenAI format
|
||||
function geminiToOpenAIRequest(model, body, stream) {
|
||||
const result = {
|
||||
model: model,
|
||||
messages: [],
|
||||
stream: stream
|
||||
};
|
||||
|
||||
// Generation config
|
||||
if (body.generationConfig) {
|
||||
const config = body.generationConfig;
|
||||
if (config.maxOutputTokens) {
|
||||
const tempBody = { max_tokens: config.maxOutputTokens, tools: body.tools };
|
||||
result.max_tokens = adjustMaxTokens(tempBody);
|
||||
}
|
||||
if (config.temperature !== undefined) {
|
||||
result.temperature = config.temperature;
|
||||
}
|
||||
if (config.topP !== undefined) {
|
||||
result.top_p = config.topP;
|
||||
}
|
||||
}
|
||||
|
||||
// System instruction
|
||||
if (body.systemInstruction) {
|
||||
const systemText = extractGeminiText(body.systemInstruction);
|
||||
if (systemText) {
|
||||
result.messages.push({
|
||||
role: "system",
|
||||
content: systemText
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Convert contents to messages
|
||||
if (body.contents && Array.isArray(body.contents)) {
|
||||
for (const content of body.contents) {
|
||||
const converted = convertGeminiContent(content);
|
||||
if (converted) {
|
||||
result.messages.push(converted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tools
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
result.tools = [];
|
||||
for (const tool of body.tools) {
|
||||
if (tool.functionDeclarations) {
|
||||
for (const func of tool.functionDeclarations) {
|
||||
result.tools.push({
|
||||
type: "function",
|
||||
function: {
|
||||
name: func.name,
|
||||
description: func.description || "",
|
||||
parameters: func.parameters || { type: "object", properties: {} }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Convert Gemini content to OpenAI message
|
||||
function convertGeminiContent(content) {
|
||||
const role = content.role === "user" ? "user" : "assistant";
|
||||
|
||||
if (!content.parts || !Array.isArray(content.parts)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts = [];
|
||||
const toolCalls = [];
|
||||
|
||||
for (const part of content.parts) {
|
||||
if (part.text !== undefined) {
|
||||
parts.push({ type: "text", text: part.text });
|
||||
}
|
||||
|
||||
if (part.inlineData) {
|
||||
parts.push({
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (part.functionCall) {
|
||||
toolCalls.push({
|
||||
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
type: "function",
|
||||
function: {
|
||||
name: part.functionCall.name,
|
||||
arguments: JSON.stringify(part.functionCall.args || {})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (part.functionResponse) {
|
||||
return {
|
||||
role: "tool",
|
||||
tool_call_id: part.functionResponse.id || part.functionResponse.name,
|
||||
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (toolCalls.length > 0) {
|
||||
const result = { role: "assistant" };
|
||||
if (parts.length > 0) {
|
||||
result.content = parts.length === 1 ? parts[0].text : parts;
|
||||
}
|
||||
result.tool_calls = toolCalls;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
return {
|
||||
role,
|
||||
content: parts.length === 1 && parts[0].type === "text" ? parts[0].text : parts
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract text from Gemini content
|
||||
function extractGeminiText(content) {
|
||||
if (typeof content === "string") return content;
|
||||
if (content.parts && Array.isArray(content.parts)) {
|
||||
return content.parts.map(p => p.text || "").join("");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Register
|
||||
register(FORMATS.GEMINI, FORMATS.OPENAI, geminiToOpenAIRequest, null);
|
||||
register(FORMATS.GEMINI_CLI, FORMATS.OPENAI, geminiToOpenAIRequest, null);
|
||||
|
||||
136
open-sse/translator/request/openai-responses.js
Normal file
136
open-sse/translator/request/openai-responses.js
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Translator: OpenAI Responses API → OpenAI Chat Completions
|
||||
*
|
||||
* Responses API uses: { input: [...], instructions: "..." }
|
||||
* Chat API uses: { messages: [...] }
|
||||
*/
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
|
||||
/**
|
||||
* Convert OpenAI Responses API request to OpenAI Chat Completions format
|
||||
*/
|
||||
function openaiResponsesToOpenAIRequest(model, body, stream, credentials) {
|
||||
if (!body.input) return body;
|
||||
|
||||
const result = { ...body };
|
||||
result.messages = [];
|
||||
|
||||
// Convert instructions to system message
|
||||
if (body.instructions) {
|
||||
result.messages.push({ role: "system", content: body.instructions });
|
||||
}
|
||||
|
||||
// Group items by conversation turn
|
||||
let currentAssistantMsg = null;
|
||||
let pendingToolResults = [];
|
||||
|
||||
for (const item of body.input) {
|
||||
if (item.type === "message") {
|
||||
// Flush any pending assistant message with tool calls
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
currentAssistantMsg = null;
|
||||
}
|
||||
// Flush pending tool results
|
||||
if (pendingToolResults.length > 0) {
|
||||
for (const tr of pendingToolResults) {
|
||||
result.messages.push(tr);
|
||||
}
|
||||
pendingToolResults = [];
|
||||
}
|
||||
|
||||
// Convert content: input_text → text, output_text → text
|
||||
const content = Array.isArray(item.content)
|
||||
? item.content.map(c => {
|
||||
if (c.type === "input_text") return { type: "text", text: c.text };
|
||||
if (c.type === "output_text") return { type: "text", text: c.text };
|
||||
return c;
|
||||
})
|
||||
: item.content;
|
||||
result.messages.push({ role: item.role, content });
|
||||
}
|
||||
else if (item.type === "function_call") {
|
||||
// Start or append to assistant message with tool_calls
|
||||
if (!currentAssistantMsg) {
|
||||
currentAssistantMsg = {
|
||||
role: "assistant",
|
||||
content: null,
|
||||
tool_calls: []
|
||||
};
|
||||
}
|
||||
currentAssistantMsg.tool_calls.push({
|
||||
id: item.call_id,
|
||||
type: "function",
|
||||
function: {
|
||||
name: item.name,
|
||||
arguments: item.arguments
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (item.type === "function_call_output") {
|
||||
// Flush assistant message first if exists
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
currentAssistantMsg = null;
|
||||
}
|
||||
// Flush any pending tool results first
|
||||
if (pendingToolResults.length > 0) {
|
||||
for (const tr of pendingToolResults) {
|
||||
result.messages.push(tr);
|
||||
}
|
||||
pendingToolResults = [];
|
||||
}
|
||||
// Add tool result immediately
|
||||
result.messages.push({
|
||||
role: "tool",
|
||||
tool_call_id: item.call_id,
|
||||
content: typeof item.output === "string" ? item.output : JSON.stringify(item.output)
|
||||
});
|
||||
}
|
||||
else if (item.type === "reasoning") {
|
||||
// Skip reasoning items - they are for display only
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining
|
||||
if (currentAssistantMsg) {
|
||||
result.messages.push(currentAssistantMsg);
|
||||
}
|
||||
if (pendingToolResults.length > 0) {
|
||||
for (const tr of pendingToolResults) {
|
||||
result.messages.push(tr);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert tools format
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
result.tools = body.tools.map(tool => {
|
||||
if (tool.function) return tool;
|
||||
return {
|
||||
type: "function",
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters,
|
||||
strict: tool.strict
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Cleanup Responses API specific fields
|
||||
delete result.input;
|
||||
delete result.instructions;
|
||||
delete result.include;
|
||||
delete result.prompt_cache_key;
|
||||
delete result.store;
|
||||
delete result.reasoning;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Register
|
||||
register(FORMATS.OPENAI_RESPONSES, FORMATS.OPENAI, openaiResponsesToOpenAIRequest, null);
|
||||
|
||||
257
open-sse/translator/request/openai-to-claude.js
Normal file
257
open-sse/translator/request/openai-to-claude.js
Normal file
@@ -0,0 +1,257 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { CLAUDE_SYSTEM_PROMPT } from "../../config/constants.js";
|
||||
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";
|
||||
|
||||
// Convert OpenAI request to Claude format
|
||||
function openaiToClaudeRequest(model, body, stream) {
|
||||
// Tool name mapping for Claude OAuth (capitalizedName → originalName)
|
||||
const toolNameMap = new Map();
|
||||
const result = {
|
||||
model: model,
|
||||
max_tokens: adjustMaxTokens(body),
|
||||
stream: stream
|
||||
};
|
||||
|
||||
// Temperature
|
||||
if (body.temperature !== undefined) {
|
||||
result.temperature = body.temperature;
|
||||
}
|
||||
|
||||
// Messages
|
||||
result.messages = [];
|
||||
const systemParts = [];
|
||||
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
// Extract system messages
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "system") {
|
||||
systemParts.push(typeof msg.content === "string" ? msg.content : extractTextContent(msg.content));
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out system messages for separate processing
|
||||
const nonSystemMessages = body.messages.filter(m => m.role !== "system");
|
||||
|
||||
// Process messages with merging logic
|
||||
// CRITICAL: tool_result must be in separate message immediately after tool_use
|
||||
let currentRole = undefined;
|
||||
let currentParts = [];
|
||||
|
||||
const flushCurrentMessage = () => {
|
||||
if (currentRole && currentParts.length > 0) {
|
||||
result.messages.push({ role: currentRole, content: currentParts });
|
||||
currentParts = [];
|
||||
}
|
||||
};
|
||||
|
||||
for (const msg of nonSystemMessages) {
|
||||
const newRole = (msg.role === "user" || msg.role === "tool") ? "user" : "assistant";
|
||||
const blocks = getContentBlocksFromMessage(msg);
|
||||
const hasToolUse = blocks.some(b => b.type === "tool_use");
|
||||
const hasToolResult = blocks.some(b => b.type === "tool_result");
|
||||
|
||||
// Separate tool_result from other content
|
||||
if (hasToolResult) {
|
||||
const toolResultBlocks = blocks.filter(b => b.type === "tool_result");
|
||||
const otherBlocks = blocks.filter(b => b.type !== "tool_result");
|
||||
|
||||
flushCurrentMessage();
|
||||
|
||||
if (toolResultBlocks.length > 0) {
|
||||
result.messages.push({ role: "user", content: toolResultBlocks });
|
||||
}
|
||||
|
||||
if (otherBlocks.length > 0) {
|
||||
currentRole = newRole;
|
||||
currentParts.push(...otherBlocks);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentRole !== newRole) {
|
||||
flushCurrentMessage();
|
||||
currentRole = newRole;
|
||||
}
|
||||
|
||||
currentParts.push(...blocks);
|
||||
|
||||
if (hasToolUse) {
|
||||
flushCurrentMessage();
|
||||
}
|
||||
}
|
||||
|
||||
flushCurrentMessage();
|
||||
|
||||
// Add cache_control to last assistant message
|
||||
for (let i = result.messages.length - 1; i >= 0; i--) {
|
||||
const message = result.messages[i];
|
||||
if (message.role === "assistant" && Array.isArray(message.content) && message.content.length > 0) {
|
||||
const lastBlock = message.content[message.content.length - 1];
|
||||
if (lastBlock) {
|
||||
lastBlock.cache_control = { type: "ephemeral" };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// System with Claude Code prompt and cache_control
|
||||
const claudeCodePrompt = { type: "text", text: CLAUDE_SYSTEM_PROMPT };
|
||||
|
||||
if (systemParts.length > 0) {
|
||||
const systemText = systemParts.join("\n");
|
||||
result.system = [
|
||||
claudeCodePrompt,
|
||||
{ type: "text", text: systemText, cache_control: { type: "ephemeral", ttl: "1h" } }
|
||||
];
|
||||
} else {
|
||||
result.system = [claudeCodePrompt];
|
||||
}
|
||||
|
||||
// Tools - convert from OpenAI format to Claude format
|
||||
if (body.tools && Array.isArray(body.tools)) {
|
||||
result.tools = body.tools.map(tool => {
|
||||
const toolData = tool.type === "function" && tool.function ? tool.function : tool;
|
||||
const originalName = toolData.name;
|
||||
|
||||
// Claude requires capitalized tool names
|
||||
const toolName = originalName.charAt(0).toUpperCase() + originalName.slice(1);
|
||||
|
||||
// Store mapping for response translation
|
||||
if (toolName !== originalName) {
|
||||
toolNameMap.set(toolName, originalName);
|
||||
}
|
||||
|
||||
return {
|
||||
name: toolName,
|
||||
description: toolData.description || "",
|
||||
input_schema: toolData.parameters || toolData.input_schema || { type: "object", properties: {}, required: [] }
|
||||
};
|
||||
});
|
||||
|
||||
if (result.tools.length > 0) {
|
||||
result.tools[result.tools.length - 1].cache_control = { type: "ephemeral", ttl: "1h" };
|
||||
}
|
||||
}
|
||||
|
||||
// Tool choice
|
||||
if (body.tool_choice) {
|
||||
result.tool_choice = convertOpenAIToolChoice(body.tool_choice);
|
||||
}
|
||||
|
||||
// Attach toolNameMap to result for response translation
|
||||
if (toolNameMap.size > 0) {
|
||||
result._toolNameMap = toolNameMap;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get content blocks from single message
|
||||
function getContentBlocksFromMessage(msg) {
|
||||
const blocks = [];
|
||||
|
||||
if (msg.role === "tool") {
|
||||
blocks.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: msg.tool_call_id,
|
||||
content: msg.content
|
||||
});
|
||||
} else if (msg.role === "user") {
|
||||
if (typeof msg.content === "string") {
|
||||
if (msg.content) {
|
||||
blocks.push({ type: "text", text: msg.content });
|
||||
}
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text" && part.text) {
|
||||
blocks.push({ type: "text", text: part.text });
|
||||
} else if (part.type === "tool_result") {
|
||||
blocks.push({
|
||||
type: "tool_result",
|
||||
tool_use_id: part.tool_use_id,
|
||||
content: part.content,
|
||||
...(part.is_error && { is_error: part.is_error })
|
||||
});
|
||||
} else if (part.type === "image_url") {
|
||||
const url = part.image_url.url;
|
||||
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
||||
if (match) {
|
||||
blocks.push({
|
||||
type: "image",
|
||||
source: { type: "base64", media_type: match[1], data: match[2] }
|
||||
});
|
||||
}
|
||||
} else if (part.type === "image" && part.source) {
|
||||
blocks.push({ type: "image", source: part.source });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (msg.role === "assistant") {
|
||||
if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text" && part.text) {
|
||||
blocks.push({ type: "text", text: part.text });
|
||||
} else if (part.type === "tool_use") {
|
||||
blocks.push({ type: "tool_use", id: part.id, name: part.name, input: part.input });
|
||||
}
|
||||
}
|
||||
} else if (msg.content) {
|
||||
const text = typeof msg.content === "string" ? msg.content : extractTextContent(msg.content);
|
||||
if (text) {
|
||||
blocks.push({ type: "text", text });
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.type === "function") {
|
||||
blocks.push({
|
||||
type: "tool_use",
|
||||
id: tc.id,
|
||||
name: tc.function.name,
|
||||
input: tryParseJSON(tc.function.arguments)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// Convert OpenAI tool choice to Claude format
|
||||
function convertOpenAIToolChoice(choice) {
|
||||
if (!choice) return { type: "auto" };
|
||||
if (typeof choice === "object" && choice.type) return choice;
|
||||
if (choice === "auto" || choice === "none") return { type: "auto" };
|
||||
if (choice === "required") return { type: "any" };
|
||||
if (typeof choice === "object" && choice.function) {
|
||||
return { type: "tool", name: choice.function.name };
|
||||
}
|
||||
return { type: "auto" };
|
||||
}
|
||||
|
||||
// Extract text from content
|
||||
function extractTextContent(content) {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content.filter(c => c.type === "text").map(c => c.text).join("\n");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Try parse JSON
|
||||
function tryParseJSON(str) {
|
||||
if (typeof str !== "string") return str;
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
// Register
|
||||
register(FORMATS.OPENAI, FORMATS.CLAUDE, openaiToClaudeRequest, null);
|
||||
|
||||
247
open-sse/translator/request/openai-to-gemini.js
Normal file
247
open-sse/translator/request/openai-to-gemini.js
Normal file
@@ -0,0 +1,247 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.js";
|
||||
import {
|
||||
DEFAULT_SAFETY_SETTINGS,
|
||||
convertOpenAIContentToParts,
|
||||
extractTextContent,
|
||||
tryParseJSON,
|
||||
generateRequestId,
|
||||
generateSessionId,
|
||||
generateProjectId,
|
||||
cleanJSONSchemaForAntigravity
|
||||
} from "../helpers/geminiHelper.js";
|
||||
|
||||
// Core: Convert OpenAI request to Gemini format (base for all variants)
|
||||
function openaiToGeminiBase(model, body, stream) {
|
||||
const result = {
|
||||
model: model,
|
||||
contents: [],
|
||||
generationConfig: {},
|
||||
safetySettings: DEFAULT_SAFETY_SETTINGS
|
||||
};
|
||||
|
||||
// Generation config
|
||||
if (body.temperature !== undefined) {
|
||||
result.generationConfig.temperature = body.temperature;
|
||||
}
|
||||
if (body.top_p !== undefined) {
|
||||
result.generationConfig.topP = body.top_p;
|
||||
}
|
||||
if (body.top_k !== undefined) {
|
||||
result.generationConfig.topK = body.top_k;
|
||||
}
|
||||
if (body.max_tokens !== undefined) {
|
||||
result.generationConfig.maxOutputTokens = body.max_tokens;
|
||||
}
|
||||
|
||||
// Build tool_call_id -> name map
|
||||
const tcID2Name = {};
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "assistant" && msg.tool_calls) {
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.type === "function" && tc.id && tc.function?.name) {
|
||||
tcID2Name[tc.id] = tc.function.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build tool responses cache
|
||||
const toolResponses = {};
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
for (const msg of body.messages) {
|
||||
if (msg.role === "tool" && msg.tool_call_id) {
|
||||
toolResponses[msg.tool_call_id] = msg.content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert messages
|
||||
if (body.messages && Array.isArray(body.messages)) {
|
||||
for (let i = 0; i < body.messages.length; i++) {
|
||||
const msg = body.messages[i];
|
||||
const role = msg.role;
|
||||
const content = msg.content;
|
||||
|
||||
if (role === "system" && body.messages.length > 1) {
|
||||
result.systemInstruction = {
|
||||
role: "user",
|
||||
parts: [{ text: typeof content === "string" ? content : extractTextContent(content) }]
|
||||
};
|
||||
} else if (role === "user" || (role === "system" && body.messages.length === 1)) {
|
||||
const parts = convertOpenAIContentToParts(content);
|
||||
if (parts.length > 0) {
|
||||
result.contents.push({ role: "user", parts });
|
||||
}
|
||||
} else if (role === "assistant") {
|
||||
const parts = [];
|
||||
|
||||
if (content) {
|
||||
const text = typeof content === "string" ? content : extractTextContent(content);
|
||||
if (text) {
|
||||
parts.push({ text });
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
||||
const toolCallIds = [];
|
||||
for (const tc of msg.tool_calls) {
|
||||
if (tc.type !== "function") continue;
|
||||
|
||||
const args = tryParseJSON(tc.function?.arguments || "{}");
|
||||
parts.push({
|
||||
thoughtSignature: DEFAULT_THINKING_GEMINI_SIGNATURE,
|
||||
functionCall: {
|
||||
id: tc.id,
|
||||
name: tc.function.name,
|
||||
args: args
|
||||
}
|
||||
});
|
||||
toolCallIds.push(tc.id);
|
||||
}
|
||||
|
||||
if (parts.length > 0) {
|
||||
result.contents.push({ role: "model", parts });
|
||||
}
|
||||
|
||||
// Append function responses
|
||||
const toolParts = [];
|
||||
for (const fid of toolCallIds) {
|
||||
let name = tcID2Name[fid];
|
||||
if (!name) {
|
||||
const idParts = fid.split("-");
|
||||
if (idParts.length > 2) {
|
||||
name = idParts.slice(0, -2).join("-");
|
||||
} else {
|
||||
name = fid;
|
||||
}
|
||||
}
|
||||
|
||||
let resp = toolResponses[fid] || "{}";
|
||||
let parsedResp = tryParseJSON(resp);
|
||||
if (parsedResp === null) {
|
||||
parsedResp = { result: resp };
|
||||
} else if (typeof parsedResp !== "object") {
|
||||
parsedResp = { result: parsedResp };
|
||||
}
|
||||
|
||||
toolParts.push({
|
||||
functionResponse: {
|
||||
id: fid,
|
||||
name: name,
|
||||
response: { result: parsedResp }
|
||||
}
|
||||
});
|
||||
}
|
||||
if (toolParts.length > 0) {
|
||||
result.contents.push({ role: "user", parts: toolParts });
|
||||
}
|
||||
} else if (parts.length > 0) {
|
||||
result.contents.push({ role: "model", parts });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert tools
|
||||
if (body.tools && Array.isArray(body.tools) && body.tools.length > 0) {
|
||||
const functionDeclarations = [];
|
||||
for (const t of body.tools) {
|
||||
if (t.type === "function" && t.function) {
|
||||
const fn = t.function;
|
||||
functionDeclarations.push({
|
||||
name: fn.name,
|
||||
description: fn.description || "",
|
||||
parameters: fn.parameters || { type: "object", properties: {} }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (functionDeclarations.length > 0) {
|
||||
result.tools = [{ functionDeclarations }];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// OpenAI -> Gemini (standard API)
|
||||
function openaiToGeminiRequest(model, body, stream) {
|
||||
return openaiToGeminiBase(model, body, stream);
|
||||
}
|
||||
|
||||
// OpenAI -> Gemini CLI (Cloud Code Assist)
|
||||
function openaiToGeminiCLIRequest(model, body, stream) {
|
||||
const gemini = openaiToGeminiBase(model, body, stream);
|
||||
const isClaude = model.toLowerCase().includes("claude");
|
||||
|
||||
// Add thinking config for CLI
|
||||
if (body.reasoning_effort) {
|
||||
const budgetMap = { low: 1024, medium: 8192, high: 32768 };
|
||||
const budget = budgetMap[body.reasoning_effort] || 8192;
|
||||
gemini.generationConfig.thinkingConfig = {
|
||||
thinkingBudget: budget,
|
||||
include_thoughts: true
|
||||
};
|
||||
}
|
||||
|
||||
// Thinking config from Claude format
|
||||
if (body.thinking?.type === "enabled" && body.thinking.budget_tokens) {
|
||||
gemini.generationConfig.thinkingConfig = {
|
||||
thinkingBudget: body.thinking.budget_tokens,
|
||||
include_thoughts: true
|
||||
};
|
||||
}
|
||||
|
||||
// Clean schema for tools
|
||||
if (gemini.tools?.[0]?.functionDeclarations) {
|
||||
for (const fn of gemini.tools[0].functionDeclarations) {
|
||||
if (fn.parameters) {
|
||||
const cleanedSchema = cleanJSONSchemaForAntigravity(fn.parameters);
|
||||
if (isClaude) {
|
||||
fn.parameters = cleanedSchema;
|
||||
} else {
|
||||
fn.parametersJsonSchema = cleanedSchema;
|
||||
delete fn.parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gemini;
|
||||
}
|
||||
|
||||
// Wrap Gemini CLI format in Cloud Code wrapper
|
||||
function wrapInCloudCodeEnvelope(model, geminiCLI, credentials = null) {
|
||||
const projectId = credentials?.projectId || generateProjectId();
|
||||
|
||||
return {
|
||||
project: projectId,
|
||||
model: model,
|
||||
userAgent: "gemini-cli",
|
||||
requestId: generateRequestId(),
|
||||
request: {
|
||||
sessionId: generateSessionId(),
|
||||
contents: geminiCLI.contents,
|
||||
systemInstruction: geminiCLI.systemInstruction,
|
||||
generationConfig: geminiCLI.generationConfig,
|
||||
safetySettings: geminiCLI.safetySettings,
|
||||
tools: geminiCLI.tools,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// OpenAI -> Antigravity (Sandbox Cloud Code with wrapper)
|
||||
function openaiToAntigravityRequest(model, body, stream, credentials = null) {
|
||||
const geminiCLI = openaiToGeminiCLIRequest(model, body, stream);
|
||||
return wrapInCloudCodeEnvelope(model, geminiCLI, credentials);
|
||||
}
|
||||
|
||||
// Register
|
||||
register(FORMATS.OPENAI, FORMATS.GEMINI, openaiToGeminiRequest, null);
|
||||
register(FORMATS.OPENAI, FORMATS.GEMINI_CLI, (model, body, stream, credentials) => wrapInCloudCodeEnvelope(model, openaiToGeminiCLIRequest(model, body, stream), credentials), null);
|
||||
register(FORMATS.OPENAI, FORMATS.ANTIGRAVITY, openaiToAntigravityRequest, null);
|
||||
|
||||
Reference in New Issue
Block a user