Spaces:
Running on Zero
Running on Zero
Fix OpenAI tool history replay
Browse files- app.py +8 -2
- test_tool_calls.py +16 -1
- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|