feat: full tool call support (OpenAI + Gemini endpoints)

- store.rs: Add tool context storage (active tools, tool config, pending
  tool results, call_id mapping, last function calls for history rewrite)
- types.rs: Add tools/tool_choice fields to ResponsesRequest, add
  build_function_call_output helper for OpenAI function_call output items
- modify.rs: Replace hardcoded get_weather with dynamic ToolContext
  injection. Add openai_tools_to_gemini and openai_tool_choice_to_gemini
  converters. Add conversation history rewriting for tool result turns
  (replaces fake 'Tool call completed' model turn with real functionCall,
  injects functionResponse before last user turn)
- proxy.rs: Build ToolContext from MitmStore before calling modify_request.
  Save last_function_calls for history rewriting on subsequent turns
- responses.rs: Store client tools in MitmStore before LS call. Detect
  function_call_output in input array for tool result submission. Return
  captured functionCalls as OpenAI function_call output items with
  generated call_ids and stringified arguments
- gemini.rs: New Gemini-native endpoint (POST /v1/gemini) with zero
  format translation. Accepts functionDeclarations directly, returns
  functionCall in Gemini format directly
- mod.rs: Wire /v1/gemini route, bump version to 3.3.0
This commit is contained in:
Nikketryhard
2026-02-14 22:56:44 -06:00
parent 8455aa674f
commit 786987116b
8 changed files with 989 additions and 51 deletions

View File

@@ -53,6 +53,13 @@ pub struct CapturedFunctionCall {
pub captured_at: u64,
}
/// A pending tool result from a client's function_call_output.
#[derive(Debug, Clone)]
pub struct PendingToolResult {
pub name: String,
pub result: serde_json::Value,
}
/// Thread-safe store for intercepted data.
///
/// Keyed by a unique request ID that we can correlate with cascade operations.
@@ -69,6 +76,18 @@ pub struct MitmStore {
/// Simple flag: set when a functionCall is captured, cleared when consumed.
/// Used to block follow-up requests regardless of cascade identification.
has_active_function_call: Arc<AtomicBool>,
// ── Tool call support ────────────────────────────────────────────────
/// Active tool definitions (Gemini format) for MITM injection.
active_tools: Arc<RwLock<Option<Vec<serde_json::Value>>>>,
/// Active tool config (Gemini toolConfig format).
active_tool_config: Arc<RwLock<Option<serde_json::Value>>>,
/// Pending tool results for MITM to inject as functionResponse.
pending_tool_results: Arc<RwLock<Vec<PendingToolResult>>>,
/// Mapping call_id → function name for tool result routing.
call_id_to_name: Arc<RwLock<HashMap<String, String>>>,
/// Last captured function calls (for conversation history rewriting).
last_function_calls: Arc<RwLock<Vec<CapturedFunctionCall>>>,
}
/// Aggregate statistics across all intercepted traffic.
@@ -102,6 +121,11 @@ impl MitmStore {
stats: Arc::new(RwLock::new(MitmStats::default())),
pending_function_calls: Arc::new(RwLock::new(HashMap::new())),
has_active_function_call: Arc::new(AtomicBool::new(false)),
active_tools: Arc::new(RwLock::new(None)),
active_tool_config: Arc::new(RwLock::new(None)),
pending_tool_results: Arc::new(RwLock::new(Vec::new())),
call_id_to_name: Arc::new(RwLock::new(HashMap::new())),
last_function_calls: Arc::new(RwLock::new(Vec::new())),
}
}
@@ -266,4 +290,63 @@ impl MitmStore {
}
None
}
// ── Tool context methods ─────────────────────────────────────────────
/// Set active tool definitions (already in Gemini format).
pub async fn set_tools(&self, tools: Vec<serde_json::Value>) {
*self.active_tools.write().await = Some(tools);
}
/// Get active tool definitions.
pub async fn get_tools(&self) -> Option<Vec<serde_json::Value>> {
self.active_tools.read().await.clone()
}
/// Clear active tool definitions.
pub async fn clear_tools(&self) {
*self.active_tools.write().await = None;
*self.active_tool_config.write().await = None;
}
/// Set active tool config (Gemini toolConfig format).
pub async fn set_tool_config(&self, config: serde_json::Value) {
*self.active_tool_config.write().await = Some(config);
}
/// Get active tool config.
pub async fn get_tool_config(&self) -> Option<serde_json::Value> {
self.active_tool_config.read().await.clone()
}
/// Add a pending tool result for MITM injection.
pub async fn add_tool_result(&self, result: PendingToolResult) {
info!(name = %result.name, "Storing pending tool result");
self.pending_tool_results.write().await.push(result);
}
/// Take (consume) all pending tool results.
pub async fn take_tool_results(&self) -> Vec<PendingToolResult> {
std::mem::take(&mut *self.pending_tool_results.write().await)
}
/// Register a call_id → function name mapping.
pub async fn register_call_id(&self, call_id: String, name: String) {
self.call_id_to_name.write().await.insert(call_id, name);
}
/// Look up function name by call_id.
pub async fn lookup_call_id(&self, call_id: &str) -> Option<String> {
self.call_id_to_name.read().await.get(call_id).cloned()
}
/// Save the last captured function calls (for history rewriting).
pub async fn set_last_function_calls(&self, calls: Vec<CapturedFunctionCall>) {
*self.last_function_calls.write().await = calls;
}
/// Get the last captured function calls.
pub async fn get_last_function_calls(&self) -> Vec<CapturedFunctionCall> {
self.last_function_calls.read().await.clone()
}
}