use ferrin_spec::JsonValue;
use serde::Deserialize;
use crate::json_accumulator::PartialArg;
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GenerateContentResponse {
#[serde(default)]
pub candidates: Option<Vec<Candidate>>,
#[serde(default)]
pub usage_metadata: Option<UsageMetadata>,
#[serde(default)]
pub prompt_feedback: Option<JsonValue>,
#[serde(default)]
pub response_id: Option<String>,
#[serde(default)]
pub model_version: Option<String>,
#[serde(default)]
pub create_time: Option<String>,
}
impl GenerateContentResponse {
#[must_use]
pub fn block_reason(&self) -> Option<&str> {
self.prompt_feedback
.as_ref()
.and_then(|feedback| feedback.get("blockReason"))
.and_then(JsonValue::as_str)
}
#[must_use]
pub fn candidate(&self) -> Option<&Candidate> {
self.candidates
.as_ref()
.and_then(|candidates| candidates.first())
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Candidate {
#[serde(default)]
pub content: Option<CandidateContent>,
#[serde(default)]
pub finish_reason: Option<String>,
#[serde(default)]
pub finish_message: Option<String>,
#[serde(default)]
pub safety_ratings: Option<JsonValue>,
#[serde(default)]
pub grounding_metadata: Option<JsonValue>,
#[serde(default)]
pub url_context_metadata: Option<JsonValue>,
}
impl Candidate {
#[must_use]
pub fn parts(&self) -> &[Part] {
self.content
.as_ref()
.and_then(|content| content.parts.as_deref())
.unwrap_or_default()
}
#[must_use]
pub fn grounding_chunks(&self) -> Vec<GroundingChunk> {
self.grounding_metadata
.as_ref()
.and_then(|metadata| metadata.get("groundingChunks"))
.and_then(JsonValue::as_array)
.map(|chunks| {
chunks
.iter()
.filter_map(|chunk| serde_json::from_value(chunk.clone()).ok())
.collect()
})
.unwrap_or_default()
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandidateContent {
#[serde(default)]
pub parts: Option<Vec<Part>>,
#[serde(default)]
pub role: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Part {
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub thought: Option<bool>,
#[serde(default)]
pub thought_signature: Option<String>,
#[serde(default)]
pub function_call: Option<FunctionCall>,
#[serde(default)]
pub inline_data: Option<InlineData>,
#[serde(default)]
pub executable_code: Option<ExecutableCode>,
#[serde(default)]
pub code_execution_result: Option<CodeExecutionResult>,
#[serde(default)]
pub tool_call: Option<ServerToolCall>,
#[serde(default)]
pub tool_response: Option<JsonValue>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FunctionCall {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub args: Option<JsonValue>,
#[serde(default)]
pub partial_args: Option<Vec<PartialArg>>,
#[serde(default)]
pub will_continue: Option<bool>,
}
impl FunctionCall {
#[must_use]
pub fn is_streaming_fragment(&self) -> bool {
self.partial_args.is_some() || (self.name.is_some() && self.will_continue == Some(true))
}
#[must_use]
pub fn is_terminal(&self) -> bool {
self.name.is_none()
&& self.args.is_none()
&& self.partial_args.is_none()
&& self.will_continue.is_none()
}
#[must_use]
pub fn completes_stream(&self) -> bool {
self.will_continue != Some(true)
&& self
.partial_args
.as_ref()
.is_none_or(|args| args.iter().all(|arg| arg.will_continue != Some(true)))
}
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InlineData {
#[serde(default)]
pub mime_type: String,
#[serde(default)]
pub data: String,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutableCode {
#[serde(default)]
pub language: Option<String>,
#[serde(default)]
pub code: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeExecutionResult {
#[serde(default)]
pub outcome: Option<String>,
#[serde(default)]
pub output: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerToolCall {
#[serde(default)]
pub tool_type: Option<String>,
#[serde(default)]
pub args: Option<JsonValue>,
#[serde(default)]
pub id: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UsageMetadata {
#[serde(default)]
pub prompt_token_count: Option<u64>,
#[serde(default)]
pub candidates_token_count: Option<u64>,
#[serde(default)]
pub cached_content_token_count: Option<u64>,
#[serde(default)]
pub thoughts_token_count: Option<u64>,
#[serde(default)]
pub total_token_count: Option<u64>,
#[serde(default)]
pub service_tier: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GroundingChunk {
#[serde(default)]
pub web: Option<WebChunk>,
#[serde(default)]
pub image: Option<ImageChunk>,
#[serde(default)]
pub retrieved_context: Option<RetrievedContextChunk>,
#[serde(default)]
pub maps: Option<WebChunk>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WebChunk {
#[serde(default)]
pub uri: Option<String>,
#[serde(default)]
pub title: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageChunk {
#[serde(default)]
pub source_uri: Option<String>,
#[serde(default)]
pub image_uri: Option<String>,
#[serde(default)]
pub title: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetrievedContextChunk {
#[serde(default)]
pub uri: Option<String>,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub text: Option<String>,
#[serde(default)]
pub file_search_store: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct RpcStatus {
#[serde(default)]
pub code: Option<i64>,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub status: Option<String>,
}
pub fn deserialize_count<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<u64>, D::Error> {
let value = Option::<JsonValue>::deserialize(deserializer)?;
Ok(match value {
Some(JsonValue::String(text)) => text.parse().ok(),
Some(JsonValue::Number(number)) => number.as_u64(),
_ => None,
})
}