fix: strip functionCall/functionResponse from history when no tools

When LS tools are stripped from the request but the conversation history
still contains functionCall/functionResponse parts referencing those
tools, Google returns MALFORMED_FUNCTION_CALL and the LS retries in an
infinite loop, causing the request to hang forever.

Now after stripping LS tools and confirming no custom tools are injected,
we also strip all functionCall/functionResponse parts from the history
and remove any messages that become empty as a result.
This commit is contained in:
Nikketryhard
2026-02-14 23:19:28 -06:00
parent 7e16a7b892
commit a52d1bf475

View File

@@ -156,6 +156,7 @@ pub fn modify_request(body: &[u8], tool_ctx: Option<&ToolContext>) -> Option<Vec
}
// ── 3. Strip LS tools, inject client tools ─────────────────────────────
let mut has_custom_tools = false;
if STRIP_ALL_TOOLS {
if let Some(tools) = json
.pointer_mut("/request/tools")
@@ -173,12 +174,45 @@ pub fn modify_request(body: &[u8], tool_ctx: Option<&ToolContext>) -> Option<Vec
for tool in custom_tools {
tools.push(tool.clone());
}
has_custom_tools = true;
changes.push(format!("inject {} custom tool group(s)", custom_tools.len()));
}
}
}
}
// ── 3a. Strip functionCall/functionResponse from history when no tools ──
// When LS tools are stripped and no custom tools are injected, the model
// may try to reference LS tools from conversation history, causing Google
// to return MALFORMED_FUNCTION_CALL in an infinite retry loop.
if STRIP_ALL_TOOLS && !has_custom_tools {
if let Some(contents) = json
.pointer_mut("/request/contents")
.and_then(|v| v.as_array_mut())
{
let mut stripped_fc = 0usize;
for msg in contents.iter_mut() {
if let Some(parts) = msg.get_mut("parts").and_then(|v| v.as_array_mut()) {
let before = parts.len();
parts.retain(|part| {
!part.get("functionCall").is_some()
&& !part.get("functionResponse").is_some()
});
stripped_fc += before - parts.len();
}
}
// Remove messages that became empty after stripping function parts
contents.retain(|msg| {
msg.get("parts")
.and_then(|v| v.as_array())
.map_or(true, |parts| !parts.is_empty())
});
if stripped_fc > 0 {
changes.push(format!("strip {stripped_fc} functionCall/Response parts from history"));
}
}
}
// Inject toolConfig if provided
if let Some(ref ctx) = tool_ctx {
if let Some(ref config) = ctx.tool_config {