mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: move array-specific properties into anyOf variant in normalizeToolSchema (#10276)
* fix: move array-specific properties into anyOf variant in normalizeToolSchema
Fixes read_file tool schema rejection with GPT-5-mini which requires
items property to be inside the { type: 'array' } variant when using
anyOf for nullable arrays.
Resolves ROO-262
* refactor: extract array-specific properties constant and helper function
This commit is contained in:
parent
6b141fbb25
commit
7fae76ec86
3 changed files with 79 additions and 15 deletions
|
|
@ -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")
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, Record<string, unknown>>
|
||||
const filesItems = properties.files.items as Record<string, unknown>
|
||||
const filesItemsProps = filesItems.properties as Record<string, Record<string, unknown>>
|
||||
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<string, unknown>).files as Record<string, unknown>
|
||||
const items = files.items as Record<string, unknown>
|
||||
const props = items.properties as Record<string, Record<string, unknown>>
|
||||
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")
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>,
|
||||
source: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
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<Record<string, unknown>, 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<string, unknown> = { ...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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue