From 30f8a68c4cc55e0f3a717b891931847c97190843 Mon Sep 17 00:00:00 2001 From: Devon Rifkin Date: Tue, 5 Aug 2025 16:46:24 -0700 Subject: [PATCH] tools: support anyOf types afaik gpt-oss is the first model that meaningfully transforms tool function definitions in its template. We found that relatively common definitions that include `anyOf` were not working because the template was assuming that types were always defined via a `type` field. anyOf allows for fully recursive types, so I exposed a `toTypeScriptType()` function to handle this recursive logic in go and keep the templates cleaner. The gpt-oss templates will need to be updated to use this. We should keep building out our function definition support to more fully support the parts of json schema that make sense for this use case, but in the meantime this will unblock some users (e.g., zed's ollama integration w/ gpt-oss). Probably the most urgent is proper array support --- api/types.go | 68 ++++++++++-- api/types_typescript_test.go | 142 ++++++++++++++++++++++++ openai/openai_test.go | 22 +--- server/routes_generate_test.go | 44 ++------ server/routes_harmony_streaming_test.go | 44 ++------ template/template.go | 10 ++ tools/tools_test.go | 88 ++++----------- 7 files changed, 264 insertions(+), 154 deletions(-) create mode 100644 api/types_typescript_test.go diff --git a/api/types.go b/api/types.go index e2c63b62..0f99de18 100644 --- a/api/types.go +++ b/api/types.go @@ -225,20 +225,68 @@ func (pt PropertyType) String() string { return fmt.Sprintf("%v", []string(pt)) } +type ToolProperty struct { + AnyOf []ToolProperty `json:"anyOf,omitempty"` + Type PropertyType `json:"type"` + Items any `json:"items,omitempty"` + Description string `json:"description"` + Enum []any `json:"enum,omitempty"` +} + +// ToTypeScriptType converts a ToolProperty to a TypeScript type string +func (tp ToolProperty) ToTypeScriptType() string { + if len(tp.AnyOf) > 0 { + var types []string + for _, anyOf := range tp.AnyOf { + types = append(types, anyOf.ToTypeScriptType()) + } + return strings.Join(types, " | ") + } + + if len(tp.Type) == 0 { + return "any" + } + + if len(tp.Type) == 1 { + return mapToTypeScriptType(tp.Type[0]) + } + + var types []string + for _, t := range tp.Type { + types = append(types, mapToTypeScriptType(t)) + } + return strings.Join(types, " | ") +} + +// mapToTypeScriptType maps JSON Schema types to TypeScript types +func mapToTypeScriptType(jsonType string) string { + switch jsonType { + case "string": + return "string" + case "number", "integer": + return "number" + case "boolean": + return "boolean" + case "array": + return "any[]" + case "object": + return "Record" + case "null": + return "null" + default: + return "any" + } +} + type ToolFunction struct { Name string `json:"name"` Description string `json:"description"` Parameters struct { - Type string `json:"type"` - Defs any `json:"$defs,omitempty"` - Items any `json:"items,omitempty"` - Required []string `json:"required"` - Properties map[string]struct { - Type PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - } `json:"properties"` + Type string `json:"type"` + Defs any `json:"$defs,omitempty"` + Items any `json:"items,omitempty"` + Required []string `json:"required"` + Properties map[string]ToolProperty `json:"properties"` } `json:"parameters"` } diff --git a/api/types_typescript_test.go b/api/types_typescript_test.go new file mode 100644 index 00000000..9902c5be --- /dev/null +++ b/api/types_typescript_test.go @@ -0,0 +1,142 @@ +package api + +import ( + "testing" +) + +func TestToolParameterToTypeScriptType(t *testing.T) { + tests := []struct { + name string + param ToolProperty + expected string + }{ + { + name: "single string type", + param: ToolProperty{ + Type: PropertyType{"string"}, + }, + expected: "string", + }, + { + name: "single number type", + param: ToolProperty{ + Type: PropertyType{"number"}, + }, + expected: "number", + }, + { + name: "integer maps to number", + param: ToolProperty{ + Type: PropertyType{"integer"}, + }, + expected: "number", + }, + { + name: "boolean type", + param: ToolProperty{ + Type: PropertyType{"boolean"}, + }, + expected: "boolean", + }, + { + name: "array type", + param: ToolProperty{ + Type: PropertyType{"array"}, + }, + expected: "any[]", + }, + { + name: "object type", + param: ToolProperty{ + Type: PropertyType{"object"}, + }, + expected: "Record", + }, + { + name: "null type", + param: ToolProperty{ + Type: PropertyType{"null"}, + }, + expected: "null", + }, + { + name: "multiple types as union", + param: ToolProperty{ + Type: PropertyType{"string", "number"}, + }, + expected: "string | number", + }, + { + name: "string or null union", + param: ToolProperty{ + Type: PropertyType{"string", "null"}, + }, + expected: "string | null", + }, + { + name: "anyOf with single types", + param: ToolProperty{ + AnyOf: []ToolProperty{ + {Type: PropertyType{"string"}}, + {Type: PropertyType{"number"}}, + }, + }, + expected: "string | number", + }, + { + name: "anyOf with multiple types in each branch", + param: ToolProperty{ + AnyOf: []ToolProperty{ + {Type: PropertyType{"string", "null"}}, + {Type: PropertyType{"number"}}, + }, + }, + expected: "string | null | number", + }, + { + name: "nested anyOf", + param: ToolProperty{ + AnyOf: []ToolProperty{ + {Type: PropertyType{"boolean"}}, + { + AnyOf: []ToolProperty{ + {Type: PropertyType{"string"}}, + {Type: PropertyType{"number"}}, + }, + }, + }, + }, + expected: "boolean | string | number", + }, + { + name: "empty type returns any", + param: ToolProperty{ + Type: PropertyType{}, + }, + expected: "any", + }, + { + name: "unknown type maps to any", + param: ToolProperty{ + Type: PropertyType{"unknown_type"}, + }, + expected: "any", + }, + { + name: "multiple types including array", + param: ToolProperty{ + Type: PropertyType{"string", "array", "null"}, + }, + expected: "string | any[] | null", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.param.ToTypeScriptType() + if result != tt.expected { + t.Errorf("ToTypeScriptType() = %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/openai/openai_test.go b/openai/openai_test.go index a24093ad..471b4737 100644 --- a/openai/openai_test.go +++ b/openai/openai_test.go @@ -280,25 +280,15 @@ func TestChatMiddleware(t *testing.T) { Name: "get_weather", Description: "Get the current weather", Parameters: struct { - Type string `json:"type"` - Defs any `json:"$defs,omitempty"` - Items any `json:"items,omitempty"` - Required []string `json:"required"` - Properties map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - } `json:"properties"` + Type string `json:"type"` + Defs any `json:"$defs,omitempty"` + Items any `json:"items,omitempty"` + Required []string `json:"required"` + Properties map[string]api.ToolProperty `json:"properties"` }{ Type: "object", Required: []string{"location"}, - Properties: map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - }{ + Properties: map[string]api.ToolProperty{ "location": { Type: api.PropertyType{"string"}, Description: "The city and state", diff --git a/server/routes_generate_test.go b/server/routes_generate_test.go index 477d6b81..506071ed 100644 --- a/server/routes_generate_test.go +++ b/server/routes_generate_test.go @@ -388,25 +388,15 @@ func TestGenerateChat(t *testing.T) { Name: "get_weather", Description: "Get the current weather", Parameters: struct { - Type string `json:"type"` - Defs any `json:"$defs,omitempty"` - Items any `json:"items,omitempty"` - Required []string `json:"required"` - Properties map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - } `json:"properties"` + Type string `json:"type"` + Defs any `json:"$defs,omitempty"` + Items any `json:"items,omitempty"` + Required []string `json:"required"` + Properties map[string]api.ToolProperty `json:"properties"` }{ Type: "object", Required: []string{"location"}, - Properties: map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - }{ + Properties: map[string]api.ToolProperty{ "location": { Type: api.PropertyType{"string"}, Description: "The city and state", @@ -489,25 +479,15 @@ func TestGenerateChat(t *testing.T) { Name: "get_weather", Description: "Get the current weather", Parameters: struct { - Type string `json:"type"` - Defs any `json:"$defs,omitempty"` - Items any `json:"items,omitempty"` - Required []string `json:"required"` - Properties map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - } `json:"properties"` + Type string `json:"type"` + Defs any `json:"$defs,omitempty"` + Items any `json:"items,omitempty"` + Required []string `json:"required"` + Properties map[string]api.ToolProperty `json:"properties"` }{ Type: "object", Required: []string{"location"}, - Properties: map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - }{ + Properties: map[string]api.ToolProperty{ "location": { Type: api.PropertyType{"string"}, Description: "The city and state", diff --git a/server/routes_harmony_streaming_test.go b/server/routes_harmony_streaming_test.go index 503cb4d7..1b86f84c 100644 --- a/server/routes_harmony_streaming_test.go +++ b/server/routes_harmony_streaming_test.go @@ -27,25 +27,15 @@ func getTestTools() []api.Tool { Name: "get_weather", Description: "Get the current weather in a given location", Parameters: struct { - Type string `json:"type"` - Defs any `json:"$defs,omitempty"` - Items any `json:"items,omitempty"` - Required []string `json:"required"` - Properties map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - } `json:"properties"` + Type string `json:"type"` + Defs any `json:"$defs,omitempty"` + Items any `json:"items,omitempty"` + Required []string `json:"required"` + Properties map[string]api.ToolProperty `json:"properties"` }{ Type: "object", Required: []string{"location"}, - Properties: map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - }{ + Properties: map[string]api.ToolProperty{ "location": { Type: api.PropertyType{"string"}, Description: "The city and state, e.g. San Francisco, CA", @@ -60,25 +50,15 @@ func getTestTools() []api.Tool { Name: "calculate", Description: "Calculate a mathematical expression", Parameters: struct { - Type string `json:"type"` - Defs any `json:"$defs,omitempty"` - Items any `json:"items,omitempty"` - Required []string `json:"required"` - Properties map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - } `json:"properties"` + Type string `json:"type"` + Defs any `json:"$defs,omitempty"` + Items any `json:"items,omitempty"` + Required []string `json:"required"` + Properties map[string]api.ToolProperty `json:"properties"` }{ Type: "object", Required: []string{"expression"}, - Properties: map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - }{ + Properties: map[string]api.ToolProperty{ "expression": { Type: api.PropertyType{"string"}, Description: "The mathematical expression to calculate", diff --git a/template/template.go b/template/template.go index bfd02a92..f2775b91 100644 --- a/template/template.go +++ b/template/template.go @@ -127,6 +127,16 @@ var funcs = template.FuncMap{ // Default format is YYYY-MM-DD return time.Now().Format("2006-01-02") }, + "toTypeScriptType": func(v any) string { + if param, ok := v.(api.ToolProperty); ok { + return param.ToTypeScriptType() + } + // Handle pointer case + if param, ok := v.(*api.ToolProperty); ok && param != nil { + return param.ToTypeScriptType() + } + return "any" + }, } func Parse(s string) (*Template, error) { diff --git a/tools/tools_test.go b/tools/tools_test.go index a0f7b6b0..7f00be20 100644 --- a/tools/tools_test.go +++ b/tools/tools_test.go @@ -41,25 +41,15 @@ func TestParser(t *testing.T) { Name: "get_temperature", Description: "Retrieve the temperature for a given location", Parameters: struct { - Type string `json:"type"` - Defs any `json:"$defs,omitempty"` - Items any `json:"items,omitempty"` - Required []string `json:"required"` - Properties map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - } `json:"properties"` + Type string `json:"type"` + Defs any `json:"$defs,omitempty"` + Items any `json:"items,omitempty"` + Required []string `json:"required"` + Properties map[string]api.ToolProperty `json:"properties"` }{ Type: "object", Required: []string{"city"}, - Properties: map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - }{ + Properties: map[string]api.ToolProperty{ "format": { Type: api.PropertyType{"string"}, Description: "The format to return the temperature in", @@ -79,24 +69,14 @@ func TestParser(t *testing.T) { Name: "get_conditions", Description: "Retrieve the current weather conditions for a given location", Parameters: struct { - Type string `json:"type"` - Defs any `json:"$defs,omitempty"` - Items any `json:"items,omitempty"` - Required []string `json:"required"` - Properties map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - } `json:"properties"` + Type string `json:"type"` + Defs any `json:"$defs,omitempty"` + Items any `json:"items,omitempty"` + Required []string `json:"required"` + Properties map[string]api.ToolProperty `json:"properties"` }{ Type: "object", - Properties: map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - }{ + Properties: map[string]api.ToolProperty{ "location": { Type: api.PropertyType{"string"}, Description: "The location to get the weather conditions for", @@ -125,24 +105,14 @@ func TestParser(t *testing.T) { Name: "get_address", Description: "Get the address of a given location", Parameters: struct { - Type string `json:"type"` - Defs any `json:"$defs,omitempty"` - Items any `json:"items,omitempty"` - Required []string `json:"required"` - Properties map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - } `json:"properties"` + Type string `json:"type"` + Defs any `json:"$defs,omitempty"` + Items any `json:"items,omitempty"` + Required []string `json:"required"` + Properties map[string]api.ToolProperty `json:"properties"` }{ Type: "object", - Properties: map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - }{ + Properties: map[string]api.ToolProperty{ "location": { Type: api.PropertyType{"string"}, Description: "The location to get the address for", @@ -157,24 +127,14 @@ func TestParser(t *testing.T) { Name: "add", Description: "Add two numbers", Parameters: struct { - Type string `json:"type"` - Defs any `json:"$defs,omitempty"` - Items any `json:"items,omitempty"` - Required []string `json:"required"` - Properties map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - } `json:"properties"` + Type string `json:"type"` + Defs any `json:"$defs,omitempty"` + Items any `json:"items,omitempty"` + Required []string `json:"required"` + Properties map[string]api.ToolProperty `json:"properties"` }{ Type: "object", - Properties: map[string]struct { - Type api.PropertyType `json:"type"` - Items any `json:"items,omitempty"` - Description string `json:"description"` - Enum []any `json:"enum,omitempty"` - }{ + Properties: map[string]api.ToolProperty{ "a": { Type: api.PropertyType{"string"}, Description: "The first number to add",