@@ -41,9 +41,35 @@ class _UndefinedToolCall(Exception):
4141 response to visible text rather than emitting a partial set (spec)."""
4242
4343
44- def _coerce (value : str ) -> Any :
45- """Parameter values are raw text; try JSON so numbers/bools/objects get their
46- natural type, otherwise keep the string."""
44+ def _coerce (value : str , declared_type : Optional [str ]) -> Any :
45+ """Cast a raw XML parameter string to the type declared in the tool's JSON
46+ schema.
47+
48+ The Qwen XML format is stringly-typed (`<parameter=k>v</parameter>`), so
49+ without the schema we'd have to guess. A bare `json.loads` guess mistypes two
50+ common ways: a value the schema wants as a string but that looks numeric
51+ (`"1234"`) becomes an int, and a value the schema wants as a bool but that the
52+ model didn't write as valid JSON (`True`) stays a string. Coercing to the
53+ declared type keeps the emitted OpenAI tool_call schema-valid. Falls back to a
54+ JSON guess (then the raw string) when the type is unknown or coercion fails,
55+ so untyped/loosely-typed params keep working.
56+ """
57+ if declared_type == "string" :
58+ return value
59+ if declared_type == "boolean" :
60+ low = value .strip ().lower ()
61+ if low in ("true" , "false" ):
62+ return low == "true"
63+ elif declared_type == "integer" :
64+ try :
65+ return int (value .strip ())
66+ except (ValueError , TypeError ):
67+ pass
68+ elif declared_type == "number" :
69+ try :
70+ return float (value .strip ())
71+ except (ValueError , TypeError ):
72+ pass
4773 try :
4874 return json .loads (value )
4975 except (ValueError , TypeError ):
@@ -59,9 +85,13 @@ class QwenFunctionCallDetector:
5985 def __init__ (self ):
6086 self ._next_index = 0
6187
62- def detect_and_parse (self , text : str , tool_names : set [str ]) -> ParseResult :
88+ def detect_and_parse (self , text : str , tools : dict [str , dict ]) -> ParseResult :
6389 """Return leading text + any complete tool calls. On no call or a parse
64- failure, return the original text unchanged (kept visible to the client)."""
90+ failure, return the original text unchanged (kept visible to the client).
91+
92+ `tools` maps each defined tool name to its JSON-schema ``parameters``
93+ object; the schema is used to coerce stringly-typed XML values to their
94+ declared types (and the key set validates names)."""
6595 first = _FUNCTION_RE .search (text )
6696 if first is None :
6797 return ParseResult (normal_text = text )
@@ -72,7 +102,7 @@ def detect_and_parse(self, text: str, tool_names: set[str]) -> ParseResult:
72102 cut = first .start ()
73103 normal = text [:cut ].strip ()
74104 try :
75- calls = self ._parse_calls (text , tool_names )
105+ calls = self ._parse_calls (text , tools )
76106 except _UndefinedToolCall as e :
77107 logger .debug ("undefined tool %s; returning raw text (no partial calls)" , e )
78108 return ParseResult (normal_text = text )
@@ -83,20 +113,21 @@ def detect_and_parse(self, text: str, tool_names: set[str]) -> ParseResult:
83113 return ParseResult (normal_text = text )
84114 return ParseResult (normal_text = normal , calls = calls )
85115
86- def _parse_calls (self , text : str , tool_names : set [str ]) -> list [ToolCallItem ]:
116+ def _parse_calls (self , text : str , tools : dict [str , dict ]) -> list [ToolCallItem ]:
87117 calls = []
88118 for name , body in _FUNCTION_RE .findall (text ):
119+ props = (tools .get (name ) or {}).get ("properties" , {})
89120 args = {
90- key : _coerce (value .strip ())
121+ key : _coerce (value .strip (), props . get ( key , {}). get ( "type" ) )
91122 for key , value in _PARAMETER_RE .findall (body )
92123 }
93- calls .append (self ._make_item (name , args , tool_names ))
124+ calls .append (self ._make_item (name , args , tools ))
94125 return calls
95126
96127 def _make_item (
97- self , name : Optional [str ], arguments : dict , tool_names : set [str ]
128+ self , name : Optional [str ], arguments : dict , tools : dict [str , dict ]
98129 ) -> ToolCallItem :
99- if not name or name not in tool_names :
130+ if not name or name not in tools :
100131 raise _UndefinedToolCall (repr (name ))
101132 item = ToolCallItem (
102133 tool_index = self ._next_index ,
0 commit comments