fix(chat_completions): strip tool_name from messages for strict providers

The 'tool_name' key on role=tool messages is an internal Hermes field
(stored in the messages.tool_name SQLite column for FTS indexing) that
is not part of the OpenAI Chat Completions schema. Strict OpenAI-compatible
providers — notably Moonshot AI (Kimi) — reject it with HTTP 400:

  Error from provider: Extra inputs are not permitted,
  field: 'messages[N].tool_name', value: 'execute_code'

Add 'tool_name' to the sanitize block in ChatCompletionsTransport.convert_messages
alongside the existing Codex Responses API fields (codex_reasoning_items,
codex_message_items) so it is popped before the request is sent.

Reproducer:
  hermes chat --model kimi-k2.6
  > list the top 5 Hacker News stories
  -> assistant emits tool_call(execute_code)
  -> tool result message gets tool_name='execute_code'
  -> next turn's payload includes messages[N].tool_name -> 400

Permissive backends (MiniMax, OpenRouter on most routes) ignore the extra
field and were masking the bug.
This commit is contained in:
Savanne Kham 2026-05-19 23:58:55 +02:00 committed by Teknium
parent 5e743559e0
commit 258965663c
2 changed files with 26 additions and 1 deletions

View file

@ -122,7 +122,11 @@ class ChatCompletionsTransport(ProviderTransport):
for msg in messages:
if not isinstance(msg, dict):
continue
if "codex_reasoning_items" in msg or "codex_message_items" in msg:
if (
"codex_reasoning_items" in msg
or "codex_message_items" in msg
or "tool_name" in msg
):
needs_sanitize = True
break
tool_calls = msg.get("tool_calls")
@ -145,6 +149,7 @@ class ChatCompletionsTransport(ProviderTransport):
continue
msg.pop("codex_reasoning_items", None)
msg.pop("codex_message_items", None)
msg.pop("tool_name", None)
tool_calls = msg.get("tool_calls")
if isinstance(tool_calls, list):
for tc in tool_calls:

View file

@ -46,6 +46,26 @@ class TestChatCompletionsBasic:
assert "codex_reasoning_items" in msgs[0]
assert "codex_message_items" in msgs[0]
def test_convert_messages_strips_tool_name(self, transport):
"""Internal `tool_name` (used for FTS indexing in the SQLite store) is
not part of the OpenAI Chat Completions schema. Strict providers like
Moonshot/Kimi reject it with HTTP 400 'Extra inputs are not permitted'.
"""
msgs = [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": None,
"tool_calls": [{"id": "call_1", "type": "function",
"function": {"name": "execute_code", "arguments": "{}"}}]},
{"role": "tool", "tool_call_id": "call_1", "tool_name": "execute_code",
"content": "result"},
]
result = transport.convert_messages(msgs)
assert "tool_name" not in result[2]
assert result[2]["content"] == "result"
assert result[2]["tool_call_id"] == "call_1"
# Original list untouched (deepcopy-on-demand)
assert msgs[2]["tool_name"] == "execute_code"
class TestChatCompletionsBuildKwargs: