fix: preserve only originally-required properties in strict tool schemas (#471)

Fixes #430. In normalizeSchemaForOpenAI(), the strict branch was adding every
property key to required[], including optional ones. This caused providers like
Groq, Azure OpenAI, and others to reject valid tool calls with a 400 /
tool_use_failed error because the model correctly omits optional arguments but
the provider sees them as missing required fields.

Root cause: the strict branch used `[...existingRequired, ...allKeys]` instead
of `existingRequired.filter(k => k in normalizedProps)`. The Gemini branch
already had the correct logic.

Fix: align the strict branch with the Gemini branch — only keep properties that
were already marked required in the original schema. The additionalProperties:
false constraint is preserved as strict-mode providers still require it.

Add regression test covering the Read tool schema (file_path required,
offset/limit/pages optional).
This commit is contained in:
Juan Camilo Auriti
2026-04-08 10:42:11 +02:00
committed by GitHub
parent 2caf2fd982
commit ccaa193eec
2 changed files with 68 additions and 13 deletions

View File

@@ -421,11 +421,13 @@ function normalizeSchemaForOpenAI(
record.properties = normalizedProps
if (strict) {
// OpenAI strict mode requires every property to be listed in required[]
const allKeys = Object.keys(normalizedProps)
record.required = Array.from(new Set([...existingRequired, ...allKeys]))
// OpenAI strict mode requires additionalProperties: false on all object
// schemas — override unconditionally to ensure nested objects comply.
// Keep only the properties that were originally marked required in the schema.
// Adding every property to required[] (the previous behaviour) caused strict
// OpenAI-compatible providers (Groq, Azure, etc.) to reject tool calls because
// the model correctly omits optional arguments — but the provider treats them
// as missing required fields and returns a 400 / tool_use_failed error.
record.required = existingRequired.filter(k => k in normalizedProps)
// additionalProperties: false is still required by strict-mode providers.
record.additionalProperties = false
} else {
// For Gemini: keep only existing required keys that are present in properties