Skip to content

Commit d6aae55

Browse files
authored
Merge pull request #2168 from gtardif/fix_openai_tool_call_params
Fix schema conversion for OpenAI Responses API strict mode - Fixes tool calls with gpt-4.1-nano
2 parents 44a1185 + 516ddb3 commit d6aae55

2 files changed

Lines changed: 69 additions & 1 deletion

File tree

pkg/model/provider/openai/schema.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,42 @@ func walkSchema(schema map[string]any, fn func(map[string]any)) {
5555
// makeAllRequired makes all object properties "required" throughout the schema,
5656
// because that's what the OpenAI Response API demands.
5757
// Properties that were not originally required are made nullable.
58+
// Also ensures all object-type schemas have additionalProperties: false.
5859
func makeAllRequired(schema shared.FunctionParameters) shared.FunctionParameters {
5960
if schema == nil {
6061
schema = map[string]any{"type": "object", "properties": map[string]any{}}
6162
}
6263

6364
walkSchema(schema, func(node map[string]any) {
65+
// Check if this node is an object type (either "object" or ["object", ...])
66+
isObject := false
67+
if typeVal, ok := node["type"]; ok {
68+
switch t := typeVal.(type) {
69+
case string:
70+
isObject = t == "object"
71+
case []any:
72+
for _, v := range t {
73+
if s, ok := v.(string); ok && s == "object" {
74+
isObject = true
75+
break
76+
}
77+
}
78+
case []string:
79+
isObject = slices.Contains(t, "object")
80+
}
81+
}
82+
83+
// All object types must have additionalProperties: false for OpenAI Responses API strict mode
84+
// But only set it if additionalProperties is not already defined as an object schema
85+
if isObject {
86+
if addProps, exists := node["additionalProperties"]; !exists || addProps == nil || addProps == true {
87+
node["additionalProperties"] = false
88+
}
89+
// If additionalProperties is already set to false or is an object schema (map[string]any),
90+
// leave it as is - the object schema case will be walked separately
91+
}
92+
93+
// If the node has explicit properties, make them all required
6494
properties, ok := node["properties"].(map[string]any)
6595
if !ok {
6696
return
@@ -88,7 +118,6 @@ func makeAllRequired(schema shared.FunctionParameters) shared.FunctionParameters
88118
}
89119

90120
node["required"] = newRequired
91-
node["additionalProperties"] = false
92121
})
93122

94123
return schema

pkg/model/provider/openai/schema_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,45 @@ func TestRemoveFormatFields_NoProperties(t *testing.T) {
333333
assert.Equal(t, schema, updated)
334334
}
335335

336+
func TestMakeAllRequired_TypeArrayWithObject(t *testing.T) {
337+
// Reproduces the user_prompt tool schema where a property has
338+
// type: ["object", "null"] with nested properties. OpenAI requires
339+
// these nested properties to also have additionalProperties: false.
340+
schema := shared.FunctionParameters{
341+
"type": "object",
342+
"properties": map[string]any{
343+
"schema": map[string]any{
344+
"type": []string{"object", "null"},
345+
"properties": map[string]any{
346+
"name": map[string]any{"type": "string"},
347+
"age": map[string]any{"type": "number"},
348+
},
349+
"required": []any{"name"},
350+
},
351+
},
352+
"required": []any{"schema"},
353+
}
354+
355+
updated := makeAllRequired(schema)
356+
357+
// Top-level should have additionalProperties: false
358+
assert.Equal(t, false, updated["additionalProperties"])
359+
360+
// The schema property should also have additionalProperties: false
361+
schemaProps := updated["properties"].(map[string]any)["schema"].(map[string]any)
362+
assert.Equal(t, false, schemaProps["additionalProperties"])
363+
364+
// All properties in schema should be required
365+
schemaRequired := schemaProps["required"].([]any)
366+
assert.Len(t, schemaRequired, 2)
367+
assert.Contains(t, schemaRequired, "name")
368+
assert.Contains(t, schemaRequired, "age")
369+
370+
// age was not originally required, so its type should be nullable
371+
age := schemaProps["properties"].(map[string]any)["age"].(map[string]any)
372+
assert.Equal(t, []string{"number", "null"}, age["type"])
373+
}
374+
336375
func TestFixSchemaArrayItems(t *testing.T) {
337376
schema := `{
338377
"properties": {

0 commit comments

Comments
 (0)