fix: strip empty tool_calls before sending to strict APIs - #58953
fix: strip empty tool_calls before sending to strict APIs#58953webtecnica wants to merge 1 commit into
Conversation
60047e0 to
11a1d65
Compare
|
Rebased on latest Relevant tests passing:
No diff changes — same fix, just refreshed on top of current main. Ready for review. |
|
@teknium1 This small fix (8 lines) resolves HTTP 400 on strict providers when empty tool_calls are replayed from conversation history. Freshly rebased on main, ready for review whenever you have a moment. 🙏 |
Strict providers (DeepSeek, Mistral, Fireworks, Moonshot/Kimi) reject a message with ``tool_calls: []`` as HTTP 400 — the field must be absent or non-empty. Two defence layers: 1. **run_agent.py – _sanitize_tool_calls_for_strict_api** — if the outgoing API copy has `tool_calls=[]`, pop the key entirely. 2. **hermes_state.py – get_messages_as_conversation** — only set `tool_calls` on the message when the parsed JSON is non-empty, preventing `[]` from entering the live history in the first place. The empty array can arise from session replay when the DB stores `"[]"` (JSON-encoded empty array), or from any code path that defaults a message to `tool_calls=[]` instead of omitting the key.
11a1d65 to
c19b785
Compare
|
The empty-array root cause you identified was correct — fixed via PR #59110 (salvage of #58768 by @xxxigm, submitted first with tests). Your PR took the same diagnosis but fixed it at two narrower sites (SQLite replay + |
Problem
Conversations with 100+ messages on strict providers (DeepSeek, Mistral, Fireworks, Moonshot/Kimi) eventually hit:
This happens because a message in the conversation history carries
tool_calls: []instead of having notool_callsfield. The empty array gets replayed verbatim to the provider, which rejects it.Root cause
Two paths converge:
get_messages_as_conversation(hermes_state.py): when the DB stores"[]"(JSON-encoded empty array), it's parsed and set asmsg["tool_calls"] = []— the key is present with an empty list._sanitize_tool_calls_for_strict_api(run_agent.py): checksisinstance(tool_calls, list)but doesn't guard against empty lists, so[]passes through unchanged to the provider.Fix
Two defence layers:
hermes_state.py— only settool_callson the hydrated message if the parsed JSON is non-empty.run_agent.py— in_sanitize_tool_calls_for_strict_api, pop the key entirely whentool_callsis an empty list.The first layer prevents
[]from entering the live history; the second catches any remaining case at the API boundary.Verification
ast.parseChanges
run_agent.py_sanitize_tool_calls_for_strict_api)hermes_state.pytool_callswhen parsed result is non-empty)