Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
45 changes: 40 additions & 5 deletions src/api/providers/__tests__/base-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,29 @@ describe("BaseProvider", () => {
expect(result.properties.level1.properties.level2.properties.level3.additionalProperties).toBe(false)
})

it("should convert nullable types to non-nullable", () => {
it("should strip maxItems from array schemas for Bedrock compatibility", () => {
const schema = {
type: "object",
properties: {
names: {
type: "array",
items: { type: "string" },
maxItems: 4,
},
},
}

const result = provider.testConvertToolSchemaForOpenAI(schema)

// maxItems should be stripped by normalizeToolSchema for Bedrock
// compatibility (Bedrock doesn't support maxItems in its JSON Schema
// 2020-12 subset)
expect(result.properties.names.type).toBe("array")
expect(result.properties.names.items).toEqual({ type: "string" })
expect(result.properties.names.maxItems).toBeUndefined()
})

it("should convert nullable types to anyOf (JSON Schema 2020-12)", () => {
Comment thread
umi008 marked this conversation as resolved.
const schema = {
type: "object",
properties: {
Expand All @@ -150,14 +172,27 @@ describe("BaseProvider", () => {

const result = provider.testConvertToolSchemaForOpenAI(schema)

expect(result.properties.name.type).toBe("string")
// After normalization, type arrays are converted to anyOf for
// JSON Schema 2020-12 compliance. The nullable types are no longer
// stripped — they're properly represented as anyOf alternatives.
expect(result.properties.name.anyOf).toEqual([{ type: "string" }, { type: "null" }])
// Verify the stale type array field is gone after normalization
expect(result.properties.name.type).toBeUndefined()
})

it("should return non-object schemas unchanged", () => {
const schema = { type: "string" }
it("should flatten type arrays on non-object schemas", () => {
// Use type: ["string"] (single-element type array) which normalization
// converts to anyOf format (JSON Schema 2020-12 compliance) - proving
// non-object schemas are processed through normalizeToolSchema
const schema = { type: ["string"] }
const result = provider.testConvertToolSchemaForOpenAI(schema)

expect(result).toEqual(schema)
// The type array is normalized to anyOf format, type field is dropped
expect(result.type).toBeUndefined()
expect(result.anyOf).toBeDefined()
expect(result.anyOf).toContainEqual({ type: "string" })
// Normalization does not add additionalProperties to non-object schemas
expect(result.additionalProperties).toBeUndefined()
})

it("should return null/undefined unchanged", () => {
Expand Down
26 changes: 24 additions & 2 deletions src/api/providers/base-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { ApiHandler, ApiHandlerCreateMessageMetadata } from "../index"
import { ApiStream } from "../transform/stream"
import { countTokens } from "../../utils/countTokens"
import { isMcpTool } from "../../utils/mcp-name"
import { normalizeToolSchema } from "../../utils/json-schema"
import { getApiRequestTimeout } from "./utils/timeout-config"

/**
Expand Down Expand Up @@ -56,6 +57,8 @@ export abstract class BaseProvider implements ApiHandler {

/**
* Converts tool schemas to be compatible with OpenAI's strict mode by:
* - Normalizing for JSON Schema 2020-12 compliance (strips unsupported
* keywords like maxItems that backends like Bedrock reject)
* - Ensuring all properties are in the required array (strict mode requirement)
* - Converting nullable types (["type", "null"]) to non-nullable ("type")
* - Adding additionalProperties: false to all object schemas (required by OpenAI Responses API)
Expand All @@ -64,11 +67,30 @@ export abstract class BaseProvider implements ApiHandler {
* This matches the behavior of ensureAllRequired in openai-native.ts
*/
protected convertToolSchemaForOpenAI(schema: any): any {
if (!schema || typeof schema !== "object" || schema.type !== "object") {
if (!schema || typeof schema !== "object") {
return schema
}

const result = { ...schema }
// Normalize for JSON Schema 2020-12 compliance first: converts
// deprecated type arrays to anyOf, strips unsupported format
// values, and adds additionalProperties: false. Required for
// backends like Bedrock that enforce strict 2020-12 compliance.
const normalized = normalizeToolSchema(schema as Record<string, unknown>)
Comment thread
umi008 marked this conversation as resolved.

if (!normalized || typeof normalized !== "object") {
return normalized
}

// Check if the schema represents an object type:
// - Direct type: "object" (standard case)
// - Has "properties" (handles nullable objects that were converted to
// anyOf by normalization — type is no longer "object" but properties
// are preserved at the top level so strict-mode processing still applies)
if (normalized.type !== "object" && !normalized.properties) {
return normalized
}

const result: Record<string, any> = { ...normalized }

// OpenAI Responses API requires additionalProperties: false on all object schemas
// Only add if not already set to false (to avoid unnecessary mutations)
Expand Down
7 changes: 6 additions & 1 deletion src/utils/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ const OPENAI_SUPPORTED_FORMATS = new Set([
/**
* Array-specific JSON Schema properties that must be nested inside array type variants
* when converting to anyOf format (JSON Schema draft 2020-12).
* Note: maxItems and minItems are excluded from this list because AWS Bedrock
* does not support them in its JSON Schema 2020-12 implementation.
*/
const ARRAY_SPECIFIC_PROPERTIES = ["items", "minItems", "maxItems", "uniqueItems"] as const
const ARRAY_SPECIFIC_PROPERTIES = ["items", "uniqueItems"] as const

/**
* Applies array-specific properties from source to target object.
Expand Down Expand Up @@ -165,6 +167,9 @@ const NormalizedToolSchemaInternal: z.ZodType<Record<string, unknown>, z.ZodType
minItems,
maxItems,
uniqueItems,
minLength,
maxLength,
pattern,
...rest
} = schema
const result: Record<string, unknown> = { ...rest }
Expand Down
Loading