mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-18 09:51:59 +00:00
fix(agent): strip schema-foreign keys from max-iterations summary request (#34436)
The max-iterations summary path (`handle_max_iterations`) hand-builds its
message list and calls `chat.completions.create()` directly, bypassing
`ChatCompletionsTransport.convert_messages()`. It only popped
("reasoning", "finish_reason", "_thinking_prefill"), so `tool_name` (SQLite
FTS bookkeeping), the `codex_*` reasoning carriers, and other internal
`_`-prefixed scaffolding leaked to the wire.
Strict OpenAI-compatible gateways (Fireworks-backed OpenCode Go, Mistral,
Moonshot/Kimi) reject these with HTTP 400 "Extra inputs are not permitted,
field: 'messages[N].tool_name'", so a long tool-using session that exhausts
the iteration budget fails to summarise instead of returning the result.
Mirror convert_messages() in this path: also drop tool_name,
codex_reasoning_items, codex_message_items, and every `_`-prefixed key.
Copy-on-write is already in place, so internal history keeps the fields for
FTS / Codex-fallback.
Adds a regression test to TestHandleMaxIterations asserting the summary
request carries none of the schema-foreign keys (fails on main, passes here).
This commit is contained in:
parent
c1b2d0917f
commit
636ff636d7
2 changed files with 46 additions and 0 deletions
|
|
@ -1283,6 +1283,18 @@ def handle_max_iterations(agent, messages: list, api_call_count: int) -> str:
|
|||
agent._copy_reasoning_content_for_api(msg, api_msg)
|
||||
for internal_field in ("reasoning", "finish_reason", "_thinking_prefill"):
|
||||
api_msg.pop(internal_field, None)
|
||||
# Strict OpenAI-compatible gateways (Fireworks-backed OpenCode Go,
|
||||
# Mistral, Moonshot/Kimi) reject any message key outside the Chat
|
||||
# Completions schema. The main loop drops these via
|
||||
# ChatCompletionsTransport.convert_messages(), but the summary path
|
||||
# hand-builds messages and calls chat.completions.create() directly,
|
||||
# bypassing the transport — so mirror that sanitization here:
|
||||
# tool_name (SQLite FTS bookkeeping), the codex_* reasoning carriers,
|
||||
# and every Hermes-internal underscore-prefixed scaffolding key.
|
||||
for schema_foreign in ("tool_name", "codex_reasoning_items", "codex_message_items"):
|
||||
api_msg.pop(schema_foreign, None)
|
||||
for internal_key in [k for k in api_msg if isinstance(k, str) and k.startswith("_")]:
|
||||
api_msg.pop(internal_key, None)
|
||||
if _needs_sanitize:
|
||||
agent._sanitize_tool_calls_for_strict_api(api_msg)
|
||||
api_messages.append(api_msg)
|
||||
|
|
|
|||
|
|
@ -2756,6 +2756,40 @@ class TestHandleMaxIterations:
|
|||
]
|
||||
assert len(stub_ids) >= 1, f"No stub result for assistant tool_call: {stub_ids}"
|
||||
|
||||
def test_summary_strips_strict_schema_foreign_fields(self, agent):
|
||||
"""Regression: the max-iterations summary request must NOT carry
|
||||
Chat-Completions-schema-foreign keys — tool_name (SQLite FTS
|
||||
bookkeeping), codex_* reasoning carriers, or internal _-prefixed
|
||||
scaffolding. Strict gateways (Fireworks-backed OpenCode Go, Mistral,
|
||||
Kimi) reject these with 'Extra inputs are not permitted, field:
|
||||
messages[N].tool_name'. The transport's convert_messages() strips
|
||||
them on the main loop; this hand-built summary path must mirror it."""
|
||||
agent.client.chat.completions.create.return_value = _mock_response(content="Summary")
|
||||
agent._cached_system_prompt = "You are helpful."
|
||||
messages = [
|
||||
{"role": "user", "content": "do stuff"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [{"id": "call_1", "function": {"name": "execute_code", "arguments": "{}"}}],
|
||||
"codex_reasoning_items": [{"id": "rs_1"}],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_1", "content": "result", "tool_name": "execute_code"},
|
||||
{"role": "assistant", "content": "Done.", "_empty_recovery_synthetic": True},
|
||||
]
|
||||
|
||||
result = agent._handle_max_iterations(messages, 60)
|
||||
|
||||
assert result == "Summary"
|
||||
sent_msgs = agent.client.chat.completions.create.call_args.kwargs.get("messages", [])
|
||||
for m in sent_msgs:
|
||||
assert "tool_name" not in m, m
|
||||
assert "codex_reasoning_items" not in m, m
|
||||
assert "codex_message_items" not in m, m
|
||||
assert not any(isinstance(k, str) and k.startswith("_") for k in m), m
|
||||
# Internal history is untouched — the path copies each message.
|
||||
assert messages[2]["tool_name"] == "execute_code"
|
||||
assert messages[1]["codex_reasoning_items"] == [{"id": "rs_1"}]
|
||||
|
||||
def test_summary_omits_provider_preferences_for_non_openrouter(self, agent):
|
||||
agent.base_url = "https://api.openai.com/v1"
|
||||
agent._base_url_lower = agent.base_url.lower()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue