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");
|
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())
|
||||||
|
|||||||
Reference in New Issue
Block a user