From 351d15eac457d1b8eb406883a7505cba2997146e Mon Sep 17 00:00:00 2001 From: umi008 Date: Fri, 10 Jul 2026 20:22:19 -0600 Subject: [PATCH 1/4] fix(providers): normalize tool schemas for JSON Schema 2020-12 in OpenAI Compatible path Call normalizeToolSchema() before applying OpenAI-specific strict mode transforms in convertToolSchemaForOpenAI(). This ensures tool schemas are 2020-12 compliant (type arrays -> anyOf, unsupported formats stripped) before backends like Bedrock validate them through OpenAI-compatible proxies like LiteLLM. Previously, only the direct provider paths (bedrock, vscode-lm) applied this normalization. The 15+ providers that inherit from BaseProvider and use convertToolsForOpenAI() were sending raw schemas that Bedrock rejects with 'maxItems is not supported' errors. Fixes #869 --- src/api/providers/base-provider.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/api/providers/base-provider.ts b/src/api/providers/base-provider.ts index 89366fb619..ff83b9d33a 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,21 @@ 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" || normalized.type !== "object") { + return normalized + } + + const result = { ...normalized } // OpenAI Responses API requires additionalProperties: false on all object schemas // Only add if not already set to false (to avoid unnecessary mutations) From feb79dd3a6f15cec26a6a436beeab8bb479a4502 Mon Sep 17 00:00:00 2001 From: umi008 Date: Fri, 10 Jul 2026 20:43:25 -0600 Subject: [PATCH 2/4] fix(providers): add explicit type to result after normalizeToolSchema call The spread of normalied (Record) caused TS7053 because the inferred type was {} and string indexing was not allowed. Added explicit Record type annotation to fix compilation. --- src/api/providers/base-provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/providers/base-provider.ts b/src/api/providers/base-provider.ts index ff83b9d33a..c540b95f1f 100644 --- a/src/api/providers/base-provider.ts +++ b/src/api/providers/base-provider.ts @@ -81,7 +81,7 @@ export abstract class BaseProvider implements ApiHandler { return normalized } - const result = { ...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) From 826eb839e413981e4c630e0a0a80e12d8c56c07d Mon Sep 17 00:00:00 2001 From: umi008 Date: Fri, 10 Jul 2026 21:19:15 -0600 Subject: [PATCH 3/4] test(providers): update schema tests for normalizeToolSchema normalization Two test updates after adding normalizeToolSchema() to the schema pipeline: 1. Nullable types test: Now asserts anyOf conversion instead of null stripping. normalizeToolSchema() converts type arrays to anyOf for JSON Schema 2020-12 compliance, which is correct behavior. 2. Non-object schemas test: Verifies type preservation through normalization. additionalProperties is NOT added to non-object schemas (strings, numbers, etc.) because the JSON Schema spec says it's only valid on object types. --- .../providers/__tests__/base-provider.spec.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/api/providers/__tests__/base-provider.spec.ts b/src/api/providers/__tests__/base-provider.spec.ts index ced452f5a5..8b7b2c5070 100644 --- a/src/api/providers/__tests__/base-provider.spec.ts +++ b/src/api/providers/__tests__/base-provider.spec.ts @@ -140,7 +140,7 @@ describe("BaseProvider", () => { expect(result.properties.level1.properties.level2.properties.level3.additionalProperties).toBe(false) }) - it("should convert nullable types to non-nullable", () => { + it("should convert nullable types to anyOf (JSON Schema 2020-12)", () => { const schema = { type: "object", properties: { @@ -150,14 +150,23 @@ 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" }, + ]) }) - it("should return non-object schemas unchanged", () => { + it("should return non-object schemas with normalization applied", () => { const schema = { type: "string" } const result = provider.testConvertToolSchemaForOpenAI(schema) - expect(result).toEqual(schema) + // Non-object schemas pass through normalization preserving their + // core type. additionalProperties is not added because JSON Schema + // spec says it's only valid on object types. + expect(result.type).toBe("string") }) it("should return null/undefined unchanged", () => { From 8289a9abd91d3e42a3b1993dee35ccf8bec37f1e Mon Sep 17 00:00:00 2001 From: umi008 Date: Fri, 17 Jul 2026 03:29:58 +0000 Subject: [PATCH 4/4] fix(providers): strip unsupported bedrock keywords, handle nullable objects, improve tests - Remove minItems/maxItems from ARRAY_SPECIFIC_PROPERTIES so they dont get forwarded to array-branch schemas (Bedrock rejects them) - Also strip minLength, maxLength, pattern from normalize output - Fix nullable object handling: check for properties field as well as type=object so nullable schemas (converted to anyOf) still get strict-mode processing (required/boolean/null removal) - Add test for maxItems stripping on array schemas - Add assertion that stale type array field is removed after normalization - Fix non-object schema test to use type: ["string"] (actually changed by normalization) and assert anyOf output --- .../providers/__tests__/base-provider.spec.ts | 46 +++++++++++++++---- src/api/providers/base-provider.ts | 11 ++++- src/utils/json-schema.ts | 7 ++- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/api/providers/__tests__/base-provider.spec.ts b/src/api/providers/__tests__/base-provider.spec.ts index 8b7b2c5070..e6b632998b 100644 --- a/src/api/providers/__tests__/base-provider.spec.ts +++ b/src/api/providers/__tests__/base-provider.spec.ts @@ -140,6 +140,28 @@ describe("BaseProvider", () => { expect(result.properties.level1.properties.level2.properties.level3.additionalProperties).toBe(false) }) + 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", @@ -153,20 +175,24 @@ describe("BaseProvider", () => { // 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" }, - ]) + 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 with normalization applied", () => { - 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) - // Non-object schemas pass through normalization preserving their - // core type. additionalProperties is not added because JSON Schema - // spec says it's only valid on object types. - expect(result.type).toBe("string") + // 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 c540b95f1f..d1491555a9 100644 --- a/src/api/providers/base-provider.ts +++ b/src/api/providers/base-provider.ts @@ -77,7 +77,16 @@ export abstract class BaseProvider implements ApiHandler { // backends like Bedrock that enforce strict 2020-12 compliance. const normalized = normalizeToolSchema(schema as Record) - if (!normalized || typeof normalized !== "object" || normalized.type !== "object") { + 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 } 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 }