fix: prevent provider 400 errors from broken tool message payloads - #18
Merged
Conversation
Three root causes were producing "Tool message must have either name or tool_call_id" 400 errors from Google AI Studio via OpenRouter, plus "Corrupted thought signature" errors on Gemini models: 1. file_read returned lossy UTF-8 for binary files (e.g. brain.db), flooding the context with heavily-escaped binary content. Now rejects binary files with a clear error steering the LLM to memory_recall. 2. truncate_tool_message_content used raw inner.len() to estimate JSON envelope overhead, which is wildly wrong for heavily-escaped content (\0 = 1 byte raw but 6 chars as \u0000). This caused the function to fall through to truncate_plain_text on the outer JSON, breaking the wrapper and severing tool_call_id. Fixed with zero-alloc json_escaped_len() arithmetic and a capped retry loop. 3. convert_messages in OpenRouter provider silently dropped tool_call_id when serde_json::from_str failed on truncation-broken JSON. Now salvages the ID via substring search as a defense-in-depth fallback. Additionally fixes ModelRouter::supports_streaming_tool_events() using any() instead of all(), which caused a stream-then-fallback cycle on every tool-bearing call (~190 events in recent trace window).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
.db,.sqlite, etc.) with a clear error steering the LLM tomemory_recall, instead of returning lossy UTF-8 that floods context with heavily-escaped content and triggers downstream truncation bugsenvelope_overheadcalculation that used rawinner.len()(wrong for JSON-escaped content like\u0000) with zero-allocationjson_escaped_len()arithmetic and a capped retry loop, preserving the JSON wrapper andtool_call_idin all casestool_call_idvia substring search whenserde_json::from_strfails on truncation-broken JSON, preventing Gemini "Tool message must have either name or tool_call_id" 400sModelRouter::supports_streaming_tool_events()fromany()toall()to prevent stream→fallback cycle on every tool-bearing call when the resolved provider doesn't support streaming tool eventsTest plan
cargo test --lib— 4279 passed, 0 failedtruncate_tool_message_content_preserves_json_with_heavy_escaping— verifies binary content (null bytes) truncation preserves valid JSON andtool_call_idconvert_messages_salvages_tool_call_id_from_broken_json— verifies ID extraction from truncation-broken JSONfile_read_rejects_binary_file,e2e_agent_file_read_rejects_binary— verify binary rejection pathfile_read_rejects_pdf_without_feature— verifies PDF-specific error withoutrag-pdffeatureruntime-trace.jsonlfor absence ofllm_stream_fallbackandchannel_message_errorevents with "tool_call_id" errors