preserve tool definition and call JSON ordering (#13525)

* preserve tool definition and call JSON ordering

This is another iteration of
<https://github.com/ollama/ollama/pull/12518>, but this time we've
simplified things by relaxing the competing requirements of being
compatible AND order-preserving with templates (vs. renderers). We
maintain backwards compatibility at the cost of not guaranteeing order
for templates. We plan on moving more and more models to renderers,
which have been updated to use these new data types, and additionally
we could add an opt-in way of templates getting an order-preserved list
(e.g., via sibling template vars)

* orderedmap_test: remove testify
This commit is contained in:
Devon Rifkin
2026-01-05 18:03:36 -08:00
committed by GitHub
parent d087e46bd1
commit e51dead636
44 changed files with 1769 additions and 562 deletions

View File

@@ -124,16 +124,21 @@ func (p *Parser) parseToolCall() *api.ToolCall {
return nil
}
var args map[string]any
var argsMap map[string]any
if found, i := findArguments(tool, p.buffer); found == nil {
return nil
} else {
args = found
argsMap = found
if i > end {
end = i
}
}
args := api.NewToolCallFunctionArguments()
for k, v := range argsMap {
args.Set(k, v)
}
tc := &api.ToolCall{
Function: api.ToolCallFunction{
Name: tool.Function.Name,

View File

@@ -9,6 +9,29 @@ import (
"github.com/ollama/ollama/api"
)
// argsComparer provides cmp options for comparing ToolCallFunctionArguments by value (order-insensitive)
var argsComparer = cmp.Comparer(func(a, b api.ToolCallFunctionArguments) bool {
return cmp.Equal(a.ToMap(), b.ToMap())
})
// testPropsMap creates a ToolPropertiesMap from a map (convenience function for tests, order not preserved)
func testPropsMap(m map[string]api.ToolProperty) *api.ToolPropertiesMap {
props := api.NewToolPropertiesMap()
for k, v := range m {
props.Set(k, v)
}
return props
}
// testArgs creates ToolCallFunctionArguments from a map (convenience function for tests, order not preserved)
func testArgs(m map[string]any) api.ToolCallFunctionArguments {
args := api.NewToolCallFunctionArguments()
for k, v := range m {
args.Set(k, v)
}
return args
}
func TestParser(t *testing.T) {
qwen, err := template.New("qwen").Parse(`{{if .ToolCalls}}<tool_call>{{range .ToolCalls}}{"name": "{{.Function.Name}}", "arguments": {{.Function.Arguments}}}{{end}}</tool_call>{{end}}`)
if err != nil {
@@ -44,7 +67,7 @@ func TestParser(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"city"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"format": {
Type: api.PropertyType{"string"},
Description: "The format to return the temperature in",
@@ -54,7 +77,7 @@ func TestParser(t *testing.T) {
Type: api.PropertyType{"string"},
Description: "The city to get the temperature for",
},
},
}),
},
},
},
@@ -65,12 +88,12 @@ func TestParser(t *testing.T) {
Description: "Retrieve the current weather conditions for a given location",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "The location to get the weather conditions for",
},
},
}),
},
},
},
@@ -95,12 +118,12 @@ func TestParser(t *testing.T) {
Description: "Get the address of a given location",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "The location to get the address for",
},
},
}),
},
},
},
@@ -111,7 +134,7 @@ func TestParser(t *testing.T) {
Description: "Add two numbers",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"a": {
Type: api.PropertyType{"string"},
Description: "The first number to add",
@@ -120,7 +143,7 @@ func TestParser(t *testing.T) {
Type: api.PropertyType{"string"},
Description: "The second number to add",
},
},
}),
},
},
},
@@ -157,9 +180,9 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "San Francisco",
},
}),
},
},
},
@@ -174,7 +197,7 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{},
Arguments: api.NewToolCallFunctionArguments(),
},
},
},
@@ -189,9 +212,9 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_temperature",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"city": "New York",
},
}),
},
},
},
@@ -213,19 +236,19 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_temperature",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"city": "London",
"format": "fahrenheit",
},
}),
},
},
{
Function: api.ToolCallFunction{
Index: 1,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Tokyo",
},
}),
},
},
},
@@ -240,19 +263,19 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_temperature",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"city": "London",
"format": "fahrenheit",
},
}),
},
},
{
Function: api.ToolCallFunction{
Index: 1,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Tokyo",
},
}),
},
},
},
@@ -267,17 +290,17 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "say_hello",
Arguments: api.ToolCallFunctionArguments{},
Arguments: api.NewToolCallFunctionArguments(),
},
},
{
Function: api.ToolCallFunction{
Index: 1,
Name: "get_temperature",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"city": "London",
"format": "fahrenheit",
},
}),
},
},
},
@@ -292,16 +315,16 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{},
Arguments: api.NewToolCallFunctionArguments(),
},
},
{
Function: api.ToolCallFunction{
Index: 1,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Tokyo",
},
}),
},
},
},
@@ -316,9 +339,9 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_temperature",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"city": "Tokyo",
},
}),
},
},
},
@@ -347,9 +370,9 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_temperature",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"city": "Tokyo",
},
}),
},
},
},
@@ -371,9 +394,9 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_temperature",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"city": "Tokyo",
},
}),
},
},
},
@@ -453,18 +476,18 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_temperature",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"city": "London",
},
}),
},
},
{
Function: api.ToolCallFunction{
Index: 1,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Tokyo",
},
}),
},
},
},
@@ -486,9 +509,9 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Tokyo",
},
}),
},
},
},
@@ -528,9 +551,9 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_conditions",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Tokyo",
},
}),
},
},
},
@@ -563,7 +586,7 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "say_hello_world",
Arguments: api.ToolCallFunctionArguments{},
Arguments: api.NewToolCallFunctionArguments(),
},
},
},
@@ -591,14 +614,14 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "say_hello_world",
Arguments: api.ToolCallFunctionArguments{},
Arguments: api.NewToolCallFunctionArguments(),
},
},
{
Function: api.ToolCallFunction{
Index: 1,
Name: "say_hello",
Arguments: api.ToolCallFunctionArguments{},
Arguments: api.NewToolCallFunctionArguments(),
},
},
},
@@ -624,14 +647,14 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "say_hello",
Arguments: api.ToolCallFunctionArguments{},
Arguments: api.NewToolCallFunctionArguments(),
},
},
{
Function: api.ToolCallFunction{
Index: 1,
Name: "say_hello_world",
Arguments: api.ToolCallFunctionArguments{},
Arguments: api.NewToolCallFunctionArguments(),
},
},
},
@@ -648,7 +671,7 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "say_hello",
Arguments: api.ToolCallFunctionArguments{},
Arguments: api.NewToolCallFunctionArguments(),
},
},
},
@@ -665,7 +688,7 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "say_hello_world",
Arguments: api.ToolCallFunctionArguments{},
Arguments: api.NewToolCallFunctionArguments(),
},
},
},
@@ -687,9 +710,9 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_address",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "London",
},
}),
},
},
},
@@ -706,9 +729,9 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "get_address",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "London",
},
}),
},
},
},
@@ -725,10 +748,10 @@ func TestParser(t *testing.T) {
Function: api.ToolCallFunction{
Index: 0,
Name: "add",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"a": "5",
"b": "10",
},
}),
},
},
},
@@ -756,7 +779,7 @@ func TestParser(t *testing.T) {
}
for i, want := range tt.calls {
if diff := cmp.Diff(calls[i], want); diff != "" {
if diff := cmp.Diff(calls[i], want, argsComparer); diff != "" {
t.Errorf("Tool call %d mismatch (-got +want):\n%s", i, diff)
}
}
@@ -1316,7 +1339,7 @@ func TestFindArguments(t *testing.T) {
got, _ := findArguments(&api.Tool{Function: api.ToolFunction{Name: tt.tool}}, tt.buffer)
if diff := cmp.Diff(got, tt.want); diff != "" {
t.Errorf("scanArguments() args mismatch (-got +want):\n%s", diff)
t.Errorf("findArguments() args mismatch (-got +want):\n%s", diff)
}
})
}