Skip to content

Conversation

@qandrew
Copy link
Contributor

@qandrew qandrew commented Dec 9, 2025

[with Daniel Salib]
Summary:
Fixes an issue where responses containing tool calls would be incomplete, missing the
required "completed" status item after tool execution.

Issue #1: Tool Parser Running After Reasoning Parser

  • Reasoning parser ran BEFORE tool parser
  • DeepSeek R1 reasoning parser has special logic: when no <think> tokens found, it treats ALL content as reasoning
  • Tool call markers (<|tool_calls_section_begin|> etc.) were being captured as reasoning content
  • Tool parser never saw them

Fix:
Tool parser runs first → extracts tool calls, returns remaining text

Issue #2: Model Sampling EOS Too Early

  • model can randomly sample EOS token before generating tool calls
  • No structured output constraints to enforce <think>...</think> + tool calls sequence
  • gpt_oss uses prepare_structured_tag to prevent this, but Kimi K2 didn't implement it

Fix:
Implemented prepare_structured_tag method in DeepSeekR1ReasoningParser

Test Plan:
run cmd:
./scripts/lufang/llm/launch_k2_thinking.sh

{ "request": {
  "input": {
    "input_items": [
      {
        "input_message": {
          "role": 1,
          "content": "write and run a simple python script to print hello world."
        }
      },
      {
        "input_message": {
          "role": 2,
          "content": " I'll write a simple Python script that prints \"hello world\" and then run it for you. "
        }
      },
      {
        "function_tool_call": {
          "arguments": "{\"file_path\": \"/tmp/hello_world.py\", \"content\": \"print(\\"hello world\\")\"}",
          "call_id": "call_4491ebf2664c44db87fb7a828126eb67",
          "name": "Write",
          "type": "function_call",
          "id": "fc_262c331d739e441cb0e49ecba8f7fbbb",
          "status": 1
        }
      },
      {
        "function_tool_call_output": {
          "call_id": "call_4491ebf2664c44db87fb7a828126eb67",
          "output": "File created: /tmp/hello_world.py\n",
          "type": "function_call_output",
          "id": "fc_262c331d739e441cb0e49ecba8f7fbbb",
          "status": 1
        }
      }
    ]
  },
  "instructions": "You are a general-purpose agent. Given the user's task, use the tools available to complete it efficiently and thoroughly.\n\nWhen to use your capabilities:\n- Searching for code, configurations, and patterns across large codebases\n- Analyzing multiple files to understand system architecture\n- Investigating complex questions that require exploring many files\n- Performing multi-step research tasks\n\nGuidelines:\n- For file searches: Use Grep Tool or Glob Tool when you need to search broadly. Use Read Tool when you know the specific file path.\n- For analysis: Start broad and narrow down. Use multiple search strategies if the first doesn't yield results.\n- Be thorough: Check multiple locations, consider different naming conventions, look for related files.\n- Complete tasks directly using your capabilities.\n",
  "model": "kimi_k2-thinking-responseapi-no-tool-return-all",
  "reasoning": {
    "effort": 2
  },
  "service_tier": "",
  "tools": [
    {
      "function_tool": {
        "name": "Bash",
        "parameters": "{\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\", \"description\": \"The command to execute\"}, \"timeout\": {\"type\": \"number\", \"description\": \"Optional timeout in milliseconds (max 600000)\"}, \"description\": {\"type\": \"string\", \"description\": \"Clear, concise description of what this command does in 5-10 words.\"}}, \"required\": [\"command\"]}",
        "strict": false,
        "type": "function",
        "description": "Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.\n\nBefore executing the command, please follow these steps:\n\n1. Directory Verification:\n   - If the command will create new directories or files, first use the LS tool to verify the parent directory exists and is the correct location\n   - For example, before running \"mkdir foo/bar\", first use LS to check that \"foo\" exists and is the intended parent directory\n\n2. Command Execution:\n   - Always quote file paths that contain spaces with double quotes (e.g., cd \"path with spaces/file.txt\")\n   - Examples of proper quoting:\n     - cd \"/Users/name/My Documents\" (correct)\n     - cd /Users/name/My Documents (incorrect - will fail)\n     - python \"/path/with spaces/script.py\" (correct)\n     - python /path/with spaces/script.py (incorrect - will fail)\n   - After ensuring proper quoting, execute the command.\n   - Capture the output of the command.\n\nUsage notes:\n  - The command argument is required.\n  - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).\n  - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.\n  - If the output exceeds 30000 characters, output will be truncated before being returned to you.\n  - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.\n - If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` first, which all users have pre-installed.\n  - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).\n  - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it."
      }
    },
    {
      "function_tool": {
        "name": "Edit",
        "parameters": "{\"type\": \"object\", \"properties\": {\"file_path\": {\"type\": \"string\", \"description\": \"The absolute path to the file to modify\"}, \"old_string\": {\"type\": \"string\", \"description\": \"The text to replace\"}, \"new_string\": {\"type\": \"string\", \"description\": \"The text to replace it with (must be different from old_string)\"}, \"replace_all\": {\"type\": \"boolean\", \"default\": false, \"description\": \"Replace all occurences of old_string (default false)\"}}, \"required\": [\"file_path\", \"old_string\", \"new_string\"]}",
        "strict": false,
        "type": "function",
        "description": "Performs exact string replacements in files.\n\nUsage:\n- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.\n- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.\n- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance."
      }
    },
    {
      "function_tool": {
        "name": "Glob",
        "parameters": "{\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\", \"description\": \"The glob pattern to match files against\"}, \"path\": {\"type\": \"string\", \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\"undefined\\" or \\"null\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\"}}, \"required\": [\"pattern\"]}",
        "strict": false,
        "type": "function",
        "description": "- Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful."
      }
    },
    {
      "function_tool": {
        "name": "Grep",
        "parameters": "{\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\", \"description\": \"The regular expression pattern to search for in file contents\"}, \"path\": {\"type\": \"string\", \"description\": \"File or directory to search in (rg PATH). Defaults to current working directory.\"}, \"glob\": {\"type\": \"string\", \"description\": \"Glob pattern to filter files (e.g. \\"*.js\\", \\"*.{ts,tsx}\\") - maps to rg --glob\"}, \"output_mode\": {\"type\": \"string\", \"enum\": [\"content\", \"files_with_matches\", \"count\"], \"description\": \"Output mode: \\"content\\" shows matching lines (supports -A/-B/-C context, -n line numbers, head_limit), \\"files_with_matches\\" shows file paths (supports head_limit), \\"count\\" shows match counts (supports head_limit). Defaults to \\"files_with_matches\\".\"}, \"-B\": {\"type\": \"number\", \"description\": \"Number of lines to show before each match (rg -B). Requires output_mode: \\"content\\", ignored otherwise.\"}, \"-A\": {\"type\": \"number\", \"description\": \"Number of lines to show after each match (rg -A). Requires output_mode: \\"content\\", ignored otherwise.\"}, \"-C\": {\"type\": \"number\", \"description\": \"Number of lines to show before and after each match (rg -C). Requires output_mode: \\"content\\", ignored otherwise.\"}, \"-n\": {\"type\": \"boolean\", \"description\": \"Show line numbers in output (rg -n). Requires output_mode: \\"content\\", ignored otherwise. Defaults to true.\"}, \"-i\": {\"type\": \"boolean\", \"description\": \"Case insensitive search (rg -i)\"}, \"type\": {\"type\": \"string\", \"description\": \"File type to search (rg --type). Common types: js, py, rust, go, java, etc. More efficient than include for standard file types.\"}, \"head_limit\": {\"type\": \"number\", \"description\": \"Limit output to first N lines/entries, equivalent to \\"| head -N\\". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). When unspecified, shows all results from ripgrep.\"}, \"multiline\": {\"type\": \"boolean\", \"description\": \"Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false.\"}}, \"required\": [\"pattern\"]}",
        "strict": false,
        "type": "function",
        "description": "A powerful search tool built on ripgrep\n\nUsage:\n- ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.\n- Supports full regex syntax (e.g., \"log.*Error\", \"function\s+\w+\")\n- Filter files with glob parameter (e.g., \"*.js\", \"**/*.tsx\") or type parameter (e.g., \"js\", \"py\", \"rust\")\n- Output modes: \"content\" shows matching lines, \"files_with_matches\" shows only file paths (default), \"count\" shows match counts\n- Use Task tool for open-ended searches requiring multiple rounds\n- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use `interface\{\}` to find `interface{}` in Go code)\n- Multiline matching: By default patterns match within single lines only. For cross-line patterns like `struct \{[\s\S]*?field`, use `multiline: true`"
      }
    },
    {
      "function_tool": {
        "name": "LS",
        "parameters": "{\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\", \"description\": \"The absolute path to the directory to list (must be absolute, not relative)\"}, \"ignore\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"description\": \"List of glob patterns to ignore\"}}, \"required\": [\"path\"]}",
        "strict": false,
        "type": "function",
        "description": "Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search."
      }
    },
    {
      "function_tool": {
        "name": "MultiEdit",
        "parameters": "{\"type\": \"object\", \"properties\": {\"file_path\": {\"type\": \"string\", \"description\": \"The absolute path to the file to modify\"}, \"edits\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"old_string\": {\"type\": \"string\", \"description\": \"The text to replace\"}, \"new_string\": {\"type\": \"string\", \"description\": \"The text to replace it with\"}, \"replace_all\": {\"type\": \"boolean\", \"default\": false, \"description\": \"Replace all occurences of old_string (default false).\"}}, \"required\": [\"old_string\", \"new_string\"], \"additionalProperties\": false}, \"minItems\": 1, \"description\": \"Array of edit operations to perform sequentially on the file\"}}, \"required\": [\"file_path\", \"edits\"]}",
        "strict": false,
        "type": "function",
        "description": "This is a tool for making multiple edits to a single file in one operation. It is built on top of the Edit tool and allows you to perform multiple find-and-replace operations efficiently. Prefer this tool over the Edit tool when you need to make multiple edits to the same file.\n\nBefore using this tool:\n1. Use the Read tool to understand the file's contents and context\n2. Verify the directory path is correct\n\nTo make multiple file edits, provide the following:\n1. file_path: The absolute path to the file to modify (must be absolute, not relative)\n2. edits: An array of edit operations to perform, where each edit contains:\n   - old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)\n   - new_string: The edited text to replace the old_string\n   - replace_all: Replace all occurences of old_string. This parameter is optional and defaults to false.\n\nIMPORTANT:\n- All edits are applied in sequence, in the order they are provided\n- Each edit operates on the result of the previous edit\n- All edits must be valid for the operation to succeed - if any edit fails, none will be applied\n- This tool is ideal when you need to make several changes to different parts of the same file\n- For Jupyter notebooks (.ipynb files), use the NotebookEdit instead\n\nCRITICAL REQUIREMENTS:\n1. All edits follow the same requirements as the single Edit tool\n2. The edits are atomic - either all succeed or none are applied\n3. Plan your edits carefully to avoid conflicts between sequential operations\n\nWARNING:\n- The tool will fail if edits.old_string doesn't match the file contents exactly (including whitespace)\n- The tool will fail if edits.old_string and edits.new_string are the same\n- Since edits are applied in sequence, ensure that earlier edits don't affect the text that later edits are trying to find\n\nWhen making edits:\n- Ensure all edits result in idiomatic, correct code\n- Do not leave the code in a broken state\n- Always use absolute file paths (starting with /)\n- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n- Use replace_all for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.\n\nIf you want to create a new file, use:\n- A new file path, including dir name if needed\n- First edit: empty old_string and the new file's contents as new_string\n- Subsequent edits: normal edit operations on the created content"
      }
    },
    {
      "function_tool": {
        "name": "Read",
        "parameters": "{\"type\": \"object\", \"properties\": {\"file_path\": {\"type\": \"string\", \"description\": \"The absolute path to the file to read\"}, \"offset\": {\"type\": \"number\", \"description\": \"The line number to start reading from. Only provide if the file is too large to read at once\"}, \"limit\": {\"type\": \"number\", \"description\": \"The number of lines to read. Only provide if the file is too large to read at once.\"}}, \"required\": [\"file_path\"]}",
        "strict": false,
        "type": "function",
        "description": "Reads a file from the local filesystem. You can access any file directly by using this tool.\n\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.\n- For Jupyter notebooks (.ipynb files), use the NotebookRead instead\n- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.\n- You will regularly be asked to read screenshots. If the user provides a path to a screenshot ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths.\n- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents."
      }
    },
    {
      "function_tool": {
        "name": "Task",
        "parameters": "{\"type\": \"object\", \"properties\": {\"description\": {\"type\": \"string\", \"description\": \"A short (3-5 word) description of the task\"}, \"prompt\": {\"type\": \"string\", \"description\": \"The task for the agent to perform\"}, \"subagent_type\": {\"type\": \"string\", \"description\": \"The type of specialized agent to use for this task\"}}, \"required\": [\"description\", \"prompt\", \"subagent_type\"]}",
        "strict": false,
        "type": "function",
        "description": "Launch a new agent to handle complex, multi-step tasks autonomously.\n\nAvailable agent types and the tools they have access to:\n{agent_list}\n\nWhen using the Task tool, you must specify a subagent_type parameter to select which agent type to use.\n\nWhen to use the Agent tool:\n- When you are instructed to execute custom slash commands. Use the Agent tool with the slash command invocation as the entire prompt. The slash command can take arguments. For example: Task(description=\"Check the file\", prompt=\"/check-file path/to/file.py\")\n\nWhen NOT to use the Agent tool:\n- If you want to read a specific file path, use the Read or Glob tool instead of the Agent tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the Glob tool instead, to find the match more quickly\n- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Agent tool, to find the match more quickly\n- Other tasks that are not related to the agent descriptions above\n\nUsage notes:\n1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.\n4. The agent's outputs should generally be trusted\n5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent\n6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement."
      }
    },
    {
      "function_tool": {
        "name": "TodoWrite",
        "parameters": "{\"type\": \"object\", \"properties\": {\"todos\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\", \"minLength\": 1}, \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\"]}, \"priority\": {\"type\": \"string\", \"enum\": [\"high\", \"medium\", \"low\"]}, \"id\": {\"type\": \"string\"}}, \"required\": [\"content\", \"status\", \"priority\", \"id\"]}, \"description\": \"The updated todo list\"}}, \"required\": [\"todos\"]}",
        "strict": false,
        "type": "function",
        "description": "Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.\nIt also helps the user understand the progress of the task and overall progress of their requests.\n\n## When to Use This Tool\nUse this tool proactively in these scenarios:\n\n1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions\n2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations\n3. User explicitly requests todo list - When the user directly asks you to use the todo list\n4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)\n5. After receiving new instructions - Immediately capture user requirements as todos\n6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time\n7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation\n\n## When NOT to Use This Tool\n\nSkip using this tool when:\n1. There is only a single, straightforward task\n2. The task is trivial and tracking it provides no organizational benefit\n3. The task can be completed in less than 3 trivial steps\n4. The task is purely conversational or informational\n\nNOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.\n\n## Task States and Management\n\n1. **Task States**: Use these states to track progress:\n   - pending: Task not yet started\n   - in_progress: Currently working on (limit to ONE task at a time)\n   - completed: Task finished successfully\n\n2. **Task Management**:\n   - Update task status in real-time as you work\n   - Mark tasks complete IMMEDIATELY after finishing (don't batch completions)\n   - Only have ONE task in_progress at any time\n   - Complete current tasks before starting new ones\n   - Remove tasks that are no longer relevant from the list entirely\n\n3. **Task Completion Requirements**:\n   - ONLY mark a task as completed when you have FULLY accomplished it\n   - If you encounter errors, blockers, or cannot finish, keep the task as in_progress\n   - When blocked, create a new task describing what needs to be resolved\n   - Never mark a task as completed if:\n     - Tests are failing\n     - Implementation is partial\n     - You encountered unresolved errors\n     - You couldn't find necessary files or dependencies\n\n4. **Task Breakdown**:\n   - Create specific, actionable items\n   - Break complex tasks into smaller, manageable steps\n   - Use clear, descriptive task names\n\nWhen in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully."
      }
    },
    {
      "function_tool": {
        "name": "Write",
        "parameters": "{\"type\": \"object\", \"properties\": {\"file_path\": {\"type\": \"string\", \"description\": \"The absolute path to the file to write (must be absolute, not relative)\"}, \"content\": {\"type\": \"string\", \"description\": \"The content to write to the file\"}}, \"required\": [\"file_path\", \"content\"]}",
        "strict": false,
        "type": "function",
        "description": "Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked."
      }
    }
  ],
  "enable_response_messages": false
}}

