diff --git a/studio/backend/core/inference/tool_call_parser.py b/studio/backend/core/inference/tool_call_parser.py index 283a3f648..a38c5b110 100644 --- a/studio/backend/core/inference/tool_call_parser.py +++ b/studio/backend/core/inference/tool_call_parser.py @@ -772,7 +772,13 @@ def _gemma_parse_value(text: str, i: int): if body[k] in " \t\n\r,": k += 1 continue - v, k = _gemma_parse_value(body, k) + v, next_k = _gemma_parse_value(body, k) + if next_k <= k: + # Malformed array (e.g. a stray ``}`` with no opening + # ``{``): the primitive branch consumed nothing, so stop + # rather than spin forever. + break + k = next_k items.append(v) return items, j + 1 # Primitive: number / true/false/null / bare identifier. diff --git a/studio/backend/tests/test_safetensors_tool_loop.py b/studio/backend/tests/test_safetensors_tool_loop.py index e8268ab0d..bbb75090a 100644 --- a/studio/backend/tests/test_safetensors_tool_loop.py +++ b/studio/backend/tests/test_safetensors_tool_loop.py @@ -419,6 +419,25 @@ class TestParserMultiFormat: result = parse_tool_calls_from_text(text) assert isinstance(result, list) + def test_gemma4_malformed_array_terminates(self): + # Regression: a stray ``}`` inside a Gemma array (unbalanced + # braces) once spun the list loop forever because + # ``_gemma_parse_value`` returned without advancing the index. + # Parsing must terminate on this malformed input. + import threading + + text = "<|tool_call>call:foo{items:[1}}" + box: dict = {} + + def _run(): + box["result"] = parse_tool_calls_from_text(text) + + worker = threading.Thread(target = _run, daemon = True) + worker.start() + worker.join(timeout = 5) + assert not worker.is_alive(), "Gemma parser hung on malformed array input" + assert isinstance(box["result"], list) + def test_gemma4_strip_markup_final(self): text = "<|tool_call>call:foo{x:1}" assert strip_tool_markup(text, final = True) == ""