use std::{collections::HashMap, path::Path};
use base64::{engine::general_purpose, Engine as _};
use dotenv::dotenv;
use log::log;
use reqwest::header;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tokio::sync::Mutex;
use crate::{errors::GemError, utils::get_mime_type};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged, rename_all = "camelCase")] pub enum PartData {
InlineData { inline_data: Blob },
FileData { file_data: FileData },
Text { text: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub(crate) enum Role {
Model,
#[default]
User,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub(crate) enum FinishReason {
FinishReasonUnspecified, Stop, MaxTokens, Safety, Recitation, Language, Other, Blocklist, ProhibitedContent, Spii, MalformedFunctionCall, }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GenerateContentResponse {
candidates: Vec<Candidate>,
prompt_feedback: Option<PromptFeedback>, usage_metadata: Option<UsageMetadata>, }
impl GenerateContentResponse {
pub fn get_candidates(&self) -> &Vec<Candidate> {
&self.candidates
}
pub fn get_results(&self) -> Vec<String> {
let mut texts = Vec::new();
for candidate in &self.candidates {
if let Some(content) = candidate.get_content() {
if let Some(text) = content.get_text() {
texts.push(text.clone());
}
}
}
texts
}
pub fn get_usage_metadata(&self) -> Option<&UsageMetadata> {
self.usage_metadata.as_ref()
}
pub(crate) fn feedback(&self) -> Option<BlockReason> {
match self.prompt_feedback.is_some()
&& self
.prompt_feedback
.as_ref()
.unwrap()
.block_reason
.is_some()
{
true => self.prompt_feedback.as_ref().unwrap().block_reason.clone(),
false => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Candidate {
content: Option<Content>, finish_reason: Option<FinishReason>, safety_ratings: Option<Vec<SafetyRating>>, token_count: Option<i32>, index: Option<i32>, }
impl Candidate {
pub(crate) fn get_content(&self) -> Option<&Content> {
self.content.as_ref()
}
pub(crate) fn is_blocked(&self) -> bool {
(self.finish_reason == Some(FinishReason::Safety))
|| (self.finish_reason == Some(FinishReason::Recitation))
|| (self.finish_reason == Some(FinishReason::ProhibitedContent))
}
pub(crate) fn get_token_count(&self) -> Option<i32> {
self.token_count
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Content {
parts: Vec<Part>, role: Option<Role>, }
impl Content {
pub fn get_text(&self) -> Option<String> {
for part in &self.parts {
match &part.data {
PartData::Text { text } => return Some(text.clone()),
_ => continue,
}
}
None
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NoRoleContent {
parts: Vec<Part>, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Part {
#[serde(flatten)] data: PartData, }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Blob {
mime_type: String,
data: String, }
impl Blob {
pub fn new(mime_type: &str, data: &[u8]) -> Self {
Blob {
mime_type: mime_type.to_string(),
data: general_purpose::STANDARD.encode(&data),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileData {
mime_type: String,
file_uri: String, }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PromptFeedback {
block_reason: Option<BlockReason>, safety_ratings: Vec<SafetyRating>, }
impl PromptFeedback {
pub(crate) fn get_block_reason(&self) -> Option<BlockReason> {
self.block_reason.clone()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub(crate) enum BlockReason {
BlockReasonUnspecified, Safety, Other, Blocklist, ProhibitedContent, }
impl std::fmt::Display for BlockReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BlockReason::BlockReasonUnspecified => write!(f, "Unspecified"),
BlockReason::Safety => write!(f, "Safety"),
BlockReason::Other => write!(f, "Other"),
BlockReason::Blocklist => write!(f, "Blocklist"),
BlockReason::ProhibitedContent => write!(f, "Prohibited Content"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct SafetyRating {
category: Option<String>, probability: Option<String>, blocked: Option<bool>, }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UsageMetadata {
prompt_token_count: Option<i32>, cached_content_token_count: Option<i32>, candidates_token_count: Option<i32>, total_token_count: Option<i32>, }
impl UsageMetadata {
pub fn get_prompt_token_count(&self) -> Option<i32> {
self.prompt_token_count
}
pub fn get_cached_content_token_count(&self) -> Option<i32> {
self.cached_content_token_count
}
pub fn get_candidates_token_count(&self) -> Option<i32> {
self.candidates_token_count
}
pub fn get_total_token_count(&self) -> Option<i32> {
self.total_token_count
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Status {
code: i32,
message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VideoMetadata {
video_duration: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct File {
name: String,
uri: String,
display_name: String,
mime_type: String,
size_bytes: String,
create_time: String,
update_time: String,
expiration_time: String,
sha256_hash: String,
state: String,
error: Option<Status>,
video_metadata: Option<VideoMetadata>,
#[serde(skip)]
api_key: String,
}
impl File {
pub(crate) async fn new(
file_name: &str,
bytes: Vec<u8>,
mime_type: &str,
api_key: &str,
) -> Result<Self, GemError> {
Self::upload(file_name, bytes, mime_type, api_key).await
}
async fn upload(
file_name: &str,
buffer: Vec<u8>,
mime_type: &str,
api_key: &str,
) -> Result<Self, GemError> {
let num_bytes = buffer.len();
let client = reqwest::Client::new();
let reserve_response = match client
.post("https://generativelanguage.googleapis.com/upload/v1beta/files")
.query(&[("key", api_key)])
.header("X-Goog-Upload-Protocol", "resumable")
.header("X-Goog-Upload-Command", "start")
.header("X-Goog-Upload-Header-Content-Length", num_bytes.to_string())
.header("X-Goog-Upload-Header-Content-Type", mime_type)
.header(header::CONTENT_TYPE, "application/json")
.json(&json!({
"file": { "display_name": file_name }
}))
.send()
.await
{
Ok(response) => response,
Err(e) => return Err(GemError::FileError(e.to_string())),
};
let location = match reserve_response.headers().get("X-Goog-Upload-URL") {
Some(loc) => match loc.to_str() {
Ok(l) => l,
Err(e) => return Err(GemError::FileError(e.to_string())),
},
None => {
return Err(GemError::FileError(
"X-Goog-Upload-URL header not found".to_string(),
))
}
};
let upload_response = match client
.put(location)
.header("Content-Length", num_bytes.to_string())
.header("X-Goog-Upload-Offset", "0")
.header("X-Goog-Upload-Command", "upload, finalize")
.body(buffer)
.send()
.await
{
Ok(response) => response,
Err(e) => return Err(GemError::FileError(e.to_string())),
};
let upload_text_response = match upload_response.text().await {
Ok(t) => t,
Err(e) => return Err(GemError::FileError(e.to_string())),
};
let mut file: File = match serde_json::from_str::<Value>(&upload_text_response) {
Ok(data) => match data.get("file") {
Some(f) => match serde_json::from_value(f.clone()) {
Ok(file) => file,
Err(e) => {
log::error!("File error [0]: {} - Response: {}", e, upload_text_response);
return Err(GemError::FileError(e.to_string()));
}
},
None => return Err(GemError::FileError("File data not found".to_string())),
},
Err(e) => {
log::error!("File error [1]: {} - Response: {}", e, upload_text_response);
return Err(GemError::FileError(e.to_string()));
}
};
let mut timeout = 0;
loop {
let file_state = match client
.get(&format!(
"https://generativelanguage.googleapis.com/v1beta/{}",
file.name
))
.query(&[("key", api_key)])
.send()
.await
{
Ok(response) => response,
Err(e) => return Err(GemError::FileError(e.to_string())),
};
let file_state_text_response = match file_state.text().await {
Ok(t) => t,
Err(e) => return Err(GemError::FileError(e.to_string())),
};
let file_state: File = match serde_json::from_str::<File>(&file_state_text_response) {
Ok(f) => f,
Err(e) => {
log::error!(
"File error [3]: {:#?}, response: {:#?}",
e,
file_state_text_response
);
return Err(GemError::FileError("File data not found".to_string()));
}
};
if file_state.state == "ACTIVE" {
break;
} else if file_state.state == "FAILED" {
return Err(GemError::FileError(
file_state
.error
.clone()
.unwrap_or(Status {
code: 0,
message: "File processing failed".to_string(),
})
.message,
));
} else if file_state.state != "PROCESSING" {
return Err(GemError::FileError(
"File processing unknown state".to_string(),
));
}
if timeout >= 3 {
return Err(GemError::FileError("File processing timeout".to_string()));
}
timeout += 1;
std::thread::sleep(std::time::Duration::from_secs(3));
}
file.api_key = api_key.to_string();
Ok(file)
}
async fn delete(self) -> Result<(), GemError> {
log::info!("Deleting file: {:#?}", self);
if self.api_key == "" {
log::info!("API key not found: {:#?}", self.display_name);
return Err(GemError::FileError("API key not found".to_string()));
}
let client = reqwest::Client::new();
match client
.delete(self.uri)
.query(&[("key", self.api_key.clone())])
.send()
.await
{
Ok(_) => {
log::info!("File deleted successfully: {:#?}", self.display_name);
Ok(())
}
Err(e) => Err(GemError::FileError(e.to_string())),
}
}
}
#[derive(Debug)]
pub struct FileManager {
files: Mutex<HashMap<String, File>>,
api_key: String,
}
impl FileManager {
pub fn new() -> Self {
dotenv().expect("Failed to load Gemini API key");
let api_key = std::env::var("GEMINI_API_KEY").unwrap();
Self {
files: Mutex::new(HashMap::new()),
api_key: api_key.to_string(),
}
}
pub async fn add_file_from_bytes(
&self,
file_name: &str,
bytes: Vec<u8>,
mime_type: &str,
api_key: &str,
) -> Result<FileData, GemError> {
let hash = sha256::digest(&bytes);
match self.get_file(&hash).await {
Some(file) => Ok(file),
None => {
let file = File::new(file_name, bytes, mime_type, api_key).await?;
let mime_type = file.mime_type.clone();
let file_uri = file.uri.clone();
let mut files = self.files.lock().await;
files.insert(hash, file);
Ok(FileData {
mime_type: mime_type,
file_uri: file_uri,
})
}
}
}
pub async fn add_file(&mut self, file_path: &Path) -> Result<FileData, GemError> {
if !file_path.exists() {
return Err(GemError::FileError("File does not exist".to_string()));
}
let file_name = match file_path.file_name() {
Some(name) if name.to_str().is_some() => name.to_str().unwrap(),
_ => return Err(GemError::FileError("Invalid file name".to_string())),
};
let mut file = match std::fs::File::open(file_path) {
Ok(f) => f,
Err(e) => return Err(GemError::FileError(e.to_string())),
};
let mut buffer = Vec::new();
match std::io::Read::read_to_end(&mut file, &mut buffer) {
Ok(_) => (),
Err(e) => return Err(GemError::FileError(e.to_string())),
};
let mime_type = match get_mime_type(file_path) {
Some(ext) => ext,
None => return Err(GemError::FileError("Unsupported file type".to_string())),
};
let hash = sha256::digest(&buffer);
match self.get_file(&hash).await {
Some(file) => Ok(file),
None => {
let file = File::new(file_name, buffer, &mime_type, &self.api_key).await?;
let mime_type = file.mime_type.clone();
let file_uri = file.uri.clone();
let mut files = self.files.lock().await;
files.insert(hash, file);
Ok(FileData {
mime_type: mime_type,
file_uri: file_uri,
})
}
}
}
pub async fn check_file(&self, hash: &str) -> bool {
let files = self.files.lock().await;
for file in files.iter() {
if file.0 == hash {
return true;
}
}
false
}
pub async fn get_file(&self, hash: &str) -> Option<FileData> {
let mut to_remove = Vec::new();
let mut files = self.files.lock().await;
for file in files.iter() {
match file.0 == hash {
true if file.1.expiration_time
> (chrono::Utc::now() + chrono::Duration::minutes(10)).to_rfc3339() =>
{
log::info!("Found cached File: {:#?}", file.1);
return Some(FileData {
mime_type: file.1.mime_type.clone(),
file_uri: file.1.uri.clone(),
});
}
true => {
to_remove.push(file.0.clone());
}
false => continue,
}
}
for hash in to_remove {
let file = files.remove(&hash);
if let Some(file) = file {
let _ = file.delete().await;
}
}
None
}
pub async fn fetch_list(&mut self) -> Result<(), GemError> {
let client = reqwest::Client::new();
let mut files = Vec::new();
let mut page_token: Option<String> = None;
loop {
let mut request = client.get("https://generativelanguage.googleapis.com/v1beta/files");
if let Some(token) = &page_token {
request = request.query(&[("pageToken", token), ("key", &self.api_key)]);
} else {
request = request.query(&[("key", &self.api_key)]);
}
let response = match request.send().await {
Ok(response) => response,
Err(e) => return Err(GemError::FileError(e.to_string())),
};
let response_text = match response.text().await {
Ok(data) => data,
Err(e) => return Err(GemError::FileError(e.to_string())),
};
let response_json: Value = match serde_json::from_str(&response_text) {
Ok(data) => data,
Err(e) => {
log::error!("File error [6]: {}, response: {}", e, response_text);
return Err(GemError::FileError(e.to_string()));
}
};
match response_json.get("files") {
Some(f) => match serde_json::from_value::<Vec<File>>(f.clone()) {
Ok(mut new_files) => files.append(&mut new_files),
Err(e) => {
log::error!("File error [7]: {}, response: {}", e, response_text);
return Err(GemError::FileError(e.to_string()));
}
},
None => return Err(GemError::FileError("Files data not found".to_string())),
};
page_token = response_json
.get("nextPageToken")
.and_then(|t| t.as_str().map(String::from));
if page_token.is_none() {
break;
}
}
let mut files_map = self.files.lock().await;
for mut file in files {
file.api_key = self.api_key.clone();
log::info!("File: {:#?}", file);
files_map.insert(file.sha256_hash.clone(), file);
}
Ok(())
}
pub async fn delete_file(&mut self, hash: &str) -> Result<(), GemError> {
let mut files = self.files.lock().await;
let file = files.remove(hash);
match file {
Some(file) => file.delete().await,
None => Ok(()),
}
}
pub async fn clear_files(&mut self) {
let mut files = self.files.lock().await;
let keys: Vec<String> = files.keys().cloned().collect();
for key in keys {
if let Some(file) = files.remove(&key) {
let _ = file.delete().await;
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SafetySetting {
category: HarmCategory, threshold: HarmBlockThreshold, }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] enum HarmCategory {
HarmCategoryHateSpeech,
HarmCategorySexuallyExplicit,
HarmCategoryDangerousContent,
HarmCategoryHarassment,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Error {
code: i32,
message: String,
status: String,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Error {}: {} ({})", self.code, self.message, self.status)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum HarmBlockThreshold {
HarmBlockThresholdUnspecified, BlockLowAndAbove, BlockMediumAndAbove, BlockOnlyHigh, BlockNone, }
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GenerationConfig {
stop_sequences: Option<Vec<String>>, response_mime_type: Option<String>, max_output_tokens: Option<u32>, temperature: Option<f32>, top_p: Option<f32>, top_k: Option<u32>, }
pub struct Settings {
safety_settings: Option<Vec<SafetySetting>>,
generation_config: Option<GenerationConfig>,
system_instruction: Option<String>,
}
impl Settings {
pub fn new() -> Self {
Settings {
safety_settings: None,
generation_config: None,
system_instruction: None,
}
}
pub fn set_all_safety_settings(&mut self, threshold: HarmBlockThreshold) {
self.safety_settings = Some(vec![
SafetySetting {
category: HarmCategory::HarmCategoryHateSpeech,
threshold: threshold.clone(),
},
SafetySetting {
category: HarmCategory::HarmCategorySexuallyExplicit,
threshold: threshold.clone(),
},
SafetySetting {
category: HarmCategory::HarmCategoryDangerousContent,
threshold: threshold.clone(),
},
SafetySetting {
category: HarmCategory::HarmCategoryHarassment,
threshold: threshold.clone(),
},
]);
}
pub fn set_advance_settings(
&mut self,
stop_sequences: Option<Vec<String>>,
response_mime_type: Option<String>,
max_output_tokens: Option<u32>,
temperature: Option<f32>,
top_p: Option<f32>,
top_k: Option<u32>,
) {
self.generation_config = Some(GenerationConfig {
stop_sequences: stop_sequences,
response_mime_type: response_mime_type,
max_output_tokens: max_output_tokens,
temperature: temperature,
top_p: top_p,
top_k: top_k,
});
}
pub fn set_temperature(&mut self, temperature: f32) {
match &mut self.generation_config {
Some(config) => config.temperature = Some(temperature),
None => {
self.generation_config = Some(GenerationConfig {
stop_sequences: None,
response_mime_type: None,
max_output_tokens: None,
temperature: Some(temperature),
top_p: None,
top_k: None,
});
}
}
}
pub fn set_max_output_tokens(&mut self, max_output_tokens: u32) {
match &mut self.generation_config {
Some(config) => config.max_output_tokens = Some(max_output_tokens),
None => {
self.generation_config = Some(GenerationConfig {
stop_sequences: None,
response_mime_type: None,
max_output_tokens: Some(max_output_tokens),
temperature: None,
top_p: None,
top_k: None,
});
}
}
}
pub fn set_system_instruction(&mut self, instruction: &str) {
self.system_instruction = Some(instruction.to_string());
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GenerateContentRequest {
contents: Vec<Content>, safety_settings: Option<Vec<SafetySetting>>, generation_config: Option<GenerationConfig>, system_instruction: Option<NoRoleContent>, }
impl GenerateContentRequest {
fn new(
context: &Context,
config: Option<GenerationConfig>,
safety: Option<Vec<SafetySetting>>,
system_instruction: Option<NoRoleContent>,
) -> Self {
GenerateContentRequest {
contents: context.contents.clone(),
safety_settings: match safety {
Some(s) => Some(s),
None => Some(vec![
SafetySetting {
category: HarmCategory::HarmCategoryHateSpeech,
threshold: HarmBlockThreshold::BlockNone,
},
SafetySetting {
category: HarmCategory::HarmCategorySexuallyExplicit,
threshold: HarmBlockThreshold::BlockNone,
},
SafetySetting {
category: HarmCategory::HarmCategoryDangerousContent,
threshold: HarmBlockThreshold::BlockNone,
},
SafetySetting {
category: HarmCategory::HarmCategoryHarassment,
threshold: HarmBlockThreshold::BlockNone,
},
]),
},
generation_config: match config {
Some(c) => Some(c),
None => Some(GenerationConfig {
max_output_tokens: Some(8192),
temperature: Some(1.0),
response_mime_type: None,
stop_sequences: None,
top_k: None,
top_p: None,
}),
},
system_instruction,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Context {
contents: Vec<Content>,
}
impl Context {
pub fn new() -> Self {
Context {
contents: Vec::new(),
}
}
pub(crate) fn push_message(&mut self, role: Option<Role>, content: String) {
self.contents.push(Content {
role: role,
parts: vec![Part {
data: PartData::Text {
text: content.to_string(),
},
}],
});
}
pub(crate) fn push_file(&mut self, role: Option<Role>, file_data: FileData) {
self.contents.push(Content {
role: role,
parts: vec![Part {
data: PartData::FileData { file_data },
}],
});
}
pub(crate) fn push_blob(&mut self, role: Option<Role>, blob: Blob) {
self.contents.push(Content {
role: role,
parts: vec![Part {
data: PartData::InlineData { inline_data: blob },
}],
});
}
pub(crate) fn push_message_with_file(
&mut self,
role: Option<Role>,
content: &str,
file_data: FileData,
) {
self.contents.push(Content {
role: role,
parts: vec![
Part {
data: PartData::Text {
text: content.to_string(),
},
},
Part {
data: PartData::FileData { file_data },
},
],
});
}
pub(crate) fn push_message_with_blob(&mut self, role: Option<Role>, content: &str, blob: Blob) {
self.contents.push(Content {
role: role,
parts: vec![
Part {
data: PartData::Text {
text: content.to_string(),
},
},
Part {
data: PartData::InlineData { inline_data: blob },
},
],
});
}
pub(crate) fn build(&self, settings: &Settings) -> GenerateContentRequest {
GenerateContentRequest::new(
self,
settings.generation_config.clone(),
settings.safety_settings.clone(),
match &settings.system_instruction {
Some(instruction) => Some(NoRoleContent {
parts: vec![Part {
data: PartData::Text {
text: instruction.clone(),
},
}],
}),
None => None,
},
)
}
pub(crate) fn clear(&mut self) {
self.contents.clear();
}
pub(crate) fn is_empty(&self) -> bool {
self.contents.is_empty()
}
pub(crate) fn len(&self) -> usize {
self.contents.len()
}
pub(crate) fn get_contents(&self) -> &Vec<Content> {
&self.contents
}
pub(crate) fn get_contents_mut(&mut self) -> &mut Vec<Content> {
&mut self.contents
}
}
mod tests {
use super::*;
#[test]
fn test_deserialize_generate_content_response() {
let json_data = r#"
{
"candidates": [
{
"content": {
"parts": [
{
"text": "Sample text"
}
],
"role": "model"
},
"finishReason": "STOP",
"safetyRatings": [
{
"category": "violence",
"probability": "low",
"blocked": false
}
],
"tokenCount": 10,
"index": 0
}
],
"promptFeedback": {
"blockReason": "SAFETY",
"safetyRatings": [
{
"category": "violence",
"probability": "low",
"blocked": false
}
]
},
"usageMetadata": {
"promptTokenCount": 5,
"cachedContentTokenCount": 3,
"candidatesTokenCount": 10,
"totalTokenCount": 18
}
}
"#;
let response: GenerateContentResponse = serde_json::from_str(json_data).unwrap();
assert_eq!(response.candidates.len(), 1);
let candidate = &response.candidates[0];
assert_eq!(candidate.content.as_ref().unwrap().parts.len(), 1);
assert_eq!(
candidate.content.as_ref().unwrap().role.as_ref().unwrap(),
&Role::Model
);
assert_eq!(
candidate.finish_reason.as_ref().unwrap(),
&FinishReason::Stop
);
assert_eq!(candidate.safety_ratings.as_ref().unwrap().len(), 1);
assert_eq!(candidate.token_count.unwrap(), 10);
assert_eq!(candidate.index.unwrap(), 0);
let prompt_feedback = response.prompt_feedback.as_ref().unwrap();
assert_eq!(
prompt_feedback.block_reason.as_ref().unwrap(),
&BlockReason::Safety
);
assert_eq!(prompt_feedback.safety_ratings.len(), 1);
let usage_metadata = response.usage_metadata.as_ref().unwrap();
assert_eq!(usage_metadata.prompt_token_count.unwrap(), 5);
assert_eq!(usage_metadata.cached_content_token_count.unwrap(), 3);
assert_eq!(usage_metadata.candidates_token_count.unwrap(), 10);
assert_eq!(usage_metadata.total_token_count.unwrap(), 18);
}
}