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:
@@ -48,7 +48,16 @@ pub fn modify_request(body: &[u8], tool_ctx: Option<&ToolContext>) -> Option<Vec
|
||||
let identity = extract_xml_section(&sys, "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();
|
||||
if stripped > 0 {
|
||||
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 ──
|
||||
// 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.
|
||||
// ── 3a. When no tools remain, clean up all tool-related config ────────
|
||||
// The LS sets toolConfig.functionCallingConfig.mode = "VALIDATED" which
|
||||
// forces Google to attempt function calls even with an empty tools array,
|
||||
// causing MALFORMED_FUNCTION_CALL in an infinite retry loop.
|
||||
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
|
||||
.pointer_mut("/request/contents")
|
||||
.and_then(|v| v.as_array_mut())
|
||||
|
||||
Reference in New Issue
Block a user