From 505d6d0f111b11e4e231742d65d2a3bbfb73864a Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 7 Feb 2026 06:17:26 -0500 Subject: [PATCH 1/2] fix(cliproxy): strip Gemini-unsupported schema fields including "examples" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace permissive JSON Schema Draft-07 whitelist with strict Gemini-compatible field set (22 fields). The "examples" field in Claude Code tool schemas caused 400 errors from Gemini API. Also strips other unsupported fields: $ref, $defs, oneOf, allOf, additionalProperties, const, if/then/else, etc. Safe change — sanitizer only runs for CLIProxy profiles (Gemini, Codex, Antigravity), never for direct Anthropic API requests. Closes #155 --- src/cliproxy/schema-sanitizer.ts | 198 +++-------- tests/unit/cliproxy/schema-sanitizer.test.ts | 329 ++++++++++++------- 2 files changed, 264 insertions(+), 263 deletions(-) diff --git a/src/cliproxy/schema-sanitizer.ts b/src/cliproxy/schema-sanitizer.ts index a63c2d6f..3c1203ac 100644 --- a/src/cliproxy/schema-sanitizer.ts +++ b/src/cliproxy/schema-sanitizer.ts @@ -1,78 +1,60 @@ /** * Schema Sanitizer * - * Sanitizes MCP tool input_schema to remove non-standard JSON Schema properties - * that Gemini/Vertex APIs reject. + * Sanitizes tool input_schema to only include fields supported by Google's + * Gemini/Vertex AI function_declarations Schema object. * - * MCP servers (especially design tools) include UI-specific metadata in schemas: - * - cornerRadius, fillColor, fontFamily, fontSize, fontWeight, gap, padding, etc. + * This sanitizer runs exclusively in the CLIProxy execution path (Gemini, Codex, + * Antigravity, etc.), never for direct Anthropic API requests. * - * These are valid as MCP hints but invalid for strict JSON Schema validation. + * Gemini supports a subset of OpenAPI 3.0 schema — fields outside this subset + * (like "examples", "$ref", "oneOf", etc.) cause 400 errors. + * + * Reference: https://ai.google.dev/api/rest/v1beta/cachedContents#Schema */ -/** Valid JSON Schema Draft-07 keywords (used by Anthropic/Gemini APIs) */ -const VALID_JSON_SCHEMA_KEYWORDS = new Set([ +/** + * Fields supported by Gemini's function_declarations Schema object. + * Source: https://ai.google.dev/api/rest/v1beta/cachedContents#Schema + */ +const GEMINI_SUPPORTED_SCHEMA_FIELDS = new Set([ // Core 'type', + 'format', + 'title', + 'description', + 'nullable', + 'example', + 'default', + + // Enum + 'enum', + + // Object 'properties', 'required', + 'minProperties', + 'maxProperties', + + // Array 'items', - 'enum', - 'const', - 'default', + 'minItems', + 'maxItems', // String validation 'minLength', 'maxLength', 'pattern', - 'format', // Number validation 'minimum', 'maximum', - 'exclusiveMinimum', - 'exclusiveMaximum', - 'multipleOf', - // Array validation - 'minItems', - 'maxItems', - 'uniqueItems', - 'contains', - 'additionalItems', - - // Object validation - 'additionalProperties', - 'patternProperties', - 'minProperties', - 'maxProperties', - 'propertyNames', - 'dependencies', - - // Composition - 'oneOf', + // Composition (only anyOf supported, NOT oneOf/allOf/not) 'anyOf', - 'allOf', - 'not', - 'if', - 'then', - 'else', - // Metadata (allowed by most APIs) - 'title', - 'description', - '$id', - '$schema', - '$ref', - '$defs', - 'definitions', - '$comment', - 'examples', - 'readOnly', - 'writeOnly', - 'deprecated', - 'contentMediaType', - 'contentEncoding', + // Gemini-specific (non-standard OpenAPI) + 'propertyOrdering', ]); /** Maximum recursion depth to prevent stack overflow */ @@ -88,15 +70,15 @@ export interface SchemaSanitizationResult { } /** - * Check if a key is a valid JSON Schema keyword. + * Check if a key is supported by Gemini's Schema object. */ function isValidSchemaKey(key: string): boolean { - return VALID_JSON_SCHEMA_KEYWORDS.has(key); + return GEMINI_SUPPORTED_SCHEMA_FIELDS.has(key); } /** * Recursively sanitize a JSON Schema object. - * Removes non-standard properties while preserving valid schema structure. + * Removes fields unsupported by Gemini while preserving valid schema structure. * * @param schema The schema object to sanitize * @param path Current path for logging (e.g., "properties.foo.items") @@ -167,115 +149,27 @@ function sanitizeSchemaRecursive( continue; } - if (key === 'additionalProperties' && typeof value === 'object') { - // Can be boolean or schema object - result[key] = sanitizeSchemaRecursive(value, keyPath, removedPaths, visited, depth + 1); - continue; - } - - if (key === 'additionalItems' && typeof value === 'object' && value !== null) { - // Can be boolean or schema object (tuple validation) - result[key] = sanitizeSchemaRecursive(value, keyPath, removedPaths, visited, depth + 1); - continue; - } - - // Composition keywords contain schema arrays - if (['oneOf', 'anyOf', 'allOf'].includes(key) && Array.isArray(value)) { + // anyOf is the only composition keyword Gemini supports + if (key === 'anyOf' && Array.isArray(value)) { result[key] = value.map((item, index) => sanitizeSchemaRecursive(item, `${keyPath}[${index}]`, removedPaths, visited, depth + 1) ); continue; } - if (key === 'not' && typeof value === 'object') { - result[key] = sanitizeSchemaRecursive(value, keyPath, removedPaths, visited, depth + 1); - continue; - } - - if (key === 'propertyNames' && typeof value === 'object') { - result[key] = sanitizeSchemaRecursive(value, keyPath, removedPaths, visited, depth + 1); - continue; - } - - // Conditional keywords - if (['if', 'then', 'else'].includes(key) && typeof value === 'object') { - result[key] = sanitizeSchemaRecursive(value, keyPath, removedPaths, visited, depth + 1); - continue; - } - - if (key === 'contains' && typeof value === 'object') { - result[key] = sanitizeSchemaRecursive(value, keyPath, removedPaths, visited, depth + 1); - continue; - } - - if (key === '$defs' || key === 'definitions') { - // Definition containers - if (typeof value === 'object' && value !== null) { - const sanitizedDefs: Record = {}; - for (const [defName, defSchema] of Object.entries(value as Record)) { - sanitizedDefs[defName] = sanitizeSchemaRecursive( - defSchema, - `${keyPath}.${defName}`, - removedPaths, - visited, - depth + 1 - ); - } - result[key] = sanitizedDefs; - continue; - } - } - - if (key === 'patternProperties') { - // Pattern property containers - preserve pattern keys and sanitize schema values - if (typeof value === 'object' && value !== null) { - const sanitizedPatterns: Record = {}; - for (const [pattern, patternSchema] of Object.entries(value as Record)) { - sanitizedPatterns[pattern] = sanitizeSchemaRecursive( - patternSchema, - `${keyPath}.${pattern}`, - removedPaths, - visited, - depth + 1 - ); - } - result[key] = sanitizedPatterns; - continue; - } - } - - if (key === 'dependencies') { - if (typeof value === 'object' && value !== null) { - const sanitizedDeps: Record = {}; - for (const [depName, depValue] of Object.entries(value as Record)) { - // Schema dependencies need recursion, property dependencies (arrays) pass through - if (typeof depValue === 'object' && depValue !== null && !Array.isArray(depValue)) { - sanitizedDeps[depName] = sanitizeSchemaRecursive( - depValue, - `${keyPath}.${depName}`, - removedPaths, - visited, - depth + 1 - ); - } else { - sanitizedDeps[depName] = depValue; - } - } - result[key] = sanitizedDeps; - continue; - } - } - - // Check if this is a valid JSON Schema keyword + // Check if this is a Gemini-supported field if (isValidSchemaKey(key)) { - // Recurse for nested objects that might contain schemas - if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + // "example" and "default" hold arbitrary values, not nested schemas — pass through as-is + if (key === 'example' || key === 'default') { + result[key] = value; + } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + // Recurse for nested objects that might contain schemas result[key] = sanitizeSchemaRecursive(value, keyPath, removedPaths, visited, depth + 1); } else { result[key] = value; } } else { - // Non-standard property - remove it + // Unsupported field — remove it removedPaths.push(keyPath); } } @@ -284,7 +178,7 @@ function sanitizeSchemaRecursive( } /** - * Sanitize an input_schema object, removing non-standard JSON Schema properties. + * Sanitize an input_schema object, removing fields unsupported by Gemini. * * @param inputSchema The tool's input_schema object * @returns Sanitization result with cleaned schema and metadata diff --git a/tests/unit/cliproxy/schema-sanitizer.test.ts b/tests/unit/cliproxy/schema-sanitizer.test.ts index c9a5cabb..8138ef5b 100644 --- a/tests/unit/cliproxy/schema-sanitizer.test.ts +++ b/tests/unit/cliproxy/schema-sanitizer.test.ts @@ -1,14 +1,15 @@ /** * Schema Sanitizer Unit Tests * - * Tests for MCP tool input_schema sanitization. + * Tests for Gemini-compatible tool input_schema sanitization. + * Verifies that only Gemini-supported fields are preserved. */ import { describe, expect, test } from 'bun:test'; import { sanitizeInputSchema, sanitizeToolSchemas } from '../../../dist/cliproxy/schema-sanitizer.js'; describe('sanitizeInputSchema', () => { - test('preserves valid JSON Schema properties', () => { + test('preserves Gemini-supported properties', () => { const schema = { type: 'object', properties: { @@ -110,10 +111,10 @@ describe('sanitizeInputSchema', () => { }); }); - test('handles oneOf/anyOf/allOf with nested schemas', () => { + test('preserves anyOf and sanitizes nested schemas', () => { const schema = { type: 'object', - oneOf: [ + anyOf: [ { type: 'string', customProp: 'remove' }, { type: 'number', anotherCustom: 123 }, ], @@ -124,10 +125,25 @@ describe('sanitizeInputSchema', () => { expect(result.removedCount).toBe(2); expect(result.schema).toEqual({ type: 'object', - oneOf: [{ type: 'string' }, { type: 'number' }], + anyOf: [{ type: 'string' }, { type: 'number' }], }); }); + test('removes oneOf and allOf (not supported by Gemini)', () => { + const schema = { + type: 'object', + oneOf: [{ type: 'string' }, { type: 'number' }], + allOf: [{ required: ['name'] }], + }; + + const result = sanitizeInputSchema(schema); + + expect(result.removedCount).toBe(2); + expect(result.removedPaths).toContain('oneOf'); + expect(result.removedPaths).toContain('allOf'); + expect(result.schema).toEqual({ type: 'object' }); + }); + test('handles deeply nested structures', () => { const schema = { type: 'object', @@ -153,51 +169,52 @@ describe('sanitizeInputSchema', () => { expect(result.removedPaths).toContain('properties.level1.properties.level2.uiHint'); }); - test('preserves $defs and definitions', () => { + test('strips "examples" field (issue #155)', () => { const schema = { type: 'object', - $defs: { - address: { - type: 'object', - customUI: 'remove', - properties: { - street: { type: 'string' }, - }, - }, - }, properties: { - home: { $ref: '#/$defs/address' }, + command: { + type: 'string', + description: 'The command to execute', + examples: ['ls -la', 'git status'], + }, + timeout: { + type: 'number', + description: 'Timeout in ms', + examples: [5000, 10000], + }, }, }; const result = sanitizeInputSchema(schema); - expect(result.removedCount).toBe(1); - expect(result.removedPaths).toContain('$defs.address.customUI'); + expect(result.removedCount).toBe(2); + expect(result.removedPaths).toContain('properties.command.examples'); + expect(result.removedPaths).toContain('properties.timeout.examples'); expect(result.schema).toEqual({ type: 'object', - $defs: { - address: { - type: 'object', - properties: { - street: { type: 'string' }, - }, - }, - }, properties: { - home: { $ref: '#/$defs/address' }, + command: { type: 'string', description: 'The command to execute' }, + timeout: { type: 'number', description: 'Timeout in ms' }, }, }); }); - test('handles empty schema', () => { - const result = sanitizeInputSchema({}); + test('keeps "example" (singular) but strips "examples" (plural)', () => { + const schema = { + type: 'string', + example: 'hello', + examples: ['hello', 'world'], + }; - expect(result.removedCount).toBe(0); - expect(result.schema).toEqual({}); + const result = sanitizeInputSchema(schema); + + expect(result.removedCount).toBe(1); + expect(result.removedPaths).toContain('examples'); + expect(result.schema).toEqual({ type: 'string', example: 'hello' }); }); - test('preserves all standard metadata keywords', () => { + test('strips Gemini-unsupported JSON Schema metadata fields', () => { const schema = { $id: 'https://example.com/schema', $schema: 'https://json-schema.org/draft-07/schema#', @@ -212,95 +229,121 @@ describe('sanitizeInputSchema', () => { const result = sanitizeInputSchema(schema); - expect(result.removedCount).toBe(0); - expect(result.schema).toEqual(schema); + // $id, $schema, examples, deprecated, readOnly, $comment = 6 removed + expect(result.removedCount).toBe(6); + expect(result.removedPaths).toContain('$id'); + expect(result.removedPaths).toContain('$schema'); + expect(result.removedPaths).toContain('examples'); + expect(result.removedPaths).toContain('deprecated'); + expect(result.removedPaths).toContain('readOnly'); + expect(result.removedPaths).toContain('$comment'); + expect(result.schema).toEqual({ + title: 'Test Schema', + description: 'A test schema', + type: 'string', + }); }); - test('preserves additionalItems keyword', () => { - const schema = { - type: 'array', - items: [{ type: 'string' }], - additionalItems: { type: 'boolean' }, - }; - const result = sanitizeInputSchema(schema); - expect(result.removedCount).toBe(0); - expect(result.schema.additionalItems).toEqual({ type: 'boolean' }); - }); - - test('sanitizes nested schemas in additionalItems', () => { - const schema = { - type: 'array', - items: [{ type: 'string' }], - additionalItems: { type: 'boolean', customProp: 'remove' }, - }; - const result = sanitizeInputSchema(schema); - expect(result.removedCount).toBe(1); - expect(result.removedPaths).toContain('additionalItems.customProp'); - expect(result.schema.additionalItems).toEqual({ type: 'boolean' }); - }); - - test('preserves if/then/else and sanitizes nested schemas', () => { - const schema = { - if: { properties: { type: { const: 'foo' } }, uiHint: 'remove' }, - then: { required: ['foo'], badProp: 123 }, - else: { required: ['bar'] }, - }; - const result = sanitizeInputSchema(schema); - expect(result.removedCount).toBe(2); - expect(result.schema.if).toEqual({ properties: { type: { const: 'foo' } } }); - expect(result.schema.then).toEqual({ required: ['foo'] }); - }); - - test('preserves patternProperties keyword and sanitizes nested schemas', () => { + test('strips $defs, $ref, definitions (not supported by Gemini)', () => { const schema = { type: 'object', - patternProperties: { '^S_': { type: 'string', customProp: 'remove' } }, - }; - const result = sanitizeInputSchema(schema); - expect(result.removedCount).toBe(1); - expect(result.removedPaths).toContain('patternProperties.^S_.customProp'); - expect(result.schema.patternProperties).toEqual({ '^S_': { type: 'string' } }); - }); - - test('preserves contains keyword and sanitizes nested schema', () => { - const schema = { - type: 'array', - contains: { type: 'number', minimum: 5, customProp: 'remove' }, - }; - const result = sanitizeInputSchema(schema); - expect(result.removedCount).toBe(1); - expect(result.removedPaths).toContain('contains.customProp'); - expect(result.schema.contains).toEqual({ type: 'number', minimum: 5 }); - }); - - test('preserves propertyNames keyword and sanitizes nested schema', () => { - const schema = { - type: 'object', - propertyNames: { type: 'string', pattern: '^[a-z]+$', customProp: 'remove' }, - }; - const result = sanitizeInputSchema(schema); - expect(result.removedCount).toBe(1); - expect(result.removedPaths).toContain('propertyNames.customProp'); - expect(result.schema.propertyNames).toEqual({ type: 'string', pattern: '^[a-z]+$' }); - }); - - test('handles dependencies with both property and schema dependencies', () => { - const schema = { - type: 'object', - dependencies: { - bar: ['foo'], // property dependency (array) - pass through - baz: { properties: { qux: { type: 'string' } }, customProp: 'remove' }, // schema dependency + $defs: { + address: { + type: 'object', + properties: { street: { type: 'string' } }, + }, + }, + properties: { + home: { $ref: '#/$defs/address' }, }, }; + const result = sanitizeInputSchema(schema); - expect(result.removedCount).toBe(1); - expect(result.removedPaths).toContain('dependencies.baz.customProp'); - expect(result.schema.dependencies).toEqual({ - bar: ['foo'], - baz: { properties: { qux: { type: 'string' } } }, + + expect(result.removedCount).toBe(2); + expect(result.removedPaths).toContain('$defs'); + expect(result.removedPaths).toContain('properties.home.$ref'); + expect(result.schema).toEqual({ + type: 'object', + properties: { + home: {}, + }, }); }); + test('strips additionalProperties (not supported by Gemini)', () => { + const schema = { + type: 'object', + properties: { name: { type: 'string' } }, + additionalProperties: false, + }; + + const result = sanitizeInputSchema(schema); + + expect(result.removedCount).toBe(1); + expect(result.removedPaths).toContain('additionalProperties'); + }); + + test('strips const, not, if/then/else (not supported by Gemini)', () => { + const schema = { + type: 'object', + const: 'fixed', + not: { type: 'number' }, + if: { properties: { type: { const: 'foo' } } }, + then: { required: ['foo'] }, + else: { required: ['bar'] }, + }; + + const result = sanitizeInputSchema(schema); + + expect(result.removedCount).toBe(5); + expect(result.removedPaths).toContain('const'); + expect(result.removedPaths).toContain('not'); + expect(result.removedPaths).toContain('if'); + expect(result.removedPaths).toContain('then'); + expect(result.removedPaths).toContain('else'); + expect(result.schema).toEqual({ type: 'object' }); + }); + + test('preserves all Gemini-supported fields', () => { + const schema = { + type: 'object', + format: 'date', + title: 'Test', + description: 'A test', + nullable: true, + example: { name: 'test' }, + default: {}, + enum: ['a', 'b'], + properties: { name: { type: 'string' } }, + required: ['name'], + minProperties: 1, + maxProperties: 10, + items: { type: 'string' }, + minItems: 0, + maxItems: 100, + minLength: 1, + maxLength: 255, + pattern: '^[a-z]+$', + minimum: 0, + maximum: 100, + anyOf: [{ type: 'string' }], + propertyOrdering: ['name'], + }; + + const result = sanitizeInputSchema(schema); + + expect(result.removedCount).toBe(0); + expect(result.removedPaths).toEqual([]); + }); + + test('handles empty schema', () => { + const result = sanitizeInputSchema({}); + + expect(result.removedCount).toBe(0); + expect(result.schema).toEqual({}); + }); + test('handles null input gracefully', () => { const result = sanitizeInputSchema(null as any); expect(result.schema).toBeNull(); @@ -361,6 +404,70 @@ describe('sanitizeToolSchemas', () => { }); }); + test('strips examples from Claude Code tool schemas (issue #155)', () => { + const tools = [ + { + name: 'Bash', + input_schema: { + type: 'object', + properties: { + command: { + type: 'string', + description: 'The command to execute', + examples: ['ls -la', 'git status', 'npm install'], + }, + timeout: { + type: 'number', + description: 'Timeout in ms', + examples: [5000, 30000], + }, + }, + required: ['command'], + }, + }, + { + name: 'Read', + input_schema: { + type: 'object', + properties: { + file_path: { + type: 'string', + description: 'File path to read', + examples: ['/src/index.ts', '/package.json'], + }, + }, + required: ['file_path'], + }, + }, + ]; + + const result = sanitizeToolSchemas(tools); + + expect(result.totalRemoved).toBe(3); + expect(result.removedByTool).toHaveLength(2); + + // Verify examples stripped from Bash tool + const bashTool = result.tools.find((t) => t.name === 'Bash'); + expect(bashTool?.input_schema).toEqual({ + type: 'object', + properties: { + command: { type: 'string', description: 'The command to execute' }, + timeout: { type: 'number', description: 'Timeout in ms' }, + }, + required: ['command'], + }); + + // Verify examples stripped from Read tool + const readTool = result.tools.find((t) => t.name === 'Read'); + expect(readTool?.input_schema).toEqual({ + type: 'object', + properties: { + file_path: { type: 'string', description: 'File path to read' }, + }, + required: ['file_path'], + }); + }); + test('handles tools without input_schema', () => { const tools = [ { name: 'simple_tool', description: 'No schema' }, From 917f0bbef7b7132dca0c37e474a82a9bb07f0df1 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Sat, 7 Feb 2026 06:25:29 -0500 Subject: [PATCH 2/2] test(cliproxy): add edge case coverage for Gemini schema sanitizer Add tests for: exclusiveMinimum/exclusiveMaximum/multipleOf stripping, uniqueItems/contains/additionalItems stripping, writeOnly/definitions stripping, example with array values, default with complex nested objects, empty properties/anyOf edge cases. Fix proxy log message to say "Gemini-unsupported" instead of "non-standard". --- src/cliproxy/tool-sanitization-proxy.ts | 2 +- tests/unit/cliproxy/schema-sanitizer.test.ts | 66 ++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/cliproxy/tool-sanitization-proxy.ts b/src/cliproxy/tool-sanitization-proxy.ts index 87c3b811..36c7c40f 100644 --- a/src/cliproxy/tool-sanitization-proxy.ts +++ b/src/cliproxy/tool-sanitization-proxy.ts @@ -218,7 +218,7 @@ export class ToolSanitizationProxy { for (const entry of schemaResult.removedByTool) { this.writeLog( 'warn', - `[tool-sanitization-proxy] Schema sanitized for "${entry.name}": removed ${entry.removed.length} non-standard properties` + `[tool-sanitization-proxy] Schema sanitized for "${entry.name}": removed ${entry.removed.length} Gemini-unsupported properties` ); } this.log( diff --git a/tests/unit/cliproxy/schema-sanitizer.test.ts b/tests/unit/cliproxy/schema-sanitizer.test.ts index 8138ef5b..88d1b922 100644 --- a/tests/unit/cliproxy/schema-sanitizer.test.ts +++ b/tests/unit/cliproxy/schema-sanitizer.test.ts @@ -305,6 +305,72 @@ describe('sanitizeInputSchema', () => { expect(result.schema).toEqual({ type: 'object' }); }); + test('strips number/array/object validation fields unsupported by Gemini', () => { + const schema = { + type: 'object', + properties: { + score: { + type: 'number', + exclusiveMinimum: 0, + exclusiveMaximum: 100, + multipleOf: 5, + }, + tags: { + type: 'array', + items: { type: 'string' }, + uniqueItems: true, + contains: { type: 'string' }, + additionalItems: false, + }, + meta: { + type: 'object', + writeOnly: true, + definitions: { addr: { type: 'string' } }, + }, + }, + }; + + const result = sanitizeInputSchema(schema); + + expect(result.removedPaths).toContain('properties.score.exclusiveMinimum'); + expect(result.removedPaths).toContain('properties.score.exclusiveMaximum'); + expect(result.removedPaths).toContain('properties.score.multipleOf'); + expect(result.removedPaths).toContain('properties.tags.uniqueItems'); + expect(result.removedPaths).toContain('properties.tags.contains'); + expect(result.removedPaths).toContain('properties.tags.additionalItems'); + expect(result.removedPaths).toContain('properties.meta.writeOnly'); + expect(result.removedPaths).toContain('properties.meta.definitions'); + expect(result.removedCount).toBe(8); + }); + + test('preserves example/default with complex values without recursion', () => { + const schema = { + type: 'object', + example: ['value1', 'value2'], + default: { nested: { deep: { unsupportedKey: true } } }, + }; + + const result = sanitizeInputSchema(schema); + + expect(result.removedCount).toBe(0); + expect(result.schema.example).toEqual(['value1', 'value2']); + expect(result.schema.default).toEqual({ nested: { deep: { unsupportedKey: true } } }); + }); + + test('handles empty properties and anyOf edge cases', () => { + const schema = { + type: 'object', + properties: {}, + anyOf: [], + }; + + const result = sanitizeInputSchema(schema); + + expect(result.removedCount).toBe(0); + expect(result.schema.properties).toEqual({}); + expect(result.schema.anyOf).toEqual([]); + }); + test('preserves all Gemini-supported fields', () => { const schema = { type: 'object',