feat: enhance tool conversion to support strict mode based on schema validation
This commit is contained in:
@@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user