Spaces:
Runtime error
Runtime error
| import { describe, it, expect } from 'vitest'; | |
| import { sanitizeForGemini } from '../../providers/google.js'; | |
| describe('sanitizeForGemini', () => { | |
| it('strips top-level JSON-Schema-only fields that Google rejects', () => { | |
| const input = { | |
| $schema: 'https://json-schema.org/draft/2020-12/schema', | |
| $id: 'http://example.com/foo.json', | |
| type: 'object', | |
| properties: { name: { type: 'string' } }, | |
| }; | |
| expect(sanitizeForGemini(input)).toEqual({ | |
| type: 'object', | |
| properties: { name: { type: 'string' } }, | |
| }); | |
| }); | |
| it('strips exclusiveMinimum / exclusiveMaximum recursively in nested properties', () => { | |
| const input = { | |
| type: 'object', | |
| properties: { | |
| temp_threshold: { type: 'number', exclusiveMinimum: 0, exclusiveMaximum: 100 }, | |
| nested: { | |
| type: 'object', | |
| properties: { | |
| count: { type: 'integer', exclusiveMinimum: 1 }, | |
| }, | |
| }, | |
| }, | |
| }; | |
| expect(sanitizeForGemini(input)).toEqual({ | |
| type: 'object', | |
| properties: { | |
| temp_threshold: { type: 'number' }, | |
| nested: { | |
| type: 'object', | |
| properties: { | |
| count: { type: 'integer' }, | |
| }, | |
| }, | |
| }, | |
| }); | |
| }); | |
| it('walks through arrays (e.g. anyOf branches)', () => { | |
| const input = { | |
| anyOf: [ | |
| { type: 'string', $ref: '#/definitions/foo' }, | |
| { type: 'number', exclusiveMinimum: 0 }, | |
| ], | |
| }; | |
| expect(sanitizeForGemini(input)).toEqual({ | |
| anyOf: [ | |
| { type: 'string' }, | |
| { type: 'number' }, | |
| ], | |
| }); | |
| }); | |
| it('removes $defs / definitions / patternProperties / if-then-else', () => { | |
| const input = { | |
| type: 'object', | |
| $defs: { Foo: { type: 'string' } }, | |
| definitions: { Bar: { type: 'integer' } }, | |
| patternProperties: { '^x-': { type: 'string' } }, | |
| if: { properties: { kind: { const: 'A' } } }, | |
| then: { required: ['extra'] }, | |
| else: { required: [] }, | |
| properties: { kind: { type: 'string' } }, | |
| }; | |
| expect(sanitizeForGemini(input)).toEqual({ | |
| type: 'object', | |
| properties: { kind: { type: 'string' } }, | |
| }); | |
| }); | |
| it('passes through supported OpenAPI fields untouched', () => { | |
| const input = { | |
| type: 'object', | |
| description: 'A tool input', | |
| required: ['city'], | |
| properties: { | |
| city: { type: 'string', description: 'City name', enum: ['Karachi', 'Lahore'] }, | |
| items: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 10 }, | |
| }, | |
| }; | |
| expect(sanitizeForGemini(input)).toEqual(input); | |
| }); | |
| it('strips additionalProperties (Gemini rejects it with 400)', () => { | |
| const input = { | |
| type: 'object', | |
| properties: { | |
| name: { type: 'string' }, | |
| address: { | |
| type: 'object', | |
| properties: { street: { type: 'string' } }, | |
| additionalProperties: false, | |
| }, | |
| }, | |
| additionalProperties: false, | |
| }; | |
| expect(sanitizeForGemini(input)).toEqual({ | |
| type: 'object', | |
| properties: { | |
| name: { type: 'string' }, | |
| address: { | |
| type: 'object', | |
| properties: { street: { type: 'string' } }, | |
| }, | |
| }, | |
| }); | |
| }); | |
| it('handles primitives and null safely', () => { | |
| expect(sanitizeForGemini(null)).toBe(null); | |
| expect(sanitizeForGemini(undefined)).toBe(undefined); | |
| expect(sanitizeForGemini('hello')).toBe('hello'); | |
| expect(sanitizeForGemini(42)).toBe(42); | |
| expect(sanitizeForGemini(true)).toBe(true); | |
| }); | |
| it('strips examples / const / readOnly / writeOnly (Gemini rejects all with 400)', () => { | |
| // These fields are emitted by Zod, Pydantic, and most JSON-Schema generators | |
| // (used by opencode, Continue, Cline tool definitions) but rejected by Google's | |
| // Gemini and Gemma 4 IT generateContent endpoint with "Invalid JSON payload | |
| // received. Unknown name 'X' at tools[0].function_declarations[0].parameters". | |
| const input = { | |
| type: 'object', | |
| properties: { | |
| command: { | |
| type: 'string', | |
| description: 'Shell command to execute', | |
| examples: ['ls -la', 'pwd'], | |
| readOnly: false, | |
| }, | |
| mode: { type: 'string', const: 'sync' }, | |
| secret: { type: 'string', writeOnly: true }, | |
| }, | |
| required: ['command'], | |
| }; | |
| expect(sanitizeForGemini(input)).toEqual({ | |
| type: 'object', | |
| properties: { | |
| command: { type: 'string', description: 'Shell command to execute' }, | |
| mode: { type: 'string' }, | |
| secret: { type: 'string' }, | |
| }, | |
| required: ['command'], | |
| }); | |
| }); | |
| it('strips uniqueItems (Gemini rejects arrays with uniqueItems with 400)', () => { | |
| // Confirmed via live Google AI Studio call: | |
| // "Invalid JSON payload received. Unknown name 'uniqueItems' at | |
| // 'tools[0].function_declarations[0].parameters.properties[2].value': | |
| // Cannot find field." | |
| const input = { | |
| type: 'object', | |
| properties: { | |
| files: { | |
| type: 'array', | |
| items: { type: 'string' }, | |
| uniqueItems: true, | |
| minItems: 0, | |
| maxItems: 10, | |
| }, | |
| }, | |
| }; | |
| expect(sanitizeForGemini(input)).toEqual({ | |
| type: 'object', | |
| properties: { | |
| files: { | |
| type: 'array', | |
| items: { type: 'string' }, | |
| minItems: 0, | |
| maxItems: 10, | |
| }, | |
| }, | |
| }); | |
| }); | |
| it('strips composition / structural keywords absent from Google Schema proto', () => { | |
| // not / allOf / oneOf / prefixItems / contains / propertyNames / multipleOf / | |
| // dependencies / deprecated are all absent from Google's Schema proto | |
| // (https://ai.google.dev/api/caching#Schema). Only anyOf is supported. | |
| const input = { | |
| type: 'object', | |
| not: { type: 'null' }, | |
| allOf: [{ type: 'object' }], | |
| oneOf: [{ type: 'string' }, { type: 'number' }], | |
| dependencies: { a: ['b'] }, | |
| deprecated: true, | |
| properties: { | |
| tuple: { type: 'array', prefixItems: [{ type: 'string' }] }, | |
| bag: { type: 'array', contains: { type: 'string' }, minContains: 1, maxContains: 5 }, | |
| names: { type: 'object', propertyNames: { pattern: '^x_' } }, | |
| count: { type: 'number', multipleOf: 0.5 }, | |
| }, | |
| }; | |
| expect(sanitizeForGemini(input)).toEqual({ | |
| type: 'object', | |
| properties: { | |
| tuple: { type: 'array' }, | |
| bag: { type: 'array' }, | |
| names: { type: 'object' }, | |
| count: { type: 'number' }, | |
| }, | |
| }); | |
| }); | |
| }); | |