diff --git a/src/api/providers/__tests__/bedrock-native-tools.spec.ts b/src/api/providers/__tests__/bedrock-native-tools.spec.ts index 8325f94bfb..0396a81744 100644 --- a/src/api/providers/__tests__/bedrock-native-tools.spec.ts +++ b/src/api/providers/__tests__/bedrock-native-tools.spec.ts @@ -168,12 +168,13 @@ describe("AwsBedrockHandler Native Tool Calling", () => { expect(executeCommandSchema.properties.cwd.description).toBe("Working directory (optional)") // Second tool: line_ranges should be transformed from type: ["array", "null"] to anyOf + // with items moved inside the array variant (required by GPT-5-mini strict schema validation) const readFileSchema = bedrockTools[1].toolSpec.inputSchema.json as any const lineRanges = readFileSchema.properties.files.items.properties.line_ranges - expect(lineRanges.anyOf).toEqual([{ type: "array" }, { type: "null" }]) + expect(lineRanges.anyOf).toEqual([{ type: "array", items: { type: "integer" } }, { type: "null" }]) expect(lineRanges.type).toBeUndefined() - // items also gets additionalProperties: false from normalization - expect(lineRanges.items.type).toBe("integer") + // items should now be inside the array variant, not at root + expect(lineRanges.items).toBeUndefined() expect(lineRanges.description).toBe("Optional line ranges") }) diff --git a/src/utils/__tests__/json-schema.spec.ts b/src/utils/__tests__/json-schema.spec.ts index c53e0d7b86..5a1510be43 100644 --- a/src/utils/__tests__/json-schema.spec.ts +++ b/src/utils/__tests__/json-schema.spec.ts @@ -26,10 +26,10 @@ describe("normalizeToolSchema", () => { const result = normalizeToolSchema(input) - // additionalProperties should NOT be added to array or primitive types + // Array-specific properties (items) should be moved inside the array variant + // This is required by strict schema validators like GPT-5-mini expect(result).toEqual({ - anyOf: [{ type: "array" }, { type: "null" }], - items: { type: "string" }, + anyOf: [{ type: "array", items: { type: "string" } }, { type: "null" }], description: "Optional array", }) }) @@ -97,6 +97,7 @@ describe("normalizeToolSchema", () => { const result = normalizeToolSchema(input) // additionalProperties: false should ONLY be on object types + // Array-specific properties (items) should be moved inside the array variant expect(result).toEqual({ type: "array", items: { @@ -104,8 +105,7 @@ describe("normalizeToolSchema", () => { properties: { path: { type: "string" }, line_ranges: { - anyOf: [{ type: "array" }, { type: "null" }], - items: { type: "integer" }, + anyOf: [{ type: "array", items: { type: "integer" } }, { type: "null" }], }, }, additionalProperties: false, @@ -143,7 +143,11 @@ describe("normalizeToolSchema", () => { const properties = result.properties as Record> const filesItems = properties.files.items as Record const filesItemsProps = filesItems.properties as Record> - expect(filesItemsProps.line_ranges.anyOf).toEqual([{ type: "array" }, { type: "null" }]) + // Array-specific properties (items) should be moved inside the array variant + expect(filesItemsProps.line_ranges.anyOf).toEqual([ + { type: "array", items: { type: "array", items: { type: "integer" } } }, + { type: "null" }, + ]) }) it("should recursively transform anyOf arrays", () => { @@ -255,13 +259,26 @@ describe("normalizeToolSchema", () => { const result = normalizeToolSchema(input) - // Verify the line_ranges was transformed + // Verify the line_ranges was transformed with items inside the array variant const files = (result.properties as Record).files as Record const items = files.items as Record const props = items.properties as Record> - expect(props.line_ranges.anyOf).toEqual([{ type: "array" }, { type: "null" }]) - // Verify other properties are preserved - expect(props.line_ranges.items).toBeDefined() + // Array-specific properties (items, minItems, maxItems) should be moved inside the array variant + expect(props.line_ranges.anyOf).toEqual([ + { + type: "array", + items: { + type: "array", + items: { type: "integer" }, + minItems: 2, + maxItems: 2, + }, + }, + { type: "null" }, + ]) + // items should NOT be at root level anymore + expect(props.line_ranges.items).toBeUndefined() + // Other properties are preserved at root level expect(props.line_ranges.description).toBe("Optional line ranges") }) diff --git a/src/utils/json-schema.ts b/src/utils/json-schema.ts index 180a51848b..8059c2ee0d 100644 --- a/src/utils/json-schema.ts +++ b/src/utils/json-schema.ts @@ -23,6 +23,28 @@ const OPENAI_SUPPORTED_FORMATS = new Set([ "uuid", ]) +/** + * Array-specific JSON Schema properties that must be nested inside array type variants + * when converting to anyOf format (JSON Schema draft 2020-12). + */ +const ARRAY_SPECIFIC_PROPERTIES = ["items", "minItems", "maxItems", "uniqueItems"] as const + +/** + * Applies array-specific properties from source to target object. + * Only copies properties that are defined in the source. + */ +function applyArrayProperties( + target: Record, + source: Record, +): Record { + for (const prop of ARRAY_SPECIFIC_PROPERTIES) { + if (source[prop] !== undefined) { + target[prop] = source[prop] + } + } + return target +} + /** * Zod schema for JSON Schema primitive types */ @@ -133,18 +155,42 @@ const NormalizedToolSchemaInternal: z.ZodType, z.ZodType }) .passthrough() .transform((schema) => { - const { type, required, properties, additionalProperties, format, ...rest } = schema + const { + type, + required, + properties, + additionalProperties, + format, + items, + minItems, + maxItems, + uniqueItems, + ...rest + } = schema const result: Record = { ...rest } // Determine if this schema represents an object type const isObjectType = type === "object" || (Array.isArray(type) && type.includes("object")) || properties !== undefined + // Collect array-specific properties for potential use in type handling + const arrayProps = { items, minItems, maxItems, uniqueItems } + // If type is an array, convert to anyOf format (JSON Schema 2020-12) + // Array-specific properties must be moved inside the array variant if (Array.isArray(type)) { - result.anyOf = type.map((t) => ({ type: t })) + result.anyOf = type.map((t) => { + if (t === "array") { + return applyArrayProperties({ type: t }, arrayProps) + } + return { type: t } + }) } else if (type !== undefined) { result.type = type + // For single "array" type, preserve array-specific properties at root + if (type === "array") { + applyArrayProperties(result, arrayProps) + } } // Strip unsupported format values for OpenAI compatibility