9router/open-sse/translator/helpers/geminiHelper.js
2026-01-13 17:19:41 +07:00

178 lines
6 KiB
JavaScript

// Gemini helper functions for translator
// Unsupported JSON Schema constraints that should be removed for Antigravity
// Reference: CLIProxyAPI/internal/util/gemini_schema.go (removeUnsupportedKeywords)
export const UNSUPPORTED_SCHEMA_CONSTRAINTS = [
// Basic constraints (not supported by Gemini API)
"minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum",
"pattern", "minItems", "maxItems", "format",
// Claude rejects these in VALIDATED mode
"default", "examples",
// JSON Schema meta keywords
"$schema", "$defs", "definitions", "const", "$ref",
// Object validation keywords (not supported)
"additionalProperties", "propertyNames", "patternProperties",
// Complex schema keywords (handled by flattenAnyOfOneOf/mergeAllOf)
"anyOf", "oneOf", "allOf", "not",
// Dependency keywords (not supported)
"dependencies", "dependentSchemas", "dependentRequired",
// Other unsupported keywords
"title", "if", "then", "else", "contentMediaType", "contentEncoding"
];
// Default safety settings
export const DEFAULT_SAFETY_SETTINGS = [
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "OFF" },
{ category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "OFF" },
{ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "OFF" },
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "OFF" },
{ category: "HARM_CATEGORY_CIVIC_INTEGRITY", threshold: "OFF" }
];
// Convert OpenAI content to Gemini parts
export function convertOpenAIContentToParts(content) {
const parts = [];
if (typeof content === "string") {
parts.push({ text: content });
} else if (Array.isArray(content)) {
for (const item of content) {
if (item.type === "text") {
parts.push({ text: item.text });
} else if (item.type === "image_url" && item.image_url?.url?.startsWith("data:")) {
const url = item.image_url.url;
const commaIndex = url.indexOf(",");
if (commaIndex !== -1) {
const mimePart = url.substring(5, commaIndex); // skip "data:"
const data = url.substring(commaIndex + 1);
const mimeType = mimePart.split(";")[0];
parts.push({
inlineData: { mime_type: mimeType, data: data }
});
}
}
}
}
return parts;
}
// Extract text content from OpenAI content
export 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("");
}
return "";
}
// Try parse JSON safely
export function tryParseJSON(str) {
if (typeof str !== "string") return str;
try {
return JSON.parse(str);
} catch {
return null;
}
}
// Generate request ID
export function generateRequestId() {
return `agent-${crypto.randomUUID()}`;
}
// Generate session ID
export function generateSessionId() {
return `-${Math.floor(Math.random() * 9000000000000000000)}`;
}
// Generate project ID
export function generateProjectId() {
const adjectives = ["useful", "bright", "swift", "calm", "bold"];
const nouns = ["fuze", "wave", "spark", "flow", "core"];
const adj = adjectives[Math.floor(Math.random() * adjectives.length)];
const noun = nouns[Math.floor(Math.random() * nouns.length)];
return `${adj}-${noun}-${crypto.randomUUID().slice(0, 5)}`;
}
// Clean JSON Schema for Antigravity API compatibility - removes unsupported keywords recursively
export function cleanJSONSchemaForAntigravity(schema) {
if (!schema || typeof schema !== "object") return schema;
// Handle anyOf/oneOf - extract the first non-null schema
if (schema.anyOf && Array.isArray(schema.anyOf)) {
const nonNullSchema = schema.anyOf.find(s => s.type !== "null" && s.type !== null);
if (nonNullSchema) {
const baseSchema = { ...nonNullSchema };
// Copy other properties from parent schema (except unsupported ones)
for (const [key, value] of Object.entries(schema)) {
if (!UNSUPPORTED_SCHEMA_CONSTRAINTS.includes(key)) {
baseSchema[key] = value;
}
}
return cleanJSONSchemaForAntigravity(baseSchema);
}
}
if (schema.oneOf && Array.isArray(schema.oneOf)) {
const nonNullSchema = schema.oneOf.find(s => s.type !== "null" && s.type !== null);
if (nonNullSchema) {
const baseSchema = { ...nonNullSchema };
// Copy other properties from parent schema (except unsupported ones)
for (const [key, value] of Object.entries(schema)) {
if (!UNSUPPORTED_SCHEMA_CONSTRAINTS.includes(key)) {
baseSchema[key] = value;
}
}
return cleanJSONSchemaForAntigravity(baseSchema);
}
}
const cleaned = Array.isArray(schema) ? [] : {};
for (const [key, value] of Object.entries(schema)) {
if (UNSUPPORTED_SCHEMA_CONSTRAINTS.includes(key)) continue;
// Handle type array like ["string", "null"] - Gemini only supports single type
if (key === "type" && Array.isArray(value)) {
const nonNullType = value.find(t => t !== "null") || "string";
cleaned[key] = nonNullType;
continue;
}
if (value && typeof value === "object") {
cleaned[key] = cleanJSONSchemaForAntigravity(value);
} else {
cleaned[key] = value;
}
}
// Cleanup required fields - only keep fields that exist in properties
if (cleaned.required && Array.isArray(cleaned.required) && cleaned.properties) {
const validRequired = cleaned.required.filter(field =>
Object.prototype.hasOwnProperty.call(cleaned.properties, field)
);
if (validRequired.length === 0) {
delete cleaned.required;
} else {
cleaned.required = validRequired;
}
}
// Add placeholder for empty object schemas (Antigravity requirement)
if (cleaned.type === "object") {
if (!cleaned.properties || Object.keys(cleaned.properties).length === 0) {
cleaned.properties = {
reason: {
type: "string",
description: "Brief explanation of why you are calling this tool"
}
};
cleaned.required = ["reason"];
}
}
return cleaned;
}