feat: enhance tool conversion to support strict mode based on schema validation
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
codexStreamToAnthropic,
|
||||
convertAnthropicMessagesToResponsesInput,
|
||||
convertCodexResponseToAnthropicMessage,
|
||||
convertToolsToResponsesTools,
|
||||
} from './codexShim.js'
|
||||
import {
|
||||
resolveCodexApiCredentials,
|
||||
@@ -71,6 +72,77 @@ describe('Codex provider config', () => {
|
||||
})
|
||||
|
||||
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', () => {
|
||||
const items = convertAnthropicMessagesToResponsesInput([
|
||||
{
|
||||
|
||||
@@ -300,13 +300,75 @@ export function convertToolsToResponsesTools(
|
||||
): ResponsesTool[] {
|
||||
return tools
|
||||
.filter(tool => tool.name && tool.name !== 'ToolSearchTool')
|
||||
.map(tool => ({
|
||||
type: 'function',
|
||||
name: tool.name ?? 'tool',
|
||||
description: tool.description ?? '',
|
||||
parameters: tool.input_schema ?? { type: 'object', properties: {} },
|
||||
strict: true,
|
||||
}))
|
||||
.map(tool => {
|
||||
const parameters = tool.input_schema ?? { type: 'object', properties: {} }
|
||||
|
||||
return {
|
||||
type: 'function',
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user