Aggregate suppressed tool-call log lines

The forced-text guard in consume_stream logged inside the per-delta loop, so one suppressed structured call could produce a warning per streamed argument fragment. Move the check to after tool calls are assembled, so it fires once per turn with the count and the names of what was dropped. consume_json's non-streaming path gets the same tool names added to its existing aggregate message. Co-Authored-By: siGit Code <noreply@sigit.si>

Seto Elkahfi committed Sep 15, 2026 at 22:09 UTC 76e88483239ba1b135687dd83da1bbe3405d63ac
1 file changed +27 -15
src/backend.rs
+27 -15
@@ -576,7 +576,15 @@ impl OpenAiBackend {
576 text = cleaned;
577 let suppressed = tool_calls.len() + recovered.len();
578 if suppressed > 0 {
579 - log::warn!("suppressed {suppressed} tool call(s) from a forced-text response");
579 + let names = tool_calls
580 + .iter()
581 + .map(|call| call.name.as_str())
582 + .chain(recovered.iter().map(|call| call.name.as_str()))
583 + .collect::<Vec<_>>()
584 + .join(", ");
585 + log::warn!(
586 + "suppressed {suppressed} tool call(s) from a forced-text response: {names}"
587 + );
588 }
589 self.history
590 .lock()
@@ -717,17 +725,12 @@ impl OpenAiBackend {
725 "recovered tool call '{}' the model emitted as text instead of a structured call",
726 call.name
727 );
720 - recovered.push(ToolCall {
721 - id: format!("call_recovered_{}", recovered.len()),
722 - name: call.name,
723 - arguments: call.arguments,
724 - });
725 - } else {
726 - log::warn!(
727 - "suppressed inline tool call '{}' from a forced-text response",
728 - call.name
729 - );
728 }
729 + recovered.push(ToolCall {
730 + id: format!("call_recovered_{}", recovered.len()),
731 + name: call.name,
732 + arguments: call.arguments,
733 + });
734 }
735 }
736 }
@@ -737,10 +740,6 @@ impl OpenAiBackend {
740 }
741 }
742 for delta in choice.delta.tool_calls.into_iter().flatten() {
740 - if !allow_tool_calls {
741 - log::warn!("suppressed a structured tool call from a forced-text response");
742 - continue;
743 - }
743 let index = delta.index.unwrap_or(0) as usize;
744 if tool_accum.len() <= index {
745 tool_accum.resize_with(index + 1, StreamingToolCall::default);
@@ -787,6 +786,19 @@ impl OpenAiBackend {
786 .collect();
787 tool_calls.extend(recovered);
788
789 + if !allow_tool_calls && !tool_calls.is_empty() {
790 + let names = tool_calls
791 + .iter()
792 + .map(|call| call.name.as_str())
793 + .collect::<Vec<_>>()
794 + .join(", ");
795 + log::warn!(
796 + "suppressed {} tool call(s) from a forced-text response: {names}",
797 + tool_calls.len()
798 + );
799 + tool_calls.clear();
800 + }
801 +
802 // Record the assistant turn so later tool results have context.
803 self.history
804 .lock()