feat: enhance tool conversion to support strict mode based on schema validation

This commit is contained in:
Daniel
2026-04-01 15:51:24 +02:00
committed by Kevin Codex
parent 8750f84464
commit 372ba31c17
2 changed files with 141 additions and 7 deletions

View File

@@ -6,6 +6,7 @@ import {
codexStreamToAnthropic, codexStreamToAnthropic,
convertAnthropicMessagesToResponsesInput, convertAnthropicMessagesToResponsesInput,
convertCodexResponseToAnthropicMessage, convertCodexResponseToAnthropicMessage,
convertToolsToResponsesTools,
} from './codexShim.js' } from './codexShim.js'
import { import {
resolveCodexApiCredentials, resolveCodexApiCredentials,
@@ -71,6 +72,77 @@ describe('Codex provider config', () => {
}) })
describe('Codex request translation', () => { describe('Codex request translation', () => {
test('disables strict mode for tools with optional parameters', () => {
const tools = convertToolsToResponsesTools([
{
name: 'Agent',
description: 'Spawn a sub-agent',
input_schema: {
type: 'object',
properties: {
description: { type: 'string' },
prompt: { type: 'string' },
subagent_type: { type: 'string' },
},
required: ['description', 'prompt'],
additionalProperties: false,
},
},
])
expect(tools).toEqual([
{
type: 'function',
name: 'Agent',
description: 'Spawn a sub-agent',
parameters: {
type: 'object',
properties: {
description: { type: 'string' },
prompt: { type: 'string' },
subagent_type: { type: 'string' },
},
required: ['description', 'prompt'],
additionalProperties: false,
},
},
])
})
test('keeps strict mode for tools whose schema already matches Responses requirements', () => {
const tools = convertToolsToResponsesTools([
{
name: 'Ping',
description: 'Ping tool',
input_schema: {
type: 'object',
properties: {
value: { type: 'string' },
},
required: ['value'],
additionalProperties: false,
},
},
])
expect(tools).toEqual([
{
type: 'function',
name: 'Ping',
description: 'Ping tool',
parameters: {
type: 'object',
properties: {
value: { type: 'string' },
},
required: ['value'],
additionalProperties: false,
},
strict: true,
},
])
})
test('converts assistant tool use and user tool result into Responses items', () => { test('converts assistant tool use and user tool result into Responses items', () => {
const items = convertAnthropicMessagesToResponsesInput([ const items = convertAnthropicMessagesToResponsesInput([
{ {

View File

@@ -300,13 +300,75 @@ export function convertToolsToResponsesTools(
): ResponsesTool[] { ): ResponsesTool[] {
return tools return tools
.filter(tool => tool.name && tool.name !== 'ToolSearchTool') .filter(tool => tool.name && tool.name !== 'ToolSearchTool')
.map(tool => ({ .map(tool => {
type: 'function', const parameters = tool.input_schema ?? { type: 'object', properties: {} }
name: tool.name ?? 'tool',
description: tool.description ?? '', return {
parameters: tool.input_schema ?? { type: 'object', properties: {} }, type: 'function',
strict: true, name: tool.name ?? 'tool',
})) description: tool.description ?? '',
parameters,
...(isStrictResponsesSchema(parameters) ? { strict: true } : {}),
}
})
}
function isStrictResponsesSchema(schema: unknown): boolean {
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) {
return true
}
const record = schema as Record<string, unknown>
const type = record.type
if (type === 'object') {
const properties =
record.properties && typeof record.properties === 'object' && !Array.isArray(record.properties)
? (record.properties as Record<string, unknown>)
: {}
const propertyKeys = Object.keys(properties)
const required = Array.isArray(record.required)
? record.required.filter((value): value is string => typeof value === 'string')
: null
if (propertyKeys.length > 0) {
if (!required) return false
const requiredSet = new Set(required)
for (const key of propertyKeys) {
if (!requiredSet.has(key)) {
return false
}
}
}
for (const child of Object.values(properties)) {
if (!isStrictResponsesSchema(child)) {
return false
}
}
}
const combinators = ['anyOf', 'oneOf', 'allOf'] as const
for (const key of combinators) {
if (key in record) {
const value = record[key]
if (!Array.isArray(value) || value.some(item => !isStrictResponsesSchema(item))) {
return false
}
}
}
if ('items' in record) {
const items = record.items
if (Array.isArray(items)) {
return items.every(item => isStrictResponsesSchema(item))
}
return isStrictResponsesSchema(items)
}
return true
} }
function convertToolChoice(toolChoice: unknown): unknown { function convertToolChoice(toolChoice: unknown): unknown {