use reqwest::Client;
use secrecy::{ExposeSecret, SecretString}; use thiserror::Error;
use tracing::{debug, instrument, trace};
use url::Url;
use super::error::GeminiError;
const DEFAULT_GEMINI_GENERATIVE_LANGUAGE_BASE_URL: &str = "https://generativelanguage.googleapis.com";
pub(crate) const EXTENSION_URI: &str = "todo";
#[derive(Clone, Debug)]
pub struct GeminiConfig {
pub(crate) api_key: SecretString,
pub(crate) base_url: Url,
pub(crate) timeout: std::time::Duration,
}
impl GeminiConfig {
pub fn new(api_key: impl Into<String>) -> Result<Self, GeminiError> {
let api_key = api_key.into();
if api_key.is_empty() {
return Err(GeminiError::InvalidConfiguration("API key cannot be empty".to_string()));
}
let base_url = Url::parse(DEFAULT_GEMINI_GENERATIVE_LANGUAGE_BASE_URL)
.map_err(|e| GeminiError::InvalidConfiguration(
format!("Internal error: Failed to parse default base URL: {}", e)
))?;
Ok(Self {
api_key: api_key.into(),
base_url,
timeout: std::time::Duration::from_secs(60),
})
}
#[must_use]
pub fn base_url(mut self, url: &str) -> Result<Self, GeminiError> {
self.base_url = Url::parse(url)
.map_err(|e| GeminiError::InvalidConfiguration(
format!("Invalid base URL '{}': {}", url, e)
))?;
Ok(self)
}
#[must_use]
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = timeout;
self
}
}
#[derive(Clone, Debug)]
pub(crate) struct SharedGeminiClient {
config: GeminiConfig,
http_client: Client,
}
impl SharedGeminiClient {
#[instrument(name = "shared_gemini_client_new", skip(config, client_override))]
pub(crate) fn new(config: GeminiConfig, client_override: Option<Client>) -> Result<Self, GeminiError> {
let client = match client_override {
Some(client) => {
debug!("Using provided HTTP client.");
client
},
None => {
debug!(timeout=?config.timeout, "Building default HTTP client.");
Client::builder()
.timeout(config.timeout)
.build()
.map_err(|e| GeminiError::InvalidConfiguration(
format!("Failed to build default HTTP client: {}", e)
))?
}
};
debug!(base_url = %config.base_url, "Shared Gemini client initialized.");
Ok(Self { config, http_client: client })
}
pub(crate) fn http_client(&self) -> &Client {
&self.http_client
}
pub(crate) fn config(&self) -> &GeminiConfig {
&self.config
}
pub(crate) fn build_url(&self, relative_path: &str ) -> Result<Url, GeminiError> {
let base_path = format!("v1beta/{}", relative_path); let mut url = self.config.base_url.clone();
url.path_segments_mut()
.map_err(|_| GeminiError::InvalidConfiguration("Base URL cannot be a 'cannot-be-a-base' URL.".to_string()))?
.extend(base_path.split('/'));
trace!(built_url = %url, "Built Gemini API URL (without auth)");
Ok(url)
}
}