refactor: decompose large functions and remove dead code
- Decompose modify_request() into 7 single-responsibility helpers - Decompose handle_http_over_tls(): extract read_full_request, dispatch_stream_events - Promote connect_upstream/resolve_upstream to module-level functions - Split standalone.rs (1238 lines) into 4 submodules: standalone/mod.rs, spawn.rs, discovery.rs, stub.rs - Extract proto wire primitives into proto/wire.rs - Remove 6 dead MitmStore methods - Remove dead SessionResult, DEFAULT_SESSION, get_or_create - Remove dead decode_varint_at, extract_conversation_id - Clean all unused imports across 10 files - Suppress structural dead_code warnings on deserialization fields Warnings: 20 -> 0. All 43 tests pass.
This commit is contained in:
@@ -18,15 +18,9 @@ use super::util::{err_response, now_unix, upstream_err_response};
|
||||
use super::AppState;
|
||||
use crate::mitm::store::{CapturedFunctionCall, PendingToolResult, ToolRound};
|
||||
|
||||
/// Extract a conversation/session ID from a flexible JSON value.
|
||||
/// Accepts a plain string or an object with an "id" field.
|
||||
fn extract_conversation_id(conv: &Option<serde_json::Value>) -> Option<String> {
|
||||
match conv {
|
||||
Some(serde_json::Value::String(s)) => Some(s.clone()),
|
||||
Some(obj) => obj["id"].as_str().map(|s| s.to_string()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/// System fingerprint for completions responses (derived from crate version at compile time).
|
||||
fn system_fingerprint() -> String {
|
||||
@@ -187,10 +181,7 @@ pub(crate) async fn handle_completions(
|
||||
model_name, body.stream
|
||||
);
|
||||
|
||||
// Diagnostic: dump OpenCode's raw request
|
||||
if let Ok(pretty) = serde_json::to_string_pretty(&body) {
|
||||
let _ = std::fs::write("/tmp/opencode-request.json", &pretty);
|
||||
}
|
||||
|
||||
|
||||
let model = match lookup_model(model_name) {
|
||||
Some(m) => m,
|
||||
@@ -204,35 +195,28 @@ pub(crate) async fn handle_completions(
|
||||
}
|
||||
};
|
||||
|
||||
// Store client tools from this request (or clear stale ones from other endpoints)
|
||||
if let Some(ref tools) = body.tools {
|
||||
let gemini_tools = crate::mitm::modify::openai_tools_to_gemini(tools);
|
||||
if !gemini_tools.is_empty() {
|
||||
state.mitm_store.set_tools(gemini_tools).await;
|
||||
if let Some(ref choice) = body.tool_choice {
|
||||
let gemini_config = crate::mitm::modify::openai_tool_choice_to_gemini(choice);
|
||||
state.mitm_store.set_tool_config(gemini_config).await;
|
||||
}
|
||||
info!(
|
||||
count = tools.len(),
|
||||
"Completions: stored client tools for MITM injection"
|
||||
);
|
||||
} else {
|
||||
state.mitm_store.clear_tools().await;
|
||||
// ── Build per-request state locally ──────────────────────────────────
|
||||
|
||||
// Convert OpenAI tools to Gemini format
|
||||
let tools = body.tools.as_ref().and_then(|t| {
|
||||
let gemini_tools = crate::mitm::modify::openai_tools_to_gemini(t);
|
||||
if gemini_tools.is_empty() { None } else {
|
||||
info!(count = t.len(), "Completions: client tools for MITM injection");
|
||||
Some(gemini_tools)
|
||||
}
|
||||
} else {
|
||||
state.mitm_store.clear_tools().await;
|
||||
}
|
||||
});
|
||||
let tool_config = body.tools.as_ref().and_then(|_| {
|
||||
body.tool_choice.as_ref().map(|choice| {
|
||||
crate::mitm::modify::openai_tool_choice_to_gemini(choice)
|
||||
})
|
||||
});
|
||||
|
||||
// ── 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 build ToolRounds: each round pairs one assistant's tool_calls with
|
||||
// the subsequent tool result messages. This enables correct per-turn
|
||||
// history rewriting for multi-step tool use.
|
||||
// Build ToolRounds from message history: each round pairs assistant tool_calls
|
||||
// with subsequent tool result messages. Local call_id_to_name mapping.
|
||||
let mut tool_rounds: Vec<ToolRound> = Vec::new();
|
||||
let mut call_id_to_name: std::collections::HashMap<String, String> = std::collections::HashMap::new();
|
||||
{
|
||||
let mut rounds: Vec<ToolRound> = Vec::new();
|
||||
let mut current_round: Option<ToolRound> = None;
|
||||
|
||||
for msg in &body.messages {
|
||||
@@ -241,7 +225,7 @@ pub(crate) async fn handle_completions(
|
||||
// Finalize any open round
|
||||
if let Some(round) = current_round.take() {
|
||||
if !round.calls.is_empty() {
|
||||
rounds.push(round);
|
||||
tool_rounds.push(round);
|
||||
}
|
||||
}
|
||||
// Start new round if this assistant has tool_calls
|
||||
@@ -255,14 +239,15 @@ pub(crate) async fn handle_completions(
|
||||
.unwrap_or(serde_json::json!({}));
|
||||
let call_id = tc["id"].as_str().unwrap_or("").to_string();
|
||||
|
||||
// Register call_id → name for lookup
|
||||
// Register call_id → name locally
|
||||
if !call_id.is_empty() {
|
||||
state.mitm_store.register_call_id(call_id, name.clone()).await;
|
||||
call_id_to_name.insert(call_id, name.clone());
|
||||
}
|
||||
|
||||
calls.push(CapturedFunctionCall {
|
||||
name,
|
||||
args,
|
||||
thought_signature: None,
|
||||
captured_at: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
@@ -281,16 +266,13 @@ pub(crate) async fn handle_completions(
|
||||
"tool" => {
|
||||
let text = extract_message_text(&msg.content);
|
||||
if let Some(ref call_id) = msg.tool_call_id {
|
||||
// Look up function name from call_id, fall back to
|
||||
// positional index within the current round's calls
|
||||
let result_index = current_round
|
||||
.as_ref()
|
||||
.map(|r| r.results.len())
|
||||
.unwrap_or(0);
|
||||
let name = state
|
||||
.mitm_store
|
||||
.lookup_call_id(call_id)
|
||||
.await
|
||||
let name = call_id_to_name
|
||||
.get(call_id.as_str())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
current_round
|
||||
.as_ref()
|
||||
@@ -314,7 +296,7 @@ pub(crate) async fn handle_completions(
|
||||
// Any other role (user, system) finalizes the current round
|
||||
if let Some(round) = current_round.take() {
|
||||
if !round.calls.is_empty() {
|
||||
rounds.push(round);
|
||||
tool_rounds.push(round);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,69 +305,86 @@ pub(crate) async fn handle_completions(
|
||||
// Finalize last round
|
||||
if let Some(round) = current_round.take() {
|
||||
if !round.calls.is_empty() {
|
||||
rounds.push(round);
|
||||
tool_rounds.push(round);
|
||||
}
|
||||
}
|
||||
|
||||
if !rounds.is_empty() {
|
||||
if !tool_rounds.is_empty() {
|
||||
info!(
|
||||
round_count = rounds.len(),
|
||||
calls = ?rounds.iter().map(|r| r.calls.iter().map(|c| &c.name).collect::<Vec<_>>()).collect::<Vec<_>>(),
|
||||
"Completions: stored {} tool round(s) for MITM history rewrite",
|
||||
rounds.len(),
|
||||
round_count = tool_rounds.len(),
|
||||
calls = ?tool_rounds.iter().map(|r| r.calls.iter().map(|c| &c.name).collect::<Vec<_>>()).collect::<Vec<_>>(),
|
||||
"Completions: {} tool round(s) for MITM history rewrite",
|
||||
tool_rounds.len(),
|
||||
);
|
||||
// Also set last_function_calls from the latest round for proxy.rs recording compat
|
||||
if let Some(last_round) = rounds.last() {
|
||||
state.mitm_store.set_last_function_calls(last_round.calls.clone()).await;
|
||||
|
||||
// Merge thought_signatures from MITM-captured function calls.
|
||||
// OpenAI format doesn't carry thought signatures, but Google requires
|
||||
// them when injecting functionCall parts back into history.
|
||||
let sigs = state.mitm_store.peek_thought_signatures().await;
|
||||
if !sigs.is_empty() {
|
||||
let mut merged = 0usize;
|
||||
for round in &mut tool_rounds {
|
||||
for fc in &mut round.calls {
|
||||
if fc.thought_signature.is_none() {
|
||||
if let Some(sig) = sigs.get(&fc.name) {
|
||||
fc.thought_signature = Some(sig.clone());
|
||||
merged += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if merged > 0 {
|
||||
info!(
|
||||
merged_count = merged,
|
||||
"Completions: merged {} thought_signature(s) from MITM capture",
|
||||
merged,
|
||||
);
|
||||
}
|
||||
}
|
||||
state.mitm_store.set_tool_rounds(rounds).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Store generation parameters for MITM injection
|
||||
// Build generation parameters locally
|
||||
use crate::mitm::store::GenerationParams;
|
||||
let (response_mime_type, response_schema) = match body.response_format.as_ref() {
|
||||
Some(rf) => match rf.format_type.as_str() {
|
||||
"json_object" | "json" => (Some("application/json".to_string()), None),
|
||||
"json_schema" => {
|
||||
let schema = rf.json_schema.as_ref().and_then(|js| js.schema.clone());
|
||||
(Some("application/json".to_string()), schema)
|
||||
}
|
||||
_ => (None, None),
|
||||
},
|
||||
None => (None, None),
|
||||
};
|
||||
let gp = GenerationParams {
|
||||
temperature: body.temperature,
|
||||
top_p: body.top_p,
|
||||
top_k: None,
|
||||
max_output_tokens: body.max_tokens.or(body.max_completion_tokens),
|
||||
stop_sequences: body.stop.clone().map(|s| s.into_vec()),
|
||||
frequency_penalty: body.frequency_penalty,
|
||||
presence_penalty: body.presence_penalty,
|
||||
reasoning_effort: body.reasoning_effort.clone(),
|
||||
response_mime_type,
|
||||
response_schema,
|
||||
google_search: body.web_search,
|
||||
};
|
||||
let generation_params = if gp.temperature.is_some()
|
||||
|| gp.top_p.is_some()
|
||||
|| gp.max_output_tokens.is_some()
|
||||
|| gp.frequency_penalty.is_some()
|
||||
|| gp.presence_penalty.is_some()
|
||||
|| gp.reasoning_effort.is_some()
|
||||
|| gp.stop_sequences.is_some()
|
||||
|| gp.response_mime_type.is_some()
|
||||
|| gp.response_schema.is_some()
|
||||
|| gp.google_search
|
||||
{
|
||||
use crate::mitm::store::GenerationParams;
|
||||
let (response_mime_type, response_schema) = match body.response_format.as_ref() {
|
||||
Some(rf) => match rf.format_type.as_str() {
|
||||
"json_object" | "json" => (Some("application/json".to_string()), None),
|
||||
"json_schema" => {
|
||||
let schema = rf.json_schema.as_ref().and_then(|js| js.schema.clone());
|
||||
(Some("application/json".to_string()), schema)
|
||||
}
|
||||
_ => (None, None),
|
||||
},
|
||||
None => (None, None),
|
||||
};
|
||||
let gp = GenerationParams {
|
||||
temperature: body.temperature,
|
||||
top_p: body.top_p,
|
||||
top_k: None, // OpenAI doesn't have top_k
|
||||
max_output_tokens: body.max_tokens.or(body.max_completion_tokens),
|
||||
stop_sequences: body.stop.clone().map(|s| s.into_vec()),
|
||||
frequency_penalty: body.frequency_penalty,
|
||||
presence_penalty: body.presence_penalty,
|
||||
reasoning_effort: body.reasoning_effort.clone(),
|
||||
response_mime_type,
|
||||
response_schema,
|
||||
google_search: body.web_search,
|
||||
};
|
||||
// Only store if at least one param is set
|
||||
if gp.temperature.is_some()
|
||||
|| gp.top_p.is_some()
|
||||
|| gp.max_output_tokens.is_some()
|
||||
|| gp.frequency_penalty.is_some()
|
||||
|| gp.presence_penalty.is_some()
|
||||
|| gp.reasoning_effort.is_some()
|
||||
|| gp.stop_sequences.is_some()
|
||||
|| gp.response_mime_type.is_some()
|
||||
|| gp.response_schema.is_some()
|
||||
|| gp.google_search
|
||||
{
|
||||
state.mitm_store.set_generation_params(gp).await;
|
||||
} else {
|
||||
state.mitm_store.clear_generation_params().await;
|
||||
}
|
||||
}
|
||||
Some(gp)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let token = state.backend.oauth_token().await;
|
||||
if token.is_empty() {
|
||||
@@ -410,23 +409,8 @@ pub(crate) async fn handle_completions(
|
||||
warn!("n={n} requested with streaming — streaming only supports n=1, ignoring n");
|
||||
}
|
||||
|
||||
// Session/conversation: reuse cascade if conversation ID provided
|
||||
let session_id_str = extract_conversation_id(&body.conversation);
|
||||
|
||||
// Helper to create a cascade (reuses session or creates fresh)
|
||||
let create_cascade = |state: Arc<AppState>, session_id: Option<String>| async move {
|
||||
if let Some(ref sid) = session_id {
|
||||
state
|
||||
.sessions
|
||||
.get_or_create(Some(sid), || state.backend.create_cascade())
|
||||
.await
|
||||
.map(|sr| sr.cascade_id)
|
||||
} else {
|
||||
state.backend.create_cascade().await
|
||||
}
|
||||
};
|
||||
|
||||
let cascade_id = match create_cascade(Arc::clone(&state), session_id_str.clone()).await {
|
||||
// Always create a new cascade for every request
|
||||
let cascade_id = match state.backend.create_cascade().await {
|
||||
Ok(cid) => cid,
|
||||
Err(e) => {
|
||||
return err_response(
|
||||
@@ -437,40 +421,54 @@ pub(crate) async fn handle_completions(
|
||||
}
|
||||
};
|
||||
|
||||
// Send message on primary cascade
|
||||
state.mitm_store.set_active_cascade(&cascade_id).await;
|
||||
// Store real user text for MITM injection — LS gets a dummy prompt
|
||||
state.mitm_store.set_pending_user_text(user_text.clone()).await;
|
||||
// Store image for MITM injection (LS doesn't forward images to Google API)
|
||||
if let Some(ref img) = image {
|
||||
// Image for MITM injection
|
||||
let pending_image = image.as_ref().map(|img| {
|
||||
use base64::Engine;
|
||||
state
|
||||
.mitm_store
|
||||
.set_pending_image(crate::mitm::store::PendingImage {
|
||||
base64_data: base64::engine::general_purpose::STANDARD.encode(&img.data),
|
||||
mime_type: img.mime_type.clone(),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
crate::mitm::store::PendingImage {
|
||||
base64_data: base64::engine::general_purpose::STANDARD.encode(&img.data),
|
||||
mime_type: img.mime_type.clone(),
|
||||
}
|
||||
});
|
||||
|
||||
// Pre-flight: install channel BEFORE send_message so the MITM proxy
|
||||
// can grab it when the LS fires its API call.
|
||||
// Only for streaming — sync paths use poll_for_response (legacy store).
|
||||
let has_custom_tools = state.mitm_store.get_tools().await.is_some();
|
||||
let mitm_rx = if has_custom_tools && body.stream {
|
||||
state.mitm_store.clear_response_async().await;
|
||||
state.mitm_store.clear_upstream_error().await;
|
||||
let _ = state.mitm_store.take_any_function_calls().await;
|
||||
// Get last calls from the latest tool round (if any) for proxy recording compat
|
||||
let last_function_calls = tool_rounds.last()
|
||||
.map(|r| r.calls.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Build event channel for streaming
|
||||
let has_custom_tools = tools.is_some();
|
||||
let (mitm_rx, event_tx) = if has_custom_tools && body.stream {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
||||
state.mitm_store.set_channel(tx).await;
|
||||
Some(rx)
|
||||
(Some(rx), Some(tx))
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
// Build pending tool results from latest round
|
||||
let pending_tool_results = tool_rounds.last()
|
||||
.map(|r| r.results.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Register all per-request state atomically
|
||||
state.mitm_store.register_request(crate::mitm::store::RequestContext {
|
||||
cascade_id: cascade_id.clone(),
|
||||
pending_user_text: user_text.clone(),
|
||||
event_channel: event_tx,
|
||||
generation_params,
|
||||
pending_image,
|
||||
tools,
|
||||
tool_config,
|
||||
pending_tool_results,
|
||||
tool_rounds,
|
||||
last_function_calls,
|
||||
call_id_to_name,
|
||||
created_at: std::time::Instant::now(),
|
||||
}).await;
|
||||
|
||||
// Send REAL user text to LS
|
||||
match state
|
||||
.backend
|
||||
.send_message_with_image(&cascade_id, ".", model.model_enum, image.as_ref())
|
||||
.send_message_with_image(&cascade_id, &format!(".<cid:{}>", cascade_id), model.model_enum, image.as_ref())
|
||||
.await
|
||||
{
|
||||
Ok((200, _)) => {
|
||||
@@ -481,7 +479,7 @@ pub(crate) async fn handle_completions(
|
||||
});
|
||||
}
|
||||
Ok((status, _)) => {
|
||||
state.mitm_store.drop_channel().await;
|
||||
state.mitm_store.remove_request(&cascade_id).await;
|
||||
return err_response(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
format!("Backend returned {status}"),
|
||||
@@ -489,7 +487,7 @@ pub(crate) async fn handle_completions(
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
state.mitm_store.drop_channel().await;
|
||||
state.mitm_store.remove_request(&cascade_id).await;
|
||||
return err_response(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
format!("Send failed: {e}"),
|
||||
@@ -537,7 +535,7 @@ pub(crate) async fn handle_completions(
|
||||
// Send the same message on each extra cascade
|
||||
match state
|
||||
.backend
|
||||
.send_message_with_image(&cid, ".", model.model_enum, image.as_ref())
|
||||
.send_message_with_image(&cid, &format!(".<cid:{}>", cid), model.model_enum, image.as_ref())
|
||||
.await
|
||||
{
|
||||
Ok((200, _)) => {
|
||||
@@ -775,7 +773,7 @@ async fn chat_completions_stream(
|
||||
)));
|
||||
}
|
||||
yield Ok(Event::default().data("[DONE]"));
|
||||
state.mitm_store.drop_channel().await;
|
||||
state.mitm_store.remove_request(&cascade_id).await;
|
||||
return;
|
||||
}
|
||||
MitmEvent::ResponseComplete => {
|
||||
@@ -803,15 +801,15 @@ async fn chat_completions_stream(
|
||||
)));
|
||||
}
|
||||
yield Ok(Event::default().data("[DONE]"));
|
||||
state.mitm_store.drop_channel().await;
|
||||
state.mitm_store.remove_request(&cascade_id).await;
|
||||
return;
|
||||
} else if !acc_thinking.is_empty() && !did_unblock_ls {
|
||||
// Thinking-only response — LS needs follow-up API calls.
|
||||
// Create a new channel and unblock the gate.
|
||||
did_unblock_ls = true;
|
||||
let (new_tx, new_rx) = tokio::sync::mpsc::channel(64);
|
||||
state.mitm_store.set_channel(new_tx).await;
|
||||
state.mitm_store.clear_request_in_flight();
|
||||
state.mitm_store.set_channel(&cascade_id, new_tx).await;
|
||||
|
||||
let _ = state.mitm_store.take_any_function_calls().await;
|
||||
*rx = new_rx;
|
||||
debug!(
|
||||
@@ -845,7 +843,7 @@ async fn chat_completions_stream(
|
||||
)));
|
||||
}
|
||||
yield Ok(Event::default().data("[DONE]"));
|
||||
state.mitm_store.drop_channel().await;
|
||||
state.mitm_store.remove_request(&cascade_id).await;
|
||||
return;
|
||||
}
|
||||
// Don't break — wait for more channel events
|
||||
@@ -861,7 +859,7 @@ async fn chat_completions_stream(
|
||||
None,
|
||||
)));
|
||||
yield Ok(Event::default().data("[DONE]"));
|
||||
state.mitm_store.drop_channel().await;
|
||||
state.mitm_store.remove_request(&cascade_id).await;
|
||||
return;
|
||||
}
|
||||
continue 'channel_loop;
|
||||
@@ -878,7 +876,7 @@ async fn chat_completions_stream(
|
||||
}
|
||||
})).unwrap()));
|
||||
yield Ok(Event::default().data("[DONE]"));
|
||||
state.mitm_store.drop_channel().await;
|
||||
state.mitm_store.remove_request(&cascade_id).await;
|
||||
return;
|
||||
}
|
||||
MitmEvent::Usage(u) => {
|
||||
@@ -891,7 +889,7 @@ async fn chat_completions_stream(
|
||||
}
|
||||
|
||||
// Channel closed or timeout — clean up
|
||||
state.mitm_store.drop_channel().await;
|
||||
state.mitm_store.remove_request(&cascade_id).await;
|
||||
|
||||
// If we got here from timeout with content, emit what we have
|
||||
if !last_text.is_empty() || last_thinking_len > 0 {
|
||||
@@ -1026,7 +1024,7 @@ async fn chat_completions_stream(
|
||||
}
|
||||
})).unwrap()));
|
||||
// Always clear in-flight flag when stream ends
|
||||
state.mitm_store.drop_channel().await;
|
||||
state.mitm_store.remove_request(&cascade_id).await;
|
||||
yield Ok(Event::default().data("[DONE]"));
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user