response:

{
    "id": "thrift_fiddle:16fb5e316d82412fa40c416c7c4558e1",
    "object": "response",
    "created_at": 1765029385,
    "status": "completed",
    "error": {
        "code": "",
        "message": ""
    },
    "incomplete_details": {
        "reason": ""
    },
    "model": "Kimi-K2-Thinking",
    "output": [
        {
            "item": {
                "reasoning_item": {
                    "id": "rs_b69642a910341d4d",
                    "content": [
                        " The user wants me to write and run a simple Python script to print \"hello world\". I've created the script at /tmp/hello_world.py. Now I need to run it using the Bash tool. "
                    ],
                    "type": "reasoning"
                }
            },
            "metadata": {
                "author": "assistant"
            }
        },
        {
            "item": {
                "output_message": {
                    "id": "msg_919a387d80b1f01e",
                    "content": [
                        " "
                    ],
                    "role": "assistant",
                    "status": "completed",
                    "type": "message"
                }
            },
            "metadata": {
                "author": "assistant"
            }
        },
        {
            "item": {
                "function_tool_call": {
                    "arguments": "{\"command\": \"python /tmp/hello_world.py\", \"description\": \"Run hello world Python script\"}",
                    "call_id": "functions.Bash:1",
                    "name": "Bash",
                    "type": "function_call",
                    "id": "functions.Bash:1",
                    "status": "completed"
                }
            },
            "metadata": {
                "author": "assistant"
            }
        }
    ],
    "parallel_tool_calls": false,
    "temperature": 1,
    "top_p": 1,
    "usage": {
        "input_tokens": 4980,
        "cached_tokens": 4896,
        "output_tokens": 74,
        "reasoning_tokens": 0,
        "total_tokens": 5054,
        "tool_output_tokens": 0,
        "input_tokens_per_turn": [],
        "cached_tokens_per_turn": [],
        "output_tokens_per_turn": [],
        "tool_output_tokens_per_turn": []
    }
}

