@@ -9,6 +9,8 @@ import (
"maps"
"slices"
"sync"
+
+ "charm.land/fantasy/schema"
)
// StepResult represents the result of a single step in an agent execution.
@@ -912,14 +914,16 @@ func (a *agent) prepareTools(tools []AgentTool, activeTools []string, disableAll
continue
}
info := tool.Info()
+ inputSchema := map[string]any{
+ "type": "object",
+ "properties": info.Parameters,
+ "required": info.Required,
+ }
+ schema.Normalize(inputSchema)
preparedTools = append(preparedTools, FunctionTool{
- Name: info.Name,
- Description: info.Description,
- InputSchema: map[string]any{
- "type": "object",
- "properties": info.Parameters,
- "required": info.Required,
- },
+ Name: info.Name,
+ Description: info.Description,
+ InputSchema: inputSchema,
ProviderOptions: tool.ProviderOptions(),
})
}
@@ -401,3 +401,42 @@ func toSnakeCase(s string) string {
}
return strings.ToLower(result.String())
}
+
+// Normalize recursively normalizes a raw JSON Schema map so it is
+// compatible with providers that reject type-arrays (e.g. OpenAI). Type
+// arrays are converted to anyOf and bare "array" types get "items":{}.
+func Normalize(node map[string]any) {
+ for _, child := range node {
+ switch v := child.(type) {
+ case map[string]any:
+ Normalize(v)
+ case []any:
+ for _, item := range v {
+ if m, ok := item.(map[string]any); ok {
+ Normalize(m)
+ }
+ }
+ }
+ }
+
+ typeArr, ok := node["type"].([]any)
+ if !ok {
+ if node["type"] == "array" {
+ if _, has := node["items"]; !has {
+ node["items"] = map[string]any{}
+ }
+ }
+ return
+ }
+
+ anyOf := make([]any, 0, len(typeArr))
+ for _, t := range typeArr {
+ variant := map[string]any{"type": t}
+ if t == "array" {
+ variant["items"] = map[string]any{}
+ }
+ anyOf = append(anyOf, variant)
+ }
+ delete(node, "type")
+ node["anyOf"] = anyOf
+}
@@ -532,3 +532,108 @@ func TestSchemaToParametersEdgeCases(t *testing.T) {
})
}
}
+
+func TestNormalize_TypeArray(t *testing.T) {
+ t.Parallel()
+
+ node := map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "value": map[string]any{
+ "description": "Config value",
+ "type": []any{"string", "number", "boolean", "object", "array", "null"},
+ },
+ },
+ }
+
+ Normalize(node)
+
+ val := node["properties"].(map[string]any)["value"].(map[string]any)
+ require.Nil(t, val["type"])
+ anyOf, ok := val["anyOf"].([]any)
+ require.True(t, ok)
+ require.Len(t, anyOf, 6)
+
+ for _, v := range anyOf {
+ variant := v.(map[string]any)
+ if variant["type"] == "array" {
+ require.Contains(t, variant, "items")
+ }
+ }
+ require.Equal(t, "Config value", val["description"])
+}
+
+func TestNormalize_SingleStringType(t *testing.T) {
+ t.Parallel()
+
+ node := map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "name": map[string]any{"type": "string"},
+ },
+ }
+
+ Normalize(node)
+
+ val := node["properties"].(map[string]any)["name"].(map[string]any)
+ require.Equal(t, "string", val["type"])
+}
+
+func TestNormalize_BareArrayGetsItems(t *testing.T) {
+ t.Parallel()
+
+ node := map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "tags": map[string]any{"type": "array"},
+ },
+ }
+
+ Normalize(node)
+
+ val := node["properties"].(map[string]any)["tags"].(map[string]any)
+ require.Equal(t, "array", val["type"])
+ require.Contains(t, val, "items")
+}
+
+func TestNormalize_SingleElementTypeArray(t *testing.T) {
+ t.Parallel()
+
+ node := map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "name": map[string]any{"type": []any{"string"}},
+ },
+ }
+
+ Normalize(node)
+
+ val := node["properties"].(map[string]any)["name"].(map[string]any)
+ require.Nil(t, val["type"])
+ anyOf, ok := val["anyOf"].([]any)
+ require.True(t, ok)
+ require.Len(t, anyOf, 1)
+ require.Equal(t, "string", anyOf[0].(map[string]any)["type"])
+}
+
+func TestNormalize_NestedProperties(t *testing.T) {
+ t.Parallel()
+
+ node := map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "config": map[string]any{
+ "type": "object",
+ "properties": map[string]any{
+ "val": map[string]any{"type": []any{"string", "number"}},
+ },
+ },
+ },
+ }
+
+ Normalize(node)
+
+ val := node["properties"].(map[string]any)["config"].(map[string]any)["properties"].(map[string]any)["val"].(map[string]any)
+ require.Nil(t, val["type"])
+ require.NotNil(t, val["anyOf"])
+}