2025-05-24 05:19:31 +08:00
package tools
import (
2025-08-23 06:22:14 +08:00
"strings"
2025-05-24 05:19:31 +08:00
"testing"
2025-06-13 05:18:54 +08:00
"text/template"
2025-05-24 05:19:31 +08:00
"github.com/google/go-cmp/cmp"
"github.com/ollama/ollama/api"
)
2025-06-13 05:18:54 +08:00
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 {
t . Fatalf ( "Failed to parse template: %v" , err )
}
2025-05-24 05:19:31 +08:00
2025-06-13 05:18:54 +08:00
deepseek , err := template . New ( "deepseek" ) . Parse ( "{{if .ToolCalls}}<|tool▁calls▁begin|>{{range .ToolCalls}}<|tool▁call▁begin|>function<|tool▁sep|>get_current_weather\n```json\n{\"location\": \"Tokyo\"}\n```<|tool▁call▁end|>{{end}}<|tool▁calls▁end|><|end▁of▁sentence|>{{end}}" )
2025-05-24 05:19:31 +08:00
if err != nil {
2025-06-13 05:18:54 +08:00
t . Fatalf ( "Failed to parse template: %v" , err )
2025-05-24 05:19:31 +08:00
}
2025-06-13 05:18:54 +08:00
json , err := template . New ( "json" ) . Parse ( ` {{ if .ToolCalls }} {{ range .ToolCalls }} { "name": " {{ .Function .Name }} ", "arguments": {{ .Function .Arguments }} } {{ end }} {{ end }} ` )
if err != nil {
t . Fatalf ( "Failed to parse template: %v" , err )
}
mistral , err := template . New ( "mistral" ) . Parse ( ` {{ if .ToolCalls }} [TOOL_CALLS] [ {{ range .ToolCalls }} { "name": " {{ .Function .Name }} ", "arguments": {{ .Function .Arguments }} } {{ end }} ][/TOOL_CALLS] {{ end }} ` )
if err != nil {
t . Fatalf ( "Failed to parse template: %v" , err )
}
list , err := template . New ( "list" ) . Parse ( ` {{ if .ToolCalls }} [ {{ range .ToolCalls }} { "name": " {{ .Function .Name }} ", "arguments": {{ .Function .Arguments }} } {{ end }} ] {{ end }} ` )
if err != nil {
t . Fatalf ( "Failed to parse template: %v" , err )
}
tools := [ ] api . Tool {
{
Type : "function" ,
Function : api . ToolFunction {
Name : "get_temperature" ,
Description : "Retrieve the temperature for a given location" ,
2025-08-23 07:26:48 +08:00
Parameters : api . ToolFunctionParameters {
2025-06-30 23:59:03 +08:00
Type : "object" ,
Required : [ ] string { "city" } ,
2025-08-06 07:46:24 +08:00
Properties : map [ string ] api . ToolProperty {
2025-06-13 05:18:54 +08:00
"format" : {
Type : api . PropertyType { "string" } ,
Description : "The format to return the temperature in" ,
Enum : [ ] any { "fahrenheit" , "celsius" } ,
} ,
"city" : {
Type : api . PropertyType { "string" } ,
Description : "The city to get the temperature for" ,
} ,
} ,
} ,
} ,
} ,
{
Type : "function" ,
Function : api . ToolFunction {
Name : "get_conditions" ,
Description : "Retrieve the current weather conditions for a given location" ,
2025-08-23 07:26:48 +08:00
Parameters : api . ToolFunctionParameters {
2025-06-13 05:18:54 +08:00
Type : "object" ,
2025-08-06 07:46:24 +08:00
Properties : map [ string ] api . ToolProperty {
2025-06-13 05:18:54 +08:00
"location" : {
Type : api . PropertyType { "string" } ,
Description : "The location to get the weather conditions for" ,
} ,
} ,
} ,
} ,
} ,
2025-06-18 01:51:43 +08:00
{
Type : "function" ,
Function : api . ToolFunction {
Name : "say_hello" ,
Description : "Say hello" ,
} ,
} ,
2025-07-21 05:55:14 +08:00
{
Type : "function" ,
Function : api . ToolFunction {
Name : "say_hello_world" ,
Description : "Say hello world" ,
} ,
} ,
{
Type : "function" ,
Function : api . ToolFunction {
Name : "get_address" ,
Description : "Get the address of a given location" ,
2025-08-23 07:26:48 +08:00
Parameters : api . ToolFunctionParameters {
2025-07-21 05:55:14 +08:00
Type : "object" ,
2025-08-06 07:46:24 +08:00
Properties : map [ string ] api . ToolProperty {
2025-07-21 05:55:14 +08:00
"location" : {
Type : api . PropertyType { "string" } ,
Description : "The location to get the address for" ,
} ,
} ,
} ,
} ,
} ,
{
Type : "function" ,
Function : api . ToolFunction {
Name : "add" ,
Description : "Add two numbers" ,
2025-08-23 07:26:48 +08:00
Parameters : api . ToolFunctionParameters {
2025-07-21 05:55:14 +08:00
Type : "object" ,
2025-08-06 07:46:24 +08:00
Properties : map [ string ] api . ToolProperty {
2025-07-21 05:55:14 +08:00
"a" : {
Type : api . PropertyType { "string" } ,
Description : "The first number to add" ,
} ,
"b" : {
Type : api . PropertyType { "string" } ,
Description : "The second number to add" ,
} ,
} ,
} ,
} ,
} ,
2025-06-13 05:18:54 +08:00
}
2025-05-24 05:19:31 +08:00
tests := [ ] struct {
2025-06-13 05:18:54 +08:00
name string
inputs [ ] string
tmpl * template . Template
content string
calls [ ] api . ToolCall
2025-05-24 05:19:31 +08:00
} {
{
2025-06-13 05:18:54 +08:00
name : "no tool calls - just text" ,
inputs : [ ] string { "Hello, how can I help you today?" } ,
content : "Hello, how can I help you today?" ,
tmpl : qwen ,
calls : nil ,
} ,
{
name : "empty input" ,
inputs : [ ] string { "" } ,
content : "" ,
tmpl : qwen ,
calls : nil ,
} ,
{
name : "tool call" ,
inputs : [ ] string { ` <tool_call> { "name": "get_conditions", "arguments": { "location": "San Francisco"}}</tool_call> ` } ,
content : "" ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
2025-05-24 05:19:31 +08:00
{
Function : api . ToolCallFunction {
2025-06-13 05:18:54 +08:00
Index : 0 ,
Name : "get_conditions" ,
Arguments : api . ToolCallFunctionArguments {
"location" : "San Francisco" ,
2025-05-24 05:19:31 +08:00
} ,
} ,
} ,
} ,
2025-06-13 05:18:54 +08:00
} ,
2025-06-18 01:51:43 +08:00
{
2025-06-30 23:59:03 +08:00
name : "empty args" ,
inputs : [ ] string { ` <tool_call> { "name": "get_conditions", "arguments": { }}</tool_call> ` } ,
content : "" ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "get_conditions" ,
Arguments : api . ToolCallFunctionArguments { } ,
} ,
} ,
} ,
} ,
2025-06-13 05:18:54 +08:00
{
name : "text before tool call" ,
inputs : [ ] string { ` Let me check the weather. <tool_call> { "name": "get_temperature", "arguments": { "city": "New York"}}</tool_call> ` } ,
content : "Let me check the weather. " ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
2025-05-24 05:19:31 +08:00
{
Function : api . ToolCallFunction {
2025-06-13 05:18:54 +08:00
Index : 0 ,
Name : "get_temperature" ,
Arguments : api . ToolCallFunctionArguments {
"city" : "New York" ,
} ,
} ,
} ,
} ,
} ,
2025-06-18 01:51:43 +08:00
{
name : "qwen no args with text" ,
inputs : [ ] string { "Let me say hello to the user. I'll use the say_hello tool. " } ,
content : "Let me say hello to the user. I'll use the say_hello tool. " ,
tmpl : qwen ,
calls : nil ,
} ,
2025-06-13 05:18:54 +08:00
{
name : "two tool calls in a list" ,
inputs : [ ] string { ` [TOOL_CALLS] [ { "name": "get_temperature", "arguments": { "city": "London", "format": "fahrenheit"}}, { "name": "get_conditions", "arguments": { "location": "Tokyo"}}][/TOOL_CALLS] ` } ,
content : "" ,
tmpl : mistral ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "get_temperature" ,
Arguments : api . ToolCallFunctionArguments {
"city" : "London" ,
"format" : "fahrenheit" ,
2025-05-24 05:19:31 +08:00
} ,
} ,
} ,
{
Function : api . ToolCallFunction {
2025-06-13 05:18:54 +08:00
Index : 1 ,
Name : "get_conditions" ,
Arguments : api . ToolCallFunctionArguments {
"location" : "Tokyo" ,
2025-05-24 05:19:31 +08:00
} ,
} ,
} ,
} ,
2025-06-13 05:18:54 +08:00
} ,
{
2025-06-18 01:51:43 +08:00
name : "qwen two tool calls" ,
2025-06-13 05:18:54 +08:00
inputs : [ ] string { ` Okay, let's call both tools! <tool_call> { "name": "get_temperature", "arguments": { "city": "London", "format": "fahrenheit"}}</tool_call><tool_call> { "name": "get_conditions", "arguments": { "location": "Tokyo"}}</tool_call> ` } ,
content : "Okay, let's call both tools! " ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
2025-05-24 05:19:31 +08:00
{
Function : api . ToolCallFunction {
2025-06-13 05:18:54 +08:00
Index : 0 ,
Name : "get_temperature" ,
Arguments : api . ToolCallFunctionArguments {
"city" : "London" ,
"format" : "fahrenheit" ,
2025-05-24 05:19:31 +08:00
} ,
} ,
} ,
{
Function : api . ToolCallFunction {
2025-06-13 05:18:54 +08:00
Index : 1 ,
Name : "get_conditions" ,
Arguments : api . ToolCallFunctionArguments {
"location" : "Tokyo" ,
2025-05-24 05:19:31 +08:00
} ,
} ,
} ,
} ,
2025-06-13 05:18:54 +08:00
} ,
2025-06-18 01:51:43 +08:00
{
2025-06-30 23:59:03 +08:00
name : "empty args followed by args" ,
inputs : [ ] string { ` Let me say hello and check the weather. <tool_call> { "name": "say_hello", "arguments": { }}</tool_call><tool_call> { "name": "get_temperature", "arguments": { "city": "London", "format": "fahrenheit"}}</tool_call> ` } ,
content : "Let me say hello and check the weather. " ,
2025-06-18 01:51:43 +08:00
tmpl : qwen ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
2025-06-18 20:20:43 +08:00
Index : 0 ,
Name : "say_hello" ,
Arguments : api . ToolCallFunctionArguments { } ,
2025-06-18 01:51:43 +08:00
} ,
} ,
2025-06-30 23:59:03 +08:00
{
Function : api . ToolCallFunction {
Index : 1 ,
Name : "get_temperature" ,
Arguments : api . ToolCallFunctionArguments {
"city" : "London" ,
"format" : "fahrenheit" ,
} ,
} ,
} ,
} ,
} ,
{
name : "qwen empty followed by args" ,
inputs : [ ] string { ` Let me check the weather. <tool_call> { "name": "get_conditions", "arguments": { }}</tool_call><tool_call> { "name": "get_conditions", "arguments": { "location": "Tokyo"}} ` } ,
content : "Let me check the weather. " ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "get_conditions" ,
Arguments : api . ToolCallFunctionArguments { } ,
} ,
} ,
2025-06-18 01:51:43 +08:00
{
Function : api . ToolCallFunction {
Index : 1 ,
Name : "get_conditions" ,
Arguments : api . ToolCallFunctionArguments {
"location" : "Tokyo" ,
} ,
} ,
} ,
} ,
} ,
2025-06-13 05:18:54 +08:00
{
name : "deepseek" ,
inputs : [ ] string { "<think>Wait, I need to call a tool</think><|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_temperature\n```json\n{\"city\": \"Tokyo\"}\n```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>" } ,
content : "<think>Wait, I need to call a tool</think>" ,
tmpl : deepseek ,
calls : [ ] api . ToolCall {
2025-05-24 05:19:31 +08:00
{
Function : api . ToolCallFunction {
2025-06-13 05:18:54 +08:00
Index : 0 ,
Name : "get_temperature" ,
Arguments : api . ToolCallFunctionArguments {
"city" : "Tokyo" ,
2025-05-24 05:19:31 +08:00
} ,
} ,
} ,
2025-06-13 05:18:54 +08:00
} ,
} ,
{
name : "deepseek incremental" ,
inputs : [ ] string {
"<think>Wait" ,
", I need" ,
" to call" ,
" a tool</think><|too" ,
"l▁calls▁begin" ,
"|>" ,
"<|tool▁call▁begin|>function<|tool▁sep|>get_temperature\n" ,
"```json\n" ,
"{\"city\": \"Tokyo\"}\n" ,
"```" ,
"<|tool▁c" , "all▁end|>" ,
"<|tool▁calls▁end|>" ,
"<|end▁of▁sentence|>" ,
} ,
content : "<think>Wait, I need to call a tool</think>" ,
tmpl : deepseek ,
calls : [ ] api . ToolCall {
2025-05-24 05:19:31 +08:00
{
Function : api . ToolCallFunction {
2025-06-13 05:18:54 +08:00
Index : 0 ,
Name : "get_temperature" ,
Arguments : api . ToolCallFunctionArguments {
"city" : "Tokyo" ,
2025-05-24 05:19:31 +08:00
} ,
} ,
} ,
} ,
} ,
{
2025-06-13 05:18:54 +08:00
name : "json" ,
inputs : [ ] string {
"{" ,
"\"name\": \"get_temperature\"," ,
"\"arguments\": {" ,
"\"city\": \"Tokyo\"" ,
"}" ,
"}" ,
} ,
content : "" ,
tmpl : json ,
calls : [ ] api . ToolCall {
2025-05-24 05:19:31 +08:00
{
Function : api . ToolCallFunction {
2025-06-13 05:18:54 +08:00
Index : 0 ,
Name : "get_temperature" ,
Arguments : api . ToolCallFunctionArguments {
"city" : "Tokyo" ,
2025-05-24 05:19:31 +08:00
} ,
} ,
} ,
} ,
2025-06-13 05:18:54 +08:00
} ,
{
name : "json maybe a tool call" ,
inputs : [ ] string {
"{" ,
"\"name\": \"get_temperature\"," ,
"\"arguments\": {" ,
} ,
content : "" ,
tmpl : json ,
calls : nil ,
} ,
{
name : "json not a tool call" ,
inputs : [ ] string {
"{" ,
"\"name\": \"search\", " ,
"\"arguments\": {" ,
"\"query\": \"What is the capital of Canada?\"" ,
"}" ,
"}" ,
} ,
content : "{\"name\": \"search\", \"arguments\": {\"query\": \"What is the capital of Canada?\"}}" ,
tmpl : json ,
calls : nil ,
} ,
{
name : "json object followed by tool call" ,
inputs : [ ] string {
"{\"name\": \"jeff\"}" ,
"{\"name\": \"get_conditions\", \"arguments\": {\"location\": \"San Francisco\"}}" ,
} ,
content : "{\"name\": \"jeff\"}{\"name\": \"get_conditions\", \"arguments\": {\"location\": \"San Francisco\"}}" ,
tmpl : json ,
} ,
{
name : "json object followed by tool call split" ,
inputs : [ ] string {
"{\"name\": \"jeff\"} {" ,
"\"name\": \"get_conditions\", \"arguments\": {\"location\": \"San Francisco\"}}" ,
} ,
content : "{\"name\": \"jeff\"} {\"name\": \"get_conditions\", \"arguments\": {\"location\": \"San Francisco\"}}" ,
tmpl : json ,
} ,
{
name : "json code" ,
inputs : [ ] string {
"for { fmt.Println(\"hello\") }" ,
} ,
content : "for { fmt.Println(\"hello\") }" ,
tmpl : json ,
} ,
{
name : "list multiple" ,
inputs : [ ] string {
"[" ,
"{" ,
"\"name\": \"get_temperature\", " ,
"\"arguments\": {" ,
"\"city\": \"London\"" ,
"}" ,
"}," ,
"{" ,
"\"name\": \"get_conditions\", " ,
"\"arguments\": {" ,
"\"location\": \"Tokyo\"" ,
"}" ,
"}]" ,
} ,
content : "" ,
tmpl : list ,
calls : [ ] api . ToolCall {
2025-05-24 05:19:31 +08:00
{
Function : api . ToolCallFunction {
2025-06-13 05:18:54 +08:00
Index : 0 ,
Name : "get_temperature" ,
Arguments : api . ToolCallFunctionArguments {
"city" : "London" ,
2025-05-24 05:19:31 +08:00
} ,
} ,
} ,
2025-06-13 05:18:54 +08:00
{
Function : api . ToolCallFunction {
Index : 1 ,
Name : "get_conditions" ,
Arguments : api . ToolCallFunctionArguments {
"location" : "Tokyo" ,
} ,
} ,
} ,
} ,
} ,
{
name : "list partial" ,
2025-06-18 01:51:43 +08:00
inputs : [ ] string {
"[{" ,
"\"name\": \"get_conditions\", " ,
"\"arguments\": {" ,
"\"location\": \"Tokyo\"" ,
"}" ,
"}" ,
} ,
content : "" ,
tmpl : list ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "get_conditions" ,
Arguments : api . ToolCallFunctionArguments {
"location" : "Tokyo" ,
} ,
} ,
} ,
} ,
} ,
{
name : "list invalid" ,
2025-06-13 05:18:54 +08:00
inputs : [ ] string {
"[" ,
"{" ,
"\"name\": \"search\", " ,
"\"arguments\": {" ,
"\"query\": \"What is the capital of Canada?\"" ,
"}" ,
"}" ,
} ,
content : "" ,
tmpl : list ,
calls : nil ,
} ,
2025-06-18 01:51:43 +08:00
{
name : "list trailing ]" ,
inputs : [ ] string {
"[" ,
"{" ,
"\"name\": \"get_conditions\", " ,
"\"arguments\": {" ,
"\"location\": \"Tokyo\"" ,
"}" ,
"}" ,
"]" ,
"]" ,
} ,
content : "" ,
tmpl : list ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "get_conditions" ,
Arguments : api . ToolCallFunctionArguments {
"location" : "Tokyo" ,
} ,
} ,
} ,
} ,
} ,
2025-06-13 05:18:54 +08:00
{
name : "list not a tool call" ,
inputs : [ ] string {
"[special" ,
" del" ,
"ivery]" ,
2025-05-24 05:19:31 +08:00
} ,
2025-06-13 05:18:54 +08:00
content : "[special delivery]" ,
tmpl : list ,
calls : nil ,
2025-05-24 05:19:31 +08:00
} ,
2025-07-21 05:55:14 +08:00
{
name : "tool name with collision" ,
inputs : [ ] string {
"<tool_call>" ,
"{" ,
"\"name\": \"say_hello" ,
"_world\"," ,
2025-07-24 12:21:29 +08:00
"\"arguments\": {}}" ,
2025-07-21 05:55:14 +08:00
"}" ,
} ,
content : "" ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "say_hello_world" ,
Arguments : api . ToolCallFunctionArguments { } ,
} ,
} ,
} ,
} ,
{
name : "tool name with collision multiple" ,
inputs : [ ] string {
"<tool_call>" ,
"{" ,
"\"name\": \"say_hello" ,
"_world\"," ,
2025-07-24 12:21:29 +08:00
"\"arguments\": {}}" ,
2025-07-21 05:55:14 +08:00
"</tool_call>" ,
"<tool_call>" ,
"{" ,
"\"name\": \"say_hello" ,
"\"," ,
2025-07-24 12:21:29 +08:00
"\"arguments\": {}}" ,
2025-07-21 05:55:14 +08:00
"</tool_call>" ,
} ,
content : "" ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "say_hello_world" ,
Arguments : api . ToolCallFunctionArguments { } ,
} ,
} ,
{
Function : api . ToolCallFunction {
Index : 1 ,
Name : "say_hello" ,
Arguments : api . ToolCallFunctionArguments { } ,
} ,
} ,
} ,
} ,
{
name : "tool name with collision non streaming" ,
inputs : [ ] string {
` <tool_call> { "name": "say_hello ` ,
} ,
content : "" ,
tmpl : qwen ,
calls : nil ,
} ,
{
name : "tool name with collision non streaming multiple" ,
inputs : [ ] string {
2025-07-24 12:21:29 +08:00
` <tool_call> { "name": "say_hello", "arguments": { }}</tool_call><tool_call> { "name": "say_hello_world", "arguments": { }} ` ,
2025-07-21 05:55:14 +08:00
} ,
content : "" ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "say_hello" ,
Arguments : api . ToolCallFunctionArguments { } ,
} ,
} ,
{
Function : api . ToolCallFunction {
Index : 1 ,
Name : "say_hello_world" ,
Arguments : api . ToolCallFunctionArguments { } ,
} ,
} ,
} ,
} ,
{
name : "tool name with collision non streaming shorter" ,
inputs : [ ] string {
2025-07-24 12:21:29 +08:00
` <tool_call> { "name": "say_hello", "arguments": { }}</tool_call> ` ,
2025-07-21 05:55:14 +08:00
} ,
content : "" ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "say_hello" ,
Arguments : api . ToolCallFunctionArguments { } ,
} ,
} ,
} ,
} ,
{
name : "tool name with collision non streaming longer" ,
inputs : [ ] string {
2025-07-24 12:21:29 +08:00
` <tool_call> { "name": "say_hello_world", "arguments": { }}</tool_call> ` ,
2025-07-21 05:55:14 +08:00
} ,
content : "" ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "say_hello_world" ,
Arguments : api . ToolCallFunctionArguments { } ,
} ,
} ,
} ,
} ,
{
name : "tool name with substring of another" ,
inputs : [ ] string {
"{" ,
"\"name\": \"get_address\"," ,
"\"arguments\": {" ,
"\"location\": \"London\"" ,
"}" ,
"}" ,
} ,
content : "" ,
tmpl : json ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "get_address" ,
Arguments : api . ToolCallFunctionArguments {
"location" : "London" ,
} ,
} ,
} ,
} ,
} ,
{
name : "tool name with substring of another" ,
inputs : [ ] string {
` <tool_call> { "name": "get_address", "arguments": { "location": "London"}}</tool_call> ` ,
} ,
content : "" ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "get_address" ,
Arguments : api . ToolCallFunctionArguments {
"location" : "London" ,
} ,
} ,
} ,
} ,
} ,
2025-07-24 12:21:29 +08:00
{
name : "args before name" ,
inputs : [ ] string {
` <tool_call> { "arguments": { "a": "5", "b": "10"}, "name": "add"}</tool_call> ` ,
} ,
content : "" ,
tmpl : qwen ,
calls : [ ] api . ToolCall {
{
Function : api . ToolCallFunction {
Index : 0 ,
Name : "add" ,
Arguments : api . ToolCallFunctionArguments {
"a" : "5" ,
"b" : "10" ,
} ,
} ,
} ,
} ,
} ,
2025-05-24 05:19:31 +08:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
2025-06-13 05:18:54 +08:00
parser := NewParser ( tt . tmpl , tools )
2025-05-24 05:19:31 +08:00
2025-06-13 05:18:54 +08:00
var calls [ ] api . ToolCall
var content string
for _ , input := range tt . inputs {
tcs , c := parser . Add ( input )
calls = append ( calls , tcs ... )
content += c
2025-05-24 05:19:31 +08:00
}
2025-06-13 05:18:54 +08:00
if content != tt . content {
t . Errorf ( "Expected content %q, got %q" , tt . content , content )
2025-05-24 05:19:31 +08:00
}
2025-06-13 05:18:54 +08:00
if len ( calls ) != len ( tt . calls ) {
t . Fatalf ( "Expected %d tool calls, got %d" , len ( tt . calls ) , len ( calls ) )
}
for i , want := range tt . calls {
if diff := cmp . Diff ( calls [ i ] , want ) ; diff != "" {
t . Errorf ( "Tool call %d mismatch (-got +want):\n%s" , i , diff )
}
2025-05-24 05:19:31 +08:00
}
} )
}
}
2025-06-13 05:18:54 +08:00
func TestDone ( t * testing . T ) {
tests := [ ] struct {
name string
tag string
buffer [ ] byte
want bool
2025-05-24 05:19:31 +08:00
} {
{
2025-06-13 05:18:54 +08:00
name : "empty" ,
tag : "<tool_call>" ,
buffer : [ ] byte { } ,
want : false ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "empty" ,
tag : "<tool_call>" ,
buffer : [ ] byte { } ,
want : false ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "json open" ,
tag : "{" ,
buffer : [ ] byte ( "{\"name\": \"get_weather\"" ) ,
want : false ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "json closed" ,
tag : "{" ,
buffer : [ ] byte ( "{\"name\": \"get_weather\"}" ) ,
want : true ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "json empty" ,
tag : "{" ,
buffer : [ ] byte ( "{}" ) ,
want : true ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "list open" ,
tag : "[" ,
buffer : [ ] byte ( "[{\"name\": \"get_weather\"" ) ,
want : false ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "list closed" ,
tag : "[" ,
buffer : [ ] byte ( "[{\"name\": \"get_weather\"}]" ) ,
want : true ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "list empty" ,
tag : "[" ,
buffer : [ ] byte ( "[]" ) ,
want : true ,
2025-05-24 05:19:31 +08:00
} ,
2025-06-13 05:18:54 +08:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
parser := & Parser {
tag : tt . tag ,
buffer : tt . buffer ,
}
got := parser . done ( )
if got != tt . want {
t . Errorf ( "done() = %t, want %t" , got , tt . want )
}
} )
}
}
func TestContent ( t * testing . T ) {
tests := [ ] struct {
name string
tag string
content [ ] byte
want string
n int
} {
2025-05-24 05:19:31 +08:00
{
2025-06-13 05:18:54 +08:00
name : "empty" ,
content : [ ] byte { } ,
tag : "{" ,
want : "" ,
n : 0 ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "tag" ,
tag : "<tool_call>" ,
content : [ ] byte ( "<tool_call>{\"name\": \"get_temperature\"" ) ,
want : "" ,
n : 0 ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "json object" ,
tag : "{" ,
content : [ ] byte ( "{\"name\": \"get_temperature\"}" ) ,
want : "{\"name\": \"get_temperature\"}" ,
n : 0 ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "json object after called" ,
tag : "{" ,
content : [ ] byte ( "{\"hello\": \"world\"}" ) ,
want : "{\"hello\": \"world\"}" ,
n : 0 ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "json object after called" ,
tag : "{" ,
content : [ ] byte ( "{\"hello\": \"world\"}" ) ,
want : "" ,
n : 1 ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "list" ,
tag : "[" ,
content : [ ] byte ( "[{\"name\": \"get_temperature\"}]" ) ,
want : "[{\"name\": \"get_temperature\"}]" ,
n : 0 ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "code" ,
tag : "{" ,
content : [ ] byte ( "{ fmt.Println(\"hello\")" ) ,
want : "{ fmt.Println(\"hello\")" ,
n : 0 ,
2025-05-24 05:19:31 +08:00
} ,
2025-06-13 05:18:54 +08:00
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
parser := & Parser {
tag : tt . tag ,
buffer : tt . content ,
n : tt . n ,
}
got := parser . Content ( )
if got != tt . want {
t . Errorf ( "Content() = %q, want %q" , got , tt . want )
}
} )
}
}
func TestFindTag ( t * testing . T ) {
cases := [ ] struct {
name string
buffer [ ] byte
tag string
i int
found bool
} {
2025-05-24 05:19:31 +08:00
{
2025-06-13 05:18:54 +08:00
name : "no overlap" ,
buffer : [ ] byte ( "hello world" ) ,
tag : "<tool_call>" ,
i : - 1 ,
found : false ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "full overlap" ,
buffer : [ ] byte ( "<tool_call>" ) ,
tag : "<tool_call>" ,
i : 0 ,
found : true ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "whitespace" ,
buffer : [ ] byte ( " <tool_call>\n {\"name\": \"bob\"}" ) ,
tag : "<tool_call>" ,
i : 4 ,
found : true ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "over" ,
buffer : [ ] byte ( "<tool_call>{\"name\"" ) ,
tag : "<tool_call>" ,
i : 0 ,
found : true ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "partial overlap" ,
buffer : [ ] byte ( "text <tool_call>" ) ,
tag : "<tool_call>" ,
i : 5 ,
found : true ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "overlap with extra" ,
buffer : [ ] byte ( "<tool_calls><tool_call>" ) ,
tag : "<tool_calls>" ,
i : 0 ,
found : true ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "delimiter longer than string" ,
buffer : [ ] byte ( "<tool>" ) ,
tag : "<tool_call>" ,
i : - 1 ,
found : false ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "empty string" ,
buffer : [ ] byte { } ,
tag : "<tool_call>" ,
i : - 1 ,
found : false ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "single char overlap" ,
buffer : [ ] byte ( "test<" ) ,
tag : "<tool_call>" ,
i : 4 ,
found : false ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "partial tool call" ,
buffer : [ ] byte ( "hello <tool_" ) ,
tag : "<tool_call>" ,
i : 6 ,
found : false ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "square bracket" ,
buffer : [ ] byte ( "calling tools: [" ) ,
tag : "[" ,
i : 15 ,
found : true ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "bracket" ,
buffer : [ ] byte ( "{\"name\": \"bob\"" ) ,
tag : "{" ,
i : 0 ,
found : true ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "bracket with whitespace" ,
buffer : [ ] byte ( "\n\n{\n\"name\": \"bob\"" ) ,
tag : "{" ,
i : 2 ,
found : true ,
2025-05-24 05:19:31 +08:00
} ,
2025-06-13 05:18:54 +08:00
}
for _ , tt := range cases {
t . Run ( tt . name , func ( t * testing . T ) {
parser := & Parser {
tag : tt . tag ,
buffer : tt . buffer ,
n : 0 ,
}
i , found := parser . findTag ( )
if i != tt . i {
t . Errorf ( "findTag(%q, %q) = %d; want %d" , tt . buffer , tt . tag , i , tt . i )
}
if found != tt . found {
t . Errorf ( "findTag(%q, %q) = %t; want %t" , tt . buffer , tt . tag , found , tt . found )
}
} )
}
}
func TestFindArguments ( t * testing . T ) {
tests := [ ] struct {
name string
buffer [ ] byte
want map [ string ] any
} {
2025-05-24 05:19:31 +08:00
{
2025-06-13 05:18:54 +08:00
name : "empty string" ,
buffer : [ ] byte { } ,
want : nil ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "whitespace only" ,
buffer : [ ] byte ( " \n\t " ) ,
want : nil ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "unbalanced braces - missing closing" ,
buffer : [ ] byte ( ` { "format": "fahrenheit", "location": "San Francisco" ` ) ,
want : nil ,
2025-05-27 09:59:06 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "unbalanced braces - extra closing" ,
buffer : [ ] byte ( ` { "format": "fahrenheit"}} ` ) ,
want : map [ string ] any {
"format" : "fahrenheit" ,
} ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "invalid JSON" ,
buffer : [ ] byte ( ` { format: fahrenheit, location: "San Francisco"} ` ) ,
want : nil ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "valid json" ,
buffer : [ ] byte ( ` { "name": "get_temperature", "arguments": { "format": "fahrenheit", "location": "San Francisco, CA"}} ` ) ,
want : map [ string ] any {
"format" : "fahrenheit" ,
"location" : "San Francisco, CA" ,
} ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "valid arguments with special tokens" ,
buffer : [ ] byte ( ` [tool]get_temperature[args] { "format": "fahrenheit", "location": "San Francisco, CA"}[end] ` ) ,
want : map [ string ] any {
"format" : "fahrenheit" ,
"location" : "San Francisco, CA" ,
} ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "valid arguments in array" ,
2025-07-24 12:21:29 +08:00
buffer : [ ] byte ( ` [ { "name": "get_temperature", "arguments": { "format": "fahrenheit", "location": "San Francisco, CA"}} ` ) ,
2025-06-13 05:18:54 +08:00
want : map [ string ] any {
"format" : "fahrenheit" ,
"location" : "San Francisco, CA" ,
} ,
2025-05-27 09:59:06 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "nested deep" ,
buffer : [ ] byte ( ` { "function": { "name": "get_temperature", "arguments": { "format": "fahrenheit", "location": "San Francisco, CA"}}} ` ) ,
want : map [ string ] any {
"format" : "fahrenheit" ,
"location" : "San Francisco, CA" ,
} ,
2025-05-27 09:59:06 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "one arg" ,
2025-06-18 01:51:43 +08:00
buffer : [ ] byte ( ` get_temperature( { "location": "San Francisco, CA"}) ` ) ,
2025-06-13 05:18:54 +08:00
want : map [ string ] any {
"location" : "San Francisco, CA" ,
} ,
2025-05-27 09:59:06 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "two args" ,
2025-06-18 01:51:43 +08:00
buffer : [ ] byte ( ` [ { "name": "get_temperature", "arguments": { "location": "San Francisco, CA", "format": "fahrenheit"}}, { "name": "get_weather", "arguments": { "location": "San Francisco, CA", "format": "fahrenheit"}}] ` ) ,
2025-06-13 05:18:54 +08:00
want : map [ string ] any {
"location" : "San Francisco, CA" ,
"format" : "fahrenheit" ,
} ,
2025-05-24 05:19:31 +08:00
} ,
{
2025-06-13 05:18:54 +08:00
name : "deepseek" ,
2025-06-18 01:51:43 +08:00
buffer : [ ] byte ( "<|tool▁calls▁begin|><|tool▁call▁begin|>function<|tool▁sep|>get_temperature\n```json\n{\"location\": \"Tokyo\"}\n```<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>" ) ,
2025-06-13 05:18:54 +08:00
want : map [ string ] any {
"location" : "Tokyo" ,
} ,
2025-05-24 05:19:31 +08:00
} ,
2025-06-30 23:59:03 +08:00
{
name : "deepseek" ,
2025-08-23 06:22:14 +08:00
buffer : [ ] byte ( ` "arguments": { "location": "Tokyo"}}</tool_call> ` ) ,
2025-06-30 23:59:03 +08:00
want : map [ string ] any {
"location" : "Tokyo" ,
} ,
} ,
2025-08-23 06:22:14 +08:00
{
name : "string with braces" ,
buffer : [ ] byte ( ` { "name": "process_code", "arguments": { "code": "if (x > 0) { return true; }"}} ` ) ,
want : map [ string ] any {
"code" : "if (x > 0) { return true; }" ,
} ,
} ,
{
name : "string with nested json" ,
buffer : [ ] byte ( ` { "name": "send_data", "arguments": { "payload": " { \"nested\": { \"key\": \"value\"}}"}} ` ) ,
want : map [ string ] any {
"payload" : ` { "nested": { "key": "value"}} ` ,
} ,
} ,
{
name : "string with escaped quotes and braces" ,
buffer : [ ] byte ( ` { "name": "analyze", "arguments": { "text": "The JSON is: { \"key\": \"val { ue}\"}"}} ` ) ,
want : map [ string ] any {
"text" : ` The JSON is: { "key": "val { ue}"} ` ,
} ,
} ,
{
name : "multiple objects with string containing braces" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "query": "find } in text"}} { "name": "other"} ` ) ,
want : map [ string ] any {
"query" : "find } in text" ,
} ,
} ,
{
name : "unmatched closing brace in string" ,
buffer : [ ] byte ( ` { "name": "search", "arguments": { "pattern": "regex: }"}} ` ) ,
want : map [ string ] any {
"pattern" : "regex: }" ,
} ,
} ,
{
name : "complex nested with mixed braces" ,
buffer : [ ] byte ( ` { "name": "analyze", "arguments": { "data": " { \"items\": [ { \"value\": \"}\"}, { \"code\": \"if (x) { return y; }\"}]}"}} ` ) ,
want : map [ string ] any {
"data" : ` { "items": [ { "value": "}"}, { "code": "if (x) { return y; }"}]} ` ,
} ,
} ,
{
name : "string with newline and braces" ,
buffer : [ ] byte ( ` { "name": "format", "arguments": { "template": " { \n \"key\": \"value\"\n}"}} ` ) ,
want : map [ string ] any {
"template" : "{\n \"key\": \"value\"\n}" ,
} ,
} ,
{
name : "string with unicode escape" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "text": "Unicode: \u007B and \u007D"}} ` ) ,
want : map [ string ] any {
"text" : "Unicode: { and }" ,
} ,
} ,
{
name : "array arguments" ,
buffer : [ ] byte ( ` { "name": "batch", "arguments": ["item1", "item2", " { \"nested\": true}"]} ` ) ,
want : nil , // This should return nil because arguments is not a map
} ,
{
name : "escaped backslash before quote" ,
buffer : [ ] byte ( ` { "name": "path", "arguments": { "dir": "C:\\Program Files\\ { App}\\"}} ` ) ,
want : map [ string ] any {
"dir" : ` C:\Program Files\ { App}\ ` ,
} ,
} ,
{
name : "single quotes not treated as string delimiters" ,
buffer : [ ] byte ( ` { "name": "query", "arguments": { "sql": "SELECT * FROM users WHERE name = ' { admin}'"}} ` ) ,
want : map [ string ] any {
"sql" : "SELECT * FROM users WHERE name = '{admin}'" ,
} ,
} ,
{
name : "incomplete json at buffer end" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "data": "some { " ` ) ,
want : nil ,
} ,
{
name : "multiple escaped quotes" ,
buffer : [ ] byte ( ` { "name": "echo", "arguments": { "msg": "He said \"Hello { World}\" loudly"}} ` ) ,
want : map [ string ] any {
"msg" : ` He said "Hello { World}" loudly ` ,
} ,
} ,
{
name : "json with comments style string" ,
buffer : [ ] byte ( ` { "name": "code", "arguments": { "snippet": "// This is a comment with { and }"}} ` ) ,
want : map [ string ] any {
"snippet" : "// This is a comment with { and }" ,
} ,
} ,
{
name : "consecutive escaped backslashes" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "path": "C:\\\\ { folder}\\\\"}} ` ) ,
want : map [ string ] any {
"path" : ` C:\\ { folder}\\ ` ,
} ,
} ,
{
name : "empty string with braces after" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "a": "", "b": " { value}"}} ` ) ,
want : map [ string ] any {
"a" : "" ,
"b" : "{value}" ,
} ,
} ,
{
name : "unicode in key names" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "key { ": "value", "key}": "value2"}} ` ) ,
want : map [ string ] any {
"key{" : "value" ,
"key}" : "value2" ,
} ,
} ,
{
name : "very long string with braces" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "data": " ` + strings . Repeat ( "a{b}c" , 100 ) + ` "}} ` ) ,
want : map [ string ] any {
"data" : strings . Repeat ( "a{b}c" , 100 ) ,
} ,
} ,
{
name : "tab characters and braces" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "code": "\tif (true) { \n\t\treturn;\n\t}"}} ` ) ,
want : map [ string ] any {
"code" : "\tif (true) {\n\t\treturn;\n\t}" ,
} ,
} ,
{
name : "null byte in string" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "data": "before\u0000 { after}"}} ` ) ,
want : map [ string ] any {
"data" : "before\x00{after}" ,
} ,
} ,
{
name : "escaped quote at end of string" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "data": "text with quote at end\\\""}} ` ) ,
want : map [ string ] any {
"data" : ` text with quote at end\" ` ,
} ,
} ,
{
name : "mixed array and object in arguments" ,
buffer : [ ] byte ( ` { "name": "test", "arguments": { "items": [" { ", "}", { "key": "value"}]}} ` ) ,
want : map [ string ] any {
"items" : [ ] any { "{" , "}" , map [ string ] any { "key" : "value" } } ,
} ,
} ,
2025-09-26 05:37:39 +08:00
{
name : "stringified arguments" ,
buffer : [ ] byte ( ` { "name": "get_temperature", "arguments": " { \"format\": \"fahrenheit\", \"location\": \"San Francisco, CA\"}"} ` ) ,
want : map [ string ] any {
"format" : "fahrenheit" ,
"location" : "San Francisco, CA" ,
} ,
} ,
{
name : "stringified parameters" ,
buffer : [ ] byte ( ` { "name": "get_temperature", "parameters": " { \"format\": \"fahrenheit\", \"location\": \"San Francisco, CA\"}"} ` ) ,
want : map [ string ] any {
"format" : "fahrenheit" ,
"location" : "San Francisco, CA" ,
} ,
} ,
2025-05-24 05:19:31 +08:00
}
2025-06-13 05:18:54 +08:00
for _ , tt := range tests {
2025-05-24 05:19:31 +08:00
t . Run ( tt . name , func ( t * testing . T ) {
2025-07-24 12:21:29 +08:00
got , _ := findArguments ( tt . buffer )
2025-05-24 05:19:31 +08:00
2025-06-13 05:18:54 +08:00
if diff := cmp . Diff ( got , tt . want ) ; diff != "" {
t . Errorf ( "scanArguments() args mismatch (-got +want):\n%s" , diff )
}
2025-05-24 05:19:31 +08:00
} )
}
}