Update jsconfig.json and package.json to correct open-sse path references from relative to local directory.
This commit is contained in:
1 parent
3857598de4
commit
e35421beb1
39 files changed
+6846
-6
No files matched your search
@@ -0,0 +1,469 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { DEFAULT_THINKING_GEMINI_SIGNATURE } from "../../config/defaultThinkingSignature.js";
|
||||
import {
|
||||
UNSUPPORTED_SCHEMA_CONSTRAINTS,
|
||||
DEFAULT_SAFETY_SETTINGS,
|
||||
convertOpenAIContentToParts,
|
||||
extractTextContent,
|
||||
tryParseJSON,
|
||||
generateRequestId,
|
||||
generateSessionId,
|
||||
generateProjectId,
|
||||
cleanJSONSchemaForAntigravity
|
||||
} from "../helpers/geminiHelper.js";
|
||||
|
||||
// ============================================
|
||||
// REQUEST TRANSLATORS: OpenAI -> Gemini/GeminiCLI/Antigravity
|
||||
// ============================================
|
||||
|
||||
// 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 - extract name from tool_call_id format "ToolName-timestamp-index"
|
||||
const toolParts = [];
|
||||
for (const fid of toolCallIds) {
|
||||
// Try to get name from tcID2Name map first, then extract from id format
|
||||
let name = tcID2Name[fid];
|
||||
if (!name) {
|
||||
// Extract name from id format: "ToolName-timestamp-index"
|
||||
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 openaiToGemini(model, body, stream) {
|
||||
return openaiToGeminiBase(model, body, stream);
|
||||
}
|
||||
|
||||
// OpenAI -> Gemini CLI (Cloud Code Assist)
|
||||
function openaiToGeminiCLI(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
|
||||
// Claude models: use "parameters" (backend converts parametersJsonSchema -> parameters)
|
||||
// Gemini native: use "parametersJsonSchema" (backend expects this field)
|
||||
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) {
|
||||
// Use real project ID if available, otherwise generate random
|
||||
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 openaiToAntigravity(model, body, stream, credentials = null) {
|
||||
const geminiCLI = openaiToGeminiCLI(model, body, stream);
|
||||
return wrapInCloudCodeEnvelope(model, geminiCLI, credentials);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// RESPONSE TRANSLATORS: Gemini/GeminiCLI/Antigravity -> OpenAI
|
||||
// ============================================
|
||||
|
||||
// Core: 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 there's text with thoughtSignature
|
||||
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
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
// Process functionCall if exists, then skip to next part
|
||||
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) - skip empty text
|
||||
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 ALL TRANSLATORS
|
||||
// ============================================
|
||||
|
||||
// Request: OpenAI -> Gemini variants
|
||||
register(FORMATS.OPENAI, FORMATS.GEMINI, openaiToGemini, null);
|
||||
register(FORMATS.OPENAI, FORMATS.GEMINI_CLI, (model, body, stream, credentials) => wrapInCloudCodeEnvelope(model, openaiToGeminiCLI(model, body, stream), credentials), null);
|
||||
register(FORMATS.OPENAI, FORMATS.ANTIGRAVITY, openaiToAntigravity, null);
|
||||
|
||||
// Response: Gemini variants -> OpenAI (all use same handler)
|
||||
register(FORMATS.GEMINI, FORMATS.OPENAI, null, geminiToOpenAIResponse);
|
||||
register(FORMATS.GEMINI_CLI, FORMATS.OPENAI, null, geminiToOpenAIResponse);
|
||||
register(FORMATS.ANTIGRAVITY, FORMATS.OPENAI, null, geminiToOpenAIResponse);
|
||||
Reference in new issue
Block a user