Strip incompatible JSON Schema keywords from tool schemas

This commit is contained in:
skfallin
2026-04-02 13:50:47 +02:00
parent 5cd95f4bb1
commit 0c88dea247
4 changed files with 104 additions and 3 deletions

View File

@@ -29,6 +29,7 @@ import {
resolveCodexApiCredentials, resolveCodexApiCredentials,
resolveProviderRequest, resolveProviderRequest,
} from './providerConfig.js' } from './providerConfig.js'
import { stripIncompatibleSchemaKeywords } from '../../utils/schemaSanitizer.js'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types — minimal subset of Anthropic SDK types we need to produce // Types — minimal subset of Anthropic SDK types we need to produce
@@ -235,11 +236,12 @@ function normalizeSchemaForOpenAI(
schema: Record<string, unknown>, schema: Record<string, unknown>,
strict = true, strict = true,
): Record<string, unknown> { ): Record<string, unknown> {
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) { const sanitizedSchema = stripIncompatibleSchemaKeywords(schema)
return (schema ?? {}) as Record<string, unknown> if (!sanitizedSchema || typeof sanitizedSchema !== 'object' || Array.isArray(sanitizedSchema)) {
return (sanitizedSchema ?? {}) as Record<string, unknown>
} }
const record = { ...schema } const record = { ...sanitizedSchema }
if (record.type === 'object' && record.properties) { if (record.type === 'object' && record.properties) {
const properties = record.properties as Record<string, Record<string, unknown>> const properties = record.properties as Record<string, Record<string, unknown>>

67
src/utils/api.test.ts Normal file
View File

@@ -0,0 +1,67 @@
import { expect, test } from 'bun:test'
import { z } from 'zod/v4'
import { getEmptyToolPermissionContext, type Tool, type Tools } from '../Tool.js'
import { toolToAPISchema } from './api.js'
test('toolToAPISchema strips incompatible schema keywords from input_schema', async () => {
const schema = await toolToAPISchema(
{
name: 'WebFetch',
inputSchema: z.strictObject({}),
inputJSONSchema: {
type: 'object',
properties: {
url: {
type: 'string',
format: 'uri',
description: 'Public HTTP or HTTPS URL',
},
metadata: {
type: 'object',
properties: {
callback: {
type: 'string',
format: 'uri-reference',
},
},
},
},
},
prompt: async () => 'Fetch a URL',
} as unknown as Tool,
{
getToolPermissionContext: async () => getEmptyToolPermissionContext(),
tools: [] as unknown as Tools,
agents: [],
},
)
expect(schema).toMatchObject({
input_schema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'Public HTTP or HTTPS URL',
},
metadata: {
type: 'object',
properties: {
callback: {
type: 'string',
},
},
},
},
},
})
const inputSchema = (schema as { input_schema: Record<string, unknown> }).input_schema
const properties = inputSchema.properties as Record<string, Record<string, unknown>>
expect(properties.url?.format).toBeUndefined()
expect(
(
properties.metadata?.properties as Record<string, Record<string, unknown>>
)?.callback?.format,
).toBeUndefined()
})

View File

@@ -60,6 +60,7 @@ import {
import { getPlatform } from './platform.js' import { getPlatform } from './platform.js'
import { countFilesRoundedRg } from './ripgrep.js' import { countFilesRoundedRg } from './ripgrep.js'
import { jsonStringify } from './slowOperations.js' import { jsonStringify } from './slowOperations.js'
import { stripIncompatibleSchemaKeywords } from './schemaSanitizer.js'
import type { SystemPrompt } from './systemPromptType.js' import type { SystemPrompt } from './systemPromptType.js'
import { getToolSchemaCache } from './toolSchemaCache.js' import { getToolSchemaCache } from './toolSchemaCache.js'
import { windowsPathToPosixPath } from './windowsPaths.js' import { windowsPathToPosixPath } from './windowsPaths.js'
@@ -165,6 +166,7 @@ export async function toolToAPISchema(
if (!isAgentSwarmsEnabled()) { if (!isAgentSwarmsEnabled()) {
input_schema = filterSwarmFieldsFromSchema(tool.name, input_schema) input_schema = filterSwarmFieldsFromSchema(tool.name, input_schema)
} }
input_schema = stripIncompatibleSchemaKeywords(input_schema)
base = { base = {
name: tool.name, name: tool.name,

View File

@@ -0,0 +1,30 @@
/**
* Anthropic-compatible tool schemas reject several JSON Schema keywords that
* Zod commonly emits, especially string `format` validators like `uri`.
* Strip those fields recursively before sending tool schemas to providers.
*/
export function stripIncompatibleSchemaKeywords<T>(
schema: T,
): T {
if (Array.isArray(schema)) {
return schema.map(item => stripIncompatibleSchemaKeywords(item)) as T
}
if (!schema || typeof schema !== 'object') {
return schema
}
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(schema as Record<string, unknown>)) {
if (key === '$schema' || key === 'format' || key === 'propertyNames') {
continue
}
result[key] =
value && typeof value === 'object'
? stripIncompatibleSchemaKeywords(value)
: value
}
return result as T
}