WIP: stable ordering for tool args

Right now we deserialize tool call definitions' arguments into golang
maps. These purposefully don't have a predictable iteration order,
whereas we want to maintain the order the user originally provided.

Unstable rendering of arguments means that we break the kv cache, which
this change fixes.

There's no way to build this in a fully backwards compatible way when
executing existing templates exactly as they are. We get around this by
rewriting templates dynamically just before they're rendered. This is
fragile, but perhaps the least bad option?
This commit is contained in:
Devon Rifkin
2025-10-07 15:38:58 -07:00
parent bc71278670
commit c87b910232
17 changed files with 935 additions and 347 deletions

View File

@@ -268,17 +268,17 @@ func parseToolCall(raw qwenEventRawToolCall, tools []api.Tool) (api.ToolCall, er
}
}
toolCall.Function.Arguments = make(api.ToolCallFunctionArguments)
toolCall.Function.Arguments = api.NewToolCallFunctionArguments()
for _, parameter := range functionCall.Parameters {
// Look up the parameter type if we found the tool
var paramType api.PropertyType
if matchedTool != nil && matchedTool.Function.Parameters.Properties != nil {
if prop, ok := matchedTool.Function.Parameters.Properties[parameter.Name]; ok {
if matchedTool != nil && matchedTool.Function.Parameters.GetProperties() != nil {
if prop, ok := matchedTool.Function.Parameters.GetProperties().Get(parameter.Name); ok {
paramType = prop.Type
}
}
toolCall.Function.Arguments[parameter.Name] = parseValue(parameter.Value, paramType)
toolCall.Function.Arguments.Set(parameter.Name, parseValue(parameter.Value, paramType))
}
return toolCall, nil

View File

@@ -11,10 +11,25 @@ import (
func tool(name string, props map[string]api.ToolProperty) api.Tool {
t := api.Tool{Type: "function", Function: api.ToolFunction{Name: name}}
t.Function.Parameters.Type = "object"
t.Function.Parameters.Properties = props
p := api.NewToolProperties()
for k, v := range props {
p.Set(k, v)
}
t.Function.Parameters.SetProperties(p)
return t
}
// Helper function to create ordered arguments for tests
func makeArgs(pairs ...any) api.ToolCallFunctionArguments {
args := api.NewToolCallFunctionArguments()
for i := 0; i < len(pairs); i += 2 {
key := pairs[i].(string)
value := pairs[i+1]
args.Set(key, value)
}
return args
}
func TestQwenParserStreaming(t *testing.T) {
type step struct {
input string
@@ -354,10 +369,7 @@ celsius
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "get_current_temperature",
Arguments: map[string]any{
"location": "San Francisco",
"unit": "celsius",
},
Arguments: makeArgs("location", "San Francisco", "unit", "celsius"),
},
},
},
@@ -375,10 +387,10 @@ celsius
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "get current temperature",
Arguments: map[string]any{
"location with spaces": "San Francisco",
"unit with spaces": "celsius",
},
Arguments: makeArgs(
"location with spaces", "San Francisco",
"unit with spaces", "celsius",
),
},
},
},
@@ -400,10 +412,10 @@ San Francisco
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "\"get current temperature\"",
Arguments: map[string]any{
"\"location with spaces\"": "San Francisco",
"\"unit with spaces\"": "\"celsius\"",
},
Arguments: makeArgs(
"\"location with spaces\"", "San Francisco",
"\"unit with spaces\"", "\"celsius\"",
),
},
},
},
@@ -434,12 +446,12 @@ true
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "calculate",
Arguments: map[string]any{
"x": 3.14,
"y": 42,
"enabled": true,
"items": []any{"a", "b", "c"},
},
Arguments: makeArgs(
"x", 3.14,
"y", 42,
"enabled", true,
"items", []any{"a", "b", "c"},
),
},
},
},
@@ -455,9 +467,7 @@ ls && echo "done"
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "exec",
Arguments: map[string]any{
"command": "ls && echo \"done\"",
},
Arguments: makeArgs("command", "ls && echo \"done\""),
},
},
},
@@ -472,9 +482,7 @@ ls && echo "a > b and a < b"
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "exec",
Arguments: map[string]any{
"command": "ls && echo \"a > b and a < b\"",
},
Arguments: makeArgs("command", "ls && echo \"a > b and a < b\""),
},
},
},
@@ -492,10 +500,7 @@ Hello! 你好! 🌟 مرحبا
wantToolCall: api.ToolCall{
Function: api.ToolCallFunction{
Name: "获取天气",
Arguments: map[string]any{
"城市": "北京",
"message": "Hello! 你好! 🌟 مرحبا",
},
Arguments: makeArgs("城市", "北京", "message", "Hello! 你好! 🌟 مرحبا"),
},
},
},