fix: block ALL LS follow-up requests, deduplicate function calls
- Add request_in_flight flag to MitmStore, set immediately when first LLM request is forwarded with custom tools active - Block ALL subsequent LS requests (agentic loop + internal flash-lite) with fake SSE responses instead of waiting for response_complete - Fix function call deduplication: drain() accumulator after storing to prevent 3x duplicate tool calls across SSE chunks - Clear all stale state (response, thinking, function calls, errors) at the start of each streaming request - Handle response_complete with no content (thoughtSignature-only) gracefully with timeout instead of infinite hang
This commit is contained in:
@@ -202,11 +202,35 @@ pub fn modify_request(body: &[u8], tool_ctx: Option<&ToolContext>) -> Option<Vec
|
||||
// Inject client-provided tools from ToolContext
|
||||
if let Some(ref ctx) = tool_ctx {
|
||||
if let Some(ref custom_tools) = ctx.tools {
|
||||
let total_decls: usize = custom_tools.iter()
|
||||
.filter_map(|t| t.get("functionDeclarations").and_then(|d| d.as_array()))
|
||||
.map(|a| a.len())
|
||||
.sum();
|
||||
for tool in custom_tools {
|
||||
tools.push(tool.clone());
|
||||
}
|
||||
has_custom_tools = true;
|
||||
changes.push(format!("inject {} custom tool group(s)", custom_tools.len()));
|
||||
|
||||
// Override LS's VALIDATED toolConfig → AUTO for custom tools.
|
||||
// VALIDATED mode forces Google to validate function calls against a
|
||||
// specific tool list that the LS controls. Our custom tools aren't in
|
||||
// that list, so they'd be rejected. AUTO lets the model freely choose
|
||||
// between text and function calls.
|
||||
if let Some(req) = json.get_mut("request").and_then(|v| v.as_object_mut()) {
|
||||
let has_validated = req.get("toolConfig")
|
||||
.and_then(|tc| tc.pointer("/functionCallingConfig/mode"))
|
||||
.and_then(|m| m.as_str())
|
||||
.map_or(false, |m| m == "VALIDATED");
|
||||
if has_validated {
|
||||
req.insert("toolConfig".to_string(), serde_json::json!({
|
||||
"functionCallingConfig": {
|
||||
"mode": "AUTO"
|
||||
}
|
||||
}));
|
||||
changes.push("override toolConfig VALIDATED → AUTO".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -230,11 +254,26 @@ pub fn modify_request(body: &[u8], tool_ctx: Option<&ToolContext>) -> Option<Vec
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3b. ALWAYS strip old functionCall/functionResponse from history ───
|
||||
// Even when custom tools are injected, the LS history contains function
|
||||
// call parts for LS-internal tools we stripped. Google rejects these as
|
||||
// MALFORMED_FUNCTION_CALL because the referenced tools don't exist.
|
||||
// ── 3b. Strip old functionCall/functionResponse from history ────────
|
||||
// The LS history contains function call parts for LS-internal tools
|
||||
// we stripped. Google rejects these as MALFORMED_FUNCTION_CALL because
|
||||
// the referenced tools don't exist. However, when custom tools are
|
||||
// injected, we must PRESERVE function calls for those tools so the
|
||||
// model retains its tool call history and doesn't re-execute them.
|
||||
if STRIP_ALL_TOOLS {
|
||||
// Build set of custom tool names to preserve
|
||||
let custom_tool_names: std::collections::HashSet<String> = tool_ctx
|
||||
.as_ref()
|
||||
.and_then(|ctx| ctx.tools.as_ref())
|
||||
.map(|tools| {
|
||||
tools.iter()
|
||||
.filter_map(|t| t["functionDeclarations"].as_array())
|
||||
.flatten()
|
||||
.filter_map(|decl| decl["name"].as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(contents) = json
|
||||
.pointer_mut("/request/contents")
|
||||
.and_then(|v| v.as_array_mut())
|
||||
@@ -244,8 +283,21 @@ pub fn modify_request(body: &[u8], tool_ctx: Option<&ToolContext>) -> Option<Vec
|
||||
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()
|
||||
// Check functionCall — keep if it's for a custom tool
|
||||
if let Some(fc) = part.get("functionCall") {
|
||||
if let Some(name) = fc.get("name").and_then(|v| v.as_str()) {
|
||||
return custom_tool_names.contains(name);
|
||||
}
|
||||
return false; // No name → strip
|
||||
}
|
||||
// Check functionResponse — keep if it's for a custom tool
|
||||
if let Some(fr) = part.get("functionResponse") {
|
||||
if let Some(name) = fr.get("name").and_then(|v| v.as_str()) {
|
||||
return custom_tool_names.contains(name);
|
||||
}
|
||||
return false; // No name → strip
|
||||
}
|
||||
true // Not a function part → keep
|
||||
});
|
||||
stripped_fc += before - parts.len();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user