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
73257915
Unverified
Commit
73257915
authored
Dec 18, 2025
by
Parth Sareen
Committed by
GitHub
Dec 18, 2025
Browse files
parsers/renderers: functiongemma (#13521)
parent
522c11a7
Changes
7
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
1553 additions
and
1 deletion
+1553
-1
convert/tokenizer_spm.go
convert/tokenizer_spm.go
+2
-1
model/parsers/functiongemma.go
model/parsers/functiongemma.go
+323
-0
model/parsers/functiongemma_test.go
model/parsers/functiongemma_test.go
+423
-0
model/parsers/parsers.go
model/parsers/parsers.go
+2
-0
model/renderers/functiongemma.go
model/renderers/functiongemma.go
+287
-0
model/renderers/functiongemma_test.go
model/renderers/functiongemma_test.go
+514
-0
model/renderers/renderer.go
model/renderers/renderer.go
+2
-0
No files found.
convert/tokenizer_spm.go
View file @
73257915
...
...
@@ -49,7 +49,8 @@ func parseSentencePiece(fsys fs.FS) (*Vocabulary, error) {
tt
:=
int32
(
sentencepiece
.
ModelProto_SentencePiece_NORMAL
)
// temporary fix to handle gemma3 broken configs
if
slices
.
Contains
([]
string
{
"<end_of_turn>"
,
"<start_of_turn>"
},
piece
.
GetPiece
())
{
// TODO(parthsareen): allow reading of tokenizer.json to allow managing special tokens when using spm
if
slices
.
Contains
([]
string
{
"<end_of_turn>"
,
"<start_of_turn>"
,
"<start_function_declaration>"
,
"<end_function_declaration>"
,
"<start_function_call>"
,
"<end_function_call>"
,
"<start_function_response>"
,
"<end_function_response>"
,
"<escape>"
},
piece
.
GetPiece
())
{
tt
=
int32
(
sentencepiece
.
ModelProto_SentencePiece_CONTROL
)
}
...
...
model/parsers/functiongemma.go
0 → 100644
View file @
73257915
package
parsers
import
(
"fmt"
"regexp"
"strings"
"github.com/ollama/ollama/api"
)
type
FunctionGemmaParserState
int
const
(
FunctionGemmaCollectingContent
FunctionGemmaParserState
=
iota
FunctionGemmaCollectingToolCalls
)
const
(
functionGemmaFunctionCallOpen
=
"<start_function_call>"
functionGemmaFunctionCallClose
=
"<end_function_call>"
)
// This format uses <start_function_call>call:name{args}<end_function_call> for tool calls.
type
FunctionGemmaParser
struct
{
state
FunctionGemmaParserState
buffer
strings
.
Builder
tools
[]
api
.
Tool
}
func
(
p
*
FunctionGemmaParser
)
HasToolSupport
()
bool
{
return
true
}
func
(
p
*
FunctionGemmaParser
)
HasThinkingSupport
()
bool
{
return
false
}
func
(
p
*
FunctionGemmaParser
)
Init
(
tools
[]
api
.
Tool
,
lastMessage
*
api
.
Message
,
thinkValue
*
api
.
ThinkValue
)
[]
api
.
Tool
{
p
.
tools
=
tools
p
.
state
=
FunctionGemmaCollectingContent
return
tools
}
type
functionGemmaEvent
interface
{
isFunctionGemmaEvent
()
}
type
FunctionGemmaEventContent
struct
{
content
string
}
type
functionGemmaEventToolCall
struct
{
toolCall
api
.
ToolCall
}
func
(
FunctionGemmaEventContent
)
isFunctionGemmaEvent
()
{}
func
(
functionGemmaEventToolCall
)
isFunctionGemmaEvent
()
{}
func
(
p
*
FunctionGemmaParser
)
Add
(
s
string
,
done
bool
)
(
content
string
,
thinking
string
,
calls
[]
api
.
ToolCall
,
err
error
)
{
p
.
buffer
.
WriteString
(
s
)
events
:=
p
.
parseEvents
()
var
toolCalls
[]
api
.
ToolCall
var
contentSb
strings
.
Builder
for
_
,
event
:=
range
events
{
switch
event
:=
event
.
(
type
)
{
case
functionGemmaEventToolCall
:
toolCalls
=
append
(
toolCalls
,
event
.
toolCall
)
case
FunctionGemmaEventContent
:
contentSb
.
WriteString
(
event
.
content
)
}
}
return
contentSb
.
String
(),
""
,
toolCalls
,
nil
}
func
(
p
*
FunctionGemmaParser
)
parseEvents
()
[]
functionGemmaEvent
{
var
all
[]
functionGemmaEvent
keepLooping
:=
true
for
keepLooping
{
var
events
[]
functionGemmaEvent
events
,
keepLooping
=
p
.
eat
()
if
len
(
events
)
>
0
{
all
=
append
(
all
,
events
...
)
}
}
return
all
}
// emitWithPartialCheck extracts unambiguous content before a potential partial tag
func
(
p
*
FunctionGemmaParser
)
emitWithPartialCheck
(
bufStr
,
tag
string
)
(
unambiguous
,
ambiguous
string
)
{
if
overlapLen
:=
overlap
(
bufStr
,
tag
);
overlapLen
>
0
{
beforePartialTag
:=
bufStr
[
:
len
(
bufStr
)
-
overlapLen
]
return
beforePartialTag
,
bufStr
[
len
(
beforePartialTag
)
:
]
}
return
bufStr
,
""
}
func
(
p
*
FunctionGemmaParser
)
eat
()
([]
functionGemmaEvent
,
bool
)
{
bufStr
:=
p
.
buffer
.
String
()
if
bufStr
==
""
{
return
nil
,
false
}
switch
p
.
state
{
case
FunctionGemmaCollectingContent
:
if
strings
.
Contains
(
bufStr
,
functionGemmaFunctionCallOpen
)
{
split
:=
strings
.
SplitN
(
bufStr
,
functionGemmaFunctionCallOpen
,
2
)
content
:=
split
[
0
]
p
.
buffer
.
Reset
()
p
.
buffer
.
WriteString
(
split
[
1
])
p
.
state
=
FunctionGemmaCollectingToolCalls
if
content
!=
""
{
return
[]
functionGemmaEvent
{
FunctionGemmaEventContent
{
content
:
content
}},
true
}
return
nil
,
true
}
unambig
,
ambig
:=
p
.
emitWithPartialCheck
(
bufStr
,
functionGemmaFunctionCallOpen
)
p
.
buffer
.
Reset
()
p
.
buffer
.
WriteString
(
ambig
)
if
unambig
!=
""
{
return
[]
functionGemmaEvent
{
FunctionGemmaEventContent
{
content
:
unambig
}},
false
}
return
nil
,
false
case
FunctionGemmaCollectingToolCalls
:
if
strings
.
Contains
(
bufStr
,
functionGemmaFunctionCallClose
)
{
split
:=
strings
.
SplitN
(
bufStr
,
functionGemmaFunctionCallClose
,
2
)
remaining
:=
split
[
1
]
p
.
buffer
.
Reset
()
p
.
buffer
.
WriteString
(
remaining
)
var
events
[]
functionGemmaEvent
if
tc
,
err
:=
p
.
parseToolCall
(
split
[
0
]);
err
==
nil
{
events
=
append
(
events
,
functionGemmaEventToolCall
{
toolCall
:
tc
})
}
if
!
strings
.
Contains
(
remaining
,
functionGemmaFunctionCallOpen
)
{
p
.
state
=
FunctionGemmaCollectingContent
}
return
events
,
true
}
return
nil
,
false
}
return
nil
,
false
}
// Matches call:function_name{args}
var
functionGemmaCallRegex
=
regexp
.
MustCompile
(
`call:([^{]+)\{(.*)\}`
)
func
(
p
*
FunctionGemmaParser
)
parseToolCall
(
content
string
)
(
api
.
ToolCall
,
error
)
{
toolCall
:=
api
.
ToolCall
{}
// Extract function name and arguments
match
:=
functionGemmaCallRegex
.
FindStringSubmatch
(
content
)
if
len
(
match
)
<
3
{
return
toolCall
,
nil
}
toolCall
.
Function
.
Name
=
match
[
1
]
argsStr
:=
match
[
2
]
// Parse arguments
toolCall
.
Function
.
Arguments
=
p
.
parseArguments
(
argsStr
)
return
toolCall
,
nil
}
// parseArguments parses the key:value,key:value format
func
(
p
*
FunctionGemmaParser
)
parseArguments
(
argsStr
string
)
api
.
ToolCallFunctionArguments
{
args
:=
make
(
api
.
ToolCallFunctionArguments
)
if
argsStr
==
""
{
return
args
}
// Split by comma, but handle nested structures
parts
:=
p
.
splitArguments
(
argsStr
)
for
_
,
part
:=
range
parts
{
// Find the first colon to split key:value
colonIdx
:=
strings
.
Index
(
part
,
":"
)
if
colonIdx
==
-
1
{
continue
}
key
:=
part
[
:
colonIdx
]
value
:=
part
[
colonIdx
+
1
:
]
// Parse the value
args
[
key
]
=
p
.
parseValue
(
value
)
}
return
args
}
// splitArguments splits arguments by comma, respecting nested structures
func
(
p
*
FunctionGemmaParser
)
splitArguments
(
argsStr
string
)
[]
string
{
var
parts
[]
string
var
current
strings
.
Builder
depth
:=
0
inEscape
:=
false
for
i
:=
0
;
i
<
len
(
argsStr
);
i
++
{
ch
:=
argsStr
[
i
]
// Check for <escape> tags
if
i
+
8
<=
len
(
argsStr
)
&&
argsStr
[
i
:
i
+
8
]
==
"<escape>"
{
inEscape
=
!
inEscape
current
.
WriteString
(
"<escape>"
)
i
+=
7
// Skip the rest of <escape>
continue
}
if
!
inEscape
{
switch
ch
{
case
'{'
,
'['
:
depth
++
current
.
WriteByte
(
ch
)
case
'}'
,
']'
:
depth
--
current
.
WriteByte
(
ch
)
case
','
:
if
depth
==
0
{
if
current
.
Len
()
>
0
{
parts
=
append
(
parts
,
current
.
String
())
current
.
Reset
()
}
continue
}
current
.
WriteByte
(
ch
)
default
:
current
.
WriteByte
(
ch
)
}
}
else
{
current
.
WriteByte
(
ch
)
}
}
if
current
.
Len
()
>
0
{
parts
=
append
(
parts
,
current
.
String
())
}
return
parts
}
// parseValue parses a single value from the FunctionGemma format
func
(
p
*
FunctionGemmaParser
)
parseValue
(
value
string
)
any
{
// Check for escaped string
if
strings
.
HasPrefix
(
value
,
"<escape>"
)
&&
strings
.
HasSuffix
(
value
,
"<escape>"
)
{
// Remove the escape tags
return
value
[
8
:
len
(
value
)
-
8
]
}
// Check for boolean
if
value
==
"true"
{
return
true
}
if
value
==
"false"
{
return
false
}
// Check for number
if
num
,
ok
:=
parseNumber
(
value
);
ok
{
return
num
}
// Check for array
if
strings
.
HasPrefix
(
value
,
"["
)
&&
strings
.
HasSuffix
(
value
,
"]"
)
{
return
p
.
parseArray
(
value
[
1
:
len
(
value
)
-
1
])
}
// Check for object
if
strings
.
HasPrefix
(
value
,
"{"
)
&&
strings
.
HasSuffix
(
value
,
"}"
)
{
return
p
.
parseObject
(
value
[
1
:
len
(
value
)
-
1
])
}
// Default to string
return
value
}
// parseArray parses an array value
func
(
p
*
FunctionGemmaParser
)
parseArray
(
content
string
)
[]
any
{
var
result
[]
any
parts
:=
p
.
splitArguments
(
content
)
for
_
,
part
:=
range
parts
{
result
=
append
(
result
,
p
.
parseValue
(
part
))
}
return
result
}
// parseObject parses an object value
func
(
p
*
FunctionGemmaParser
)
parseObject
(
content
string
)
map
[
string
]
any
{
result
:=
make
(
map
[
string
]
any
)
parts
:=
p
.
splitArguments
(
content
)
for
_
,
part
:=
range
parts
{
colonIdx
:=
strings
.
Index
(
part
,
":"
)
if
colonIdx
==
-
1
{
continue
}
key
:=
part
[
:
colonIdx
]
value
:=
part
[
colonIdx
+
1
:
]
result
[
key
]
=
p
.
parseValue
(
value
)
}
return
result
}
// parseNumber tries to parse a string as a number
func
parseNumber
(
s
string
)
(
any
,
bool
)
{
// Try integer first
var
intVal
int64
if
_
,
err
:=
fmt
.
Sscanf
(
s
,
"%d"
,
&
intVal
);
err
==
nil
{
// Check if the entire string was consumed
if
fmt
.
Sprintf
(
"%d"
,
intVal
)
==
s
{
return
intVal
,
true
}
}
// Try float
var
floatVal
float64
if
_
,
err
:=
fmt
.
Sscanf
(
s
,
"%f"
,
&
floatVal
);
err
==
nil
{
return
floatVal
,
true
}
return
nil
,
false
}
model/parsers/functiongemma_test.go
0 → 100644
View file @
73257915
package
parsers
import
(
"testing"
"github.com/ollama/ollama/api"
"github.com/stretchr/testify/assert"
)
func
TestFunctionGemmaParser
(
t
*
testing
.
T
)
{
tests
:=
[]
struct
{
name
string
chunks
[]
string
tools
[]
api
.
Tool
expectedCalls
[]
api
.
ToolCall
expectedText
string
}{
{
name
:
"plain_content"
,
chunks
:
[]
string
{
"H"
,
"e"
,
"l"
,
"l"
,
"o"
,
","
,
" "
,
"w"
,
"o"
,
"r"
,
"l"
,
"d"
,
"!"
},
expectedCalls
:
nil
,
expectedText
:
"Hello, world!"
,
},
{
name
:
"simple_tool_call"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"get"
,
"_"
,
"weather"
,
"{"
,
"city"
,
":"
,
"<"
,
"escape"
,
">"
,
"Paris"
,
"<"
,
"escape"
,
">"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
tools
:
[]
api
.
Tool
{
{
Type
:
"function"
,
Function
:
api
.
ToolFunction
{
Name
:
"get_weather"
,
Parameters
:
api
.
ToolFunctionParameters
{
Type
:
"object"
,
Properties
:
map
[
string
]
api
.
ToolProperty
{
"city"
:
{
Type
:
api
.
PropertyType
{
"string"
}},
},
},
},
},
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"get_weather"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"city"
:
"Paris"
},
},
},
},
expectedText
:
""
,
},
{
name
:
"content_before_tool_call"
,
chunks
:
[]
string
{
"L"
,
"et"
,
" "
,
"me"
,
" "
,
"check"
,
"."
,
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"get"
,
"_"
,
"weather"
,
"{"
,
"city"
,
":"
,
"<"
,
"escape"
,
">"
,
"Paris"
,
"<"
,
"escape"
,
">"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"get_weather"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"city"
:
"Paris"
},
},
},
},
expectedText
:
"Let me check."
,
},
{
name
:
"numeric_arguments"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"add"
,
"{"
,
"a"
,
":"
,
"1"
,
","
,
"b"
,
":"
,
"2"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"add"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"a"
:
int64
(
1
),
"b"
:
int64
(
2
)},
},
},
},
expectedText
:
""
,
},
{
name
:
"boolean_arguments"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"set"
,
"_"
,
"flag"
,
"{"
,
"enabled"
,
":"
,
"true"
,
","
,
"verbose"
,
":"
,
"false"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"set_flag"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"enabled"
:
true
,
"verbose"
:
false
},
},
},
},
expectedText
:
""
,
},
{
name
:
"multiple_tool_calls"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"get"
,
"_"
,
"weather"
,
"{"
,
"city"
,
":"
,
"<"
,
"escape"
,
">"
,
"Paris"
,
"<"
,
"escape"
,
">"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"get"
,
"_"
,
"weather"
,
"{"
,
"city"
,
":"
,
"<"
,
"escape"
,
">"
,
"London"
,
"<"
,
"escape"
,
">"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"get_weather"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"city"
:
"Paris"
},
},
},
{
Function
:
api
.
ToolCallFunction
{
Name
:
"get_weather"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"city"
:
"London"
},
},
},
},
expectedText
:
""
,
},
{
name
:
"array_argument"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"process"
,
"{"
,
"items"
,
":"
,
"["
,
"<"
,
"escape"
,
">"
,
"a"
,
"<"
,
"escape"
,
">"
,
","
,
"<"
,
"escape"
,
">"
,
"b"
,
"<"
,
"escape"
,
">"
,
","
,
"<"
,
"escape"
,
">"
,
"c"
,
"<"
,
"escape"
,
">"
,
"]"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"process"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"items"
:
[]
any
{
"a"
,
"b"
,
"c"
}},
},
},
},
expectedText
:
""
,
},
{
name
:
"object_argument"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"update"
,
"{"
,
"data"
,
":"
,
"{"
,
"name"
,
":"
,
"<"
,
"escape"
,
">"
,
"test"
,
"<"
,
"escape"
,
">"
,
","
,
"value"
,
":"
,
"42"
,
"}"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"update"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"data"
:
map
[
string
]
any
{
"name"
:
"test"
,
"value"
:
int64
(
42
)},
},
},
},
},
expectedText
:
""
,
},
{
name
:
"empty_input"
,
chunks
:
[]
string
{},
expectedCalls
:
nil
,
expectedText
:
""
,
},
{
name
:
"tool_call_with_no_arguments"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"get"
,
"_"
,
"time"
,
"{"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"get_time"
,
Arguments
:
api
.
ToolCallFunctionArguments
{},
},
},
},
expectedText
:
""
,
},
{
name
:
"content_with_angle_brackets"
,
chunks
:
[]
string
{
"The"
,
" "
,
"result"
,
" "
,
"is"
,
" "
,
"a"
,
" "
,
"<"
,
"value"
,
">"
,
" "
,
"tag"
,
},
expectedCalls
:
nil
,
expectedText
:
"The result is a <value> tag"
,
},
{
name
:
"float_argument"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"set"
,
"_"
,
"temp"
,
"{"
,
"value"
,
":"
,
"3"
,
"."
,
"14"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"set_temp"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"value"
:
3.14
},
},
},
},
expectedText
:
""
,
},
{
name
:
"content_after_tool_call"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"test"
,
"{"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"Done"
,
"!"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"test"
,
Arguments
:
api
.
ToolCallFunctionArguments
{},
},
},
},
expectedText
:
"Done!"
,
},
{
name
:
"unicode_content_and_arguments"
,
chunks
:
[]
string
{
"こんにちは"
,
" "
,
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"greet"
,
"{"
,
"name"
,
":"
,
"<"
,
"escape"
,
">"
,
"日本語"
,
"<"
,
"escape"
,
">"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"greet"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"name"
:
"日本語"
},
},
},
},
expectedText
:
"こんにちは "
,
},
{
name
:
"multiple_params_sorted"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"search"
,
"{"
,
"query"
,
":"
,
"<"
,
"escape"
,
">"
,
"test"
,
"<"
,
"escape"
,
">"
,
","
,
"limit"
,
":"
,
"10"
,
","
,
"offset"
,
":"
,
"0"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"search"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"query"
:
"test"
,
"limit"
:
int64
(
10
),
"offset"
:
int64
(
0
),
},
},
},
},
expectedText
:
""
,
},
{
name
:
"nested_object_argument"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"create"
,
"{"
,
"config"
,
":"
,
"{"
,
"settings"
,
":"
,
"{"
,
"enabled"
,
":"
,
"true"
,
","
,
"name"
,
":"
,
"<"
,
"escape"
,
">"
,
"test"
,
"<"
,
"escape"
,
">"
,
"}"
,
"}"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"create"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"config"
:
map
[
string
]
any
{
"settings"
:
map
[
string
]
any
{
"enabled"
:
true
,
"name"
:
"test"
,
},
},
},
},
},
},
expectedText
:
""
,
},
{
name
:
"partial_start_tag_in_content"
,
chunks
:
[]
string
{
"Hello"
,
" "
,
"<"
,
"start"
,
" "
,
"world"
,
},
expectedCalls
:
nil
,
expectedText
:
"Hello <start world"
,
},
{
name
:
"parallel_tool_calls"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"get"
,
"_"
,
"weather"
,
"{"
,
"city"
,
":"
,
"<"
,
"escape"
,
">"
,
"Paris"
,
"<"
,
"escape"
,
">"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"get"
,
"_"
,
"time"
,
"{"
,
"timezone"
,
":"
,
"<"
,
"escape"
,
">"
,
"UTC"
,
"<"
,
"escape"
,
">"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"get_weather"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"city"
:
"Paris"
},
},
},
{
Function
:
api
.
ToolCallFunction
{
Name
:
"get_time"
,
Arguments
:
api
.
ToolCallFunctionArguments
{
"timezone"
:
"UTC"
},
},
},
},
expectedText
:
""
,
},
{
name
:
"content_between_tool_calls"
,
chunks
:
[]
string
{
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"first"
,
"{"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"Some"
,
" "
,
"text"
,
" "
,
"here"
,
"<"
,
"start"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
"call"
,
":"
,
"second"
,
"{"
,
"}"
,
"<"
,
"end"
,
"_"
,
"function"
,
"_"
,
"call"
,
">"
,
},
expectedCalls
:
[]
api
.
ToolCall
{
{
Function
:
api
.
ToolCallFunction
{
Name
:
"first"
,
Arguments
:
api
.
ToolCallFunctionArguments
{},
},
},
{
Function
:
api
.
ToolCallFunction
{
Name
:
"second"
,
Arguments
:
api
.
ToolCallFunctionArguments
{},
},
},
},
expectedText
:
"Some text here"
,
},
}
for
_
,
tt
:=
range
tests
{
t
.
Run
(
tt
.
name
,
func
(
t
*
testing
.
T
)
{
parser
:=
&
FunctionGemmaParser
{}
parser
.
Init
(
tt
.
tools
,
nil
,
nil
)
var
allContent
string
var
allCalls
[]
api
.
ToolCall
for
i
,
chunk
:=
range
tt
.
chunks
{
done
:=
i
==
len
(
tt
.
chunks
)
-
1
content
,
_
,
calls
,
err
:=
parser
.
Add
(
chunk
,
done
)
assert
.
NoError
(
t
,
err
)
allContent
+=
content
allCalls
=
append
(
allCalls
,
calls
...
)
}
// Handle empty chunks case
if
len
(
tt
.
chunks
)
==
0
{
content
,
_
,
calls
,
err
:=
parser
.
Add
(
""
,
true
)
assert
.
NoError
(
t
,
err
)
allContent
=
content
allCalls
=
calls
}
assert
.
Equal
(
t
,
tt
.
expectedText
,
allContent
)
assert
.
Equal
(
t
,
tt
.
expectedCalls
,
allCalls
)
})
}
}
func
TestFunctionGemmaParser_HasSupport
(
t
*
testing
.
T
)
{
parser
:=
&
FunctionGemmaParser
{}
assert
.
True
(
t
,
parser
.
HasToolSupport
())
assert
.
False
(
t
,
parser
.
HasThinkingSupport
())
}
model/parsers/parsers.go
View file @
73257915
...
...
@@ -66,6 +66,8 @@ func ParserForName(name string) Parser {
return
&
Olmo3ThinkParser
{}
case
"nemotron-3-nano"
:
return
&
Nemotron3NanoParser
{}
case
"functiongemma"
:
return
&
FunctionGemmaParser
{}
default
:
return
nil
}
...
...
model/renderers/functiongemma.go
0 → 100644
View file @
73257915
package
renderers
import
(
"fmt"
"sort"
"strings"
"github.com/ollama/ollama/api"
)
type
FunctionGemmaRenderer
struct
{}
const
defaultSystemMessage
=
"You can do function calling with the following functions:"
func
(
r
*
FunctionGemmaRenderer
)
Render
(
messages
[]
api
.
Message
,
tools
[]
api
.
Tool
,
thinkValue
*
api
.
ThinkValue
)
(
string
,
error
)
{
var
sb
strings
.
Builder
sb
.
WriteString
(
"<bos>"
)
var
systemMessage
string
var
loopMessages
[]
api
.
Message
if
len
(
messages
)
>
0
&&
(
messages
[
0
]
.
Role
==
"system"
||
messages
[
0
]
.
Role
==
"developer"
)
{
systemMessage
=
messages
[
0
]
.
Content
loopMessages
=
messages
[
1
:
]
}
else
{
loopMessages
=
messages
}
if
systemMessage
!=
""
||
len
(
tools
)
>
0
{
sb
.
WriteString
(
"<start_of_turn>developer
\n
"
)
if
systemMessage
!=
""
{
sb
.
WriteString
(
strings
.
TrimSpace
(
systemMessage
))
}
if
len
(
tools
)
>
0
{
if
systemMessage
!=
""
{
sb
.
WriteString
(
"
\n
"
)
}
if
strings
.
TrimSpace
(
systemMessage
)
!=
defaultSystemMessage
{
// Only add default message if user does not provide it
sb
.
WriteString
(
defaultSystemMessage
)
}
}
for
_
,
tool
:=
range
tools
{
sb
.
WriteString
(
r
.
renderToolDeclaration
(
tool
))
}
sb
.
WriteString
(
"<end_of_turn>
\n
"
)
}
// Track previous message type for tool response handling
prevMessageType
:=
""
for
i
,
message
:=
range
loopMessages
{
switch
message
.
Role
{
case
"assistant"
:
if
prevMessageType
!=
"tool_response"
{
sb
.
WriteString
(
"<start_of_turn>model
\n
"
)
}
prevMessageType
=
""
if
message
.
Content
!=
""
{
sb
.
WriteString
(
strings
.
TrimSpace
(
message
.
Content
))
}
if
len
(
message
.
ToolCalls
)
>
0
{
for
_
,
tc
:=
range
message
.
ToolCalls
{
sb
.
WriteString
(
r
.
formatToolCall
(
tc
))
}
// After tool calls, expect tool responses
if
i
+
1
<
len
(
loopMessages
)
&&
loopMessages
[
i
+
1
]
.
Role
==
"tool"
{
sb
.
WriteString
(
"<start_function_response>"
)
prevMessageType
=
"tool_call"
}
else
{
sb
.
WriteString
(
"<end_of_turn>
\n
"
)
}
}
else
{
sb
.
WriteString
(
"<end_of_turn>
\n
"
)
}
case
"user"
:
if
prevMessageType
!=
"tool_response"
{
sb
.
WriteString
(
"<start_of_turn>user
\n
"
)
}
prevMessageType
=
""
sb
.
WriteString
(
strings
.
TrimSpace
(
message
.
Content
))
sb
.
WriteString
(
"<end_of_turn>
\n
"
)
case
"tool"
:
toolName
:=
""
// Find the tool name from the previous assistant's tool call
for
j
:=
i
-
1
;
j
>=
0
;
j
--
{
if
loopMessages
[
j
]
.
Role
==
"assistant"
&&
len
(
loopMessages
[
j
]
.
ToolCalls
)
>
0
{
// Count how many tool messages came before this one
toolIdx
:=
0
for
k
:=
j
+
1
;
k
<
i
;
k
++
{
if
loopMessages
[
k
]
.
Role
==
"tool"
{
toolIdx
++
}
}
if
toolIdx
<
len
(
loopMessages
[
j
]
.
ToolCalls
)
{
toolName
=
loopMessages
[
j
]
.
ToolCalls
[
toolIdx
]
.
Function
.
Name
}
break
}
}
if
prevMessageType
!=
"tool_call"
{
sb
.
WriteString
(
"<start_function_response>"
)
}
sb
.
WriteString
(
"response:"
+
toolName
+
"{"
+
r
.
formatArgValue
(
message
.
Content
)
+
"}<end_function_response>"
)
prevMessageType
=
"tool_response"
default
:
sb
.
WriteString
(
"<start_of_turn>"
+
message
.
Role
+
"
\n
"
)
sb
.
WriteString
(
strings
.
TrimSpace
(
message
.
Content
))
sb
.
WriteString
(
"<end_of_turn>
\n
"
)
}
}
if
prevMessageType
!=
"tool_response"
{
sb
.
WriteString
(
"<start_of_turn>model
\n
"
)
}
return
sb
.
String
(),
nil
}
func
(
r
*
FunctionGemmaRenderer
)
renderToolDeclaration
(
tool
api
.
Tool
)
string
{
var
sb
strings
.
Builder
fn
:=
tool
.
Function
sb
.
WriteString
(
"<start_function_declaration>declaration:"
+
fn
.
Name
+
"{"
)
sb
.
WriteString
(
"description:<escape>"
+
fn
.
Description
+
"<escape>"
)
if
fn
.
Parameters
.
Properties
!=
nil
||
fn
.
Parameters
.
Type
!=
""
{
sb
.
WriteString
(
",parameters:{"
)
needsComma
:=
false
// Only include properties:{} if there are actual properties
if
len
(
fn
.
Parameters
.
Properties
)
>
0
{
sb
.
WriteString
(
"properties:{"
)
r
.
writeProperties
(
&
sb
,
fn
.
Parameters
.
Properties
)
sb
.
WriteString
(
"}"
)
needsComma
=
true
}
if
len
(
fn
.
Parameters
.
Required
)
>
0
{
if
needsComma
{
sb
.
WriteString
(
","
)
}
sb
.
WriteString
(
"required:["
)
for
i
,
req
:=
range
fn
.
Parameters
.
Required
{
if
i
>
0
{
sb
.
WriteString
(
","
)
}
sb
.
WriteString
(
"<escape>"
+
req
+
"<escape>"
)
}
sb
.
WriteString
(
"]"
)
needsComma
=
true
}
if
fn
.
Parameters
.
Type
!=
""
{
if
needsComma
{
sb
.
WriteString
(
","
)
}
sb
.
WriteString
(
"type:<escape>"
+
strings
.
ToUpper
(
fn
.
Parameters
.
Type
)
+
"<escape>"
)
}
sb
.
WriteString
(
"}"
)
}
sb
.
WriteString
(
"}<end_function_declaration>"
)
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
{
keys
=
append
(
keys
,
k
)
}
sort
.
Strings
(
keys
)
first
:=
true
for
_
,
name
:=
range
keys
{
prop
:=
props
[
name
]
if
!
first
{
sb
.
WriteString
(
","
)
}
first
=
false
sb
.
WriteString
(
name
+
":{description:<escape>"
)
sb
.
WriteString
(
prop
.
Description
)
sb
.
WriteString
(
"<escape>"
)
if
len
(
prop
.
Type
)
>
0
{
sb
.
WriteString
(
",type:<escape>"
+
strings
.
ToUpper
(
prop
.
Type
[
0
])
+
"<escape>"
)
}
sb
.
WriteString
(
"}"
)
}
}
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
=
append
(
keys
,
k
)
}
sort
.
Strings
(
keys
)
first
:=
true
for
_
,
key
:=
range
keys
{
value
:=
tc
.
Function
.
Arguments
[
key
]
if
!
first
{
sb
.
WriteString
(
","
)
}
first
=
false
sb
.
WriteString
(
key
+
":"
+
r
.
formatArgValue
(
value
))
}
sb
.
WriteString
(
"}<end_function_call>"
)
return
sb
.
String
()
}
func
(
r
*
FunctionGemmaRenderer
)
formatArgValue
(
value
any
)
string
{
switch
v
:=
value
.
(
type
)
{
case
string
:
return
"<escape>"
+
v
+
"<escape>"
case
bool
:
if
v
{
return
"true"
}
return
"false"
case
float64
:
if
v
==
float64
(
int64
(
v
))
{
return
fmt
.
Sprintf
(
"%d"
,
int64
(
v
))
}
return
fmt
.
Sprintf
(
"%v"
,
v
)
case
int
,
int64
,
int32
:
return
fmt
.
Sprintf
(
"%d"
,
v
)
case
map
[
string
]
any
:
return
r
.
formatMapValue
(
v
)
case
[]
any
:
return
r
.
formatArrayValue
(
v
)
default
:
return
fmt
.
Sprintf
(
"%v"
,
v
)
}
}
func
(
r
*
FunctionGemmaRenderer
)
formatMapValue
(
m
map
[
string
]
any
)
string
{
var
sb
strings
.
Builder
sb
.
WriteString
(
"{"
)
keys
:=
make
([]
string
,
0
,
len
(
m
))
for
k
:=
range
m
{
keys
=
append
(
keys
,
k
)
}
sort
.
Strings
(
keys
)
first
:=
true
for
_
,
key
:=
range
keys
{
if
!
first
{
sb
.
WriteString
(
","
)
}
first
=
false
sb
.
WriteString
(
key
+
":"
+
r
.
formatArgValue
(
m
[
key
]))
}
sb
.
WriteString
(
"}"
)
return
sb
.
String
()
}
func
(
r
*
FunctionGemmaRenderer
)
formatArrayValue
(
arr
[]
any
)
string
{
var
sb
strings
.
Builder
sb
.
WriteString
(
"["
)
for
i
,
item
:=
range
arr
{
if
i
>
0
{
sb
.
WriteString
(
","
)
}
sb
.
WriteString
(
r
.
formatArgValue
(
item
))
}
sb
.
WriteString
(
"]"
)
return
sb
.
String
()
}
model/renderers/functiongemma_test.go
0 → 100644
View file @
73257915
This diff is collapsed.
Click to expand it.
model/renderers/renderer.go
View file @
73257915
...
...
@@ -78,6 +78,8 @@ func rendererForName(name string) Renderer {
return
renderer
case
"nemotron-3-nano"
:
return
&
Nemotron3NanoRenderer
{}
case
"functiongemma"
:
return
&
FunctionGemmaRenderer
{}
default
:
return
nil
}
...
...
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