Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/api/providers/__tests__/bedrock-native-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})

Expand Down
37 changes: 27 additions & 10 deletions src/utils/__tests__/json-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
})
})
Expand Down Expand Up @@ -97,15 +97,15 @@ 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: {
type: "object",
properties: {
path: { type: "string" },
line_ranges: {
anyOf: [{ type: "array" }, { type: "null" }],
items: { type: "integer" },
anyOf: [{ type: "array", items: { type: "integer" } }, { type: "null" }],
},
},
additionalProperties: false,
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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")
})

Expand Down
35 changes: 33 additions & 2 deletions src/utils/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,49 @@ 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

// If type is an array, convert to anyOf format (JSON Schema 2020-12)
// Array-specific properties (items, minItems, maxItems, uniqueItems) must be moved
// inside the array variant, not left at root level where they're invalid
if (Array.isArray(type)) {
result.anyOf = type.map((t) => ({ type: t }))
result.anyOf = type.map((t) => {
if (t === "array") {
// Move array-specific properties into the array variant
const arrayVariant: Record<string, unknown> = { type: t }
if (items !== undefined) arrayVariant.items = items
if (minItems !== undefined) arrayVariant.minItems = minItems
if (maxItems !== undefined) arrayVariant.maxItems = maxItems
if (uniqueItems !== undefined) arrayVariant.uniqueItems = uniqueItems
return arrayVariant
}
return { type: t }
})
} else if (type !== undefined) {
result.type = type
// For single "array" type, preserve array-specific properties at root
if (type === "array") {
if (items !== undefined) result.items = items
if (minItems !== undefined) result.minItems = minItems
if (maxItems !== undefined) result.maxItems = maxItems
if (uniqueItems !== undefined) result.uniqueItems = uniqueItems
}
}

// Strip unsupported format values for OpenAI compatibility
Expand Down
Loading