Skip to content

Commit c3a4d14

Browse files
authored
fix: strip unsupported JSON Schema format values for OpenAI compatibility (#10198)
1 parent 9c03476 commit c3a4d14

File tree

2 files changed

+209
-3
lines changed

2 files changed

+209
-3
lines changed

src/utils/__tests__/json-schema.spec.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,183 @@ describe("normalizeToolSchema", () => {
265265
expect(props.line_ranges.items).toBeDefined()
266266
expect(props.line_ranges.description).toBe("Optional line ranges")
267267
})
268+
269+
describe("format field handling", () => {
270+
it("should preserve supported format values (date-time)", () => {
271+
const input = {
272+
type: "string",
273+
format: "date-time",
274+
description: "Timestamp",
275+
}
276+
277+
const result = normalizeToolSchema(input)
278+
279+
expect(result).toEqual({
280+
type: "string",
281+
format: "date-time",
282+
description: "Timestamp",
283+
additionalProperties: false,
284+
})
285+
})
286+
287+
it("should preserve supported format values (email)", () => {
288+
const input = {
289+
type: "string",
290+
format: "email",
291+
}
292+
293+
const result = normalizeToolSchema(input)
294+
295+
expect(result.format).toBe("email")
296+
})
297+
298+
it("should preserve supported format values (uuid)", () => {
299+
const input = {
300+
type: "string",
301+
format: "uuid",
302+
}
303+
304+
const result = normalizeToolSchema(input)
305+
306+
expect(result.format).toBe("uuid")
307+
})
308+
309+
it("should preserve all supported format values", () => {
310+
const supportedFormats = [
311+
"date-time",
312+
"time",
313+
"date",
314+
"duration",
315+
"email",
316+
"hostname",
317+
"ipv4",
318+
"ipv6",
319+
"uuid",
320+
]
321+
322+
for (const format of supportedFormats) {
323+
const input = { type: "string", format }
324+
const result = normalizeToolSchema(input)
325+
expect(result.format).toBe(format)
326+
}
327+
})
328+
329+
it("should strip unsupported format value (uri)", () => {
330+
const input = {
331+
type: "string",
332+
format: "uri",
333+
description: "URL field",
334+
}
335+
336+
const result = normalizeToolSchema(input)
337+
338+
expect(result).toEqual({
339+
type: "string",
340+
description: "URL field",
341+
additionalProperties: false,
342+
})
343+
expect(result.format).toBeUndefined()
344+
})
345+
346+
it("should strip unsupported format value (uri-reference)", () => {
347+
const input = {
348+
type: "string",
349+
format: "uri-reference",
350+
}
351+
352+
const result = normalizeToolSchema(input)
353+
354+
expect(result.format).toBeUndefined()
355+
})
356+
357+
it("should strip unsupported format values (various)", () => {
358+
const unsupportedFormats = ["uri", "uri-reference", "iri", "iri-reference", "regex", "json-pointer"]
359+
360+
for (const format of unsupportedFormats) {
361+
const input = { type: "string", format }
362+
const result = normalizeToolSchema(input)
363+
expect(result.format).toBeUndefined()
364+
}
365+
})
366+
367+
it("should strip unsupported format in nested properties", () => {
368+
const input = {
369+
type: "object",
370+
properties: {
371+
url: {
372+
type: "string",
373+
format: "uri",
374+
description: "A URL",
375+
},
376+
email: {
377+
type: "string",
378+
format: "email",
379+
description: "An email",
380+
},
381+
},
382+
}
383+
384+
const result = normalizeToolSchema(input)
385+
386+
const props = result.properties as Record<string, Record<string, unknown>>
387+
expect(props.url.format).toBeUndefined()
388+
expect(props.url.description).toBe("A URL")
389+
expect(props.email.format).toBe("email")
390+
expect(props.email.description).toBe("An email")
391+
})
392+
393+
it("should strip unsupported format in deeply nested structures", () => {
394+
const input = {
395+
type: "object",
396+
properties: {
397+
items: {
398+
type: "array",
399+
items: {
400+
type: "object",
401+
properties: {
402+
link: {
403+
type: "string",
404+
format: "uri",
405+
},
406+
timestamp: {
407+
type: "string",
408+
format: "date-time",
409+
},
410+
},
411+
},
412+
},
413+
},
414+
}
415+
416+
const result = normalizeToolSchema(input)
417+
418+
const props = result.properties as Record<string, Record<string, unknown>>
419+
const itemsItems = props.items.items as Record<string, unknown>
420+
const nestedProps = itemsItems.properties as Record<string, Record<string, unknown>>
421+
expect(nestedProps.link.format).toBeUndefined()
422+
expect(nestedProps.timestamp.format).toBe("date-time")
423+
})
424+
425+
it("should handle MCP fetch server schema with uri format", () => {
426+
// This is similar to the actual fetch MCP server schema that caused the error
427+
const input = {
428+
type: "object",
429+
properties: {
430+
url: {
431+
type: "string",
432+
format: "uri",
433+
description: "URL to fetch",
434+
},
435+
},
436+
required: ["url"],
437+
}
438+
439+
const result = normalizeToolSchema(input)
440+
441+
const props = result.properties as Record<string, Record<string, unknown>>
442+
expect(props.url.format).toBeUndefined()
443+
expect(props.url.type).toBe("string")
444+
expect(props.url.description).toBe("URL to fetch")
445+
})
446+
})
268447
})

src/utils/json-schema.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ import { z } from "zod"
66
*/
77
export type JsonSchema = z4.core.JSONSchema.JSONSchema
88

9+
/**
10+
* Set of format values supported by OpenAI's Structured Outputs (strict mode).
11+
* Unsupported format values will be stripped during schema normalization.
12+
* @see https://platform.openai.com/docs/guides/structured-outputs#supported-schemas
13+
*/
14+
const OPENAI_SUPPORTED_FORMATS = new Set([
15+
"date-time",
16+
"time",
17+
"date",
18+
"duration",
19+
"email",
20+
"hostname",
21+
"ipv4",
22+
"ipv6",
23+
"uuid",
24+
])
25+
926
/**
1027
* Zod schema for JSON Schema primitive types
1128
*/
@@ -76,10 +93,11 @@ const TypeFieldSchema = z.union([JsonSchemaTypeSchema, z.array(JsonSchemaTypeSch
7693
/**
7794
* Internal Zod schema that normalizes tool input JSON Schema to be compliant with JSON Schema draft 2020-12.
7895
*
79-
* This schema performs two key transformations:
96+
* This schema performs three key transformations:
8097
* 1. Sets `additionalProperties: false` by default (required by OpenAI strict mode)
8198
* 2. Converts deprecated `type: ["T", "null"]` array syntax to `anyOf` format
8299
* (required by Claude on Bedrock which enforces JSON Schema draft 2020-12)
100+
* 3. Strips unsupported `format` values (e.g., "uri") for OpenAI Structured Outputs compatibility
83101
*
84102
* Uses recursive parsing so transformations apply to all nested schemas automatically.
85103
*/
@@ -109,10 +127,12 @@ const NormalizedToolSchemaInternal: z.ZodType<Record<string, unknown>, z.ZodType
109127
minItems: z.number().optional(),
110128
maxItems: z.number().optional(),
111129
uniqueItems: z.boolean().optional(),
130+
// Format field - unsupported values will be stripped in transform
131+
format: z.string().optional(),
112132
})
113133
.passthrough()
114134
.transform((schema) => {
115-
const { type, required, properties, ...rest } = schema
135+
const { type, required, properties, format, ...rest } = schema
116136
const result: Record<string, unknown> = { ...rest }
117137

118138
// 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
122142
result.type = type
123143
}
124144

145+
// Strip unsupported format values for OpenAI compatibility
146+
// Only include format if it's a supported value
147+
if (format && OPENAI_SUPPORTED_FORMATS.has(format)) {
148+
result.format = format
149+
}
150+
125151
// Handle properties and required for strict mode
126152
if (properties) {
127153
result.properties = properties
@@ -145,10 +171,11 @@ const NormalizedToolSchemaInternal: z.ZodType<Record<string, unknown>, z.ZodType
145171
/**
146172
* Normalizes a tool input JSON Schema to be compliant with JSON Schema draft 2020-12.
147173
*
148-
* This function performs two key transformations:
174+
* This function performs three key transformations:
149175
* 1. Sets `additionalProperties: false` by default (required by OpenAI strict mode)
150176
* 2. Converts deprecated `type: ["T", "null"]` array syntax to `anyOf` format
151177
* (required by Claude on Bedrock which enforces JSON Schema draft 2020-12)
178+
* 3. Strips unsupported `format` values (e.g., "uri") for OpenAI Structured Outputs compatibility
152179
*
153180
* Uses recursive parsing so transformations apply to all nested schemas automatically.
154181
*

0 commit comments

Comments
 (0)