Files
gemini-nexus/src/lib/antigravity/JsonSchemaUtils.ts
T

183 lines
5.5 KiB
TypeScript

/**
* Recursively cleans JSON Schema to meet Gemini interface requirements
*
* 1. [New] Flatten $ref and $defs: Replace references with actual definitions to solve Gemini's lack of $ref support
* 2. Remove unsupported fields: $schema, additionalProperties, format, default, uniqueItems, validation fields
* 3. Handle Union types: ["string", "null"] -> "string"
* 4. Convert type field values to lowercase (Gemini v1internal requirement)
* 5. Remove numeric validation fields: multipleOf, exclusiveMinimum, exclusiveMaximum, etc.
*/
export function cleanJsonSchema(value: any) {
// 0. Preprocessing: Expand $ref (Schema Flattening)
if (value && typeof value === 'object' && !Array.isArray(value)) {
const defs: Record<string, any> = {};
// Extract $defs or definitions
if (value['$defs']) {
Object.assign(defs, value['$defs']);
delete value['$defs'];
}
if (value['definitions']) {
Object.assign(defs, value['definitions']);
delete value['definitions'];
}
if (Object.keys(defs).length > 0) {
// Recursively replace references
flattenRefs(value, defs);
}
}
// Recursive cleaning
cleanJsonSchemaRecursive(value);
}
/**
* Recursively expand $ref
*/
function flattenRefs(map: any, defs: Record<string, any>) {
if (!map || typeof map !== 'object') return;
// 检查并替换 $ref
if (typeof map['$ref'] === 'string') {
const refPath = map['$ref'];
// Parse reference name (e.g. #/$defs/MyType -> MyType)
const parts = refPath.split('/');
const refName = parts[parts.length - 1] || refPath;
if (defs[refName]) {
const defSchema = defs[refName];
// $ref nodes should not have other properties, remove $ref directly
delete map['$ref'];
if (defSchema && typeof defSchema === 'object') {
for (const [k, v] of Object.entries(defSchema)) {
// Only insert if the key does not exist in current map (avoid overwrite)
if (map[k] === undefined) {
// Clone deep to avoid reference issues
map[k] = JSON.parse(JSON.stringify(v));
}
}
// Recursively process $refs in the newly merged content
flattenRefs(map, defs);
}
}
}
// Recursively process all children
for (const k in map) {
if (map.hasOwnProperty(k)) {
const v = map[k];
if (typeof v === 'object' && v !== null) {
flattenRefs(v, defs);
}
}
}
}
function cleanJsonSchemaRecursive(value: any) {
if (!value || typeof value !== 'object') {
return;
}
if (Array.isArray(value)) {
// Array: Recursively process each element
for (const v of value) {
cleanJsonSchemaRecursive(v);
}
} else {
const map = value;
// 1. Recursively process all children first to ensure nested structures are cleaned
for (const k in map) {
if (map.hasOwnProperty(k)) {
cleanJsonSchemaRecursive(map[k]);
}
}
// 2. Collect and process validation fields (Migration logic: Downgrade constraints to Hints in description)
const constraints: string[] = [];
// Validation fields blacklist for migration
const validationFields = [
['pattern', 'pattern'],
['minLength', 'minLen'],
['maxLength', 'maxLen'],
['minimum', 'min'],
['maximum', 'max'],
['minItems', 'minItems'],
['maxItems', 'maxItems'],
['exclusiveMinimum', 'exclMin'],
['exclusiveMaximum', 'exclMax'],
['multipleOf', 'multipleOf'],
['format', 'format'],
];
for (const [field, label] of validationFields) {
if (map[field] !== undefined) {
const val = map[field];
// Only migrate if value is primitive type
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
constraints.push(`${label}: ${val}`);
delete map[field];
} else {
// If not expected type, leave as is (JS pass by reference, just don't delete)
}
}
}
// 3. Append constraint info to description
if (constraints.length > 0) {
const suffix = ` [Constraint: ${constraints.join(', ')}]`;
map['description'] = (map['description'] || '') + suffix;
}
// 4. Physically remove "hard" blacklist items that interfere with generation
const hardRemoveFields = [
'$schema',
'additionalProperties',
'enumCaseInsensitive',
'enumNormalizeWhitespace',
'uniqueItems',
'default',
'const',
'examples',
// Advanced logic fields common in MCP tools but unsupported by Gemini
'propertyNames',
'anyOf',
'oneOf',
'allOf',
'not',
'if',
'then',
'else',
'dependencies',
'dependentSchemas',
'dependentRequired',
'cache_control', // Fixes 400 error triggered by cache_control mentioned by user
];
for (const field of hardRemoveFields) {
delete map[field];
}
// 5. Handle type field (Gemini requires single lowercase string)
if (map['type']) {
const typeVal = map['type'];
if (typeof typeVal === 'string') {
map['type'] = typeVal.toLowerCase();
} else if (Array.isArray(typeVal)) {
// Union type downgrade: take the first non-null type
let selectedType = 'string';
for (const item of typeVal) {
if (typeof item === 'string' && item !== 'null') {
selectedType = item.toLowerCase();
break;
}
}
map['type'] = selectedType;
}
}
}
}