Erinaldorodrigues commited on
Commit
b386bac
·
verified ·
1 Parent(s): 28b52ca

Fix OpenAI tool history replay

Browse files
Files changed (3) hide show
  1. app.py +8 -2
  2. test_tool_calls.py +16 -1
  3. tool_calls.py +12 -0
app.py CHANGED
@@ -36,7 +36,11 @@ from transformers import (
36
  StoppingCriteria,
37
  StoppingCriteriaList,
38
  )
39
- from tool_calls import extract_tool_call, has_complete_tool_call
 
 
 
 
40
 
41
 
42
  # Qwen3-Coder-30B-A3B is the strongest coding-specialized Qwen checkpoint that
@@ -266,7 +270,9 @@ def _normalized_tool_calls(raw_calls: object) -> list[dict[str, Any]]:
266
  "type": "function",
267
  "function": {
268
  "name": name,
269
- "arguments": function.get("arguments", "{}"),
 
 
270
  },
271
  }
272
  if isinstance(raw_call.get("id"), str) and raw_call["id"]:
 
36
  StoppingCriteria,
37
  StoppingCriteriaList,
38
  )
39
+ from tool_calls import (
40
+ extract_tool_call,
41
+ has_complete_tool_call,
42
+ normalize_openai_tool_arguments,
43
+ )
44
 
45
 
46
  # Qwen3-Coder-30B-A3B is the strongest coding-specialized Qwen checkpoint that
 
270
  "type": "function",
271
  "function": {
272
  "name": name,
273
+ "arguments": normalize_openai_tool_arguments(
274
+ function.get("arguments", {})
275
+ ),
276
  },
277
  }
278
  if isinstance(raw_call.get("id"), str) and raw_call["id"]:
test_tool_calls.py CHANGED
@@ -5,7 +5,11 @@ from __future__ import annotations
5
  import json
6
  import unittest
7
 
8
- from tool_calls import extract_tool_call, has_complete_tool_call
 
 
 
 
9
 
10
 
11
  ALLOWED = {"Bash", "Read"}
@@ -88,6 +92,17 @@ class ToolCallTests(unittest.TestCase):
88
  self.assertIsNone(call)
89
  self.assertIn("Delete", visible)
90
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  if __name__ == "__main__":
93
  unittest.main()
 
5
  import json
6
  import unittest
7
 
8
+ from tool_calls import (
9
+ extract_tool_call,
10
+ has_complete_tool_call,
11
+ normalize_openai_tool_arguments,
12
+ )
13
 
14
 
15
  ALLOWED = {"Bash", "Read"}
 
92
  self.assertIsNone(call)
93
  self.assertIn("Delete", visible)
94
 
95
+ def test_openai_history_arguments_are_mappings_for_qwen_template(self) -> None:
96
+ self.assertEqual(
97
+ normalize_openai_tool_arguments('{"command":"pwd"}'),
98
+ {"command": "pwd"},
99
+ )
100
+ self.assertEqual(
101
+ normalize_openai_tool_arguments({"file_path": "/tmp/a.txt"}),
102
+ {"file_path": "/tmp/a.txt"},
103
+ )
104
+ self.assertEqual(normalize_openai_tool_arguments("not-json"), {})
105
+
106
 
107
  if __name__ == "__main__":
108
  unittest.main()
tool_calls.py CHANGED
@@ -147,6 +147,18 @@ def _arguments(value: Any) -> dict[str, Any] | None:
147
  return dict(parsed) if isinstance(parsed, Mapping) else None
148
 
149
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  def _openai_call(
151
  name: Any,
152
  arguments: Any,
 
147
  return dict(parsed) if isinstance(parsed, Mapping) else None
148
 
149
 
150
+ def normalize_openai_tool_arguments(value: Any) -> dict[str, Any]:
151
+ """Return the mapping required by Qwen3's chat-template ``items`` filter.
152
+
153
+ OpenAI serializes function arguments as a JSON string, while Qwen3's
154
+ official template iterates them as a mapping when replaying tool history.
155
+ Accept both representations so a completed tool call can be followed by a
156
+ tool result without raising a template ``TypeError``.
157
+ """
158
+ parsed = _arguments(value)
159
+ return parsed if parsed is not None else {}
160
+
161
+
162
  def _openai_call(
163
  name: Any,
164
  arguments: Any,