From 25193187e098ffeee52f020389cad042e178f4f9 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sat, 18 Apr 2026 20:05:22 -0400 Subject: [PATCH] chore(proxy): remove unused schema sanitizer draft --- src/utils/schema-sanitizer.ts | 285 ---------------------- tests/unit/utils/schema-sanitizer.test.ts | 85 ------- 2 files changed, 370 deletions(-) delete mode 100644 src/utils/schema-sanitizer.ts delete mode 100644 tests/unit/utils/schema-sanitizer.test.ts diff --git a/src/utils/schema-sanitizer.ts b/src/utils/schema-sanitizer.ts deleted file mode 100644 index 0639f14e..00000000 --- a/src/utils/schema-sanitizer.ts +++ /dev/null @@ -1,285 +0,0 @@ -/** - * Schema Sanitizer - * - * Strips JSON Schema keywords that OpenAI-compatible providers reject, - * cleans enum/const values, and normalizes type fields. - */ - -const OPENAI_INCOMPATIBLE_SCHEMA_KEYWORDS = new Set([ - '$comment', - '$schema', - 'default', - 'else', - 'examples', - 'format', - 'if', - 'maxLength', - 'maximum', - 'minLength', - 'minimum', - 'multipleOf', - 'pattern', - 'patternProperties', - 'propertyNames', - 'then', - 'unevaluatedProperties', -]); - -function isSchemaRecord(value: unknown): value is Record { - return value !== null && typeof value === 'object' && !Array.isArray(value); -} - -function stripSchemaKeywords(schema: unknown, keywords: Set): unknown { - if (Array.isArray(schema)) { - return schema.map((item) => stripSchemaKeywords(item, keywords)); - } - - if (!isSchemaRecord(schema)) { - return schema; - } - - const result: Record = {}; - for (const [key, value] of Object.entries(schema)) { - if (key === 'properties' && isSchemaRecord(value)) { - const sanitizedProps: Record = {}; - for (const [propName, propSchema] of Object.entries(value)) { - sanitizedProps[propName] = stripSchemaKeywords(propSchema, keywords); - } - result[key] = sanitizedProps; - continue; - } - - if (keywords.has(key)) { - continue; - } - - result[key] = stripSchemaKeywords(value, keywords); - } - - return result; -} - -function deepEqualJsonValue(a: unknown, b: unknown): boolean { - if (Object.is(a, b)) return true; - if (typeof a !== typeof b) return false; - - if (Array.isArray(a) && Array.isArray(b)) { - return a.length === b.length && a.every((value, index) => deepEqualJsonValue(value, b[index])); - } - - if (isSchemaRecord(a) && isSchemaRecord(b)) { - const aKeys = Object.keys(a); - const bKeys = Object.keys(b); - return ( - aKeys.length === bKeys.length && - aKeys.every((key) => key in b && deepEqualJsonValue(a[key], b[key])) - ); - } - - return false; -} - -function matchesJsonSchemaType(type: string, value: unknown): boolean { - switch (type) { - case 'string': - return typeof value === 'string'; - case 'number': - return typeof value === 'number' && Number.isFinite(value); - case 'integer': - return typeof value === 'number' && Number.isInteger(value); - case 'boolean': - return typeof value === 'boolean'; - case 'object': - return value !== null && typeof value === 'object' && !Array.isArray(value); - case 'array': - return Array.isArray(value); - case 'null': - return value === null; - default: - return true; - } -} - -function getJsonSchemaTypes(record: Record): string[] { - const raw = record.type; - if (typeof raw === 'string') { - return [raw]; - } - if (Array.isArray(raw)) { - return raw.filter((value): value is string => typeof value === 'string'); - } - return []; -} - -function schemaAllowsValue(schema: Record, value: unknown): boolean { - if (Array.isArray(schema.anyOf)) { - return schema.anyOf.some((item) => - schemaAllowsValue(sanitizeSchemaForOpenAICompat(item), value) - ); - } - - if (Array.isArray(schema.oneOf)) { - return ( - schema.oneOf.filter((item) => schemaAllowsValue(sanitizeSchemaForOpenAICompat(item), value)) - .length === 1 - ); - } - - if (Array.isArray(schema.allOf)) { - return schema.allOf.every((item) => - schemaAllowsValue(sanitizeSchemaForOpenAICompat(item), value) - ); - } - - if ('const' in schema && !deepEqualJsonValue(schema.const, value)) { - return false; - } - - if (Array.isArray(schema.enum)) { - if (!schema.enum.some((item) => deepEqualJsonValue(item, value))) { - return false; - } - } - - const types = getJsonSchemaTypes(schema); - if (types.length > 0 && !types.some((type) => matchesJsonSchemaType(type, value))) { - return false; - } - - return true; -} - -function sanitizeTypeField(record: Record): void { - const allowed = new Set(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']); - - const raw = record.type; - if (typeof raw === 'string') { - if (!allowed.has(raw)) delete record.type; - return; - } - - if (!Array.isArray(raw)) return; - - const filtered = raw.filter( - (value, index): value is string => - typeof value === 'string' && allowed.has(value) && raw.indexOf(value) === index - ); - - if (filtered.length === 0) { - delete record.type; - } else if (filtered.length === 1) { - record.type = filtered[0]; - } else { - record.type = filtered; - } -} - -export function sanitizeSchemaForOpenAICompat(schema: unknown): Record { - const stripped = stripSchemaKeywords(schema, OPENAI_INCOMPATIBLE_SCHEMA_KEYWORDS); - if (!isSchemaRecord(stripped)) { - return {}; - } - - const record = { ...stripped }; - - sanitizeTypeField(record); - - if (isSchemaRecord(record.properties)) { - const sanitizedProps: Record = {}; - for (const [key, value] of Object.entries(record.properties)) { - sanitizedProps[key] = sanitizeSchemaForOpenAICompat(value); - } - record.properties = sanitizedProps; - } - - if ('items' in record) { - if (Array.isArray(record.items)) { - record.items = record.items.map((item) => sanitizeSchemaForOpenAICompat(item)); - } else { - record.items = sanitizeSchemaForOpenAICompat(record.items); - } - } - - for (const key of ['anyOf', 'oneOf', 'allOf'] as const) { - if (Array.isArray(record[key])) { - record[key] = (record[key] as unknown[]).map((item) => sanitizeSchemaForOpenAICompat(item)); - } - } - - const properties = isSchemaRecord(record.properties) ? record.properties : undefined; - - if (Array.isArray(record.required) && properties) { - record.required = record.required.filter( - (value): value is string => typeof value === 'string' && value in properties - ); - } - - const schemaWithoutEnum = { ...record }; - delete schemaWithoutEnum.enum; - delete schemaWithoutEnum.const; - - if (Array.isArray(record.enum)) { - const filteredEnum = record.enum.filter((value) => schemaAllowsValue(schemaWithoutEnum, value)); - if (filteredEnum.length > 0) { - record.enum = filteredEnum; - } else { - delete record.enum; - } - } - - const schemaWithoutConst = { ...record }; - delete schemaWithoutConst.const; - if ('const' in record && !schemaAllowsValue(schemaWithoutConst, record.const)) { - delete record.const; - } - - return record; -} - -/** - * Normalize a tool parameter schema for OpenAI-compatible providers. - * Strips incompatible keywords and optionally enforces strict mode - * (additionalProperties: false, required = all property keys). - */ -export function normalizeSchemaForOpenAI( - schema: Record, - strict = true -): Record { - const record = sanitizeSchemaForOpenAICompat(schema); - - if (record.type === 'object' && record.properties) { - const properties = record.properties as Record>; - const existingRequired = Array.isArray(record.required) ? (record.required as string[]) : []; - - const normalizedProps: Record = {}; - for (const [key, value] of Object.entries(properties)) { - normalizedProps[key] = normalizeSchemaForOpenAI(value as Record, strict); - } - record.properties = normalizedProps; - - record.required = existingRequired.filter((k) => k in normalizedProps); - if (strict) { - record.additionalProperties = false; - } - } - - if ('items' in record) { - if (Array.isArray(record.items)) { - record.items = (record.items as unknown[]).map((item) => - normalizeSchemaForOpenAI(item as Record, strict) - ); - } else { - record.items = normalizeSchemaForOpenAI(record.items as Record, strict); - } - } - - for (const key of ['anyOf', 'oneOf', 'allOf'] as const) { - if (key in record && Array.isArray(record[key])) { - record[key] = (record[key] as unknown[]).map((item) => - normalizeSchemaForOpenAI(item as Record, strict) - ); - } - } - - return record; -} diff --git a/tests/unit/utils/schema-sanitizer.test.ts b/tests/unit/utils/schema-sanitizer.test.ts deleted file mode 100644 index baf3290b..00000000 --- a/tests/unit/utils/schema-sanitizer.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { describe, expect, it } from 'bun:test'; - -import { normalizeSchemaForOpenAI } from '../../../src/utils/schema-sanitizer'; - -describe('normalizeSchemaForOpenAI', () => { - it('strips incompatible keywords and enforces strict object schemas', () => { - const result = normalizeSchemaForOpenAI({ - type: 'object', - properties: { - query: { type: 'string', pattern: '^[a-z]+$', minLength: 3 }, - limit: { type: 'integer', minimum: 1 }, - }, - required: ['query', 'missing'], - additionalProperties: true, - default: { query: 'docs' }, - }); - - expect(result).toEqual({ - type: 'object', - properties: { - query: { type: 'string' }, - limit: { type: 'integer' }, - }, - required: ['query'], - additionalProperties: false, - }); - }); - - it('drops enum and const values that no longer match the schema type', () => { - const result = normalizeSchemaForOpenAI({ - type: 'string', - enum: ['ok', 1, null], - const: 1, - }); - - expect(result).toEqual({ - type: 'string', - enum: ['ok'], - }); - }); - - it('normalizes nested combinators and arrays recursively', () => { - const result = normalizeSchemaForOpenAI({ - anyOf: [ - { - type: 'object', - properties: { - image: { - type: 'array', - items: { - type: 'object', - properties: { - url: { type: 'string', format: 'uri' }, - }, - }, - }, - }, - }, - ], - }); - - expect(result).toEqual({ - anyOf: [ - { - type: 'object', - properties: { - image: { - type: 'array', - items: { - type: 'object', - properties: { - url: { type: 'string' }, - }, - required: [], - additionalProperties: false, - }, - }, - }, - required: [], - additionalProperties: false, - }, - ], - }); - }); -});