674 lines
23 KiB
JavaScript
674 lines
23 KiB
JavaScript
/**
|
|
* Schema Sanitizer
|
|
* Cleans and transforms JSON schemas for Gemini/Antigravity API compatibility
|
|
*
|
|
* Uses a multi-phase pipeline matching opencode-antigravity-auth approach:
|
|
* - Phase 1: Convert $refs to description hints
|
|
* - Phase 2a: Merge allOf schemas
|
|
* - Phase 2b: Flatten anyOf/oneOf (select best option)
|
|
* - Phase 2c: Flatten type arrays + update required for nullable
|
|
* - Phase 3: Remove unsupported keywords
|
|
* - Phase 4: Final cleanup (required array validation)
|
|
*/
|
|
|
|
/**
|
|
* Append a hint to a schema's description field.
|
|
* Format: "existing (hint)" or just "hint" if no existing description.
|
|
*
|
|
* @param {Object} schema - Schema object to modify
|
|
* @param {string} hint - Hint text to append
|
|
* @returns {Object} Modified schema with appended description
|
|
*/
|
|
function appendDescriptionHint(schema, hint) {
|
|
if (!schema || typeof schema !== 'object') return schema;
|
|
const result = { ...schema };
|
|
result.description = result.description
|
|
? `${result.description} (${hint})`
|
|
: hint;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Score a schema option for anyOf/oneOf selection.
|
|
* Higher scores = more preferred schemas.
|
|
*
|
|
* @param {Object} schema - Schema option to score
|
|
* @returns {number} Score (0-3)
|
|
*/
|
|
function scoreSchemaOption(schema) {
|
|
if (!schema || typeof schema !== 'object') return 0;
|
|
|
|
// Score 3: Object types with properties (most informative)
|
|
if (schema.type === 'object' || schema.properties) return 3;
|
|
|
|
// Score 2: Array types with items
|
|
if (schema.type === 'array' || schema.items) return 2;
|
|
|
|
// Score 1: Any other non-null type
|
|
if (schema.type && schema.type !== 'null') return 1;
|
|
|
|
// Score 0: Null or no type
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Convert $ref references to description hints.
|
|
* Replaces { $ref: "#/$defs/Foo" } with { type: "object", description: "See: Foo" }
|
|
*
|
|
* @param {Object} schema - Schema to process
|
|
* @returns {Object} Schema with refs converted to hints
|
|
*/
|
|
function convertRefsToHints(schema) {
|
|
if (!schema || typeof schema !== 'object') return schema;
|
|
if (Array.isArray(schema)) return schema.map(convertRefsToHints);
|
|
|
|
const result = { ...schema };
|
|
|
|
// Handle $ref at this level
|
|
if (result.$ref && typeof result.$ref === 'string') {
|
|
// Extract definition name from ref path (e.g., "#/$defs/Foo" -> "Foo")
|
|
const parts = result.$ref.split('/');
|
|
const defName = parts[parts.length - 1] || 'unknown';
|
|
const hint = `See: ${defName}`;
|
|
|
|
// Merge with existing description if present
|
|
const description = result.description
|
|
? `${result.description} (${hint})`
|
|
: hint;
|
|
|
|
// Replace with object type and hint
|
|
return { type: 'object', description };
|
|
}
|
|
|
|
// Recursively process properties
|
|
if (result.properties && typeof result.properties === 'object') {
|
|
result.properties = {};
|
|
for (const [key, value] of Object.entries(schema.properties)) {
|
|
result.properties[key] = convertRefsToHints(value);
|
|
}
|
|
}
|
|
|
|
// Recursively process items
|
|
if (result.items) {
|
|
if (Array.isArray(result.items)) {
|
|
result.items = result.items.map(convertRefsToHints);
|
|
} else if (typeof result.items === 'object') {
|
|
result.items = convertRefsToHints(result.items);
|
|
}
|
|
}
|
|
|
|
// Recursively process anyOf/oneOf/allOf
|
|
for (const key of ['anyOf', 'oneOf', 'allOf']) {
|
|
if (Array.isArray(result[key])) {
|
|
result[key] = result[key].map(convertRefsToHints);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Merge all schemas in an allOf array into a single schema.
|
|
* Properties and required arrays are merged; other fields use first occurrence.
|
|
*
|
|
* @param {Object} schema - Schema with potential allOf to merge
|
|
* @returns {Object} Schema with allOf merged
|
|
*/
|
|
function mergeAllOf(schema) {
|
|
if (!schema || typeof schema !== 'object') return schema;
|
|
if (Array.isArray(schema)) return schema.map(mergeAllOf);
|
|
|
|
let result = { ...schema };
|
|
|
|
// Process allOf if present
|
|
if (Array.isArray(result.allOf) && result.allOf.length > 0) {
|
|
const mergedProperties = {};
|
|
const mergedRequired = new Set();
|
|
const otherFields = {};
|
|
|
|
for (const subSchema of result.allOf) {
|
|
if (!subSchema || typeof subSchema !== 'object') continue;
|
|
|
|
// Merge properties (later overrides earlier)
|
|
if (subSchema.properties) {
|
|
for (const [key, value] of Object.entries(subSchema.properties)) {
|
|
mergedProperties[key] = value;
|
|
}
|
|
}
|
|
|
|
// Union required arrays
|
|
if (Array.isArray(subSchema.required)) {
|
|
for (const req of subSchema.required) {
|
|
mergedRequired.add(req);
|
|
}
|
|
}
|
|
|
|
// Copy other fields (first occurrence wins)
|
|
for (const [key, value] of Object.entries(subSchema)) {
|
|
if (key !== 'properties' && key !== 'required' && !(key in otherFields)) {
|
|
otherFields[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply merged content
|
|
delete result.allOf;
|
|
|
|
// Merge other fields first (parent takes precedence)
|
|
for (const [key, value] of Object.entries(otherFields)) {
|
|
if (!(key in result)) {
|
|
result[key] = value;
|
|
}
|
|
}
|
|
|
|
// Merge properties (allOf properties override parent for same keys)
|
|
if (Object.keys(mergedProperties).length > 0) {
|
|
result.properties = { ...mergedProperties, ...(result.properties || {}) };
|
|
}
|
|
|
|
// Merge required
|
|
if (mergedRequired.size > 0) {
|
|
const parentRequired = Array.isArray(result.required) ? result.required : [];
|
|
result.required = [...new Set([...mergedRequired, ...parentRequired])];
|
|
}
|
|
}
|
|
|
|
// Recursively process properties
|
|
if (result.properties && typeof result.properties === 'object') {
|
|
const newProps = {};
|
|
for (const [key, value] of Object.entries(result.properties)) {
|
|
newProps[key] = mergeAllOf(value);
|
|
}
|
|
result.properties = newProps;
|
|
}
|
|
|
|
// Recursively process items
|
|
if (result.items) {
|
|
if (Array.isArray(result.items)) {
|
|
result.items = result.items.map(mergeAllOf);
|
|
} else if (typeof result.items === 'object') {
|
|
result.items = mergeAllOf(result.items);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Flatten anyOf/oneOf by selecting the best option based on scoring.
|
|
* Adds type hints to description when multiple types existed.
|
|
*
|
|
* @param {Object} schema - Schema with potential anyOf/oneOf
|
|
* @returns {Object} Flattened schema
|
|
*/
|
|
function flattenAnyOfOneOf(schema) {
|
|
if (!schema || typeof schema !== 'object') return schema;
|
|
if (Array.isArray(schema)) return schema.map(flattenAnyOfOneOf);
|
|
|
|
let result = { ...schema };
|
|
|
|
// Handle anyOf or oneOf
|
|
for (const unionKey of ['anyOf', 'oneOf']) {
|
|
if (Array.isArray(result[unionKey]) && result[unionKey].length > 0) {
|
|
const options = result[unionKey];
|
|
|
|
// Collect type names for hint
|
|
const typeNames = [];
|
|
let bestOption = null;
|
|
let bestScore = -1;
|
|
|
|
for (const option of options) {
|
|
if (!option || typeof option !== 'object') continue;
|
|
|
|
// Collect type name
|
|
const typeName = option.type || (option.properties ? 'object' : null);
|
|
if (typeName && typeName !== 'null') {
|
|
typeNames.push(typeName);
|
|
}
|
|
|
|
// Score and track best option
|
|
const score = scoreSchemaOption(option);
|
|
if (score > bestScore) {
|
|
bestScore = score;
|
|
bestOption = option;
|
|
}
|
|
}
|
|
|
|
// Remove the union key
|
|
delete result[unionKey];
|
|
|
|
// Merge best option into result
|
|
if (bestOption) {
|
|
// Preserve parent description
|
|
const parentDescription = result.description;
|
|
|
|
// Recursively flatten the best option
|
|
const flattenedOption = flattenAnyOfOneOf(bestOption);
|
|
|
|
// Merge fields from selected option
|
|
for (const [key, value] of Object.entries(flattenedOption)) {
|
|
if (key === 'description') {
|
|
// Merge descriptions if different
|
|
if (value && value !== parentDescription) {
|
|
result.description = parentDescription
|
|
? `${parentDescription} (${value})`
|
|
: value;
|
|
}
|
|
} else if (!(key in result) || key === 'type' || key === 'properties' || key === 'items') {
|
|
result[key] = value;
|
|
}
|
|
}
|
|
|
|
// Add type hint if multiple types existed
|
|
if (typeNames.length > 1) {
|
|
const uniqueTypes = [...new Set(typeNames)];
|
|
result = appendDescriptionHint(result, `Accepts: ${uniqueTypes.join(' | ')}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recursively process properties
|
|
if (result.properties && typeof result.properties === 'object') {
|
|
const newProps = {};
|
|
for (const [key, value] of Object.entries(result.properties)) {
|
|
newProps[key] = flattenAnyOfOneOf(value);
|
|
}
|
|
result.properties = newProps;
|
|
}
|
|
|
|
// Recursively process items
|
|
if (result.items) {
|
|
if (Array.isArray(result.items)) {
|
|
result.items = result.items.map(flattenAnyOfOneOf);
|
|
} else if (typeof result.items === 'object') {
|
|
result.items = flattenAnyOfOneOf(result.items);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Enhanced Schema Hints (for preserving semantic information)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Add hints for enum values (if ≤10 values).
|
|
* This preserves enum information in the description since Gemini
|
|
* may not fully support enums in all cases.
|
|
*
|
|
* @param {Object} schema - Schema to process
|
|
* @returns {Object} Schema with enum hints added to description
|
|
*/
|
|
function addEnumHints(schema) {
|
|
if (!schema || typeof schema !== 'object') return schema;
|
|
if (Array.isArray(schema)) return schema.map(addEnumHints);
|
|
|
|
let result = { ...schema };
|
|
|
|
// Add enum hint if present and reasonable size
|
|
if (Array.isArray(result.enum) && result.enum.length > 1 && result.enum.length <= 10) {
|
|
const vals = result.enum.map(v => String(v)).join(', ');
|
|
result = appendDescriptionHint(result, `Allowed: ${vals}`);
|
|
}
|
|
|
|
// Recursively process properties
|
|
if (result.properties && typeof result.properties === 'object') {
|
|
const newProps = {};
|
|
for (const [key, value] of Object.entries(result.properties)) {
|
|
newProps[key] = addEnumHints(value);
|
|
}
|
|
result.properties = newProps;
|
|
}
|
|
|
|
// Recursively process items
|
|
if (result.items) {
|
|
result.items = Array.isArray(result.items)
|
|
? result.items.map(addEnumHints)
|
|
: addEnumHints(result.items);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Add hints for additionalProperties: false.
|
|
* This informs the model that extra properties are not allowed.
|
|
*
|
|
* @param {Object} schema - Schema to process
|
|
* @returns {Object} Schema with additionalProperties hints added
|
|
*/
|
|
function addAdditionalPropertiesHints(schema) {
|
|
if (!schema || typeof schema !== 'object') return schema;
|
|
if (Array.isArray(schema)) return schema.map(addAdditionalPropertiesHints);
|
|
|
|
let result = { ...schema };
|
|
|
|
if (result.additionalProperties === false) {
|
|
result = appendDescriptionHint(result, 'No extra properties allowed');
|
|
}
|
|
|
|
// Recursively process properties
|
|
if (result.properties && typeof result.properties === 'object') {
|
|
const newProps = {};
|
|
for (const [key, value] of Object.entries(result.properties)) {
|
|
newProps[key] = addAdditionalPropertiesHints(value);
|
|
}
|
|
result.properties = newProps;
|
|
}
|
|
|
|
// Recursively process items
|
|
if (result.items) {
|
|
result.items = Array.isArray(result.items)
|
|
? result.items.map(addAdditionalPropertiesHints)
|
|
: addAdditionalPropertiesHints(result.items);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Move unsupported constraints to description hints.
|
|
* This preserves constraint information that would otherwise be lost
|
|
* when we strip unsupported keywords.
|
|
*
|
|
* @param {Object} schema - Schema to process
|
|
* @returns {Object} Schema with constraint hints added to description
|
|
*/
|
|
function moveConstraintsToDescription(schema) {
|
|
if (!schema || typeof schema !== 'object') return schema;
|
|
if (Array.isArray(schema)) return schema.map(moveConstraintsToDescription);
|
|
|
|
const CONSTRAINTS = ['minLength', 'maxLength', 'pattern', 'minimum', 'maximum',
|
|
'minItems', 'maxItems', 'format'];
|
|
|
|
let result = { ...schema };
|
|
|
|
for (const constraint of CONSTRAINTS) {
|
|
if (result[constraint] !== undefined && typeof result[constraint] !== 'object') {
|
|
result = appendDescriptionHint(result, `${constraint}: ${result[constraint]}`);
|
|
}
|
|
}
|
|
|
|
// Recursively process properties
|
|
if (result.properties && typeof result.properties === 'object') {
|
|
const newProps = {};
|
|
for (const [key, value] of Object.entries(result.properties)) {
|
|
newProps[key] = moveConstraintsToDescription(value);
|
|
}
|
|
result.properties = newProps;
|
|
}
|
|
|
|
// Recursively process items
|
|
if (result.items) {
|
|
result.items = Array.isArray(result.items)
|
|
? result.items.map(moveConstraintsToDescription)
|
|
: moveConstraintsToDescription(result.items);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Flatten array type fields and track nullable properties.
|
|
* Converts { type: ["string", "null"] } to { type: "string" } with nullable hint.
|
|
*
|
|
* @param {Object} schema - Schema to process
|
|
* @param {Set<string>} nullableProps - Set to collect nullable property names (mutated)
|
|
* @param {string} currentPropName - Current property name (for tracking)
|
|
* @returns {Object} Flattened schema
|
|
*/
|
|
function flattenTypeArrays(schema, nullableProps = null, currentPropName = null) {
|
|
if (!schema || typeof schema !== 'object') return schema;
|
|
if (Array.isArray(schema)) return schema.map(s => flattenTypeArrays(s, nullableProps));
|
|
|
|
let result = { ...schema };
|
|
|
|
// Handle array type fields
|
|
if (Array.isArray(result.type)) {
|
|
const types = result.type;
|
|
const hasNull = types.includes('null');
|
|
const nonNullTypes = types.filter(t => t !== 'null' && t);
|
|
|
|
// Select first non-null type, or 'string' as fallback
|
|
const firstType = nonNullTypes.length > 0 ? nonNullTypes[0] : 'string';
|
|
result.type = firstType;
|
|
|
|
// Add hint for multiple types
|
|
if (nonNullTypes.length > 1) {
|
|
result = appendDescriptionHint(result, `Accepts: ${nonNullTypes.join(' | ')}`);
|
|
}
|
|
|
|
// Track nullable and add hint
|
|
if (hasNull) {
|
|
result = appendDescriptionHint(result, 'nullable');
|
|
// Track this property as nullable for required array update
|
|
if (nullableProps && currentPropName) {
|
|
nullableProps.add(currentPropName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recursively process properties, tracking nullable ones
|
|
if (result.properties && typeof result.properties === 'object') {
|
|
const childNullableProps = new Set();
|
|
const newProps = {};
|
|
|
|
for (const [key, value] of Object.entries(result.properties)) {
|
|
newProps[key] = flattenTypeArrays(value, childNullableProps, key);
|
|
}
|
|
result.properties = newProps;
|
|
|
|
// Remove nullable properties from required array
|
|
if (Array.isArray(result.required) && childNullableProps.size > 0) {
|
|
result.required = result.required.filter(prop => !childNullableProps.has(prop));
|
|
if (result.required.length === 0) {
|
|
delete result.required;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recursively process items
|
|
if (result.items) {
|
|
if (Array.isArray(result.items)) {
|
|
result.items = result.items.map(item => flattenTypeArrays(item, nullableProps));
|
|
} else if (typeof result.items === 'object') {
|
|
result.items = flattenTypeArrays(result.items, nullableProps);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Sanitize JSON Schema for Antigravity API compatibility.
|
|
* Uses allowlist approach - only permit known-safe JSON Schema features.
|
|
* Converts "const" to equivalent "enum" for compatibility.
|
|
* Generates placeholder schema for empty tool schemas.
|
|
*/
|
|
export function sanitizeSchema(schema) {
|
|
if (!schema || typeof schema !== 'object') {
|
|
// Empty/missing schema - generate placeholder with reason property
|
|
return {
|
|
type: 'object',
|
|
properties: {
|
|
reason: {
|
|
type: 'string',
|
|
description: 'Reason for calling this tool'
|
|
}
|
|
},
|
|
required: ['reason']
|
|
};
|
|
}
|
|
|
|
// Allowlist of permitted JSON Schema fields
|
|
const ALLOWED_FIELDS = new Set([
|
|
'type',
|
|
'description',
|
|
'properties',
|
|
'required',
|
|
'items',
|
|
'enum',
|
|
'title'
|
|
]);
|
|
|
|
const sanitized = {};
|
|
|
|
for (const [key, value] of Object.entries(schema)) {
|
|
// Convert "const" to "enum" for compatibility
|
|
if (key === 'const') {
|
|
sanitized.enum = [value];
|
|
continue;
|
|
}
|
|
|
|
// Skip fields not in allowlist
|
|
if (!ALLOWED_FIELDS.has(key)) {
|
|
continue;
|
|
}
|
|
|
|
if (key === 'properties' && value && typeof value === 'object') {
|
|
sanitized.properties = {};
|
|
for (const [propKey, propValue] of Object.entries(value)) {
|
|
sanitized.properties[propKey] = sanitizeSchema(propValue);
|
|
}
|
|
} else if (key === 'items' && value && typeof value === 'object') {
|
|
if (Array.isArray(value)) {
|
|
sanitized.items = value.map(item => sanitizeSchema(item));
|
|
} else {
|
|
sanitized.items = sanitizeSchema(value);
|
|
}
|
|
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
sanitized[key] = sanitizeSchema(value);
|
|
} else {
|
|
sanitized[key] = value;
|
|
}
|
|
}
|
|
|
|
// Ensure we have at least a type
|
|
if (!sanitized.type) {
|
|
sanitized.type = 'object';
|
|
}
|
|
|
|
// If object type with no properties, add placeholder
|
|
if (sanitized.type === 'object' && (!sanitized.properties || Object.keys(sanitized.properties).length === 0)) {
|
|
sanitized.properties = {
|
|
reason: {
|
|
type: 'string',
|
|
description: 'Reason for calling this tool'
|
|
}
|
|
};
|
|
sanitized.required = ['reason'];
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Convert JSON Schema type names to Google's Protobuf-style uppercase type names.
|
|
* Google's Generative AI API expects uppercase types: STRING, OBJECT, ARRAY, etc.
|
|
*
|
|
* @param {string} type - JSON Schema type name (lowercase)
|
|
* @returns {string} Google-format type name (uppercase)
|
|
*/
|
|
function toGoogleType(type) {
|
|
if (!type || typeof type !== 'string') return type;
|
|
const typeMap = {
|
|
'string': 'STRING',
|
|
'number': 'NUMBER',
|
|
'integer': 'INTEGER',
|
|
'boolean': 'BOOLEAN',
|
|
'array': 'ARRAY',
|
|
'object': 'OBJECT',
|
|
'null': 'STRING' // Fallback for null type
|
|
};
|
|
return typeMap[type.toLowerCase()] || type.toUpperCase();
|
|
}
|
|
|
|
/**
|
|
* Cleans JSON schema for Gemini API compatibility.
|
|
* Uses a multi-phase pipeline matching opencode-antigravity-auth approach.
|
|
*
|
|
* @param {Object} schema - The JSON schema to clean
|
|
* @returns {Object} Cleaned schema safe for Gemini API
|
|
*/
|
|
export function cleanSchema(schema) {
|
|
if (!schema || typeof schema !== 'object') return schema;
|
|
if (Array.isArray(schema)) return schema.map(cleanSchema);
|
|
|
|
// Phase 1: Convert $refs to hints
|
|
let result = convertRefsToHints(schema);
|
|
|
|
// Phase 1b: Add enum hints (preserves enum info in description)
|
|
result = addEnumHints(result);
|
|
|
|
// Phase 1c: Add additionalProperties hints
|
|
result = addAdditionalPropertiesHints(result);
|
|
|
|
// Phase 1d: Move constraints to description (before they get stripped)
|
|
result = moveConstraintsToDescription(result);
|
|
|
|
// Phase 2a: Merge allOf schemas
|
|
result = mergeAllOf(result);
|
|
|
|
// Phase 2b: Flatten anyOf/oneOf
|
|
result = flattenAnyOfOneOf(result);
|
|
|
|
// Phase 2c: Flatten type arrays and update required for nullable
|
|
result = flattenTypeArrays(result);
|
|
|
|
// Phase 3: Remove unsupported keywords
|
|
const unsupported = [
|
|
'additionalProperties', 'default', '$schema', '$defs',
|
|
'definitions', '$ref', '$id', '$comment', 'title',
|
|
'minLength', 'maxLength', 'pattern', 'format',
|
|
'minItems', 'maxItems', 'examples', 'allOf', 'anyOf', 'oneOf'
|
|
];
|
|
|
|
for (const key of unsupported) {
|
|
delete result[key];
|
|
}
|
|
|
|
// Check for unsupported 'format' in string types
|
|
if (result.type === 'string' && result.format) {
|
|
const allowed = ['enum', 'date-time'];
|
|
if (!allowed.includes(result.format)) {
|
|
delete result.format;
|
|
}
|
|
}
|
|
|
|
// Phase 4: Final cleanup - recursively clean nested schemas and validate required
|
|
if (result.properties && typeof result.properties === 'object') {
|
|
const newProps = {};
|
|
for (const [key, value] of Object.entries(result.properties)) {
|
|
newProps[key] = cleanSchema(value);
|
|
}
|
|
result.properties = newProps;
|
|
}
|
|
|
|
if (result.items) {
|
|
if (Array.isArray(result.items)) {
|
|
result.items = result.items.map(cleanSchema);
|
|
} else if (typeof result.items === 'object') {
|
|
result.items = cleanSchema(result.items);
|
|
}
|
|
}
|
|
|
|
// Validate that required array only contains properties that exist
|
|
if (result.required && Array.isArray(result.required) && result.properties) {
|
|
const definedProps = new Set(Object.keys(result.properties));
|
|
result.required = result.required.filter(prop => definedProps.has(prop));
|
|
if (result.required.length === 0) {
|
|
delete result.required;
|
|
}
|
|
}
|
|
|
|
// Phase 5: Convert type to Google's uppercase format (STRING, OBJECT, ARRAY, etc.)
|
|
// Only convert at current level - nested types already converted by recursive cleanSchema calls
|
|
if (result.type && typeof result.type === 'string') {
|
|
result.type = toGoogleType(result.type);
|
|
}
|
|
|
|
return result;
|
|
}
|