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
17 changes: 13 additions & 4 deletions src/api/providers/__tests__/base-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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", () => {
Expand Down
17 changes: 15 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,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<string, unknown>)

if (!normalized || typeof normalized !== "object" || normalized.type !== "object") {
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
Loading