Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
OpenDAS
ollama
Commits
4f8a0166
Unverified
Commit
4f8a0166
authored
Jul 23, 2025
by
Jeffrey Morgan
Committed by
GitHub
Jul 23, 2025
Browse files
tools: loosen tool argument parsing (#11509)
parent
1e6eab5c
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
78 additions
and
244 deletions
+78
-244
tools/tools.go
tools/tools.go
+50
-75
tools/tools_test.go
tools/tools_test.go
+28
-169
No files found.
tools/tools.go
View file @
4f8a0166
...
@@ -120,16 +120,14 @@ func (p *Parser) parseToolCall() *api.ToolCall {
...
@@ -120,16 +120,14 @@ func (p *Parser) parseToolCall() *api.ToolCall {
return
nil
return
nil
}
}
// only look for arguments after the tool name if the tool has parameters
var
args
map
[
string
]
any
// TODO (jmorganca): while probably uncommon, this doesn't support
if
found
,
i
:=
findArguments
(
p
.
buffer
);
found
==
nil
{
// parsing arguments before the tool name, which may be needed in the future
args
:=
map
[
string
]
any
{}
if
len
(
tool
.
Function
.
Parameters
.
Properties
)
>
0
{
var
i
int
if
args
,
i
=
findArguments
(
*
tool
,
p
.
buffer
[
end
:
]);
args
==
nil
{
return
nil
return
nil
}
else
{
args
=
found
if
i
>
end
{
end
=
i
}
}
end
+=
i
}
}
tc
:=
&
api
.
ToolCall
{
tc
:=
&
api
.
ToolCall
{
...
@@ -217,93 +215,70 @@ func findTool(tools []api.Tool, buf []byte) (*api.Tool, int) {
...
@@ -217,93 +215,70 @@ func findTool(tools []api.Tool, buf []byte) (*api.Tool, int) {
// objects for functions that have all-optional parameters
// objects for functions that have all-optional parameters
// e.g. `{"name": "get_conditions", "arguments": {}}` will work but
// e.g. `{"name": "get_conditions", "arguments": {}}` will work but
// `{"name": "get_conditions"}` will not currently work
// `{"name": "get_conditions"}` will not currently work
func
findArguments
(
tool
api
.
Tool
,
buffer
[]
byte
)
(
map
[
string
]
any
,
int
)
{
func
findArguments
(
buffer
[]
byte
)
(
map
[
string
]
any
,
int
)
{
if
len
(
buffer
)
==
0
{
if
len
(
buffer
)
==
0
{
return
nil
,
0
return
nil
,
0
}
}
var
braces
int
var
braces
int
var
start
int
=
-
1
var
start
int
=
-
1
var
end
int
var
object
[]
byte
// find any outer json object
for
i
,
c
:=
range
buffer
{
for
i
,
c
:=
range
buffer
{
if
c
==
'{'
{
if
c
==
'{'
{
braces
++
if
braces
==
0
{
if
start
==
-
1
{
start
=
i
start
=
i
}
}
}
braces
++
}
else
if
c
==
'}'
&&
braces
>
0
{
if
c
==
'}'
{
if
start
!=
-
1
{
braces
--
braces
--
if
braces
==
0
{
if
braces
==
0
&&
start
!=
-
1
{
end
=
i
+
1
object
:=
buffer
[
start
:
i
+
1
]
object
=
buffer
[
start
:
end
]
break
}
}
}
}
if
braces
>
0
{
return
nil
,
0
}
var
data
map
[
string
]
any
var
data
map
[
string
]
any
if
err
:=
json
.
Unmarshal
(
object
,
&
data
);
err
!=
nil
{
if
err
:=
json
.
Unmarshal
(
object
,
&
data
);
err
!=
nil
{
return
nil
,
0
start
=
-
1
}
continue
var
find
func
(
obj
any
)
map
[
string
]
any
find
=
func
(
obj
any
)
map
[
string
]
any
{
switch
obj
:=
obj
.
(
type
)
{
case
map
[
string
]
any
:
valid
:=
true
// check if all keys in the object exist in the tool's parameters
for
key
:=
range
obj
{
if
_
,
exists
:=
tool
.
Function
.
Parameters
.
Properties
[
key
];
!
exists
{
valid
=
false
break
}
}
}
// check for required parameters
var
findObject
func
(
obj
map
[
string
]
any
)
(
map
[
string
]
any
,
bool
)
// TODO (jmorganca): this should error instead of silently failing
findObject
=
func
(
obj
map
[
string
]
any
)
(
map
[
string
]
any
,
bool
)
{
if
valid
{
if
_
,
hasName
:=
obj
[
"name"
];
hasName
{
for
_
,
required
:=
range
tool
.
Function
.
Parameters
.
Required
{
if
args
,
ok
:=
obj
[
"arguments"
]
.
(
map
[
string
]
any
);
ok
{
if
_
,
exists
:=
obj
[
required
];
!
exists
{
return
args
,
true
valid
=
false
break
}
}
if
args
,
ok
:=
obj
[
"parameters"
]
.
(
map
[
string
]
any
);
ok
{
return
args
,
true
}
}
return
nil
,
true
}
}
if
valid
{
for
_
,
v
:=
range
obj
{
return
obj
switch
child
:=
v
.
(
type
)
{
case
map
[
string
]
any
:
if
result
,
found
:=
findObject
(
child
);
found
{
return
result
,
true
}
}
case
[]
any
:
for
_
,
value
:=
range
obj
{
for
_
,
item
:=
range
child
{
if
result
:=
find
(
value
);
result
!=
nil
{
if
childObj
,
ok
:=
item
.
(
map
[
string
]
any
);
ok
{
return
result
if
result
,
found
:=
findObject
(
childObj
);
found
{
return
result
,
true
}
}
}
}
case
[]
any
:
for
_
,
item
:=
range
obj
{
if
result
:=
find
(
item
);
result
!=
nil
{
return
result
}
}
}
}
}
}
return
nil
return
nil
,
false
}
}
result
:=
find
(
data
)
if
args
,
found
:=
findObject
(
data
);
found
{
if
result
!=
nil
{
return
args
,
i
return
result
,
end
}
return
data
,
i
}
}
}
}
return
nil
,
0
return
nil
,
0
...
...
tools/tools_test.go
View file @
4f8a0166
...
@@ -227,13 +227,6 @@ func TestParser(t *testing.T) {
...
@@ -227,13 +227,6 @@ func TestParser(t *testing.T) {
},
},
},
},
},
},
{
name
:
"invalid arguments"
,
inputs
:
[]
string
{
`<tool_call>{"name": "get_conditions", "arguments": {"city": "San Francisco"}}</tool_call>`
},
content
:
""
,
tmpl
:
qwen
,
calls
:
nil
,
},
{
{
name
:
"empty args"
,
name
:
"empty args"
,
inputs
:
[]
string
{
`<tool_call>{"name": "get_conditions", "arguments": {}}</tool_call>`
},
inputs
:
[]
string
{
`<tool_call>{"name": "get_conditions", "arguments": {}}</tool_call>`
},
...
@@ -249,13 +242,6 @@ func TestParser(t *testing.T) {
...
@@ -249,13 +242,6 @@ func TestParser(t *testing.T) {
},
},
},
},
},
},
{
name
:
"missing required args"
,
inputs
:
[]
string
{
`<tool_call>{"name": "get_temperature", "arguments": {}}</tool_call>`
},
content
:
""
,
tmpl
:
qwen
,
calls
:
nil
,
},
{
{
name
:
"text before tool call"
,
name
:
"text before tool call"
,
inputs
:
[]
string
{
`Let me check the weather. <tool_call>{"name": "get_temperature", "arguments": {"city": "New York"}}</tool_call>`
},
inputs
:
[]
string
{
`Let me check the weather. <tool_call>{"name": "get_temperature", "arguments": {"city": "New York"}}</tool_call>`
},
...
@@ -273,21 +259,6 @@ func TestParser(t *testing.T) {
...
@@ -273,21 +259,6 @@ func TestParser(t *testing.T) {
},
},
},
},
},
},
{
name
:
"qwen no args tool call"
,
inputs
:
[]
string
{
`Let me say hello to the user. I'll use the say_hello tool <tool_call>{"name": "say_hello"}</tool_call>`
},
content
:
"Let me say hello to the user. I'll use the say_hello tool "
,
tmpl
:
qwen
,
calls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Index
:
0
,
Name
:
"say_hello"
,
Arguments
:
api
.
ToolCallFunctionArguments
{},
},
},
},
},
{
{
name
:
"qwen no args with text"
,
name
:
"qwen no args with text"
,
inputs
:
[]
string
{
"Let me say hello to the user. I'll use the say_hello tool. "
},
inputs
:
[]
string
{
"Let me say hello to the user. I'll use the say_hello tool. "
},
...
@@ -521,52 +492,6 @@ func TestParser(t *testing.T) {
...
@@ -521,52 +492,6 @@ func TestParser(t *testing.T) {
content
:
"for { fmt.Println(
\"
hello
\"
) }"
,
content
:
"for { fmt.Println(
\"
hello
\"
) }"
,
tmpl
:
json
,
tmpl
:
json
,
},
},
{
name
:
"json no args tool call"
,
inputs
:
[]
string
{
"{
\"
name
\"
:
\"
say_hello
\"
}"
,
},
content
:
""
,
tmpl
:
json
,
calls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Index
:
0
,
Name
:
"say_hello"
,
Arguments
:
api
.
ToolCallFunctionArguments
{},
},
},
},
},
{
name
:
"json no args no tool call"
,
inputs
:
[]
string
{
"I'll use the say_hello tool to say hello to the user."
,
},
content
:
"I'll use the say_hello tool to say hello to the user."
,
tmpl
:
json
,
calls
:
nil
,
},
// TODO (jmorganca): this is a false positive, we should
// not be parsing this as a tool call
{
name
:
"json no args false positive"
,
inputs
:
[]
string
{
`{say_hello!!!}`
,
},
content
:
""
,
tmpl
:
json
,
calls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Index
:
0
,
Name
:
"say_hello"
,
Arguments
:
api
.
ToolCallFunctionArguments
{},
},
},
},
},
{
{
name
:
"list multiple"
,
name
:
"list multiple"
,
inputs
:
[]
string
{
inputs
:
[]
string
{
...
@@ -684,26 +609,6 @@ func TestParser(t *testing.T) {
...
@@ -684,26 +609,6 @@ func TestParser(t *testing.T) {
tmpl
:
list
,
tmpl
:
list
,
calls
:
nil
,
calls
:
nil
,
},
},
{
name
:
"list with no arguments"
,
inputs
:
[]
string
{
"["
,
"{"
,
"
\"
name
\"
:
\"
say_hello
\"
"
,
"}"
,
},
content
:
""
,
tmpl
:
list
,
calls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Index
:
0
,
Name
:
"say_hello"
,
Arguments
:
api
.
ToolCallFunctionArguments
{},
},
},
},
},
{
{
name
:
"tool name with collision"
,
name
:
"tool name with collision"
,
inputs
:
[]
string
{
inputs
:
[]
string
{
...
@@ -711,7 +616,7 @@ func TestParser(t *testing.T) {
...
@@ -711,7 +616,7 @@ func TestParser(t *testing.T) {
"{"
,
"{"
,
"
\"
name
\"
:
\"
say_hello"
,
"
\"
name
\"
:
\"
say_hello"
,
"_world
\"
,"
,
"_world
\"
,"
,
"}"
,
"
\"
arguments
\"
: {}
}"
,
"}"
,
"}"
,
},
},
content
:
""
,
content
:
""
,
...
@@ -733,13 +638,13 @@ func TestParser(t *testing.T) {
...
@@ -733,13 +638,13 @@ func TestParser(t *testing.T) {
"{"
,
"{"
,
"
\"
name
\"
:
\"
say_hello"
,
"
\"
name
\"
:
\"
say_hello"
,
"_world
\"
,"
,
"_world
\"
,"
,
"}"
,
"
\"
arguments
\"
: {}
}"
,
"</tool_call>"
,
"</tool_call>"
,
"<tool_call>"
,
"<tool_call>"
,
"{"
,
"{"
,
"
\"
name
\"
:
\"
say_hello"
,
"
\"
name
\"
:
\"
say_hello"
,
"
\"
,"
,
"
\"
,"
,
"}"
,
"
\"
arguments
\"
: {}
}"
,
"</tool_call>"
,
"</tool_call>"
,
},
},
content
:
""
,
content
:
""
,
...
@@ -773,7 +678,7 @@ func TestParser(t *testing.T) {
...
@@ -773,7 +678,7 @@ func TestParser(t *testing.T) {
{
{
name
:
"tool name with collision non streaming multiple"
,
name
:
"tool name with collision non streaming multiple"
,
inputs
:
[]
string
{
inputs
:
[]
string
{
`<tool_call>{"name": "say_hello"}</tool_call><tool_call>{"name": "say_hello_world"}`
,
`<tool_call>{"name": "say_hello"
, "arguments": {}
}</tool_call><tool_call>{"name": "say_hello_world"
, "arguments": {}
}`
,
},
},
content
:
""
,
content
:
""
,
tmpl
:
qwen
,
tmpl
:
qwen
,
...
@@ -797,7 +702,7 @@ func TestParser(t *testing.T) {
...
@@ -797,7 +702,7 @@ func TestParser(t *testing.T) {
{
{
name
:
"tool name with collision non streaming shorter"
,
name
:
"tool name with collision non streaming shorter"
,
inputs
:
[]
string
{
inputs
:
[]
string
{
`<tool_call>{"name": "say_hello"}</tool_call>`
,
`<tool_call>{"name": "say_hello"
, "arguments": {}
}</tool_call>`
,
},
},
content
:
""
,
content
:
""
,
tmpl
:
qwen
,
tmpl
:
qwen
,
...
@@ -814,7 +719,7 @@ func TestParser(t *testing.T) {
...
@@ -814,7 +719,7 @@ func TestParser(t *testing.T) {
{
{
name
:
"tool name with collision non streaming longer"
,
name
:
"tool name with collision non streaming longer"
,
inputs
:
[]
string
{
inputs
:
[]
string
{
`<tool_call>{"name": "say_hello_world"}</tool_call>`
,
`<tool_call>{"name": "say_hello_world"
, "arguments": {}
}</tool_call>`
,
},
},
content
:
""
,
content
:
""
,
tmpl
:
qwen
,
tmpl
:
qwen
,
...
@@ -871,6 +776,26 @@ func TestParser(t *testing.T) {
...
@@ -871,6 +776,26 @@ func TestParser(t *testing.T) {
},
},
},
},
},
},
{
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"
,
},
},
},
},
},
}
}
for
_
,
tt
:=
range
tests
{
for
_
,
tt
:=
range
tests
{
...
@@ -1167,75 +1092,25 @@ func TestFindTag(t *testing.T) {
...
@@ -1167,75 +1092,25 @@ func TestFindTag(t *testing.T) {
}
}
func
TestFindArguments
(
t
*
testing
.
T
)
{
func
TestFindArguments
(
t
*
testing
.
T
)
{
tool
:=
api
.
Tool
{
Type
:
"function"
,
Function
:
api
.
ToolFunction
{
Name
:
"get_temperature"
,
Description
:
"Retrieve the temperature for a given location"
,
Parameters
:
struct
{
Type
string
`json:"type"`
Defs
any
`json:"$defs,omitempty"`
Items
any
`json:"items,omitempty"`
Required
[]
string
`json:"required"`
Properties
map
[
string
]
struct
{
Type
api
.
PropertyType
`json:"type"`
Items
any
`json:"items,omitempty"`
Description
string
`json:"description"`
Enum
[]
any
`json:"enum,omitempty"`
}
`json:"properties"`
}{
Type
:
"object"
,
Properties
:
map
[
string
]
struct
{
Type
api
.
PropertyType
`json:"type"`
Items
any
`json:"items,omitempty"`
Description
string
`json:"description"`
Enum
[]
any
`json:"enum,omitempty"`
}{
"format"
:
{
Type
:
api
.
PropertyType
{
"string"
},
Description
:
"The format to return the temperature in"
,
Enum
:
[]
any
{
"fahrenheit"
,
"celsius"
},
},
"location"
:
{
Type
:
api
.
PropertyType
{
"string"
},
Description
:
"The location to get the temperature for"
,
},
},
},
},
}
tool2
:=
api
.
Tool
{
Type
:
"function"
,
Function
:
api
.
ToolFunction
{
Name
:
"say_hello"
,
Description
:
"Say hello to the user"
,
},
}
tests
:=
[]
struct
{
tests
:=
[]
struct
{
name
string
name
string
buffer
[]
byte
buffer
[]
byte
want
map
[
string
]
any
want
map
[
string
]
any
tool
api
.
Tool
}{
}{
{
{
name
:
"empty string"
,
name
:
"empty string"
,
buffer
:
[]
byte
{},
buffer
:
[]
byte
{},
want
:
nil
,
want
:
nil
,
tool
:
tool
,
},
},
{
{
name
:
"whitespace only"
,
name
:
"whitespace only"
,
buffer
:
[]
byte
(
"
\n\t
"
),
buffer
:
[]
byte
(
"
\n\t
"
),
want
:
nil
,
want
:
nil
,
tool
:
tool
,
},
},
{
{
name
:
"unbalanced braces - missing closing"
,
name
:
"unbalanced braces - missing closing"
,
buffer
:
[]
byte
(
`{"format": "fahrenheit", "location": "San Francisco"`
),
buffer
:
[]
byte
(
`{"format": "fahrenheit", "location": "San Francisco"`
),
want
:
nil
,
want
:
nil
,
tool
:
tool
,
},
},
{
{
name
:
"unbalanced braces - extra closing"
,
name
:
"unbalanced braces - extra closing"
,
...
@@ -1243,13 +1118,11 @@ func TestFindArguments(t *testing.T) {
...
@@ -1243,13 +1118,11 @@ func TestFindArguments(t *testing.T) {
want
:
map
[
string
]
any
{
want
:
map
[
string
]
any
{
"format"
:
"fahrenheit"
,
"format"
:
"fahrenheit"
,
},
},
tool
:
tool
,
},
},
{
{
name
:
"invalid JSON"
,
name
:
"invalid JSON"
,
buffer
:
[]
byte
(
`{format: fahrenheit, location: "San Francisco"}`
),
buffer
:
[]
byte
(
`{format: fahrenheit, location: "San Francisco"}`
),
want
:
nil
,
want
:
nil
,
tool
:
tool
,
},
},
{
{
name
:
"valid json"
,
name
:
"valid json"
,
...
@@ -1258,7 +1131,6 @@ func TestFindArguments(t *testing.T) {
...
@@ -1258,7 +1131,6 @@ func TestFindArguments(t *testing.T) {
"format"
:
"fahrenheit"
,
"format"
:
"fahrenheit"
,
"location"
:
"San Francisco, CA"
,
"location"
:
"San Francisco, CA"
,
},
},
tool
:
tool
,
},
},
{
{
name
:
"valid arguments with special tokens"
,
name
:
"valid arguments with special tokens"
,
...
@@ -1267,16 +1139,14 @@ func TestFindArguments(t *testing.T) {
...
@@ -1267,16 +1139,14 @@ func TestFindArguments(t *testing.T) {
"format"
:
"fahrenheit"
,
"format"
:
"fahrenheit"
,
"location"
:
"San Francisco, CA"
,
"location"
:
"San Francisco, CA"
,
},
},
tool
:
tool
,
},
},
{
{
name
:
"valid arguments in array"
,
name
:
"valid arguments in array"
,
buffer
:
[]
byte
(
`[{"arguments": {"format": "fahrenheit", "location": "San Francisco, CA"}}`
),
buffer
:
[]
byte
(
`[{
"name": "get_temperature",
"arguments": {"format": "fahrenheit", "location": "San Francisco, CA"}}`
),
want
:
map
[
string
]
any
{
want
:
map
[
string
]
any
{
"format"
:
"fahrenheit"
,
"format"
:
"fahrenheit"
,
"location"
:
"San Francisco, CA"
,
"location"
:
"San Francisco, CA"
,
},
},
tool
:
tool
,
},
},
{
{
name
:
"nested deep"
,
name
:
"nested deep"
,
...
@@ -1285,7 +1155,6 @@ func TestFindArguments(t *testing.T) {
...
@@ -1285,7 +1155,6 @@ func TestFindArguments(t *testing.T) {
"format"
:
"fahrenheit"
,
"format"
:
"fahrenheit"
,
"location"
:
"San Francisco, CA"
,
"location"
:
"San Francisco, CA"
,
},
},
tool
:
tool
,
},
},
{
{
name
:
"one arg"
,
name
:
"one arg"
,
...
@@ -1293,7 +1162,6 @@ func TestFindArguments(t *testing.T) {
...
@@ -1293,7 +1162,6 @@ func TestFindArguments(t *testing.T) {
want
:
map
[
string
]
any
{
want
:
map
[
string
]
any
{
"location"
:
"San Francisco, CA"
,
"location"
:
"San Francisco, CA"
,
},
},
tool
:
tool
,
},
},
{
{
name
:
"two args"
,
name
:
"two args"
,
...
@@ -1302,13 +1170,6 @@ func TestFindArguments(t *testing.T) {
...
@@ -1302,13 +1170,6 @@ func TestFindArguments(t *testing.T) {
"location"
:
"San Francisco, CA"
,
"location"
:
"San Francisco, CA"
,
"format"
:
"fahrenheit"
,
"format"
:
"fahrenheit"
,
},
},
tool
:
tool
,
},
{
name
:
"no args"
,
buffer
:
[]
byte
(
`{"name": "say_hello"}`
),
want
:
nil
,
tool
:
tool2
,
},
},
{
{
name
:
"deepseek"
,
name
:
"deepseek"
,
...
@@ -1316,7 +1177,6 @@ func TestFindArguments(t *testing.T) {
...
@@ -1316,7 +1177,6 @@ func TestFindArguments(t *testing.T) {
want
:
map
[
string
]
any
{
want
:
map
[
string
]
any
{
"location"
:
"Tokyo"
,
"location"
:
"Tokyo"
,
},
},
tool
:
tool
,
},
},
{
{
name
:
"deepseek"
,
name
:
"deepseek"
,
...
@@ -1324,13 +1184,12 @@ func TestFindArguments(t *testing.T) {
...
@@ -1324,13 +1184,12 @@ func TestFindArguments(t *testing.T) {
want
:
map
[
string
]
any
{
want
:
map
[
string
]
any
{
"location"
:
"Tokyo"
,
"location"
:
"Tokyo"
,
},
},
tool
:
tool
,
},
},
}
}
for
_
,
tt
:=
range
tests
{
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
got
,
_
:=
findArguments
(
tt
.
tool
,
tt
.
buffer
)
got
,
_
:=
findArguments
(
tt
.
buffer
)
if
diff
:=
cmp
.
Diff
(
got
,
tt
.
want
);
diff
!=
""
{
if
diff
:=
cmp
.
Diff
(
got
,
tt
.
want
);
diff
!=
""
{
t
.
Errorf
(
"scanArguments() args mismatch (-got +want):
\n
%s"
,
diff
)
t
.
Errorf
(
"scanArguments() args mismatch (-got +want):
\n
%s"
,
diff
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment