From 372ba31c17fcf531f225d6c6791ed98bcba0100d Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 1 Apr 2026 15:51:24 +0200 Subject: [PATCH] feat: enhance tool conversion to support strict mode based on schema validation --- src/services/api/codexShim.test.ts | 72 ++++++++++++++++++++++++++++ src/services/api/codexShim.ts | 76 +++++++++++++++++++++++++++--- 2 files changed, 141 insertions(+), 7 deletions(-) diff --git a/src/services/api/codexShim.test.ts b/src/services/api/codexShim.test.ts index 176cbc25..6597c0f7 100644 --- a/src/services/api/codexShim.test.ts +++ b/src/services/api/codexShim.test.ts @@ -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([ { diff --git a/src/services/api/codexShim.ts b/src/services/api/codexShim.ts index 16ed184f..7e9a07f2 100644 --- a/src/services/api/codexShim.ts +++ b/src/services/api/codexShim.ts @@ -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 + const type = record.type + + if (type === 'object') { + const properties = + record.properties && typeof record.properties === 'object' && !Array.isArray(record.properties) + ? (record.properties as Record) + : {} + + 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 {