NousResearch/hermes-agent

DeepSeek V3 parser multi-tool call handling

Cluster 989 · environments. Data as of

Nothing open on GitHub. Fix merged in #1365 on 15 Mar 2026. Check it covers your case.

The short answer

0open issues
0open PRs
5items grouped
Known?
Yes. 5 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #1365. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #1300. That is separate from the fix event.
Still open
Nothing open on GitHub.

These issues and fixes address a bug in the DeepSeek V3 parser where multiple parallel tool calls are dropped, likely due to a greedy regex. The members consistently describe the same parsing defect and proposed corrections.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #1300 fix: DeepSeek V3 parser drops multi-tool calls

    merged to main

    files: environments/tool_call_parsers/deepseek_v3_parser.py, tests/test_tool_call_parsers.py

  • #1365 fix: support multiple parallel tool calls in DeepSeek V3 parser (#989)

    merged to main

    files: environments/tool_call_parsers/deepseek_v3_parser.py

    Show the diff
    • environments/tool_call_parsers/deepseek_v3_parser.py
      @@ -10,12 +10,13 @@
           <|tool▁call▁end|>
           <|tool▁calls▁end|>
       
      -Based on VLLM's DeepSeekV3ToolParser.extract_tool_calls()
      +Fixes Issue #989: Support for multiple simultaneous tool calls.
       """
       
       import re
       import uuid
      -from typing import List, Optional
      +import logging
      +from typing import List, Optional, Tuple
       
       from openai.types.chat.chat_completion_message_tool_call import (
           ChatCompletionMessageToolCall,
      @@ -24,6 +25,7 @@
       
       from environments.tool_call_parsers import ParseResult, ToolCallParser, register_parser
       
      +logger = logging.getLogger(__name__)
       
       @register_parser("deepseek_v3")
       class DeepSeekV3ToolCallParser(ToolCallParser):
      @@ -32,45 +34,56 @@ class DeepSeekV3ToolCallParser(ToolCallParser):
       
           Uses special unicode tokens with fullwidth angle brackets and block elements.
           Extracts type, function name, and JSON arguments from the structured format.
      +    Ensures all tool calls are captured when the model executes multiple actions.
           """
       
           START_TOKEN = "[redacted:secret-assignment]"
       
      -    # Regex captures: type, function_name, function_arguments
      +    # Updated PATTERN: Using \s* instead of literal \n for increased robustness
      +    # against variations in model formatting (Issue #989).
           PATTERN = re.compile(
      -        r"<|tool▁call▁begin|>(?P<type>.*?)<|tool▁sep|>(?P<function_name>.*?)\n```json\n(?P<function_arguments>.*?)\n```<|tool▁call▁end|>",
      +        r"<|tool▁call▁begin|>(?P<type>.*?)<|tool▁sep|>(?P<function_name>.*?)\s*```json\s*(?P<function_arguments>.*?)\s*```\s*<|tool▁call▁end|>",
               re.DOTALL,
           )
       
           def parse(self, text: str) -> ParseResult:
      +        """
      +        Parses the input text and extracts all available tool calls.
      +        """
               if self.START_TOKEN not in text:
                   return text, None
       
               try:
      -            matches = self.PATTERN.findall(text)
      +            # Using finditer to capture ALL tool calls in the sequence
      +            matches = list(self.PATTERN.finditer(text))
                   if not matches:
                       return text, None
       
                   tool_calls: List[ChatCompletionMessageToolCall] = []
      +            
                   for match in matches:
      -                tc_type, func_name, func_args = match
      +                func_name = match.group("function_name").strip()
      +                func_args = match.group("function_arguments").strip()
      +                
                       tool_calls.append(
                           ChatCompletionMessageToolCall(
                               id=f"call_{uuid.uuid4().hex[:8]}",
                               type="function",
                               function=Function(
      -                            name=func_name.strip(),
      -                            arguments=func_args.strip(),
      +                            name=func_name,
      +                            arguments=func_args,
                               ),
                           )
                       )
       
      -            if not tool_calls:
      -                return text, None
      +            if tool_calls:
      +                # Content is text before the first tool call block
      +                content_index = text.find(self.START_TOKEN)
      +                content = text[:content_index].strip()
      +                return content if content else None, tool_calls
       
      -            # Content is everything before the tool calls section
      -            content = text[: text.find(self.START_TOKEN)].strip()
      -            return content if content else None, tool_calls
      +            return text, None
       
      -        except Exception:
      +        except Exception as e:
      +            logger.error(f"Error parsing DeepSeek V3 tool calls: {e}")
                   return text, None

2 PRs closed without merging

From the close comments: duplicate 1 · merged via another change 1

  • #992 fix: DeepSeek V3 parser drops multi-tool calls (greedy regex) · merged via another change → #1300
  • #1015 fix: DeepSeek V3 parser drops tool calls when model returns multiple (#989) · duplicate → #992
Closed issues · 1
  • Closed#989DeepSeek V3 parser drops tool calls when model returns multiple
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.40: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
environments
Merged PRs
#1300, #1365
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
15 Mar 2026. This confirmation is old and may be out of date (a revert or reopen would not have invalidated it here); re-verify.

For your agent

The same facts, typed.