fix: strip unsupported JSON Schema format values for OpenAI compatibility (#10198)

This commit is contained in:
Daniel 2025-12-18 19:57:06 -05:00 committed by GitHub
parent 9c03476a0f
commit c3a4d14b6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 209 additions and 3 deletions

View file

@ -265,4 +265,183 @@ describe("normalizeToolSchema", () => {
expect(props.line_ranges.items).toBeDefined()
expect(props.line_ranges.description).toBe("Optional line ranges")
})
describe("format field handling", () => {
it("should preserve supported format values (date-time)", () => {
const input = {
type: "string",
format: "date-time",
description: "Timestamp",
}
const result = normalizeToolSchema(input)
expect(result).toEqual({
type: "string",
format: "date-time",
description: "Timestamp",
additionalProperties: false,
})
})
it("should preserve supported format values (email)", () => {
const input = {
type: "string",
format: "email",
}
const result = normalizeToolSchema(input)
expect(result.format).toBe("email")
})
it("should preserve supported format values (uuid)", () => {
const input = {
type: "string",
format: "uuid",
}
const result = normalizeToolSchema(input)
expect(result.format).toBe("uuid")
})
it("should preserve all supported format values", () => {
const supportedFormats = [
"date-time",
"time",
"date",
"duration",
"email",
"hostname",
"ipv4",
"ipv6",
"uuid",
]
for (const format of supportedFormats) {
const input = { type: "string", format }
const result = normalizeToolSchema(input)
expect(result.format).toBe(format)
}
})
it("should strip unsupported format value (uri)", () => {
const input = {
type: "string",
format: "uri",
description: "URL field",
}
const result = normalizeToolSchema(input)
expect(result).toEqual({
type: "string",
description: "URL field",
additionalProperties: false,
})
expect(result.format).toBeUndefined()
})
it("should strip unsupported format value (uri-reference)", () => {
const input = {
type: "string",
format: "uri-reference",
}
const result = normalizeToolSchema(input)
expect(result.format).toBeUndefined()
})
it("should strip unsupported format values (various)", () => {
const unsupportedFormats = ["uri", "uri-reference", "iri", "iri-reference", "regex", "json-pointer"]
for (const format of unsupportedFormats) {
const input = { type: "string", format }
const result = normalizeToolSchema(input)
expect(result.format).toBeUndefined()
}
})
it("should strip unsupported format in nested properties", () => {
const input = {
type: "object",
properties: {
url: {
type: "string",
format: "uri",
description: "A URL",
},
email: {
type: "string",
format: "email",
description: "An email",
},
},
}
const result = normalizeToolSchema(input)
const props = result.properties as Record<string, Record<string, unknown>>
expect(props.url.format).toBeUndefined()
expect(props.url.description).toBe("A URL")
expect(props.email.format).toBe("email")
expect(props.email.description).toBe("An email")
})
it("should strip unsupported format in deeply nested structures", () => {
const input = {
type: "object",
properties: {
items: {
type: "array",
items: {
type: "object",
properties: {
link: {
type: "string",
format: "uri",
},
timestamp: {
type: "string",
format: "date-time",
},
},
},
},
},
}
const result = normalizeToolSchema(input)
const props = result.properties as Record<string, Record<string, unknown>>
const itemsItems = props.items.items as Record<string, unknown>
const nestedProps = itemsItems.properties as Record<string, Record<string, unknown>>
expect(nestedProps.link.format).toBeUndefined()
expect(nestedProps.timestamp.format).toBe("date-time")
})
it("should handle MCP fetch server schema with uri format", () => {
// This is similar to the actual fetch MCP server schema that caused the error
const input = {
type: "object",
properties: {
url: {
type: "string",
format: "uri",
description: "URL to fetch",
},
},
required: ["url"],
}
const result = normalizeToolSchema(input)
const props = result.properties as Record<string, Record<string, unknown>>
expect(props.url.format).toBeUndefined()
expect(props.url.type).toBe("string")
expect(props.url.description).toBe("URL to fetch")
})
})
})

View file

@ -6,6 +6,23 @@ import { z } from "zod"
*/
export type JsonSchema = z4.core.JSONSchema.JSONSchema
/**
* Set of format values supported by OpenAI's Structured Outputs (strict mode).
* Unsupported format values will be stripped during schema normalization.
* @see https://platform.openai.com/docs/guides/structured-outputs#supported-schemas
*/
const OPENAI_SUPPORTED_FORMATS = new Set([
"date-time",
"time",
"date",
"duration",
"email",
"hostname",
"ipv4",
"ipv6",
"uuid",
])
/**
* Zod schema for JSON Schema primitive types
*/
@ -76,10 +93,11 @@ const TypeFieldSchema = z.union([JsonSchemaTypeSchema, z.array(JsonSchemaTypeSch
/**
* Internal Zod schema that normalizes tool input JSON Schema to be compliant with JSON Schema draft 2020-12.
*
* This schema performs two key transformations:
* This schema performs three key transformations:
* 1. Sets `additionalProperties: false` by default (required by OpenAI strict mode)
* 2. Converts deprecated `type: ["T", "null"]` array syntax to `anyOf` format
* (required by Claude on Bedrock which enforces JSON Schema draft 2020-12)
* 3. Strips unsupported `format` values (e.g., "uri") for OpenAI Structured Outputs compatibility
*
* Uses recursive parsing so transformations apply to all nested schemas automatically.
*/
@ -109,10 +127,12 @@ const NormalizedToolSchemaInternal: z.ZodType<Record<string, unknown>, z.ZodType
minItems: z.number().optional(),
maxItems: z.number().optional(),
uniqueItems: z.boolean().optional(),
// Format field - unsupported values will be stripped in transform
format: z.string().optional(),
})
.passthrough()
.transform((schema) => {
const { type, required, properties, ...rest } = schema
const { type, required, properties, format, ...rest } = schema
const result: Record<string, unknown> = { ...rest }
// If type is an array, convert to anyOf format (JSON Schema 2020-12)
@ -122,6 +142,12 @@ const NormalizedToolSchemaInternal: z.ZodType<Record<string, unknown>, z.ZodType
result.type = type
}
// Strip unsupported format values for OpenAI compatibility
// Only include format if it's a supported value
if (format && OPENAI_SUPPORTED_FORMATS.has(format)) {
result.format = format
}
// Handle properties and required for strict mode
if (properties) {
result.properties = properties
@ -145,10 +171,11 @@ const NormalizedToolSchemaInternal: z.ZodType<Record<string, unknown>, z.ZodType
/**
* Normalizes a tool input JSON Schema to be compliant with JSON Schema draft 2020-12.
*
* This function performs two key transformations:
* This function performs three key transformations:
* 1. Sets `additionalProperties: false` by default (required by OpenAI strict mode)
* 2. Converts deprecated `type: ["T", "null"]` array syntax to `anyOf` format
* (required by Claude on Bedrock which enforces JSON Schema draft 2020-12)
* 3. Strips unsupported `format` values (e.g., "uri") for OpenAI Structured Outputs compatibility
*
* Uses recursive parsing so transformations apply to all nested schemas automatically.
*