fix: tool call race conditions and missing completions tool result extraction

- store.rs: record_function_call now falls back to active_cascade_id
  (matching record_usage behavior) instead of blind _latest fallback
- store.rs: add cascade-aware take_function_calls(cascade_id) method
  with priority: exact match → active cascade → _latest → any key
- completions.rs: extract tool_calls from assistant messages and tool
  results from tool messages, storing them for MITM injection. This was
  the ROOT CAUSE — the completions handler stored tool definitions but
  never extracted tool results, so modify_request couldn't rewrite the
  LS conversation history with proper functionCall/functionResponse
- responses.rs: use cascade-aware take_function_calls for consistency
This commit is contained in:
Nikketryhard
2026-02-16 18:43:16 -06:00
parent 38b4130c55
commit 6bda2ecafa
3 changed files with 153 additions and 7 deletions

View File

@@ -16,6 +16,7 @@ use super::polling::{
use super::types::*;
use super::util::{err_response, now_unix, upstream_err_response};
use super::AppState;
use crate::mitm::store::{CapturedFunctionCall, PendingToolResult};
/// Extract a conversation/session ID from a flexible JSON value.
/// Accepts a plain string or an object with an "id" field.
@@ -224,6 +225,100 @@ pub(crate) async fn handle_completions(
}
state.mitm_store.clear_active_function_call();
// ── Extract tool results from messages for MITM injection ──────────
// When OpenCode sends back tool results, the messages array contains:
// 1. assistant message with tool_calls (the model's previous function calls)
// 2. tool messages with results (the executed tool outputs)
// We need to store these so modify_request can rewrite the LS's
// conversation history with proper functionCall/functionResponse parts
// instead of the placeholder "Tool call completed" text.
{
let mut last_calls: Vec<CapturedFunctionCall> = Vec::new();
let mut pending_results: Vec<PendingToolResult> = Vec::new();
for msg in &body.messages {
match msg.role.as_str() {
"assistant" => {
// Extract function calls from assistant's tool_calls
if let Some(ref tool_calls) = msg.tool_calls {
for tc in tool_calls {
if let Some(func) = tc.get("function") {
let name = func["name"].as_str().unwrap_or("unknown").to_string();
let args_str = func["arguments"].as_str().unwrap_or("{}");
let args = serde_json::from_str::<serde_json::Value>(args_str)
.unwrap_or(serde_json::json!({}));
let call_id = tc["id"].as_str().unwrap_or("").to_string();
// Register call_id → name for lookup
if !call_id.is_empty() {
state.mitm_store.register_call_id(call_id, name.clone()).await;
}
last_calls.push(CapturedFunctionCall {
name,
args,
captured_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
});
}
}
}
}
"tool" => {
// Extract tool results
let text = extract_message_text(&msg.content);
if let Some(ref call_id) = msg.tool_call_id {
// Look up function name from call_id
let name = state
.mitm_store
.lookup_call_id(call_id)
.await
.unwrap_or_else(|| {
// Fallback: try to find the name from last_calls by position
last_calls
.first()
.map(|fc| fc.name.clone())
.unwrap_or_else(|| "unknown_function".to_string())
});
let result_value = serde_json::from_str::<serde_json::Value>(&text)
.unwrap_or_else(|_| serde_json::json!({"result": text}));
pending_results.push(PendingToolResult {
name,
result: result_value,
});
}
}
_ => {}
}
}
if !last_calls.is_empty() {
info!(
count = last_calls.len(),
tools = ?last_calls.iter().map(|c| &c.name).collect::<Vec<_>>(),
"Completions: stored last function calls for MITM history rewrite"
);
state.mitm_store.set_last_function_calls(last_calls).await;
}
if !pending_results.is_empty() {
info!(
count = pending_results.len(),
tools = ?pending_results.iter().map(|r| &r.name).collect::<Vec<_>>(),
"Completions: stored tool results for MITM injection"
);
for result in pending_results {
state.mitm_store.add_tool_result(result).await;
}
// Clear awaiting flag — we have the results now
state.mitm_store.clear_awaiting_tool_result();
}
}
// Store generation parameters for MITM injection
{
use crate::mitm::store::GenerationParams;
@@ -584,7 +679,7 @@ async fn chat_completions_stream(
// ── Check for MITM-captured function calls FIRST ──
// This runs independently of LS steps — the MITM captures tool calls
// at the proxy layer, so we don't need to wait for LS processing.
let captured = state.mitm_store.take_any_function_calls().await;
let captured = state.mitm_store.take_function_calls(&cascade_id).await;
if let Some(ref calls) = captured {
if !calls.is_empty() {
let mut tool_calls = Vec::new();