use aither_core::llm::{Classify, RetryPolicy};
use std::time::Duration;
use aither_core::time::sleep;
use serde::{Deserialize, Serialize};
use zenwave::{Client, client, header};
use crate::{
config::{AuthMode, GeminiConfig, USER_AGENT},
error::GeminiError,
types::{
EmbedContentRequest, EmbedContentResponse, GenerateContentRequest, GenerateContentResponse,
},
};
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelInfo {
#[allow(dead_code)]
pub name: String,
pub input_token_limit: u32,
#[allow(dead_code)]
pub output_token_limit: u32,
}
pub async fn call_generate(
cfg: &GeminiConfig,
model: &str,
request: GenerateContentRequest,
) -> Result<GenerateContentResponse, GeminiError> {
post_json(cfg, cfg.model_endpoint(model, "generateContent"), &request).await
}
pub const RETRY_POLICY: RetryPolicy = RetryPolicy {
max_retries: 2,
initial_delay: Duration::from_secs(2),
max_delay: Duration::from_secs(60),
multiplier: 2,
};
pub async fn stream_generate(
cfg: &GeminiConfig,
model: &str,
request: GenerateContentRequest,
) -> Result<
impl futures_core::Stream<Item = Result<GenerateContentResponse, GeminiError>>,
GeminiError,
> {
let mut endpoint = cfg.model_endpoint(model, "streamGenerateContent");
let separator = if endpoint.contains('?') { '&' } else { '?' };
endpoint.push(separator);
endpoint.push_str("alt=sse");
let debug = std::env::var("AITHER_GEMINI_DEBUG").as_deref() == Ok("1");
let mut retries = 0u32;
let sse_stream = loop {
let mut backend = client();
let mut builder = backend
.post(endpoint.clone())
.map_err(GeminiError::from_http)?
.header(header::USER_AGENT.as_str(), USER_AGENT)
.map_err(GeminiError::from_http)?;
if cfg.auth == AuthMode::Header {
builder = builder
.header("x-goog-api-key", cfg.api_key.clone())
.map_err(GeminiError::from_http)?;
}
let builder = builder
.json_body(&request)
.map_err(GeminiError::from_http)?;
match builder.sse().await {
Ok(stream) => break stream,
Err(e) => {
let err = GeminiError::from_http(e);
let failure = err.failure();
if RETRY_POLICY.allows(retries, &failure) {
let delay = RETRY_POLICY.delay(retries, &failure);
retries += 1;
if debug {
tracing::info!(
"Gemini streaming error (retry {}/{}), retrying in {:?}: {}",
retries,
RETRY_POLICY.max_retries,
delay,
err
);
}
sleep(delay).await;
continue;
}
return Err(err);
}
}
};
Ok(futures_lite::stream::unfold(
sse_stream,
|mut stream| async move {
use futures_lite::StreamExt;
loop {
match stream.next().await {
None => return None,
Some(result) => {
let event = match result {
Ok(e) => e,
Err(e) => {
return Some((Err(GeminiError::Stream(e)), stream));
}
};
let data = event.text_data();
if data.is_empty() || data == "[DONE]" {
continue;
}
match serde_json::from_str::<GenerateContentResponse>(data) {
Ok(response) => return Some((Ok(response), stream)),
Err(e) => {
tracing::debug!("SSE parse error: {} for data: {}", e, data);
}
}
}
}
}
},
))
}
pub async fn get_model_info(cfg: &GeminiConfig, model: &str) -> Result<ModelInfo, GeminiError> {
let model = crate::config::sanitize_model(model);
get_json(cfg, cfg.endpoint(&model)).await
}
pub async fn embed_content(
cfg: &GeminiConfig,
request: EmbedContentRequest,
) -> Result<EmbedContentResponse, GeminiError> {
post_json(
cfg,
cfg.model_endpoint(&cfg.embedding_model, "embedContent"),
&request,
)
.await
}
#[allow(clippy::future_not_send)]
async fn get_json<T: for<'de> serde::Deserialize<'de>>(
cfg: &GeminiConfig,
endpoint: String,
) -> Result<T, GeminiError> {
let mut backend = client();
let mut builder = backend
.get(endpoint)
.map_err(GeminiError::from_http)?
.header(header::USER_AGENT.as_str(), USER_AGENT)
.map_err(GeminiError::from_http)?;
if cfg.auth == AuthMode::Header {
builder = builder
.header("x-goog-api-key", cfg.api_key.clone())
.map_err(GeminiError::from_http)?;
}
builder.json().await.map_err(GeminiError::from_http)
}
#[allow(clippy::future_not_send)]
async fn post_json<T: for<'de> serde::Deserialize<'de> + serde::Serialize, S: Serialize>(
cfg: &GeminiConfig,
endpoint: String,
body: &S,
) -> Result<T, GeminiError> {
let debug = std::env::var("AITHER_GEMINI_DEBUG").as_deref() == Ok("1");
if debug {
if let Ok(json) = serde_json::to_string_pretty(body) {
tracing::debug!("Gemini request to {endpoint}:\n{json}");
}
if cfg.auth == AuthMode::Query {
tracing::debug!("Gemini using query auth (key redacted)");
}
}
let mut retries = 0u32;
loop {
let mut backend = client();
let mut builder = backend
.post(endpoint.clone())
.map_err(GeminiError::from_http)?
.header(header::USER_AGENT.as_str(), USER_AGENT)
.map_err(GeminiError::from_http)?;
if cfg.auth == AuthMode::Header {
builder = builder
.header("x-goog-api-key", cfg.api_key.clone())
.map_err(GeminiError::from_http)?;
}
let builder = builder.json_body(body).map_err(GeminiError::from_http)?;
match builder.json().await {
Ok(res) => {
if debug && let Ok(json) = serde_json::to_string_pretty(&res) {
tracing::debug!("Gemini response from {endpoint}:\n{json}");
}
return Ok(res);
}
Err(e) => {
let err = GeminiError::from_http(e);
let failure = err.failure();
if RETRY_POLICY.allows(retries, &failure) {
let delay = RETRY_POLICY.delay(retries, &failure);
retries += 1;
if debug {
tracing::info!(
"Gemini POST error (retry {}/{}), retrying in {:?}: {}",
retries,
RETRY_POLICY.max_retries,
delay,
err
);
}
sleep(delay).await;
continue;
}
return Err(err);
}
}
}
}