fix(agent): normalize empty tool-call content for strict chat APIs - #31615
Nigelkil01 wants to merge 1 commit into
Conversation
|
Maintainer ping: can you confirm which PR you want to carry this fix —
If |
|
I independently reproduced this against a Bedrock-backed OpenAI-compatible Claude/Sonnet endpoint, and this transport-layer normalization matches the observed provider behavior. Repro shape: {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_probe_123",
"type": "function",
"function": {
"name": "probe_tool",
"arguments": "{\"x\":1}"
}
}
]
}When replayed through the Bedrock-backed Claude/Sonnet OpenAI-compatible gateway, the request fails with: Changing only the assistant tool-call turn from I also verified this in a real Hermes tool-call flow using a custom OpenAI-compatible endpoint routing to Claude/Sonnet on Bedrock: the model called the So this affects custom OpenAI-compatible endpoints that route Claude/Sonnet traffic to AWS Bedrock, not just synthetic strict-schema validators. |
|
Thanks for the focused strict-provider compatibility fix. The underlying bug is still present on current main: Problems
Suggested changes
Automated hermes-sweeper review. |
History stores a textless tool-call turn as content:"" (build_assistant_message coerces absent text). Strict OpenAI-compatible validators reject that beside tool_calls — Bedrock-backed Claude via a compatible gateway fails with "messages: text content blocks must be non-empty"; Mistral/Fireworks 400 — and null is the schema-compatible form. Normalize at both outgoing boundaries (per the sweeper review on NousResearch#31615): the transport's copy-on-write sanitizer (`_sanitize_message`, used by convert_messages/build_kwargs) and `_sanitize_tool_calls_for_strict_api`, which the max-iteration summary request uses instead of the transport. One shared predicate, `_blank_tool_call_content`; a missing content key is already valid and left alone; substantive text and tool-call-free turns are untouched; the history copy is never mutated. The mapping is deterministic on every send, so prompt-cache prefixes stay byte-stable across turns. The send-time placeholder pad still exempts tool-call turns (the tool_calls are the payload); its test now asserts that instead of content == "". Regression: tests/agent/transports/test_chat_completions_empty_tool_call_content.py covers empty, whitespace-only, missing, substantive and no-tool-call content at both boundaries. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
805eda0 to
268c5a2
Compare
Summary
nullfor pure tool-call turns sent through strict Chat Completions pathsFixes #31583.
Test Plan
../hermes-upstream-full-20260513-201914/.venv/bin/pytest -o addopts='' tests/run_agent/test_strict_api_validation.py -q../hermes-upstream-full-20260513-201914/.venv/bin/pytest -o addopts='' tests/run_agent/test_provider_parity.py -q../hermes-upstream-full-20260513-201914/.venv/bin/pytest -o addopts='' tests/agent/transports/test_transport.py -q../hermes-upstream-full-20260513-201914/.venv/bin/python -m py_compile run_agent.py agent/transports/chat_completions.py tests/run_agent/test_strict_api_validation.pygit diff --check