mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
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:
parent
570f8bab8f
commit
9eeaaa4f1b
1 changed files with 75 additions and 1 deletions
76
run_agent.py
76
run_agent.py
|
|
@ -9662,7 +9662,81 @@ class AIAgent:
|
|||
),
|
||||
}}
|
||||
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)
|
||||
am["tool_calls"] = new_tcs
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue