[object Object]

← back to Exo

fix glm5 tool calling (#1612)

73e50df8275cd0a95124c970b82797b70c41e8b9 · 2026-02-24 18:21:18 +0000 · Evan Quiney

glm5 is a deepseekv32 model, so was parsing dsml style tool calls
instead of glm style tool calls. fix it!!

Files touched

Diff

commit 73e50df8275cd0a95124c970b82797b70c41e8b9
Author: Evan Quiney <evanev7@gmail.com>
Date:   Tue Feb 24 18:21:18 2026 +0000

    fix glm5 tool calling (#1612)
    
    glm5 is a deepseekv32 model, so was parsing dsml style tool calls
    instead of glm style tool calls. fix it!!
---
 src/exo/worker/runner/llm_inference/runner.py      | 25 ++++++++++++----------
 .../worker/runner/llm_inference/tool_parsers.py    | 13 +++++------
 2 files changed, 21 insertions(+), 17 deletions(-)

diff --git a/src/exo/worker/runner/llm_inference/runner.py b/src/exo/worker/runner/llm_inference/runner.py
index 4c762de2..5fb1459f 100644
--- a/src/exo/worker/runner/llm_inference/runner.py
+++ b/src/exo/worker/runner/llm_inference/runner.py
@@ -97,6 +97,7 @@ def main(
         bound_instance.bound_runner_id,
         bound_instance.bound_shard,
     )
+    model_id = shard_metadata.model_card.model_id
     device_rank = shard_metadata.device_rank
     logger.info("hello from the runner")
     if getattr(shard_metadata, "immediate_exception", False):
@@ -281,7 +282,7 @@ def main(
                                 ChunkGenerated(
                                     command_id=command_id,
                                     chunk=PrefillProgressChunk(
-                                        model=shard_metadata.model_card.model_id,
+                                        model=model_id,
                                         processed_tokens=processed,
                                         total_tokens=total,
                                     ),
@@ -325,7 +326,10 @@ def main(
                         # Model-specific output parsing for tool calls.
                         if isinstance(inference_model, GptOssModel):
                             mlx_generator = parse_gpt_oss(mlx_generator)
-                        elif isinstance(inference_model, DeepseekV32Model):
+                        elif (
+                            isinstance(inference_model, DeepseekV32Model)
+                            and "deepseek" in model_id.normalize().lower()
+                        ):
                             mlx_generator = parse_deepseek_v32(mlx_generator)
                         elif tool_parser:
                             mlx_generator = parse_tool_calls(mlx_generator, tool_parser)
@@ -355,7 +359,7 @@ def main(
                                                 command_id=command_id,
                                                 chunk=ErrorChunk(
                                                     error_message=response.text,
-                                                    model=shard_metadata.model_card.model_id,
+                                                    model=model_id,
                                                 ),
                                             )
                                         )
@@ -370,7 +374,7 @@ def main(
                                             ChunkGenerated(
                                                 command_id=command_id,
                                                 chunk=TokenChunk(
-                                                    model=shard_metadata.model_card.model_id,
+                                                    model=model_id,
                                                     text=response.text,
                                                     token_id=response.token,
                                                     usage=response.usage,
@@ -389,7 +393,7 @@ def main(
                                                 command_id=command_id,
                                                 chunk=ToolCallChunk(
                                                     tool_calls=response.tool_calls,
-                                                    model=shard_metadata.model_card.model_id,
+                                                    model=model_id,
                                                     usage=response.usage,
                                                     stats=response.stats,
                                                 ),
@@ -405,7 +409,7 @@ def main(
                                 ChunkGenerated(
                                     command_id=command_id,
                                     chunk=ErrorChunk(
-                                        model=shard_metadata.model_card.model_id,
+                                        model=model_id,
                                         finish_reason="error",
                                         error_message=str(e),
                                     ),
@@ -727,7 +731,7 @@ def parse_tool_calls(
     in_tool_call = False
     tool_call_text_parts: list[str] = []
     for response in responses:
-        if response.text.startswith(tool_parser.start_parsing):
+        if not in_tool_call and response.text.startswith(tool_parser.start_parsing):
             in_tool_call = True
 
         if in_tool_call:
@@ -765,10 +769,9 @@ def parse_tool_calls(
                 )
                 yield response
 
-            continue
-
-        # fallthrough
-        yield response
+        else:
+            # fallthrough
+            yield response
 
 
 EXO_RUNNER_MUST_FAIL = "EXO RUNNER MUST FAIL"
diff --git a/src/exo/worker/runner/llm_inference/tool_parsers.py b/src/exo/worker/runner/llm_inference/tool_parsers.py
index 88cbf0be..ee6d1bb5 100644
--- a/src/exo/worker/runner/llm_inference/tool_parsers.py
+++ b/src/exo/worker/runner/llm_inference/tool_parsers.py
@@ -58,15 +58,16 @@ def _flatten(p: dict[str, Any]) -> dict[str, str]:
     }
 
 
-json_tool_parser = ToolParser(
-    start_parsing="<tool_call>",
-    end_parsing="</tool_call>",
-    parse_tool_calls=_parse_json_calls,
-)
+def make_json_parser() -> ToolParser:
+    return ToolParser(
+        start_parsing="<tool_call>",
+        end_parsing="</tool_call>",
+        parse_tool_calls=_parse_json_calls,
+    )
 
 
 def infer_tool_parser(chat_template: str) -> ToolParser | None:
     """Attempt to auto-infer a tool parser from the chat template."""
     if "<tool_call>" in chat_template and "tool_call.name" in chat_template:
-        return json_tool_parser
+        return make_json_parser()
     return None

← 2b417f28 fix: sync model selectors between sidebar and chat input (#1  ·  back to Exo  ·  update mlx 2 (#1611) 14526d28 →