1414_tools_bootstrap_lock : asyncio .Lock | None = None
1515
1616
17+ def _try_coerce_int (val : Any ) -> int | None :
18+ if isinstance (val , bool ) or val is None :
19+ return None
20+ if isinstance (val , int ):
21+ return int (val )
22+ if isinstance (val , float ):
23+ return int (val ) if val .is_integer () else None
24+ if isinstance (val , str ):
25+ s = val .strip ()
26+ if not s :
27+ return None
28+ try :
29+ if s .lstrip ("+-" ).isdigit ():
30+ return int (s )
31+ f = float (s )
32+ if f .is_integer ():
33+ return int (f )
34+ except (TypeError , ValueError , OverflowError ):
35+ return None
36+ return None
37+
38+
39+ def _try_coerce_number (val : Any ) -> int | float | None :
40+ if isinstance (val , bool ) or val is None :
41+ return None
42+ if isinstance (val , int ):
43+ return int (val )
44+ if isinstance (val , float ):
45+ return val
46+ if isinstance (val , str ):
47+ s = val .strip ()
48+ if not s :
49+ return None
50+ try :
51+ n = float (s )
52+ except (TypeError , ValueError , OverflowError ):
53+ return None
54+ if n .is_integer () and "." not in s and "e" not in s .lower () and "E" not in s :
55+ try :
56+ return int (s )
57+ except (TypeError , ValueError , OverflowError ):
58+ return n
59+ return n
60+ return None
61+
62+
63+ def _try_coerce_bool (val : Any ) -> bool | None :
64+ if isinstance (val , bool ):
65+ return val
66+ if isinstance (val , str ):
67+ s = val .strip ().lower ()
68+ if s in {"true" , "yes" , "on" , "y" }:
69+ return True
70+ if s in {"false" , "no" , "off" , "n" }:
71+ return False
72+ return None
73+
74+
1775def _tools_bootstrap_lock_get () -> asyncio .Lock :
1876 global _tools_bootstrap_lock
1977 if _tools_bootstrap_lock is None :
@@ -320,6 +378,8 @@ def _validate_tool_args(self, schema: dict | None, arguments: dict) -> dict:
320378
321379 if not schema :
322380 return cleaned
381+ # LLM JSON 常把整数写成 "3";先按 schema 类型软转换,再 clamp / validate
382+ cleaned = self ._coerce_tool_args (schema , cleaned )
323383 # 软夹紧:数值 min/max 越界时 clamp,减少「max_results=3 < min 5」类无意义失败
324384 cleaned = self ._clamp_tool_args (schema , cleaned )
325385 try :
@@ -332,6 +392,44 @@ def _validate_tool_args(self, schema: dict | None, arguments: dict) -> dict:
332392 raise ValueError (f"Invalid tool arguments: { e .message } " ) from e
333393 return cleaned
334394
395+ @staticmethod
396+ def _schema_prop_types (spec : dict ) -> set [str ]:
397+ t = spec .get ("type" )
398+ if isinstance (t , str ) and t :
399+ return {t }
400+ if isinstance (t , (list , tuple )):
401+ return {str (x ) for x in t if x }
402+ return set ()
403+
404+ def _coerce_tool_args (self , schema : dict , cleaned : dict ) -> dict :
405+ """把 LLM 常见的字符串标量收成 schema 声明的 integer/number/boolean。"""
406+ props = schema .get ("properties" ) if isinstance (schema , dict ) else None
407+ if not isinstance (props , dict ) or not cleaned :
408+ return cleaned
409+ out = dict (cleaned )
410+ for key , spec in props .items ():
411+ if key not in out or not isinstance (spec , dict ):
412+ continue
413+ types = self ._schema_prop_types (spec )
414+ if not types :
415+ continue
416+ val = out [key ]
417+ if "integer" in types :
418+ coerced = _try_coerce_int (val )
419+ if coerced is not None :
420+ out [key ] = coerced
421+ continue
422+ if "number" in types :
423+ coerced = _try_coerce_number (val )
424+ if coerced is not None :
425+ out [key ] = coerced
426+ continue
427+ if "boolean" in types :
428+ coerced = _try_coerce_bool (val )
429+ if coerced is not None :
430+ out [key ] = coerced
431+ return out
432+
335433 def _clamp_tool_args (self , schema : dict , cleaned : dict ) -> dict :
336434 """按 JSON Schema properties 的 minimum/maximum/minLength 做软夹紧(不改类型)。"""
337435 props = schema .get ("properties" ) if isinstance (schema , dict ) else None
0 commit comments