preiviously, the response would occasionaly end with only 1 reasoning item and no completion items

Differential Revision: D88561095

Summary:
Fixes an issue where responses containing tool calls would be incomplete, missing the
  required "completed" status item after tool execution.

Issue #1: Tool Parser Running After Reasoning Parser 

- Reasoning parser ran BEFORE tool parser
- DeepSeek R1 reasoning parser has special logic: when no `<think>` tokens found, it treats ALL content as reasoning
- Tool call markers (`<|tool_calls_section_begin|>` etc.) were being captured as reasoning content
- Tool parser never saw them

Fix:
Tool parser runs first → extracts tool calls, returns remaining text

Issue #2: Model Sampling EOS Too Early 
- model can randomly sample EOS token before generating tool calls
- No structured output constraints to enforce `<think>...</think>` + tool calls sequence
- gpt_oss uses `prepare_structured_tag` to prevent this, but Kimi K2 didn't implement it

Fix:
Implemented `prepare_structured_tag` method in `DeepSeekR1ReasoningParser`

Test Plan:
run cmd:
./scripts/lufang/llm/launch_k2_thinking.sh

```
{ "request": {
  "input": {
    "input_items": [
      {
        "input_message": {
          "role": 1,
          "content": "write and run a simple python script to print hello world."
        }
      },
      {
        "input_message": {
          "role": 2,
          "content": " I'll write a simple Python script that prints \"hello world\" and then run it for you. "
        }
      },
      {
        "function_tool_call": {
          "arguments": "{\"file_path\": \"/tmp/hello_world.py\", \"content\": \"print(\\"hello world\\")\"}",
          "call_id": "call_4491ebf2664c44db87fb7a828126eb67",
          "name": "Write",
          "type": "function_call",
          "id": "fc_262c331d739e441cb0e49ecba8f7fbbb",
          "status": 1
        }
      },
      {
        "function_tool_call_output": {
          "call_id": "call_4491ebf2664c44db87fb7a828126eb67",
          "output": "File created: /tmp/hello_world.py\n",
          "type": "function_call_output",
          "id": "fc_262c331d739e441cb0e49ecba8f7fbbb",
          "status": 1
        }
      }
    ]
  },
  "instructions": "You are a general-purpose agent. Given the user's task, use the tools available to complete it efficiently and thoroughly.\n\nWhen to use your capabilities:\n- Searching for code, configurations, and patterns across large codebases\n- Analyzing multiple files to understand system architecture\n- Investigating complex questions that require exploring many files\n- Performing multi-step research tasks\n\nGuidelines:\n- For file searches: Use Grep Tool or Glob Tool when you need to search broadly. Use Read Tool when you know the specific file path.\n- For analysis: Start broad and narrow down. Use multiple search strategies if the first doesn't yield results.\n- Be thorough: Check multiple locations, consider different naming conventions, look for related files.\n- Complete tasks directly using your capabilities.\n",
  "model": "kimi_k2-thinking-responseapi-no-tool-return-all",
  "reasoning": {
    "effort": 2
  },
  "service_tier": "",
  "tools": [
    {
      "function_tool": {
        "name": "Bash",
        "parameters": "{\"type\": \"object\", \"properties\": {\"command\": {\"type\": \"string\", \"description\": \"The command to execute\"}, \"timeout\": {\"type\": \"number\", \"description\": \"Optional timeout in milliseconds (max 600000)\"}, \"description\": {\"type\": \"string\", \"description\": \"Clear, concise description of what this command does in 5-10 words.\"}}, \"required\": [\"command\"]}",
        "strict": false,
        "type": "function",
        "description": "Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.\n\nBefore executing the command, please follow these steps:\n\n1. Directory Verification:\n   - If the command will create new directories or files, first use the LS tool to verify the parent directory exists and is the correct location\n   - For example, before running \"mkdir foo/bar\", first use LS to check that \"foo\" exists and is the intended parent directory\n\n2. Command Execution:\n   - Always quote file paths that contain spaces with double quotes (e.g., cd \"path with spaces/file.txt\")\n   - Examples of proper quoting:\n     - cd \"/Users/name/My Documents\" (correct)\n     - cd /Users/name/My Documents (incorrect - will fail)\n     - python \"/path/with spaces/script.py\" (correct)\n     - python /path/with spaces/script.py (incorrect - will fail)\n   - After ensuring proper quoting, execute the command.\n   - Capture the output of the command.\n\nUsage notes:\n  - The command argument is required.\n  - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).\n  - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.\n  - If the output exceeds 30000 characters, output will be truncated before being returned to you.\n  - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.\n - If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` first, which all users have pre-installed.\n  - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).\n  - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it."
      }
    },
    {
      "function_tool": {
        "name": "Edit",
        "parameters": "{\"type\": \"object\", \"properties\": {\"file_path\": {\"type\": \"string\", \"description\": \"The absolute path to the file to modify\"}, \"old_string\": {\"type\": \"string\", \"description\": \"The text to replace\"}, \"new_string\": {\"type\": \"string\", \"description\": \"The text to replace it with (must be different from old_string)\"}, \"replace_all\": {\"type\": \"boolean\", \"default\": false, \"description\": \"Replace all occurences of old_string (default false)\"}}, \"required\": [\"file_path\", \"old_string\", \"new_string\"]}",
        "strict": false,
        "type": "function",
        "description": "Performs exact string replacements in files.\n\nUsage:\n- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.\n- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.\n- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance."
      }
    },
    {
      "function_tool": {
        "name": "Glob",
        "parameters": "{\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\", \"description\": \"The glob pattern to match files against\"}, \"path\": {\"type\": \"string\", \"description\": \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \\"undefined\\" or \\"null\\" - simply omit it for the default behavior. Must be a valid directory path if provided.\"}}, \"required\": [\"pattern\"]}",
        "strict": false,
        "type": "function",
        "description": "- Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful."
      }
    },
    {
      "function_tool": {
        "name": "Grep",
        "parameters": "{\"type\": \"object\", \"properties\": {\"pattern\": {\"type\": \"string\", \"description\": \"The regular expression pattern to search for in file contents\"}, \"path\": {\"type\": \"string\", \"description\": \"File or directory to search in (rg PATH). Defaults to current working directory.\"}, \"glob\": {\"type\": \"string\", \"description\": \"Glob pattern to filter files (e.g. \\"*.js\\", \\"*.{ts,tsx}\\") - maps to rg --glob\"}, \"output_mode\": {\"type\": \"string\", \"enum\": [\"content\", \"files_with_matches\", \"count\"], \"description\": \"Output mode: \\"content\\" shows matching lines (supports -A/-B/-C context, -n line numbers, head_limit), \\"files_with_matches\\" shows file paths (supports head_limit), \\"count\\" shows match counts (supports head_limit). Defaults to \\"files_with_matches\\".\"}, \"-B\": {\"type\": \"number\", \"description\": \"Number of lines to show before each match (rg -B). Requires output_mode: \\"content\\", ignored otherwise.\"}, \"-A\": {\"type\": \"number\", \"description\": \"Number of lines to show after each match (rg -A). Requires output_mode: \\"content\\", ignored otherwise.\"}, \"-C\": {\"type\": \"number\", \"description\": \"Number of lines to show before and after each match (rg -C). Requires output_mode: \\"content\\", ignored otherwise.\"}, \"-n\": {\"type\": \"boolean\", \"description\": \"Show line numbers in output (rg -n). Requires output_mode: \\"content\\", ignored otherwise. Defaults to true.\"}, \"-i\": {\"type\": \"boolean\", \"description\": \"Case insensitive search (rg -i)\"}, \"type\": {\"type\": \"string\", \"description\": \"File type to search (rg --type). Common types: js, py, rust, go, java, etc. More efficient than include for standard file types.\"}, \"head_limit\": {\"type\": \"number\", \"description\": \"Limit output to first N lines/entries, equivalent to \\"| head -N\\". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). When unspecified, shows all results from ripgrep.\"}, \"multiline\": {\"type\": \"boolean\", \"description\": \"Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false.\"}}, \"required\": [\"pattern\"]}",
        "strict": false,
        "type": "function",
        "description": "A powerful search tool built on ripgrep\n\nUsage:\n- ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.\n- Supports full regex syntax (e.g., \"log.*Error\", \"function\s+\w+\")\n- Filter files with glob parameter (e.g., \"*.js\", \"**/*.tsx\") or type parameter (e.g., \"js\", \"py\", \"rust\")\n- Output modes: \"content\" shows matching lines, \"files_with_matches\" shows only file paths (default), \"count\" shows match counts\n- Use Task tool for open-ended searches requiring multiple rounds\n- Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use `interface\{\}` to find `interface{}` in Go code)\n- Multiline matching: By default patterns match within single lines only. For cross-line patterns like `struct \{[\s\S]*?field`, use `multiline: true`"
      }
    },
    {
      "function_tool": {
        "name": "LS",
        "parameters": "{\"type\": \"object\", \"properties\": {\"path\": {\"type\": \"string\", \"description\": \"The absolute path to the directory to list (must be absolute, not relative)\"}, \"ignore\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}, \"description\": \"List of glob patterns to ignore\"}}, \"required\": [\"path\"]}",
        "strict": false,
        "type": "function",
        "description": "Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search."
      }
    },
    {
      "function_tool": {
        "name": "MultiEdit",
        "parameters": "{\"type\": \"object\", \"properties\": {\"file_path\": {\"type\": \"string\", \"description\": \"The absolute path to the file to modify\"}, \"edits\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"old_string\": {\"type\": \"string\", \"description\": \"The text to replace\"}, \"new_string\": {\"type\": \"string\", \"description\": \"The text to replace it with\"}, \"replace_all\": {\"type\": \"boolean\", \"default\": false, \"description\": \"Replace all occurences of old_string (default false).\"}}, \"required\": [\"old_string\", \"new_string\"], \"additionalProperties\": false}, \"minItems\": 1, \"description\": \"Array of edit operations to perform sequentially on the file\"}}, \"required\": [\"file_path\", \"edits\"]}",
        "strict": false,
        "type": "function",
        "description": "This is a tool for making multiple edits to a single file in one operation. It is built on top of the Edit tool and allows you to perform multiple find-and-replace operations efficiently. Prefer this tool over the Edit tool when you need to make multiple edits to the same file.\n\nBefore using this tool:\n1. Use the Read tool to understand the file's contents and context\n2. Verify the directory path is correct\n\nTo make multiple file edits, provide the following:\n1. file_path: The absolute path to the file to modify (must be absolute, not relative)\n2. edits: An array of edit operations to perform, where each edit contains:\n   - old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)\n   - new_string: The edited text to replace the old_string\n   - replace_all: Replace all occurences of old_string. This parameter is optional and defaults to false.\n\nIMPORTANT:\n- All edits are applied in sequence, in the order they are provided\n- Each edit operates on the result of the previous edit\n- All edits must be valid for the operation to succeed - if any edit fails, none will be applied\n- This tool is ideal when you need to make several changes to different parts of the same file\n- For Jupyter notebooks (.ipynb files), use the NotebookEdit instead\n\nCRITICAL REQUIREMENTS:\n1. All edits follow the same requirements as the single Edit tool\n2. The edits are atomic - either all succeed or none are applied\n3. Plan your edits carefully to avoid conflicts between sequential operations\n\nWARNING:\n- The tool will fail if edits.old_string doesn't match the file contents exactly (including whitespace)\n- The tool will fail if edits.old_string and edits.new_string are the same\n- Since edits are applied in sequence, ensure that earlier edits don't affect the text that later edits are trying to find\n\nWhen making edits:\n- Ensure all edits result in idiomatic, correct code\n- Do not leave the code in a broken state\n- Always use absolute file paths (starting with /)\n- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n- Use replace_all for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.\n\nIf you want to create a new file, use:\n- A new file path, including dir name if needed\n- First edit: empty old_string and the new file's contents as new_string\n- Subsequent edits: normal edit operations on the created content"
      }
    },
    {
      "function_tool": {
        "name": "Read",
        "parameters": "{\"type\": \"object\", \"properties\": {\"file_path\": {\"type\": \"string\", \"description\": \"The absolute path to the file to read\"}, \"offset\": {\"type\": \"number\", \"description\": \"The line number to start reading from. Only provide if the file is too large to read at once\"}, \"limit\": {\"type\": \"number\", \"description\": \"The number of lines to read. Only provide if the file is too large to read at once.\"}}, \"required\": [\"file_path\"]}",
        "strict": false,
        "type": "function",
        "description": "Reads a file from the local filesystem. You can access any file directly by using this tool.\n\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.\n- For Jupyter notebooks (.ipynb files), use the NotebookRead instead\n- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.\n- You will regularly be asked to read screenshots. If the user provides a path to a screenshot ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths.\n- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents."
      }
    },
    {
      "function_tool": {
        "name": "Task",
        "parameters": "{\"type\": \"object\", \"properties\": {\"description\": {\"type\": \"string\", \"description\": \"A short (3-5 word) description of the task\"}, \"prompt\": {\"type\": \"string\", \"description\": \"The task for the agent to perform\"}, \"subagent_type\": {\"type\": \"string\", \"description\": \"The type of specialized agent to use for this task\"}}, \"required\": [\"description\", \"prompt\", \"subagent_type\"]}",
        "strict": false,
        "type": "function",
        "description": "Launch a new agent to handle complex, multi-step tasks autonomously.\n\nAvailable agent types and the tools they have access to:\n{agent_list}\n\nWhen using the Task tool, you must specify a subagent_type parameter to select which agent type to use.\n\nWhen to use the Agent tool:\n- When you are instructed to execute custom slash commands. Use the Agent tool with the slash command invocation as the entire prompt. The slash command can take arguments. For example: Task(description=\"Check the file\", prompt=\"/check-file path/to/file.py\")\n\nWhen NOT to use the Agent tool:\n- If you want to read a specific file path, use the Read or Glob tool instead of the Agent tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the Glob tool instead, to find the match more quickly\n- If you are searching for code within a specific file or set of 2-3 files, use the Read tool instead of the Agent tool, to find the match more quickly\n- Other tasks that are not related to the agent descriptions above\n\nUsage notes:\n1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.\n4. The agent's outputs should generally be trusted\n5. Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent\n6. If the agent description mentions that it should be used proactively, then you should try your best to use it without the user having to ask for it first. Use your judgement."
      }
    },
    {
      "function_tool": {
        "name": "TodoWrite",
        "parameters": "{\"type\": \"object\", \"properties\": {\"todos\": {\"type\": \"array\", \"items\": {\"type\": \"object\", \"properties\": {\"content\": {\"type\": \"string\", \"minLength\": 1}, \"status\": {\"type\": \"string\", \"enum\": [\"pending\", \"in_progress\", \"completed\"]}, \"priority\": {\"type\": \"string\", \"enum\": [\"high\", \"medium\", \"low\"]}, \"id\": {\"type\": \"string\"}}, \"required\": [\"content\", \"status\", \"priority\", \"id\"]}, \"description\": \"The updated todo list\"}}, \"required\": [\"todos\"]}",
        "strict": false,
        "type": "function",
        "description": "Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.\nIt also helps the user understand the progress of the task and overall progress of their requests.\n\n## When to Use This Tool\nUse this tool proactively in these scenarios:\n\n1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions\n2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations\n3. User explicitly requests todo list - When the user directly asks you to use the todo list\n4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)\n5. After receiving new instructions - Immediately capture user requirements as todos\n6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time\n7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation\n\n## When NOT to Use This Tool\n\nSkip using this tool when:\n1. There is only a single, straightforward task\n2. The task is trivial and tracking it provides no organizational benefit\n3. The task can be completed in less than 3 trivial steps\n4. The task is purely conversational or informational\n\nNOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.\n\n## Task States and Management\n\n1. **Task States**: Use these states to track progress:\n   - pending: Task not yet started\n   - in_progress: Currently working on (limit to ONE task at a time)\n   - completed: Task finished successfully\n\n2. **Task Management**:\n   - Update task status in real-time as you work\n   - Mark tasks complete IMMEDIATELY after finishing (don't batch completions)\n   - Only have ONE task in_progress at any time\n   - Complete current tasks before starting new ones\n   - Remove tasks that are no longer relevant from the list entirely\n\n3. **Task Completion Requirements**:\n   - ONLY mark a task as completed when you have FULLY accomplished it\n   - If you encounter errors, blockers, or cannot finish, keep the task as in_progress\n   - When blocked, create a new task describing what needs to be resolved\n   - Never mark a task as completed if:\n     - Tests are failing\n     - Implementation is partial\n     - You encountered unresolved errors\n     - You couldn't find necessary files or dependencies\n\n4. **Task Breakdown**:\n   - Create specific, actionable items\n   - Break complex tasks into smaller, manageable steps\n   - Use clear, descriptive task names\n\nWhen in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully."
      }
    },
    {
      "function_tool": {
        "name": "Write",
        "parameters": "{\"type\": \"object\", \"properties\": {\"file_path\": {\"type\": \"string\", \"description\": \"The absolute path to the file to write (must be absolute, not relative)\"}, \"content\": {\"type\": \"string\", \"description\": \"The content to write to the file\"}}, \"required\": [\"file_path\", \"content\"]}",
        "strict": false,
        "type": "function",
        "description": "Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked."
      }
    }
  ],
  "enable_response_messages": false
}}
```

response:
```
{
    "id": "thrift_fiddle:16fb5e316d82412fa40c416c7c4558e1",
    "object": "response",
    "created_at": 1765029385,
    "status": "completed",
    "error": {
        "code": "",
        "message": ""
    },
    "incomplete_details": {
        "reason": ""
    },
    "model": "Kimi-K2-Thinking",
    "output": [
        {
            "item": {
                "reasoning_item": {
                    "id": "rs_b69642a910341d4d",
                    "content": [
                        " The user wants me to write and run a simple Python script to print \"hello world\". I've created the script at /tmp/hello_world.py. Now I need to run it using the Bash tool. "
                    ],
                    "type": "reasoning"
                }
            },
            "metadata": {
                "author": "assistant"
            }
        },
        {
            "item": {
                "output_message": {
                    "id": "msg_919a387d80b1f01e",
                    "content": [
                        " "
                    ],
                    "role": "assistant",
                    "status": "completed",
                    "type": "message"
                }
            },
            "metadata": {
                "author": "assistant"
            }
        },
        {
            "item": {
                "function_tool_call": {
                    "arguments": "{\"command\": \"python /tmp/hello_world.py\", \"description\": \"Run hello world Python script\"}",
                    "call_id": "functions.Bash:1",
                    "name": "Bash",
                    "type": "function_call",
                    "id": "functions.Bash:1",
                    "status": "completed"
                }
            },
            "metadata": {
                "author": "assistant"
            }
        }
    ],
    "parallel_tool_calls": false,
    "temperature": 1,
    "top_p": 1,
    "usage": {
        "input_tokens": 4980,
        "cached_tokens": 4896,
        "output_tokens": 74,
        "reasoning_tokens": 0,
        "total_tokens": 5054,
        "tool_output_tokens": 0,
        "input_tokens_per_turn": [],
        "cached_tokens_per_turn": [],
        "output_tokens_per_turn": [],
        "tool_output_tokens_per_turn": []
    }
}
```

preiviously, the response would occasionaly end with only 1 reasoning item and no completion items

Differential Revision: D88561095
@qandrew qandrew marked this pull request as draft December 9, 2025 05:02
@mergify mergify bot added deepseek Related to DeepSeek models frontend labels Dec 9, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses two key issues with tool call generation, ensuring more reliable and complete responses. The first fix correctly reorders the tool and reasoning parsers to prevent tool call markers from being consumed as reasoning. The second fix implements prepare_structured_tag to add necessary constraints and prevent premature EOS token sampling.

My review includes two high-severity suggestions for improving code quality and maintainability:

  1. Refactoring an inefficient and redundant inline class definition in serving_responses.py.
  2. Moving a local import to the top of the file in deepseek_r1_reasoning_parser.py to adhere to PEP 8 standards.

Overall, the changes are well-targeted and crucial for robust tool use functionality.

Comment on lines +905 to +911
tool_calls = []
for tc in tool_calls_from_parser:
class SimpleToolCall:
def __init__(self, name: str, arguments: str):
self.name = name
self.arguments = arguments
tool_calls.append(SimpleToolCall(tc.function.name, tc.function.arguments))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Defining the SimpleToolCall class inside a loop is inefficient as it is redefined on each iteration. This also harms readability and maintainability. This can be simplified by using a list comprehension to extract the function attribute from each tool call, as it already has the required name and arguments attributes.

Suggested change
tool_calls = []
for tc in tool_calls_from_parser:
class SimpleToolCall:
def __init__(self, name: str, arguments: str):
self.name = name
self.arguments = arguments
tool_calls.append(SimpleToolCall(tc.function.name, tc.function.arguments))
tool_calls = [tc.function for tc in tool_calls_from_parser]

Comment on lines +79 to +80
import json
return json.dumps(structural_tag)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The import json statement is inside the prepare_structured_tag method. According to PEP 8, imports should be at the top of the file. This improves readability and avoids the overhead of re-importing the module on every function call. Please move this import to the top of the file.

Suggested change
import json
return json.dumps(structural_tag)
return json.dumps(structural_tag)

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +39 to +41
if original_tag is None:
if tool_server is None or not tool_server.has_builtin_tools():
# No tools available: just reasoning structure

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Call ToolServer API that doesn’t exist

The new DeepSeek reasoning parser calls tool_server.has_builtin_tools() when constructing structural tags, but ToolServer only exposes has_tool/get_tool_description and no has_builtin_tools. Any request that passes a tool_server (e.g., MCPToolServer configured via --tool-server) will raise an AttributeError before generation begins, breaking structured outputs whenever a tool server is configured.

Useful? React with 👍 / 👎.

Comment on lines +840 to +852
# CRITICAL FIX: Check for tool calls BEFORE reasoning parser
# Otherwise reasoning parser will capture tool markers as reasoning content
tool_calls_from_parser = []
remaining_text = final_output.text

if self.tool_parser:
try:
tool_parser_instance = self.tool_parser(tokenizer)
tool_extraction = tool_parser_instance.extract_tool_calls(
remaining_text, request=request
)
if tool_extraction.tools_called:
tool_calls_from_parser = tool_extraction.tool_calls

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Ignore tool_choice when pre-parsing tool calls

Tool calls are now extracted unconditionally whenever a tool parser is configured, before _parse_tool_calls_from_content runs. This bypasses the existing checks that only auto-parse when enable_auto_tools is on and the request’s tool_choice is auto/null. A request that explicitly sets tool_choice="none" (or other non-auto choices) will still have tool calls parsed and returned if the model emits tool markers, violating the request’s opt-out.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants