File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -78,7 +78,14 @@ def __call__(self, tool_calls):
7878
7979 result = []
8080 for tool_text in tool_calls :
81- parsed = self ._tool_parser (tool_text , self ._tools )
81+ try :
82+ parsed = self ._tool_parser (tool_text , self ._tools )
83+ except (ValueError , json .JSONDecodeError ) as e :
84+ logging .warning (
85+ f"Failed to parse tool call ({ type (e ).__name__ } : { e } ) — "
86+ f"tool text was likely truncated mid-generation."
87+ )
88+ continue
8289 if not isinstance (parsed , list ):
8390 parsed = [parsed ]
8491 result .extend (self ._format (tc ) for tc in parsed )
Original file line number Diff line number Diff line change 55
66import regex as re
77
8+ # Matches <|"|>...<|"|> string literals (Gemma 4's string delimiter).
9+ _GEMMA4_STR = r'<\|"\|>(?:(?!<\|"\|>)[\s\S])*?<\|"\|>'
10+
811# Matches call:name{...} with balanced braces via the regex module's
9- # recursive (?R)-style support. (\{(?:[^{}]|(?2))*\}) recurses on the
10- # second capture group so nested objects like {a:{b:1}} are captured whole.
11- _tool_call_regex = re .compile (r"call:(\w+)(\{(?:[^{}]|(?2))*\})" , re .DOTALL )
12+ # recursive (?R)-style support. The inner alternatives handle:
13+ # [^{}<] – any char that is not a brace or start of <|"|>
14+ # <(?!\|"\|>) – a lone '<' that is NOT the start of <|"|>
15+ # <|"|>...<|"|> – a complete string literal (braces inside are ignored)
16+ # (?2) – recursively balanced nested brace group
17+ _tool_call_regex = re .compile (
18+ r"call:([\w-]+)(\{(?:[^{}<]|<(?!\|\"\|>)|" + _GEMMA4_STR + r"|(?2))*\})" ,
19+ re .DOTALL ,
20+ )
1221
1322
1423def _gemma4_args_to_json (text : str ) -> str :
Original file line number Diff line number Diff line change @@ -254,6 +254,27 @@ def test_gemma4(self):
254254 {"settings" : {"enabled" : True , "name" : "test" }},
255255 )
256256
257+ # Hyphenated function name (e.g. manim-video)
258+ test_case = (
259+ 'call:manim-video{mode:<|"|>plan<|"|>,prompt:<|"|>explain KV caching<|"|>}'
260+ )
261+ tool_call = gemma4 .parse_tool_call (test_case , None )
262+ self .assertEqual (tool_call ["name" ], "manim-video" )
263+ self .assertEqual (
264+ tool_call ["arguments" ],
265+ {"mode" : "plan" , "prompt" : "explain KV caching" },
266+ )
267+
268+ # Braces inside a string argument (e.g. code snippets or markdown in content)
269+ test_case = (
270+ 'call:skill_manage{action:<|"|>create<|"|>,'
271+ 'content:<|"|>use a dict like {key: value} in your code<|"|>}'
272+ )
273+ tool_call = gemma4 .parse_tool_call (test_case , None )
274+ self .assertEqual (tool_call ["name" ], "skill_manage" )
275+ self .assertEqual (tool_call ["arguments" ]["action" ], "create" )
276+ self .assertIn ("{" , tool_call ["arguments" ]["content" ])
277+
257278 def test_kimi_k2 (self ):
258279 # Single tool call
259280 test_case = (
You can’t perform that action at this time.
0 commit comments