fix: prevent MALFORMED_FUNCTION_CALL infinite retry loop

Root cause: after stripping LS tool definitions, two things remained:
1. toolConfig with mode=VALIDATED (forces function calling even with
   empty tools array)
2. Model's training/identity context causing it to attempt function
   calls in text

Fix:
- Remove empty tools array and toolConfig when no custom tools injected
- Strip functionCall/functionResponse parts from conversation history
- Append explicit 'no tools available' instruction to system prompt
- Remove debug dump code
This commit is contained in:
Nikketryhard
2026-02-14 23:31:26 -06:00
parent a52d1bf475
commit 19090b79f0

View File

@@ -48,7 +48,16 @@ pub fn modify_request(body: &[u8], tool_ctx: Option<&ToolContext>) -> Option<Vec
let identity = extract_xml_section(&sys, "identity"); let identity = extract_xml_section(&sys, "identity");
if let Some(identity_text) = identity { if let Some(identity_text) = identity {
let new_sys = format!("<identity>\n{}\n</identity>", identity_text.trim()); let mut new_sys = format!("<identity>\n{}\n</identity>", identity_text.trim());
// When no tools are available, explicitly tell the model not to attempt
// function calls. Without this, the model's training causes it to try
// calling tools from its identity context, resulting in MALFORMED_FUNCTION_CALL.
let has_tools = tool_ctx.as_ref().map_or(false, |ctx| ctx.tools.is_some());
if !has_tools {
new_sys.push_str("\n\nIMPORTANT: You have NO tools available. Do not attempt to call any functions or tools. Respond with text only.");
}
let stripped = original_len - new_sys.len(); let stripped = original_len - new_sys.len();
if stripped > 0 { if stripped > 0 {
changes.push(format!( changes.push(format!(
@@ -181,11 +190,24 @@ pub fn modify_request(body: &[u8], tool_ctx: Option<&ToolContext>) -> Option<Vec
} }
} }
// ── 3a. Strip functionCall/functionResponse from history when no tools ── // ── 3a. When no tools remain, clean up all tool-related config ────────
// When LS tools are stripped and no custom tools are injected, the model // The LS sets toolConfig.functionCallingConfig.mode = "VALIDATED" which
// may try to reference LS tools from conversation history, causing Google // forces Google to attempt function calls even with an empty tools array,
// to return MALFORMED_FUNCTION_CALL in an infinite retry loop. // causing MALFORMED_FUNCTION_CALL in an infinite retry loop.
if STRIP_ALL_TOOLS && !has_custom_tools { if STRIP_ALL_TOOLS && !has_custom_tools {
if let Some(req) = json.get_mut("request").and_then(|v| v.as_object_mut()) {
// Remove the empty tools array entirely
if req.get("tools").and_then(|v| v.as_array()).map_or(false, |a| a.is_empty()) {
req.remove("tools");
changes.push("remove empty tools array".to_string());
}
// Remove toolConfig (VALIDATED mode with no tools = MALFORMED_FUNCTION_CALL)
if req.remove("toolConfig").is_some() {
changes.push("remove toolConfig (no tools)".to_string());
}
}
// Also strip functionCall/functionResponse from conversation history
if let Some(contents) = json if let Some(contents) = json
.pointer_mut("/request/contents") .pointer_mut("/request/contents")
.and_then(|v| v.as_array_mut()) .and_then(|v| v.as_array_mut())