fix: forge dummy STOP response to LS on functionCall capture
When the MITM detects a functionCall in Google's response AND custom tools are active, send a forged clean text response to the LS instead of the real one. This prevents the LS from seeing function calls for tools it doesn't manage, eliminating the retry loop entirely. The real function call data is captured in MitmStore and returned to the client (OpenCode) through the completions handler. Also removes the complex chunked-encoding response rewriting approach in favor of this simpler forge-and-break strategy.
This commit is contained in:
@@ -735,15 +735,54 @@ async fn handle_http_over_tls(
|
||||
// Save body for usage parsing
|
||||
response_body_buf.extend_from_slice(&header_buf[hdr_end..]);
|
||||
|
||||
// Forward to client immediately
|
||||
if let Err(e) = client.write_all(&header_buf).await {
|
||||
warn!(error = %e, "MITM: write to client failed");
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse ORIGINAL initial body for MITM interception
|
||||
let mut has_function_call = false;
|
||||
if is_streaming_response && hdr_end < header_buf.len() {
|
||||
let body = String::from_utf8_lossy(&header_buf[hdr_end..]);
|
||||
parse_streaming_chunk(&body, &mut streaming_acc);
|
||||
has_function_call = body.contains("functionCall");
|
||||
}
|
||||
|
||||
// If we detected a functionCall AND custom tools are active,
|
||||
// forge a dummy "STOP" response for the LS so it doesn't
|
||||
// freak out and retry. The real function call data is already
|
||||
// captured in MitmStore.
|
||||
if has_function_call && modify_requests && store.get_tools().await.is_some() {
|
||||
info!("MITM: functionCall detected → sending dummy STOP response to LS");
|
||||
|
||||
// Build a clean SSE response the LS will accept
|
||||
let dummy_json = serde_json::json!({
|
||||
"response": {
|
||||
"candidates": [{
|
||||
"content": {
|
||||
"role": "model",
|
||||
"parts": [{"text": "Tool call completed. Awaiting external tool result."}]
|
||||
},
|
||||
"finishReason": "STOP"
|
||||
}],
|
||||
"modelVersion": "gemini-3-flash"
|
||||
},
|
||||
"metadata": {}
|
||||
});
|
||||
let dummy_data = format!("data: {}\r\n\r\n", serde_json::to_string(&dummy_json).unwrap());
|
||||
let dummy_chunk = format!("{:x}\r\n{}\r\n0\r\n\r\n", dummy_data.len(), dummy_data);
|
||||
|
||||
// Send headers (from original response) + dummy body
|
||||
let headers_only = &header_buf[..hdr_end];
|
||||
if let Err(e) = client.write_all(headers_only).await {
|
||||
warn!(error = %e, "MITM: write headers failed");
|
||||
}
|
||||
if let Err(e) = client.write_all(dummy_chunk.as_bytes()).await {
|
||||
warn!(error = %e, "MITM: write dummy body failed");
|
||||
}
|
||||
// Done — don't forward the real response
|
||||
break;
|
||||
}
|
||||
|
||||
// Normal path: forward headers+body as-is
|
||||
if let Err(e) = client.write_all(&header_buf).await {
|
||||
warn!(error = %e, "MITM: write to client failed");
|
||||
break;
|
||||
}
|
||||
|
||||
if let Some(cl) = response_content_length {
|
||||
@@ -759,17 +798,30 @@ async fn handle_http_over_tls(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Forward to client immediately
|
||||
// ── Response body interception ────────────────────────────────
|
||||
// Parse ORIGINAL chunk for MITM interception (captures functionCalls)
|
||||
let mut chunk_has_fc = false;
|
||||
if is_streaming_response {
|
||||
let s = String::from_utf8_lossy(chunk);
|
||||
parse_streaming_chunk(&s, &mut streaming_acc);
|
||||
chunk_has_fc = s.contains("functionCall");
|
||||
}
|
||||
|
||||
// If functionCall in body chunk + custom tools → send dummy + stop
|
||||
if chunk_has_fc && modify_requests && store.get_tools().await.is_some() {
|
||||
info!("MITM: functionCall in body chunk → sending chunked terminator to LS");
|
||||
// Send the chunked terminator to end the stream
|
||||
let _ = client.write_all(b"0\r\n\r\n").await;
|
||||
break;
|
||||
}
|
||||
|
||||
// Normal path: forward chunk to client (LS)
|
||||
if let Err(e) = client.write_all(chunk).await {
|
||||
warn!(error = %e, "MITM: write to client failed");
|
||||
break;
|
||||
}
|
||||
response_body_buf.extend_from_slice(chunk);
|
||||
|
||||
if is_streaming_response {
|
||||
let s = String::from_utf8_lossy(chunk);
|
||||
parse_streaming_chunk(&s, &mut streaming_acc);
|
||||
}
|
||||
if let Some(cl) = response_content_length {
|
||||
if response_body_buf.len() >= cl { break; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user