diff --git a/src/api/providers/__tests__/base-provider.spec.ts b/src/api/providers/__tests__/base-provider.spec.ts index ced452f5a5..e6b632998b 100644 --- a/src/api/providers/__tests__/base-provider.spec.ts +++ b/src/api/providers/__tests__/base-provider.spec.ts @@ -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)", () => { const schema = { type: "object", properties: { @@ -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", () => { diff --git a/src/api/providers/base-provider.ts b/src/api/providers/base-provider.ts index 89366fb619..d1491555a9 100644 --- a/src/api/providers/base-provider.ts +++ b/src/api/providers/base-provider.ts @@ -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" /** @@ -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) @@ -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) + + 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 = { ...normalized } // OpenAI Responses API requires additionalProperties: false on all object schemas // Only add if not already set to false (to avoid unnecessary mutations) diff --git a/src/utils/json-schema.ts b/src/utils/json-schema.ts index cbcd3486d2..c126c90248 100644 --- a/src/utils/json-schema.ts +++ b/src/utils/json-schema.ts @@ -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. @@ -165,6 +167,9 @@ const NormalizedToolSchemaInternal: z.ZodType, z.ZodType minItems, maxItems, uniqueItems, + minLength, + maxLength, + pattern, ...rest } = schema const result: Record = { ...rest }