Strip incompatible JSON Schema keywords from tool schemas
This commit is contained in:
@@ -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
67
src/utils/api.test.ts
Normal 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()
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
|
|||||||
30
src/utils/schemaSanitizer.ts
Normal file
30
src/utils/schemaSanitizer.ts
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user