Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion pkg/model/provider/openai/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,42 @@ func walkSchema(schema map[string]any, fn func(map[string]any)) {
// makeAllRequired makes all object properties "required" throughout the schema,
// because that's what the OpenAI Response API demands.
// Properties that were not originally required are made nullable.
// Also ensures all object-type schemas have additionalProperties: false.
func makeAllRequired(schema shared.FunctionParameters) shared.FunctionParameters {
if schema == nil {
schema = map[string]any{"type": "object", "properties": map[string]any{}}
}

walkSchema(schema, func(node map[string]any) {
// Check if this node is an object type (either "object" or ["object", ...])
isObject := false
if typeVal, ok := node["type"]; ok {
switch t := typeVal.(type) {
case string:
isObject = t == "object"
case []any:
for _, v := range t {
if s, ok := v.(string); ok && s == "object" {
isObject = true
break
}
}
case []string:
isObject = slices.Contains(t, "object")
}
}

// All object types must have additionalProperties: false for OpenAI Responses API strict mode
// But only set it if additionalProperties is not already defined as an object schema
if isObject {
if addProps, exists := node["additionalProperties"]; !exists || addProps == nil || addProps == true {
node["additionalProperties"] = false
}
// If additionalProperties is already set to false or is an object schema (map[string]any),
// leave it as is - the object schema case will be walked separately
}

// If the node has explicit properties, make them all required
properties, ok := node["properties"].(map[string]any)
if !ok {
return
Expand Down Expand Up @@ -88,7 +118,6 @@ func makeAllRequired(schema shared.FunctionParameters) shared.FunctionParameters
}

node["required"] = newRequired
node["additionalProperties"] = false
})

return schema
Expand Down
39 changes: 39 additions & 0 deletions pkg/model/provider/openai/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,45 @@ func TestRemoveFormatFields_NoProperties(t *testing.T) {
assert.Equal(t, schema, updated)
}

func TestMakeAllRequired_TypeArrayWithObject(t *testing.T) {
// Reproduces the user_prompt tool schema where a property has
// type: ["object", "null"] with nested properties. OpenAI requires
// these nested properties to also have additionalProperties: false.
schema := shared.FunctionParameters{
"type": "object",
"properties": map[string]any{
"schema": map[string]any{
"type": []string{"object", "null"},
"properties": map[string]any{
"name": map[string]any{"type": "string"},
"age": map[string]any{"type": "number"},
},
"required": []any{"name"},
},
},
"required": []any{"schema"},
}

updated := makeAllRequired(schema)

// Top-level should have additionalProperties: false
assert.Equal(t, false, updated["additionalProperties"])

// The schema property should also have additionalProperties: false
schemaProps := updated["properties"].(map[string]any)["schema"].(map[string]any)
assert.Equal(t, false, schemaProps["additionalProperties"])

// All properties in schema should be required
schemaRequired := schemaProps["required"].([]any)
assert.Len(t, schemaRequired, 2)
assert.Contains(t, schemaRequired, "name")
assert.Contains(t, schemaRequired, "age")

// age was not originally required, so its type should be nullable
age := schemaProps["properties"].(map[string]any)["age"].(map[string]any)
assert.Equal(t, []string{"number", "null"}, age["type"])
}

func TestFixSchemaArrayItems(t *testing.T) {
schema := `{
"properties": {
Expand Down
Loading