Unverified Commit e51dead6 authored by Devon Rifkin's avatar Devon Rifkin Committed by GitHub
Browse files

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
parent d087e46b
......@@ -550,10 +550,10 @@ func TestQwen3VLNonThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "get-current-weather",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"location": "San Francisco, CA",
"unit": "fahrenheit",
},
}),
},
},
},
......@@ -564,10 +564,10 @@ func TestQwen3VLNonThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "get current temperature",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"location with spaces": "San Francisco",
"unit with spaces": "celsius",
},
}),
},
},
},
......@@ -578,10 +578,10 @@ func TestQwen3VLNonThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "\"get current temperature\"",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"\"location with spaces\"": "San Francisco",
"\"unit with spaces\"": "\"celsius\"",
},
}),
},
},
},
......@@ -592,12 +592,12 @@ func TestQwen3VLNonThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "calculate",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"x": 3.14,
"y": float64(42),
"enabled": true,
"items": []any{"a", "b", "c"},
},
}),
},
},
},
......@@ -608,9 +608,9 @@ func TestQwen3VLNonThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "exec",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"command": "ls && echo \"done\"",
},
}),
},
},
},
......@@ -621,9 +621,9 @@ func TestQwen3VLNonThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "exec",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"command": "ls && echo \"a > b and a < b\"",
},
}),
},
},
},
......@@ -634,10 +634,10 @@ func TestQwen3VLNonThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "获取天气",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"城市": "北京",
"message": "Hello! 你好! 🌟 مرحبا",
},
}),
},
},
},
......@@ -648,7 +648,7 @@ func TestQwen3VLNonThinkingToolParser(t *testing.T) {
if err != nil {
t.Errorf("step %d (%s): %v", i, step.name, err)
}
if !reflect.DeepEqual(gotToolCall, step.wantToolCall) {
if !toolCallEqual(gotToolCall, step.wantToolCall) {
t.Errorf("step %d (%s): got tool call %#v, want %#v", i, step.name, gotToolCall, step.wantToolCall)
}
}
......
......@@ -241,10 +241,10 @@ func TestQwen3VLThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "get-current-weather",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"location": "San Francisco, CA",
"unit": "fahrenheit",
},
}),
},
},
},
......@@ -255,10 +255,10 @@ func TestQwen3VLThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "get current temperature",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"location with spaces": "San Francisco",
"unit with spaces": "celsius",
},
}),
},
},
},
......@@ -269,10 +269,10 @@ func TestQwen3VLThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "\"get current temperature\"",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"\"location with spaces\"": "San Francisco",
"\"unit with spaces\"": "\"celsius\"",
},
}),
},
},
},
......@@ -283,12 +283,12 @@ func TestQwen3VLThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "calculate",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"x": 3.14,
"y": float64(42),
"enabled": true,
"items": []any{"a", "b", "c"},
},
}),
},
},
},
......@@ -299,9 +299,9 @@ func TestQwen3VLThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "exec",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"command": "ls && echo \"done\"",
},
}),
},
},
},
......@@ -312,9 +312,9 @@ func TestQwen3VLThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "exec",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"command": "ls && echo \"a > b and a < b\"",
},
}),
},
},
},
......@@ -325,10 +325,10 @@ func TestQwen3VLThinkingToolParser(t *testing.T) {
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "获取天气",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"城市": "北京",
"message": "Hello! 你好! 🌟 مرحبا",
},
}),
},
},
},
......@@ -339,7 +339,7 @@ func TestQwen3VLThinkingToolParser(t *testing.T) {
if err != nil {
t.Errorf("step %d (%s): %v", i, step.name, err)
}
if !reflect.DeepEqual(gotToolCall, step.wantToolCall) {
if !toolCallEqual(gotToolCall, step.wantToolCall) {
t.Errorf("step %d (%s): got tool call %#v, want %#v", i, step.name, gotToolCall, step.wantToolCall)
}
}
......
package parsers
import (
"encoding/json"
"github.com/google/go-cmp/cmp"
"github.com/ollama/ollama/api"
)
// argsComparer provides cmp options for comparing ToolCallFunctionArguments
// It compares by logical equality (same keys with same values) not by order
var argsComparer = cmp.Comparer(func(a, b api.ToolCallFunctionArguments) bool {
// Convert both to maps and compare
aMap := a.ToMap()
bMap := b.ToMap()
if len(aMap) != len(bMap) {
return false
}
for k, av := range aMap {
bv, ok := bMap[k]
if !ok {
return false
}
// Use JSON encoding for deep comparison of values
aJSON, _ := json.Marshal(av)
bJSON, _ := json.Marshal(bv)
if string(aJSON) != string(bJSON) {
return false
}
}
return true
})
// propsComparer provides cmp options for comparing ToolPropertiesMap
var propsComparer = cmp.Comparer(func(a, b *api.ToolPropertiesMap) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
aJSON, _ := json.Marshal(a)
bJSON, _ := json.Marshal(b)
return string(aJSON) == string(bJSON)
})
// toolsComparer combines argsComparer and propsComparer for comparing tools
var toolsComparer = cmp.Options{argsComparer, propsComparer}
// toolCallEqual compares two tool calls by comparing their components
// It compares arguments by logical equality (same keys with same values) not by order
func toolCallEqual(a, b api.ToolCall) bool {
if a.ID != b.ID {
return false
}
if a.Function.Index != b.Function.Index {
return false
}
if a.Function.Name != b.Function.Name {
return false
}
// Compare arguments by logical equality using argsComparer logic
aMap := a.Function.Arguments.ToMap()
bMap := b.Function.Arguments.ToMap()
if len(aMap) != len(bMap) {
return false
}
for k, av := range aMap {
bv, ok := bMap[k]
if !ok {
return false
}
aJSON, _ := json.Marshal(av)
bJSON, _ := json.Marshal(bv)
if string(aJSON) != string(bJSON) {
return false
}
}
return true
}
// 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
}
......@@ -94,12 +94,12 @@ You are a helpful assistant.
Description: "Get current weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "City name",
},
},
}),
Required: []string{"location"},
},
},
......@@ -139,9 +139,9 @@ You have the following functions available:
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -162,9 +162,9 @@ You have the following functions available:
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -186,17 +186,17 @@ You have the following functions available:
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "London",
},
}),
},
},
},
......@@ -226,12 +226,12 @@ You have the following functions available:
Description: "Get current weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "City name",
},
},
}),
Required: []string{"location"},
},
},
......@@ -378,9 +378,9 @@ You are a pirate chatbot who always responds in pirate speak!
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -401,14 +401,14 @@ You are a pirate chatbot who always responds in pirate speak!
{
Function: api.ToolCallFunction{
Name: "process_data",
Arguments: api.ToolCallFunctionArguments{
"items": []any{"item1", "item2", "item3"},
"config": map[string]any{
Arguments: testArgsOrdered([]orderedArg{
{"config", map[string]any{
"enabled": true,
"threshold": 0.95,
"tags": []string{"important", "urgent"},
},
},
}},
{"items", []any{"item1", "item2", "item3"}},
}),
},
},
},
......
......@@ -82,9 +82,9 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -104,9 +104,9 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -125,9 +125,9 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -147,17 +147,17 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "London",
},
}),
},
},
},
......@@ -214,9 +214,9 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -235,9 +235,9 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "process",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"data": "test",
},
}),
},
},
},
......@@ -281,9 +281,9 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -305,9 +305,9 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -355,9 +355,9 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -379,9 +379,9 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -436,17 +436,17 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Tokyo",
},
}),
},
},
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "New York",
},
}),
},
},
},
......@@ -489,12 +489,12 @@ Second instruction<|User|>Hello<|Assistant|></think>`,
Description: "Get current weather information",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "City name",
},
},
}),
Required: []string{"location"},
},
},
......@@ -535,12 +535,12 @@ Where:
Description: "Get current weather information",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "City name",
},
},
}),
Required: []string{"location"},
},
},
......@@ -578,9 +578,9 @@ Where:
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -594,12 +594,12 @@ Where:
Description: "Get current weather information",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "City name",
},
},
}),
Required: []string{"location"},
},
},
......@@ -638,9 +638,9 @@ Where:
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
},
......@@ -656,12 +656,12 @@ Where:
Description: "Get current weather information",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "City name",
},
},
}),
Required: []string{"location"},
},
},
......@@ -701,9 +701,9 @@ Where:
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Tokyo",
},
}),
},
},
},
......@@ -724,12 +724,12 @@ Where:
Description: "Get current weather information",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "City name",
},
},
}),
Required: []string{"location"},
},
},
......@@ -770,12 +770,12 @@ Where:
Description: "Get current weather information",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "City name",
},
},
}),
Required: []string{"location"},
},
},
......@@ -787,12 +787,12 @@ Where:
Description: "Perform mathematical calculations",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"expression": {
Type: api.PropertyType{"string"},
Description: "Mathematical expression to evaluate",
},
},
}),
Required: []string{"expression"},
},
},
......@@ -834,17 +834,17 @@ Where:
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Paris",
},
}),
},
},
{
Function: api.ToolCallFunction{
Name: "calculate",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"expression": "25 * 4",
},
}),
},
},
},
......@@ -860,12 +860,12 @@ Where:
Description: "Get current weather information",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "City name",
},
},
}),
Required: []string{"location"},
},
},
......@@ -877,12 +877,12 @@ Where:
Description: "Perform mathematical calculations",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"expression": {
Type: api.PropertyType{"string"},
Description: "Mathematical expression to evaluate",
},
},
}),
Required: []string{"expression"},
},
},
......@@ -927,12 +927,12 @@ Where:
Description: "Get current weather information",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "City name",
},
},
}),
Required: []string{"location"},
},
},
......
......@@ -136,7 +136,7 @@ func (r *FunctionGemmaRenderer) renderToolDeclaration(tool api.Tool) string {
needsComma := false
// Only include properties:{} if there are actual properties
if len(fn.Parameters.Properties) > 0 {
if fn.Parameters.Properties != nil && fn.Parameters.Properties.Len() > 0 {
sb.WriteString("properties:{")
r.writeProperties(&sb, fn.Parameters.Properties)
sb.WriteString("}")
......@@ -172,16 +172,16 @@ func (r *FunctionGemmaRenderer) renderToolDeclaration(tool api.Tool) string {
return sb.String()
}
func (r *FunctionGemmaRenderer) writeProperties(sb *strings.Builder, props map[string]api.ToolProperty) {
keys := make([]string, 0, len(props))
for k := range props {
func (r *FunctionGemmaRenderer) writeProperties(sb *strings.Builder, props *api.ToolPropertiesMap) {
keys := make([]string, 0, props.Len())
for k := range props.All() {
keys = append(keys, k)
}
sort.Strings(keys)
first := true
for _, name := range keys {
prop := props[name]
prop, _ := props.Get(name)
if !first {
sb.WriteString(",")
}
......@@ -203,15 +203,15 @@ func (r *FunctionGemmaRenderer) formatToolCall(tc api.ToolCall) string {
var sb strings.Builder
sb.WriteString("<start_function_call>call:" + tc.Function.Name + "{")
keys := make([]string, 0, len(tc.Function.Arguments))
for k := range tc.Function.Arguments {
keys := make([]string, 0, tc.Function.Arguments.Len())
for k := range tc.Function.Arguments.All() {
keys = append(keys, k)
}
sort.Strings(keys)
first := true
for _, key := range keys {
value := tc.Function.Arguments[key]
value, _ := tc.Function.Arguments.Get(key)
if !first {
sb.WriteString(",")
}
......
......@@ -51,9 +51,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Get weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "City"},
},
}),
},
},
},
......@@ -75,9 +75,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Get weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "City"},
},
}),
},
},
},
......@@ -107,9 +107,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Get weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "City"},
},
}),
},
},
},
......@@ -126,7 +126,7 @@ func TestFunctionGemmaRenderer(t *testing.T) {
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{"city": "Paris"},
Arguments: testArgs(map[string]any{"city": "Paris"}),
},
},
},
......@@ -141,9 +141,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Get weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "City"},
},
}),
},
},
},
......@@ -161,7 +161,7 @@ func TestFunctionGemmaRenderer(t *testing.T) {
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{"city": "Paris"},
Arguments: testArgs(map[string]any{"city": "Paris"}),
},
},
},
......@@ -176,9 +176,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Get weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "City"},
},
}),
},
},
},
......@@ -195,7 +195,7 @@ func TestFunctionGemmaRenderer(t *testing.T) {
{
Function: api.ToolCallFunction{
Name: "add",
Arguments: api.ToolCallFunctionArguments{"a": float64(1), "b": float64(2)},
Arguments: testArgs(map[string]any{"a": float64(1), "b": float64(2)}),
},
},
},
......@@ -210,10 +210,10 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Add numbers",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"a": {Type: api.PropertyType{"number"}},
"b": {Type: api.PropertyType{"number"}},
},
}),
},
},
},
......@@ -239,10 +239,10 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"city"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "City Name"},
"country": {Type: api.PropertyType{"string"}, Description: "Country Name"},
},
}),
},
},
},
......@@ -263,9 +263,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Get weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "City"},
},
}),
},
},
},
......@@ -276,9 +276,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Get current time",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"timezone": {Type: api.PropertyType{"string"}, Description: "Timezone"},
},
}),
},
},
},
......@@ -296,13 +296,13 @@ func TestFunctionGemmaRenderer(t *testing.T) {
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{"city": "Paris"},
Arguments: testArgs(map[string]any{"city": "Paris"}),
},
},
{
Function: api.ToolCallFunction{
Name: "get_time",
Arguments: api.ToolCallFunctionArguments{"timezone": "UTC"},
Arguments: testArgs(map[string]any{"timezone": "UTC"}),
},
},
},
......@@ -318,9 +318,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Get weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "City"},
},
}),
},
},
},
......@@ -331,9 +331,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Get current time",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"timezone": {Type: api.PropertyType{"string"}, Description: "Timezone"},
},
}),
},
},
},
......@@ -351,7 +351,7 @@ func TestFunctionGemmaRenderer(t *testing.T) {
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{"city": "Paris"},
Arguments: testArgs(map[string]any{"city": "Paris"}),
},
},
},
......@@ -367,9 +367,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Get weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "City"},
},
}),
},
},
},
......@@ -391,7 +391,7 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{},
Properties: testPropsMap(map[string]api.ToolProperty{}),
},
},
},
......@@ -430,7 +430,7 @@ func TestFunctionGemmaRenderer(t *testing.T) {
{
Function: api.ToolCallFunction{
Name: "set_flag",
Arguments: api.ToolCallFunctionArguments{"enabled": true},
Arguments: testArgs(map[string]any{"enabled": true}),
},
},
},
......@@ -445,9 +445,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Set a flag",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"enabled": {Type: api.PropertyType{"boolean"}, Description: "Flag value"},
},
}),
},
},
},
......@@ -468,11 +468,11 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"a", "b", "c"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"a": {Type: api.PropertyType{"string"}, Description: "A"},
"b": {Type: api.PropertyType{"string"}, Description: "B"},
"c": {Type: api.PropertyType{"string"}, Description: "C"},
},
}),
},
},
},
......@@ -492,9 +492,9 @@ func TestFunctionGemmaRenderer(t *testing.T) {
Description: "Test",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"items": {Type: api.PropertyType{"array"}, Description: "List of items"},
},
}),
},
},
},
......
......@@ -114,7 +114,7 @@ func (r *Nemotron3NanoRenderer) renderTools(tools []api.Tool) string {
sb.WriteString("\n<parameters>")
if fn.Parameters.Properties != nil {
for paramName, paramFields := range fn.Parameters.Properties {
for paramName, paramFields := range fn.Parameters.Properties.All() {
sb.WriteString("\n<parameter>")
sb.WriteString("\n<name>" + paramName + "</name>")
......@@ -202,7 +202,7 @@ func (r *Nemotron3NanoRenderer) formatContent(content string, truncate bool, add
func (r *Nemotron3NanoRenderer) writeToolCalls(sb *strings.Builder, toolCalls []api.ToolCall) {
for _, tc := range toolCalls {
sb.WriteString("<tool_call>\n<function=" + tc.Function.Name + ">\n")
for name, value := range tc.Function.Arguments {
for name, value := range tc.Function.Arguments.All() {
sb.WriteString("<parameter=" + name + ">\n" + r.formatArgValue(value) + "\n</parameter>\n")
}
sb.WriteString("</function>\n</tool_call>\n")
......
......@@ -75,9 +75,9 @@ func TestNemotron3NanoRenderer(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"city"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "The city name"},
},
}),
},
},
},
......@@ -113,7 +113,7 @@ func TestNemotron3NanoRenderer(t *testing.T) {
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: map[string]any{"city": "Paris"},
Arguments: testArgs(map[string]any{"city": "Paris"}),
},
},
},
......@@ -129,9 +129,9 @@ func TestNemotron3NanoRenderer(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"city"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}, Description: "The city name"},
},
}),
},
},
},
......@@ -171,7 +171,7 @@ func TestNemotron3NanoRenderer(t *testing.T) {
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: map[string]any{"city": "Paris"},
Arguments: testArgs(map[string]any{"city": "Paris"}),
},
},
},
......@@ -185,9 +185,9 @@ func TestNemotron3NanoRenderer(t *testing.T) {
Name: "get_weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}},
},
}),
},
},
},
......@@ -238,13 +238,13 @@ func TestNemotron3NanoRenderer(t *testing.T) {
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: map[string]any{"city": "Paris"},
Arguments: testArgs(map[string]any{"city": "Paris"}),
},
},
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: map[string]any{"city": "London"},
Arguments: testArgs(map[string]any{"city": "London"}),
},
},
},
......@@ -259,9 +259,9 @@ func TestNemotron3NanoRenderer(t *testing.T) {
Name: "get_weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}},
},
}),
},
},
},
......@@ -304,13 +304,13 @@ func TestNemotron3NanoRenderer(t *testing.T) {
msgs: []api.Message{
{Role: "user", Content: "What's the weather in Paris and London? Also, what's 2+2?"},
{Role: "assistant", Content: "", Thinking: "I need to check the weather for both cities and calculate 2+2. Let me start with the weather calls.", ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{Name: "get_weather", Arguments: api.ToolCallFunctionArguments{"city": "Paris"}}},
{Function: api.ToolCallFunction{Name: "get_weather", Arguments: api.ToolCallFunctionArguments{"city": "London"}}},
{Function: api.ToolCallFunction{Name: "get_weather", Arguments: testArgs(map[string]any{"city": "Paris"})}},
{Function: api.ToolCallFunction{Name: "get_weather", Arguments: testArgs(map[string]any{"city": "London"})}},
}},
{Role: "tool", Content: "Sunny, 22°C", ToolCallID: "call1"},
{Role: "tool", Content: "Rainy, 15°C", ToolCallID: "call2"},
{Role: "assistant", Content: "", Thinking: "Now I have the weather data. Let me calculate 2+2.", ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{Name: "calculate", Arguments: api.ToolCallFunctionArguments{"expression": "2+2"}}},
{Function: api.ToolCallFunction{Name: "calculate", Arguments: testArgs(map[string]any{"expression": "2+2"})}},
}},
{Role: "tool", Content: "4", ToolCallID: "call3"},
{Role: "assistant", Content: "Based on the weather data, Paris is sunny at 22°C and London is rainy at 15°C. Also, 2+2 equals 4.", Thinking: "Perfect! I have all the information needed to provide a complete answer."},
......@@ -322,9 +322,9 @@ func TestNemotron3NanoRenderer(t *testing.T) {
Name: "get_weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"city": {Type: api.PropertyType{"string"}},
},
}),
},
},
},
......@@ -334,9 +334,9 @@ func TestNemotron3NanoRenderer(t *testing.T) {
Name: "calculate",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"expression": {Type: api.PropertyType{"string"}},
},
}),
},
},
},
......@@ -389,7 +389,7 @@ func TestNemotron3NanoRenderer(t *testing.T) {
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{Name: "get_user", Arguments: map[string]any{"id": "123"}}},
{Function: api.ToolCallFunction{Name: "get_user", Arguments: testArgs(map[string]any{"id": "123"})}},
},
},
{Role: "tool", Content: `{"name": "John", "age": 30, "active": true}`},
......@@ -401,7 +401,7 @@ func TestNemotron3NanoRenderer(t *testing.T) {
Name: "get_user",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{"id": {Type: api.PropertyType{"string"}}},
Properties: testPropsMap(map[string]api.ToolProperty{"id": {Type: api.PropertyType{"string"}}}),
},
},
},
......@@ -450,9 +450,9 @@ func TestNemotron3NanoRenderer(t *testing.T) {
ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{
Name: "create",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"data": map[string]any{"nested": "value", "count": 42},
},
}),
}},
},
},
......@@ -465,7 +465,7 @@ func TestNemotron3NanoRenderer(t *testing.T) {
Name: "create",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{"data": {Type: api.PropertyType{"object"}}},
Properties: testPropsMap(map[string]api.ToolProperty{"data": {Type: api.PropertyType{"object"}}}),
},
},
},
......@@ -512,7 +512,7 @@ func TestNemotron3NanoRenderer(t *testing.T) {
{
Role: "assistant",
ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{Name: "translate", Arguments: map[string]any{"text": "你好"}}},
{Function: api.ToolCallFunction{Name: "translate", Arguments: testArgs(map[string]any{"text": "你好"})}},
},
},
{Role: "tool", Content: "Hello"},
......@@ -524,9 +524,9 @@ func TestNemotron3NanoRenderer(t *testing.T) {
Name: "translate",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"text": {Type: api.PropertyType{"string"}},
},
}),
},
},
},
......
......@@ -100,8 +100,8 @@ func (r *Olmo3Renderer) Render(messages []api.Message, tools []api.Tool, _ *api.
sb.WriteString("(")
// Get sorted keys for deterministic output
keys := make([]string, 0, len(tc.Function.Arguments))
for k := range tc.Function.Arguments {
keys := make([]string, 0, tc.Function.Arguments.Len())
for k := range tc.Function.Arguments.All() {
keys = append(keys, k)
}
sort.Strings(keys)
......@@ -110,7 +110,8 @@ func (r *Olmo3Renderer) Render(messages []api.Message, tools []api.Tool, _ *api.
if k > 0 {
sb.WriteString(", ")
}
value, err := json.Marshal(tc.Function.Arguments[key])
val, _ := tc.Function.Arguments.Get(key)
value, err := json.Marshal(val)
if err != nil {
return "", err
}
......
......@@ -53,9 +53,9 @@ func TestOlmo3Renderer(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"location"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {Type: api.PropertyType{"string"}, Description: "The city"},
},
}),
},
},
},
......@@ -80,9 +80,9 @@ func TestOlmo3Renderer(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"location"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {Type: api.PropertyType{"string"}, Description: "The city"},
},
}),
},
},
},
......@@ -108,9 +108,9 @@ func TestOlmo3Renderer(t *testing.T) {
ID: "call_1",
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"location": "San Francisco",
},
}),
},
},
},
......@@ -126,9 +126,9 @@ func TestOlmo3Renderer(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"location"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {Type: api.PropertyType{"string"}, Description: "The city"},
},
}),
},
},
},
......@@ -172,14 +172,14 @@ func TestOlmo3Renderer(t *testing.T) {
ID: "call_1",
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: map[string]any{"location": "San Francisco"},
Arguments: testArgs(map[string]any{"location": "San Francisco"}),
},
},
{
ID: "call_2",
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: map[string]any{"location": "New York"},
Arguments: testArgs(map[string]any{"location": "New York"}),
},
},
},
......@@ -194,9 +194,9 @@ func TestOlmo3Renderer(t *testing.T) {
Name: "get_weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {Type: api.PropertyType{"string"}},
},
}),
},
},
},
......@@ -227,10 +227,10 @@ func TestOlmo3Renderer(t *testing.T) {
ID: "call_1",
Function: api.ToolCallFunction{
Name: "book_flight",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"from": "SFO",
"to": "NYC",
},
}),
},
},
},
......@@ -243,10 +243,10 @@ func TestOlmo3Renderer(t *testing.T) {
Name: "book_flight",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"from": {Type: api.PropertyType{"string"}},
"to": {Type: api.PropertyType{"string"}},
},
}),
},
},
},
......
......@@ -78,7 +78,7 @@ func TestOlmo3ThinkRenderer(t *testing.T) {
ID: "call_1",
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: map[string]any{"location": "San Francisco"},
Arguments: testArgs(map[string]any{"location": "San Francisco"}),
},
},
},
......
......@@ -96,7 +96,7 @@ func (r *Qwen3CoderRenderer) Render(messages []api.Message, tools []api.Tool, _
}
sb.WriteString("\n<parameters>")
for name, prop := range tool.Function.Parameters.Properties {
for name, prop := range tool.Function.Parameters.Properties.All() {
sb.WriteString("\n<parameter>")
sb.WriteString("\n<name>" + name + "</name>")
......@@ -147,7 +147,7 @@ func (r *Qwen3CoderRenderer) Render(messages []api.Message, tools []api.Tool, _
}
for _, toolCall := range message.ToolCalls {
sb.WriteString("\n<tool_call>\n<function=" + toolCall.Function.Name + ">")
for name, value := range toolCall.Function.Arguments {
for name, value := range toolCall.Function.Arguments.All() {
valueStr := formatToolCallArgument(value)
sb.WriteString("\n<parameter=" + name + ">\n" + valueStr + "\n</parameter>")
}
......
......@@ -39,9 +39,9 @@ Hello, how are you?<|im_end|>
{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"unit": "fahrenheit",
},
}),
},
},
},
......@@ -55,7 +55,7 @@ Hello, how are you?<|im_end|>
Description: "Get the current weather in a given location",
Parameters: api.ToolFunctionParameters{
Required: []string{"unit"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"unit": {Type: api.PropertyType{"string"}, Enum: []any{"celsius", "fahrenheit"}, Description: "The unit of temperature"},
// TODO(drifkin): add multiple params back once we have predictable
// order via some sort of ordered map type (see
......@@ -63,7 +63,7 @@ Hello, how are you?<|im_end|>
/*
"location": {Type: api.PropertyType{"string"}, Description: "The city and state, e.g. San Francisco, CA"},
*/
},
}),
},
}},
},
......@@ -140,19 +140,19 @@ That sounds nice! What about New York?<|im_end|>
{Role: "system", Content: "You are a helpful assistant with access to tools."},
{Role: "user", Content: "call double(1) and triple(2)"},
{Role: "assistant", Content: "I'll call double(1) and triple(2) for you.", ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{Name: "double", Arguments: map[string]any{"number": "1"}}},
{Function: api.ToolCallFunction{Name: "triple", Arguments: map[string]any{"number": "2"}}},
{Function: api.ToolCallFunction{Name: "double", Arguments: testArgs(map[string]any{"number": "1"})}},
{Function: api.ToolCallFunction{Name: "triple", Arguments: testArgs(map[string]any{"number": "2"})}},
}},
{Role: "tool", Content: "{\"number\": 2}", ToolName: "double"},
{Role: "tool", Content: "{\"number\": 6}", ToolName: "triple"},
},
tools: []api.Tool{
{Function: api.ToolFunction{Name: "double", Description: "Double a number", Parameters: api.ToolFunctionParameters{Properties: map[string]api.ToolProperty{
{Function: api.ToolFunction{Name: "double", Description: "Double a number", Parameters: api.ToolFunctionParameters{Properties: testPropsMap(map[string]api.ToolProperty{
"number": {Type: api.PropertyType{"string"}, Description: "The number to double"},
}}}},
{Function: api.ToolFunction{Name: "triple", Description: "Triple a number", Parameters: api.ToolFunctionParameters{Properties: map[string]api.ToolProperty{
})}}},
{Function: api.ToolFunction{Name: "triple", Description: "Triple a number", Parameters: api.ToolFunctionParameters{Properties: testPropsMap(map[string]api.ToolProperty{
"number": {Type: api.PropertyType{"string"}, Description: "The number to triple"},
}}}},
})}}},
},
expected: `<|im_start|>system
You are a helpful assistant with access to tools.
......@@ -259,9 +259,9 @@ I'll tell you something interesting about cats`,
{Role: "assistant", ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{
Name: "echo",
Arguments: map[string]any{
Arguments: testArgs(map[string]any{
"payload": map[string]any{"foo": "bar"},
},
}),
}},
}},
{Role: "tool", Content: "{\"payload\": {\"foo\": \"bar\"}}", ToolName: "echo"},
......
......@@ -337,7 +337,7 @@ Let me analyze this image.`,
Role: "assistant",
Content: "I'll check.",
ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{Name: "get-current-weather", Arguments: map[string]any{"location": "Paris", "unit": "celsius"}}},
{Function: api.ToolCallFunction{Name: "get-current-weather", Arguments: testArgsOrdered([]orderedArg{{"location", "Paris"}, {"unit", "celsius"}})}},
},
},
{Role: "user", Content: "<tool_response>\n18\n</tool_response>"},
......@@ -367,8 +367,8 @@ Thanks!<|im_end|>
Role: "assistant",
Content: "before",
ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{Name: "add", Arguments: map[string]any{"a": 2, "b": 3}}},
{Function: api.ToolCallFunction{Name: "mul", Arguments: map[string]any{"x": 4, "y": 5}}},
{Function: api.ToolCallFunction{Name: "add", Arguments: testArgsOrdered([]orderedArg{{"a", 2}, {"b", 3}})}},
{Function: api.ToolCallFunction{Name: "mul", Arguments: testArgsOrdered([]orderedArg{{"x", 4}, {"y", 5}})}},
},
},
},
......@@ -387,7 +387,7 @@ before
name: "consecutive tool responses grouped",
msgs: []api.Message{
{Role: "user", Content: "Compute results"},
{Role: "assistant", Content: "ok", ToolCalls: []api.ToolCall{{Function: api.ToolCallFunction{Name: "job", Arguments: map[string]any{"n": 1}}}}},
{Role: "assistant", Content: "ok", ToolCalls: []api.ToolCall{{Function: api.ToolCallFunction{Name: "job", Arguments: testArgs(map[string]any{"n": 1})}}}},
{Role: "tool", Content: "5", ToolName: "job"},
{Role: "tool", Content: "6", ToolName: "job"},
},
......@@ -412,7 +412,7 @@ ok
name: "last message is tool then prefill",
msgs: []api.Message{
{Role: "user", Content: "run"},
{Role: "assistant", Content: "ok", ToolCalls: []api.ToolCall{{Function: api.ToolCallFunction{Name: "exec", Arguments: map[string]any{"cmd": "ls"}}}}},
{Role: "assistant", Content: "ok", ToolCalls: []api.ToolCall{{Function: api.ToolCallFunction{Name: "exec", Arguments: testArgs(map[string]any{"cmd": "ls"})}}}},
{Role: "tool", Content: "done", ToolName: "exec"},
},
expected: `<|im_start|>user
......@@ -447,7 +447,7 @@ done
Role: "assistant",
Content: "I'll check.",
ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{Name: "get-current-weather", Arguments: map[string]any{"location": "Paris", "unit": "celsius"}}},
{Function: api.ToolCallFunction{Name: "get-current-weather", Arguments: testArgsOrdered([]orderedArg{{"location", "Paris"}, {"unit", "celsius"}})}},
},
},
{Role: "user", Content: "<tool_response>\n18\n</tool_response>"},
......@@ -477,7 +477,7 @@ Thanks!<|im_end|>
Role: "assistant",
Content: "I'll check.",
ToolCalls: []api.ToolCall{
{Function: api.ToolCallFunction{Name: "get-current-weather", Arguments: map[string]any{"location": "Paris", "unit": "celsius"}}},
{Function: api.ToolCallFunction{Name: "get-current-weather", Arguments: testArgsOrdered([]orderedArg{{"location", "Paris"}, {"unit", "celsius"}})}},
},
},
{Role: "user", Content: "\n\n\n\n<tool_response>\n18\n</tool_response> extra\n\n\n\n\n\n"},
......
......@@ -128,10 +128,10 @@ Speak poetry after the first sentence.</think><think>Speak poetry after the seco
// {
// Function: api.ToolCallFunction{
// Name: "get-current-weather",
// Arguments: map[string]any{
// Arguments: testArgs(map[string]any{
// "location": "New York",
// "unit": "fahrenheit",
// },
// }),
// },
// },
// },
......@@ -148,7 +148,7 @@ Speak poetry after the first sentence.</think><think>Speak poetry after the seco
// Parameters: api.ToolFunctionParameters{
// Type: "object",
// Required: []string{"location"},
// Properties: map[string]api.ToolProperty{
// Properties: testPropsMap(map[string]api.ToolProperty{
// "location": {
// Type: api.PropertyType{"string"},
// Description: "The city and state, e.g. San Francisco, CA",
......@@ -158,7 +158,7 @@ Speak poetry after the first sentence.</think><think>Speak poetry after the seco
// Enum: []any{"celsius", "fahrenheit"},
// Description: "The temperature unit",
// },
// },
// }),
// },
// },
// },
......@@ -216,19 +216,19 @@ Speak poetry after the first sentence.</think><think>Speak poetry after the seco
// {
// Function: api.ToolCallFunction{
// Name: "add",
// Arguments: map[string]any{
// Arguments: testArgs(map[string]any{
// "a": 2,
// "b": 3,
// },
// }),
// },
// },
// {
// Function: api.ToolCallFunction{
// Name: "multiply",
// Arguments: map[string]any{
// Arguments: testArgs(map[string]any{
// "x": 4,
// "y": 5,
// },
// }),
// },
// },
// },
......@@ -257,10 +257,10 @@ Speak poetry after the first sentence.</think><think>Speak poetry after the seco
// Parameters: api.ToolFunctionParameters{
// Type: "object",
// Required: []string{"a", "b"},
// Properties: map[string]api.ToolProperty{
// Properties: testPropsMap(map[string]api.ToolProperty{
// "a": {Type: api.PropertyType{"integer"}, Description: "First number"},
// "b": {Type: api.PropertyType{"integer"}, Description: "Second number"},
// },
// }),
// },
// },
// },
......@@ -272,10 +272,10 @@ Speak poetry after the first sentence.</think><think>Speak poetry after the seco
// Parameters: api.ToolFunctionParameters{
// Type: "object",
// Required: []string{"x", "y"},
// Properties: map[string]api.ToolProperty{
// Properties: testPropsMap(map[string]api.ToolProperty{
// "x": {Type: api.PropertyType{"integer"}, Description: "First factor"},
// "y": {Type: api.PropertyType{"integer"}, Description: "Second factor"},
// },
// }),
// },
// },
// },
......
package renderers
import "github.com/ollama/ollama/api"
// 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
}
// orderedArg represents a key-value pair for ordered argument creation
type orderedArg struct {
Key string
Value any
}
// testArgsOrdered creates ToolCallFunctionArguments with a specific key order
func testArgsOrdered(pairs []orderedArg) api.ToolCallFunctionArguments {
args := api.NewToolCallFunctionArguments()
for _, p := range pairs {
args.Set(p.Key, p.Value)
}
return args
}
......@@ -10,6 +10,20 @@ import (
"github.com/ollama/ollama/api"
)
// testArgs creates ToolCallFunctionArguments from a map (convenience function for tests)
func testArgs(m map[string]any) api.ToolCallFunctionArguments {
args := api.NewToolCallFunctionArguments()
for k, v := range m {
args.Set(k, v)
}
return args
}
// argsComparer provides cmp options for comparing ToolCallFunctionArguments by value
var argsComparer = cmp.Comparer(func(a, b api.ToolCallFunctionArguments) bool {
return cmp.Equal(a.ToMap(), b.ToMap())
})
const (
prefix = `data:image/jpeg;base64,`
image = `iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=`
......@@ -159,9 +173,9 @@ func TestToToolCallsPreservesIDs(t *testing.T) {
Function: api.ToolCallFunction{
Index: 2,
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Seattle",
},
}),
},
},
{
......@@ -169,9 +183,9 @@ func TestToToolCallsPreservesIDs(t *testing.T) {
Function: api.ToolCallFunction{
Index: 7,
Name: "get_time",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"timezone": "UTC",
},
}),
},
},
}
......@@ -215,7 +229,7 @@ func TestToToolCallsPreservesIDs(t *testing.T) {
t.Errorf("tool calls mismatch (-want +got):\n%s", diff)
}
if diff := cmp.Diff(original, toolCalls); diff != "" {
if diff := cmp.Diff(original, toolCalls, argsComparer); diff != "" {
t.Errorf("input tool calls mutated (-want +got):\n%s", diff)
}
}
......
......@@ -925,7 +925,7 @@ func TestResponsesStreamConverter_ToolCalls(t *testing.T) {
ID: "call_abc",
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{"city": "Paris"},
Arguments: testArgs(map[string]any{"city": "Paris"}),
},
},
},
......@@ -1800,7 +1800,7 @@ func TestResponsesStreamConverter_FunctionCallStatus(t *testing.T) {
ID: "call_abc",
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{"city": "Paris"},
Arguments: testArgs(map[string]any{"city": "Paris"}),
},
},
},
......
......@@ -22,6 +22,29 @@ import (
"github.com/ollama/ollama/ml"
)
// testPropsMap creates a ToolPropertiesMap from a map (convenience function for tests)
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)
func testArgs(m map[string]any) api.ToolCallFunctionArguments {
args := api.NewToolCallFunctionArguments()
for k, v := range m {
args.Set(k, v)
}
return args
}
// argsComparer provides cmp options for comparing ToolCallFunctionArguments by value
var argsComparer = cmp.Comparer(func(a, b api.ToolCallFunctionArguments) bool {
return cmp.Equal(a.ToMap(), b.ToMap())
})
type mockRunner struct {
llm.LlamaServer
......@@ -488,7 +511,7 @@ func TestGenerateChat(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"location"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "The city and state",
......@@ -497,7 +520,7 @@ func TestGenerateChat(t *testing.T) {
Type: api.PropertyType{"string"},
Enum: []any{"celsius", "fahrenheit"},
},
},
}),
},
},
},
......@@ -559,15 +582,15 @@ func TestGenerateChat(t *testing.T) {
expectedToolCall := api.ToolCall{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Seattle, WA",
"unit": "celsius",
},
}),
},
}
expectedToolCall.ID = gotToolCall.ID
if diff := cmp.Diff(gotToolCall, expectedToolCall); diff != "" {
if diff := cmp.Diff(gotToolCall, expectedToolCall, argsComparer); diff != "" {
t.Errorf("tool call mismatch (-got +want):\n%s", diff)
}
})
......@@ -582,7 +605,7 @@ func TestGenerateChat(t *testing.T) {
Parameters: api.ToolFunctionParameters{
Type: "object",
Required: []string{"location"},
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {
Type: api.PropertyType{"string"},
Description: "The city and state",
......@@ -591,7 +614,7 @@ func TestGenerateChat(t *testing.T) {
Type: api.PropertyType{"string"},
Enum: []any{"celsius", "fahrenheit"},
},
},
}),
},
},
},
......@@ -688,10 +711,10 @@ func TestGenerateChat(t *testing.T) {
expectedToolCall := api.ToolCall{
Function: api.ToolCallFunction{
Name: "get_weather",
Arguments: api.ToolCallFunctionArguments{
Arguments: testArgs(map[string]any{
"location": "Seattle, WA",
"unit": "celsius",
},
}),
},
}
......@@ -703,7 +726,7 @@ func TestGenerateChat(t *testing.T) {
}
expectedToolCall.ID = finalToolCall.ID
if diff := cmp.Diff(finalToolCall, expectedToolCall); diff != "" {
if diff := cmp.Diff(finalToolCall, expectedToolCall, argsComparer); diff != "" {
t.Errorf("final tool call mismatch (-got +want):\n%s", diff)
}
})
......@@ -716,9 +739,9 @@ func TestGenerateChat(t *testing.T) {
Name: "get_weather",
Parameters: api.ToolFunctionParameters{
Type: "object",
Properties: map[string]api.ToolProperty{
Properties: testPropsMap(map[string]api.ToolProperty{
"location": {Type: api.PropertyType{"string"}},
},
}),
},
},
},
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment