use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "UPPERCASE")]
pub enum VertexFunctionCallingMode {
#[default]
Auto,
Any,
None,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct VertexThinkingConfig {
#[serde(rename = "thinkingLevel", skip_serializing_if = "Option::is_none")]
pub thinking_level: Option<String>,
#[serde(rename = "includeThoughts", skip_serializing_if = "Option::is_none")]
pub include_thoughts: Option<bool>,
}
pub type VertexRetryConfig = crate::llm::openai::RetryConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VertexConfig {
#[serde(default)]
pub api_key: Option<String>,
#[serde(default)]
pub access_token: Option<String>,
#[serde(default = "VertexConfig::default_base_url")]
pub base_url: String,
#[serde(default = "VertexConfig::default_model")]
pub model: String,
#[serde(default)]
pub project_id: Option<String>,
#[serde(default = "VertexConfig::default_location")]
pub location: String,
#[serde(default = "VertexConfig::default_stream")]
pub stream: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_k: Option<u32>,
#[serde(default)]
pub function_calling_mode: VertexFunctionCallingMode,
#[serde(default)]
pub allowed_function_names: Option<Vec<String>>,
#[serde(default)]
pub stream_function_call_arguments: bool,
#[serde(default)]
pub thinking: Option<VertexThinkingConfig>,
#[serde(default)]
pub retry: Option<VertexRetryConfig>,
}
impl VertexConfig {
fn default_base_url() -> String {
"https://aiplatform.googleapis.com".to_string()
}
fn default_model() -> String {
"gemini-2.5-flash".to_string()
}
fn default_location() -> String {
"us-central1".to_string()
}
fn default_stream() -> bool {
true
}
pub fn validate(&self) -> Result<()> {
if self.model.trim().is_empty() {
return Err(anyhow!("Vertex model cannot be empty"));
}
if self.location.trim().is_empty() {
return Err(anyhow!("Vertex location cannot be empty"));
}
if let Some(temperature) = self.temperature {
if !(0.0..=2.0).contains(&temperature) {
return Err(anyhow!(
"Vertex temperature must be within [0.0, 2.0], got {}",
temperature
));
}
}
if let Some(top_p) = self.top_p {
if !(0.0..=1.0).contains(&top_p) {
return Err(anyhow!(
"Vertex top_p must be within [0.0, 1.0], got {}",
top_p
));
}
}
Ok(())
}
}
impl Default for VertexConfig {
fn default() -> Self {
Self {
api_key: None,
access_token: None,
base_url: Self::default_base_url(),
model: Self::default_model(),
project_id: None,
location: Self::default_location(),
stream: Self::default_stream(),
max_output_tokens: Some(4096),
temperature: None,
top_p: None,
top_k: None,
function_calling_mode: VertexFunctionCallingMode::Auto,
allowed_function_names: None,
stream_function_call_arguments: false,
thinking: None,
retry: Some(VertexRetryConfig::default()),
}
}
}