1use ferrin_spec::JsonValue;
5use serde::Deserialize;
6
7use crate::json_accumulator::PartialArg;
8
9#[derive(Debug, Clone, Default, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct GenerateContentResponse {
13 #[serde(default)]
15 pub candidates: Option<Vec<Candidate>>,
16 #[serde(default)]
18 pub usage_metadata: Option<UsageMetadata>,
19 #[serde(default)]
21 pub prompt_feedback: Option<JsonValue>,
22 #[serde(default)]
24 pub response_id: Option<String>,
25 #[serde(default)]
27 pub model_version: Option<String>,
28 #[serde(default)]
30 pub create_time: Option<String>,
31}
32
33impl GenerateContentResponse {
34 #[must_use]
36 pub fn block_reason(&self) -> Option<&str> {
37 self.prompt_feedback
38 .as_ref()
39 .and_then(|feedback| feedback.get("blockReason"))
40 .and_then(JsonValue::as_str)
41 }
42
43 #[must_use]
45 pub fn candidate(&self) -> Option<&Candidate> {
46 self.candidates
47 .as_ref()
48 .and_then(|candidates| candidates.first())
49 }
50}
51
52#[derive(Debug, Clone, Default, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct Candidate {
56 #[serde(default)]
58 pub content: Option<CandidateContent>,
59 #[serde(default)]
61 pub finish_reason: Option<String>,
62 #[serde(default)]
64 pub finish_message: Option<String>,
65 #[serde(default)]
67 pub safety_ratings: Option<JsonValue>,
68 #[serde(default)]
70 pub grounding_metadata: Option<JsonValue>,
71 #[serde(default)]
73 pub url_context_metadata: Option<JsonValue>,
74}
75
76impl Candidate {
77 #[must_use]
79 pub fn parts(&self) -> &[Part] {
80 self.content
81 .as_ref()
82 .and_then(|content| content.parts.as_deref())
83 .unwrap_or_default()
84 }
85
86 #[must_use]
88 pub fn grounding_chunks(&self) -> Vec<GroundingChunk> {
89 self.grounding_metadata
90 .as_ref()
91 .and_then(|metadata| metadata.get("groundingChunks"))
92 .and_then(JsonValue::as_array)
93 .map(|chunks| {
94 chunks
95 .iter()
96 .filter_map(|chunk| serde_json::from_value(chunk.clone()).ok())
97 .collect()
98 })
99 .unwrap_or_default()
100 }
101}
102
103#[derive(Debug, Clone, Default, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct CandidateContent {
107 #[serde(default)]
109 pub parts: Option<Vec<Part>>,
110 #[serde(default)]
112 pub role: Option<String>,
113}
114
115#[derive(Debug, Clone, Default, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct Part {
119 #[serde(default)]
121 pub text: Option<String>,
122 #[serde(default)]
124 pub thought: Option<bool>,
125 #[serde(default)]
127 pub thought_signature: Option<String>,
128 #[serde(default)]
130 pub function_call: Option<FunctionCall>,
131 #[serde(default)]
133 pub inline_data: Option<InlineData>,
134 #[serde(default)]
136 pub executable_code: Option<ExecutableCode>,
137 #[serde(default)]
139 pub code_execution_result: Option<CodeExecutionResult>,
140 #[serde(default)]
142 pub tool_call: Option<ServerToolCall>,
143 #[serde(default)]
145 pub tool_response: Option<JsonValue>,
146}
147
148#[derive(Debug, Clone, Default, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct FunctionCall {
152 #[serde(default)]
154 pub id: Option<String>,
155 #[serde(default)]
157 pub name: Option<String>,
158 #[serde(default)]
160 pub args: Option<JsonValue>,
161 #[serde(default)]
163 pub partial_args: Option<Vec<PartialArg>>,
164 #[serde(default)]
166 pub will_continue: Option<bool>,
167}
168
169impl FunctionCall {
170 #[must_use]
173 pub fn is_streaming_fragment(&self) -> bool {
174 self.partial_args.is_some() || (self.name.is_some() && self.will_continue == Some(true))
175 }
176
177 #[must_use]
179 pub fn is_terminal(&self) -> bool {
180 self.name.is_none()
181 && self.args.is_none()
182 && self.partial_args.is_none()
183 && self.will_continue.is_none()
184 }
185
186 #[must_use]
188 pub fn completes_stream(&self) -> bool {
189 self.will_continue != Some(true)
190 && self
191 .partial_args
192 .as_ref()
193 .is_none_or(|args| args.iter().all(|arg| arg.will_continue != Some(true)))
194 }
195}
196
197#[derive(Debug, Clone, Default, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct InlineData {
201 #[serde(default)]
203 pub mime_type: String,
204 #[serde(default)]
206 pub data: String,
207}
208
209#[derive(Debug, Clone, Default, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct ExecutableCode {
213 #[serde(default)]
215 pub language: Option<String>,
216 #[serde(default)]
218 pub code: Option<String>,
219}
220
221#[derive(Debug, Clone, Default, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub struct CodeExecutionResult {
225 #[serde(default)]
227 pub outcome: Option<String>,
228 #[serde(default)]
230 pub output: Option<String>,
231}
232
233#[derive(Debug, Clone, Default, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct ServerToolCall {
237 #[serde(default)]
239 pub tool_type: Option<String>,
240 #[serde(default)]
242 pub args: Option<JsonValue>,
243 #[serde(default)]
245 pub id: Option<String>,
246}
247
248#[derive(Debug, Clone, Default, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct UsageMetadata {
252 #[serde(default)]
254 pub prompt_token_count: Option<u64>,
255 #[serde(default)]
257 pub candidates_token_count: Option<u64>,
258 #[serde(default)]
260 pub cached_content_token_count: Option<u64>,
261 #[serde(default)]
263 pub thoughts_token_count: Option<u64>,
264 #[serde(default)]
266 pub total_token_count: Option<u64>,
267 #[serde(default)]
269 pub service_tier: Option<String>,
270}
271
272#[derive(Debug, Clone, Default, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct GroundingChunk {
276 #[serde(default)]
278 pub web: Option<WebChunk>,
279 #[serde(default)]
281 pub image: Option<ImageChunk>,
282 #[serde(default)]
284 pub retrieved_context: Option<RetrievedContextChunk>,
285 #[serde(default)]
287 pub maps: Option<WebChunk>,
288}
289
290#[derive(Debug, Clone, Default, Deserialize)]
292#[serde(rename_all = "camelCase")]
293pub struct WebChunk {
294 #[serde(default)]
296 pub uri: Option<String>,
297 #[serde(default)]
299 pub title: Option<String>,
300}
301
302#[derive(Debug, Clone, Default, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct ImageChunk {
306 #[serde(default)]
308 pub source_uri: Option<String>,
309 #[serde(default)]
311 pub image_uri: Option<String>,
312 #[serde(default)]
314 pub title: Option<String>,
315}
316
317#[derive(Debug, Clone, Default, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct RetrievedContextChunk {
321 #[serde(default)]
323 pub uri: Option<String>,
324 #[serde(default)]
326 pub title: Option<String>,
327 #[serde(default)]
329 pub text: Option<String>,
330 #[serde(default)]
332 pub file_search_store: Option<String>,
333}
334
335#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
337pub struct RpcStatus {
338 #[serde(default)]
340 pub code: Option<i64>,
341 #[serde(default)]
343 pub message: Option<String>,
344 #[serde(default)]
346 pub status: Option<String>,
347}
348
349pub fn deserialize_count<'de, D: serde::Deserializer<'de>>(
356 deserializer: D,
357) -> Result<Option<u64>, D::Error> {
358 let value = Option::<JsonValue>::deserialize(deserializer)?;
359 Ok(match value {
360 Some(JsonValue::String(text)) => text.parse().ok(),
361 Some(JsonValue::Number(number)) => number.as_u64(),
362 _ => None,
363 })
364}