fix(agent): repair malformed tool_call arguments before API send
Cherry-picked from PR #12252 by @sirEven. Models like GLM-5.1 via Ollama can produce malformed tool_call arguments (truncated JSON, trailing commas, Python None). The existing except Exception: pass silently passes broken args to the API, which rejects them with HTTP 400, crashing the session. Adds a multi-stage repair pipeline at the pre-send normalization point: 1. Empty/whitespace-only → {} 2. Python None literal → {} 3. Strip trailing commas 4. Auto-close unclosed brackets 5. Remove excess closing delimiters 6. Last resort: replace with {} (logged at WARNING)
This commit is contained in:
committed by
Teknium
parent
570f8bab8f
commit
9eeaaa4f1b
76
run_agent.py
76
run_agent.py
@@ -9662,7 +9662,81 @@ class AIAgent:
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
# GLM-5.1 and similar models can generate
|
||||||
|
# malformed tool_call arguments (truncated JSON,
|
||||||
|
# trailing commas, Python None, etc.). The API
|
||||||
|
# proxy rejects these with HTTP 400 "invalid tool
|
||||||
|
# call arguments". Attempt common repairs; if
|
||||||
|
# all fail, replace with "{}" so the request
|
||||||
|
# succeeds (better than crashing the session).
|
||||||
|
raw_args = tc["function"]["arguments"]
|
||||||
|
repaired = False
|
||||||
|
raw_stripped = raw_args.strip() if isinstance(raw_args, str) else ""
|
||||||
|
|
||||||
|
# Fast-path: empty / whitespace-only → empty object
|
||||||
|
if not raw_stripped:
|
||||||
|
tc["function"]["arguments"] = "{}"
|
||||||
|
repaired = True
|
||||||
|
logger.warning(
|
||||||
|
"Sanitized empty tool_call arguments for %s",
|
||||||
|
tc["function"].get("name", "?"),
|
||||||
|
)
|
||||||
|
# Python-literal None → JSON null → normalise to {}
|
||||||
|
elif raw_stripped == "None":
|
||||||
|
tc["function"]["arguments"] = "{}"
|
||||||
|
repaired = True
|
||||||
|
logger.warning(
|
||||||
|
"Sanitized Python-None tool_call arguments for %s",
|
||||||
|
tc["function"].get("name", "?"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not repaired:
|
||||||
|
# Attempt common JSON repairs
|
||||||
|
import re as _re
|
||||||
|
fixed = raw_stripped
|
||||||
|
# 1. Strip trailing commas before } or ]
|
||||||
|
fixed = _re.sub(r',\s*([}\]])', r'\1', fixed)
|
||||||
|
# 2. Close unclosed structures
|
||||||
|
open_curly = fixed.count('{') - fixed.count('}')
|
||||||
|
open_bracket = fixed.count('[') - fixed.count(']')
|
||||||
|
if open_curly > 0:
|
||||||
|
fixed += '}' * open_curly
|
||||||
|
if open_bracket > 0:
|
||||||
|
fixed += ']' * open_bracket
|
||||||
|
# 3. Remove extra closing braces/brackets
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
json.loads(fixed)
|
||||||
|
break
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
if fixed.endswith('}') and fixed.count('}') > fixed.count('{'):
|
||||||
|
fixed = fixed[:-1]
|
||||||
|
elif fixed.endswith(']') and fixed.count(']') > fixed.count('['):
|
||||||
|
fixed = fixed[:-1]
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
json.loads(fixed)
|
||||||
|
tc["function"]["arguments"] = fixed
|
||||||
|
repaired = True
|
||||||
|
logger.warning(
|
||||||
|
"Repaired malformed tool_call arguments for %s: %s → %s",
|
||||||
|
tc["function"].get("name", "?"),
|
||||||
|
raw_stripped[:80], fixed[:80],
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not repaired:
|
||||||
|
# Last resort: replace with empty object so the
|
||||||
|
# API request doesn't crash the entire session.
|
||||||
|
tc["function"]["arguments"] = "{}"
|
||||||
|
logger.warning(
|
||||||
|
"Unrepairable tool_call arguments for %s — "
|
||||||
|
"replaced with empty object (was: %s)",
|
||||||
|
tc["function"].get("name", "?"),
|
||||||
|
raw_stripped[:80],
|
||||||
|
)
|
||||||
new_tcs.append(tc)
|
new_tcs.append(tc)
|
||||||
am["tool_calls"] = new_tcs
|
am["tool_calls"] = new_tcs
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user