From f56ed7d005199567e8eabe9d8ce5918fb37bc29a Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 30 Jan 2026 19:03:41 -0700 Subject: [PATCH] feat: Added gemini-3-pro to the supported vertexai models --- models.yaml | 8 +++ src/function/mod.rs | 127 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/models.yaml b/models.yaml index 9f1006d..6c3dbdf 100644 --- a/models.yaml +++ b/models.yaml @@ -659,6 +659,14 @@ # - https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini - provider: vertexai models: + - name: gemini-3-pro-preview + hipaa_safe: true + max_input_tokens: 1048576 + max_output_tokens: 65536 + input_price: 0 + output_price: 0 + supports_vision: true + supports_function_calling: true - name: gemini-2.5-flash max_input_tokens: 1048576 max_output_tokens: 65536 diff --git a/src/function/mod.rs b/src/function/mod.rs index 151d941..1b98f22 100644 --- a/src/function/mod.rs +++ b/src/function/mod.rs @@ -15,6 +15,7 @@ use indoc::formatdoc; use rust_embed::Embed; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; +use std::collections::VecDeque; use std::ffi::OsStr; use std::fs::File; use std::io::Write; @@ -90,6 +91,19 @@ pub async fn eval_tool_calls( } let mut is_all_null = true; for call in calls { + if let Some(checker) = &config.read().tool_call_tracker + && let Some(msg) = checker.check_loop(&call.clone()) + { + let dup_msg = format!("{{\"tool_call_loop_alert\":{}}}", &msg.trim()); + println!( + "{}", + warning_text(format!("{}: ⚠️ Tool-call loop detected! ⚠️", &call.name).as_str()) + ); + let val = json!(dup_msg); + output.push(ToolResult::new(call, val)); + is_all_null = false; + continue; + } let mut result = call.eval(config).await?; if result.is_null() { result = json!("DONE"); @@ -841,11 +855,14 @@ impl ToolCall { _ if cmd_name.starts_with(MCP_INVOKE_META_FUNCTION_NAME_PREFIX) => { Self::invoke_mcp_tool(config, &cmd_name, &json_data).await? } - _ => match run_llm_function(cmd_name, cmd_args, envs, agent_name)? { - Some(contents) => serde_json::from_str(&contents) + _ => match run_llm_function(cmd_name, cmd_args, envs, agent_name) { + Ok(Some(contents)) => serde_json::from_str(&contents) .ok() .unwrap_or_else(|| json!({"output": contents})), - None => Value::Null, + Ok(None) => Value::Null, + Err(e) => serde_json::from_str(&e.to_string()) + .ok() + .unwrap_or_else(|| json!({"output": e.to_string()})), }, }; @@ -978,7 +995,9 @@ pub fn run_llm_function( agent_name: Option, ) -> Result> { let mut bin_dirs: Vec = vec![]; + let mut command_name = cmd_name.clone(); if let Some(agent_name) = agent_name { + command_name = cmd_args[0].clone(); let dir = Config::agent_bin_dir(&agent_name); if dir.exists() { bin_dirs.push(dir); @@ -1001,9 +1020,13 @@ pub fn run_llm_function( let cmd_name = polyfill_cmd_name(&cmd_name, &bin_dirs); let exit_code = run_command(&cmd_name, &cmd_args, Some(envs)) - .map_err(|err| anyhow!("Unable to run {cmd_name}, {err}"))?; + .map_err(|err| anyhow!("Unable to run {command_name}, {err}"))?; if exit_code != 0 { - bail!("Tool call exited with {exit_code}"); + let tool_error_message = + format!("⚠️ Tool call '{command_name}' threw exit code {exit_code} ⚠️"); + println!("{}", warning_text(&tool_error_message)); + let tool_error_json = format!("{{\"tool_call_error\":\"{}\"}}", &tool_error_message); + return Ok(Some(tool_error_json)); } let mut output = None; if temp_file.exists() { @@ -1032,3 +1055,97 @@ fn polyfill_cmd_name>(cmd_name: &str, bin_dir: &[T]) -> String { } cmd_name } + +#[derive(Debug, Clone)] +pub struct ToolCallTracker { + last_calls: VecDeque, + max_repeats: usize, + chain_len: usize, +} + +impl ToolCallTracker { + pub fn new(max_repeats: usize, chain_len: usize) -> Self { + Self { + last_calls: VecDeque::new(), + max_repeats, + chain_len, + } + } + + pub fn default() -> Self { + Self::new(2, 3) + } + + pub fn check_loop(&self, new_call: &ToolCall) -> Option { + if self.last_calls.len() < self.max_repeats { + return None; + } + + if let Some(last) = self.last_calls.back() + && self.calls_match(last, new_call) + { + let mut repeat_count = 1; + for i in (1..self.last_calls.len()).rev() { + if self.calls_match(&self.last_calls[i - 1], &self.last_calls[i]) { + repeat_count += 1; + if repeat_count >= self.max_repeats { + return Some(self.create_loop_message()); + } + } else { + break; + } + } + } + + let start = self.last_calls.len().saturating_sub(self.chain_len); + let chain: Vec<_> = self.last_calls.iter().skip(start).collect(); + if chain.len() == self.chain_len { + let mut is_repeating = true; + for i in 0..chain.len() - 1 { + if !self.calls_match(chain[i], chain[i + 1]) { + is_repeating = false; + break; + } + } + if is_repeating && self.calls_match(chain[chain.len() - 1], new_call) { + return Some(self.create_loop_message()); + } + } + + None + } + + fn calls_match(&self, a: &ToolCall, b: &ToolCall) -> bool { + a.name == b.name && a.arguments == b.arguments + } + + fn create_loop_message(&self) -> String { + let message = r#"{"error":{"message":"⚠️ Tool-call loop detected! ⚠️","code":400,"param":"Use the output of the last call to this function and parameter-set then move on to the next step of workflow, change tools/parameters called, or request assistance in the conversation sream"}}"#; + + if self.last_calls.len() >= self.chain_len { + let start = self.last_calls.len().saturating_sub(self.chain_len); + let chain: Vec<_> = self.last_calls.iter().skip(start).collect(); + let mut loopset = "[".to_string(); + for c in chain { + loopset += + format!("{{\"name\":{},\"parameters\":{}}},", c.name, c.arguments).as_str(); + } + let _ = loopset.pop(); + loopset.push(']'); + format!( + "{},\"call_history\":{}}}}}", + &message[..(&message.len() - 2)], + loopset + ) + } else { + message.to_string() + } + } + + pub fn record_call(&mut self, call: ToolCall) { + if self.last_calls.len() >= self.chain_len * self.max_repeats { + self.last_calls.pop_front(); + } + self.last_calls.push_back(call); + } +}