Skip to content
Merged
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
87 changes: 78 additions & 9 deletions src/api/providers/__tests__/bedrock-native-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,90 @@ describe("AwsBedrockHandler Native Tool Calling", () => {
const bedrockTools = convertToolsForBedrock(testTools)

expect(bedrockTools).toHaveLength(2)
expect(bedrockTools[0]).toEqual({
toolSpec: {
name: "read_file",
description: "Read a file from the filesystem",
inputSchema: {
json: {

// Check structure and key properties (normalizeToolSchema adds additionalProperties: false)
const tool = bedrockTools[0]
expect(tool.toolSpec.name).toBe("read_file")
expect(tool.toolSpec.description).toBe("Read a file from the filesystem")
expect(tool.toolSpec.inputSchema.json.type).toBe("object")
expect(tool.toolSpec.inputSchema.json.properties.path.type).toBe("string")
expect(tool.toolSpec.inputSchema.json.properties.path.description).toBe("The path to the file")
expect(tool.toolSpec.inputSchema.json.required).toEqual(["path"])
// normalizeToolSchema adds additionalProperties: false by default
expect(tool.toolSpec.inputSchema.json.additionalProperties).toBe(false)
})

it("should transform type arrays to anyOf for JSON Schema 2020-12 compliance", () => {
const convertToolsForBedrock = (handler as any).convertToolsForBedrock.bind(handler)

// Tools with type: ["string", "null"] syntax (valid in draft-07 but not 2020-12)
const toolsWithNullableTypes = [
{
type: "function" as const,
function: {
name: "execute_command",
description: "Execute a command",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "The path to the file" },
command: { type: "string", description: "The command to execute" },
cwd: {
type: ["string", "null"],
description: "Working directory (optional)",
},
},
required: ["path"],
required: ["command", "cwd"],
},
},
},
})
{
type: "function" as const,
function: {
name: "read_file",
description: "Read files",
parameters: {
type: "object",
properties: {
files: {
type: "array",
items: {
type: "object",
properties: {
path: { type: "string" },
line_ranges: {
type: ["array", "null"],
items: { type: "integer" },
description: "Optional line ranges",
},
},
required: ["path", "line_ranges"],
},
},
},
required: ["files"],
},
},
},
]

const bedrockTools = convertToolsForBedrock(toolsWithNullableTypes)

expect(bedrockTools).toHaveLength(2)

// First tool: cwd should be transformed from type: ["string", "null"] to anyOf
const executeCommandSchema = bedrockTools[0].toolSpec.inputSchema.json as any
expect(executeCommandSchema.properties.cwd.anyOf).toEqual([{ type: "string" }, { type: "null" }])
expect(executeCommandSchema.properties.cwd.type).toBeUndefined()
expect(executeCommandSchema.properties.cwd.description).toBe("Working directory (optional)")

// Second tool: line_ranges should be transformed from type: ["array", "null"] to anyOf
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.type).toBeUndefined()
// items also gets additionalProperties: false from normalization
expect(lineRanges.items.type).toBe("integer")
expect(lineRanges.description).toBe("Optional line ranges")
})

it("should filter non-function tools", () => {
Expand Down
6 changes: 5 additions & 1 deletion src/api/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { ModelInfo as CacheModelInfo } from "../transform/cache-strategy/types"
import { convertToBedrockConverseMessages as sharedConverter } from "../transform/bedrock-converse-format"
import { getModelParams } from "../transform/model-params"
import { shouldUseReasoningBudget } from "../../shared/api"
import { normalizeToolSchema } from "../../utils/json-schema"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"

/************************************************************************************
Expand Down Expand Up @@ -1208,6 +1209,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH

/**
* Convert OpenAI tool definitions to Bedrock Converse format
* Transforms JSON Schema to draft 2020-12 compliant format required by Claude models.
* @param tools Array of OpenAI ChatCompletionTool definitions
* @returns Array of Bedrock Tool definitions
*/
Expand All @@ -1221,7 +1223,9 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
name: tool.function.name,
description: tool.function.description,
inputSchema: {
json: tool.function.parameters as Record<string, unknown>,
// Normalize schema to JSON Schema draft 2020-12 compliant format
// This converts type: ["T", "null"] to anyOf: [{type: "T"}, {type: "null"}]
json: normalizeToolSchema(tool.function.parameters as Record<string, unknown>),
},
},
}) as Tool,
Expand Down
9 changes: 4 additions & 5 deletions src/core/prompts/tools/native-tools/mcp_server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type OpenAI from "openai"
import { McpHub } from "../../../../services/mcp/McpHub"
import { buildMcpToolName } from "../../../../utils/mcp-name"
import { ToolInputSchema, type JsonSchema } from "../../../../utils/json-schema"
import { normalizeToolSchema, type JsonSchema } from "../../../../utils/json-schema"

/**
* Dynamically generates native tool definitions for all enabled tools across connected MCP servers.
Expand Down Expand Up @@ -43,14 +43,13 @@ export function getMcpServerTools(mcpHub?: McpHub): OpenAI.Chat.ChatCompletionTo

const originalSchema = tool.inputSchema as Record<string, unknown> | undefined

// Parse with ToolInputSchema to ensure additionalProperties: false is set recursively
// Normalize schema for JSON Schema 2020-12 compliance (type arrays → anyOf)
let parameters: JsonSchema
if (originalSchema) {
const result = ToolInputSchema.safeParse(originalSchema)
parameters = result.success ? result.data : (originalSchema as JsonSchema)
parameters = normalizeToolSchema(originalSchema) as JsonSchema
} else {
// No schema provided - create a minimal valid schema
parameters = ToolInputSchema.parse({ type: "object" })
parameters = { type: "object", additionalProperties: false } as JsonSchema
}

const toolDefinition: OpenAI.Chat.ChatCompletionTool = {
Expand Down
Loading
Loading