Skip to main content

ferrin_google/
api_types.rs

1//! Wire types of the `generateContent` responses (requests are built as JSON
2//! objects in [`crate::request`]).
3
4use ferrin_spec::JsonValue;
5use serde::Deserialize;
6
7use crate::json_accumulator::PartialArg;
8
9/// A `generateContent` response or stream chunk.
10#[derive(Debug, Clone, Default, Deserialize)]
11#[serde(rename_all = "camelCase")]
12pub struct GenerateContentResponse {
13    /// Candidates (the first one is used).
14    #[serde(default)]
15    pub candidates: Option<Vec<Candidate>>,
16    /// Token usage.
17    #[serde(default)]
18    pub usage_metadata: Option<UsageMetadata>,
19    /// Prompt feedback (`{blockReason, blockReasonMessage, safetyRatings}`).
20    #[serde(default)]
21    pub prompt_feedback: Option<JsonValue>,
22    /// Response id.
23    #[serde(default)]
24    pub response_id: Option<String>,
25    /// Model version that produced the response.
26    #[serde(default)]
27    pub model_version: Option<String>,
28    /// Creation time (RFC 3339).
29    #[serde(default)]
30    pub create_time: Option<String>,
31}
32
33impl GenerateContentResponse {
34    /// `promptFeedback.blockReason`.
35    #[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    /// The first candidate.
44    #[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/// A response candidate.
53#[derive(Debug, Clone, Default, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct Candidate {
56    /// Generated content.
57    #[serde(default)]
58    pub content: Option<CandidateContent>,
59    /// Finish reason (`STOP`, `MAX_TOKENS`, `SAFETY`, ...).
60    #[serde(default)]
61    pub finish_reason: Option<String>,
62    /// Finish message.
63    #[serde(default)]
64    pub finish_message: Option<String>,
65    /// Safety ratings.
66    #[serde(default)]
67    pub safety_ratings: Option<JsonValue>,
68    /// Grounding metadata (search and file search results).
69    #[serde(default)]
70    pub grounding_metadata: Option<JsonValue>,
71    /// URL context metadata.
72    #[serde(default)]
73    pub url_context_metadata: Option<JsonValue>,
74}
75
76impl Candidate {
77    /// Parts of the candidate content.
78    #[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    /// Grounding chunks of the grounding metadata.
87    #[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/// Content of a candidate.
104#[derive(Debug, Clone, Default, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct CandidateContent {
107    /// Parts.
108    #[serde(default)]
109    pub parts: Option<Vec<Part>>,
110    /// Role (`model`).
111    #[serde(default)]
112    pub role: Option<String>,
113}
114
115/// A content part.
116#[derive(Debug, Clone, Default, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct Part {
119    /// Text.
120    #[serde(default)]
121    pub text: Option<String>,
122    /// Whether the part is a thought summary.
123    #[serde(default)]
124    pub thought: Option<bool>,
125    /// Thought signature to send back with the part.
126    #[serde(default)]
127    pub thought_signature: Option<String>,
128    /// Function call.
129    #[serde(default)]
130    pub function_call: Option<FunctionCall>,
131    /// Inline binary data.
132    #[serde(default)]
133    pub inline_data: Option<InlineData>,
134    /// Code produced by the code execution tool.
135    #[serde(default)]
136    pub executable_code: Option<ExecutableCode>,
137    /// Result of the code execution tool.
138    #[serde(default)]
139    pub code_execution_result: Option<CodeExecutionResult>,
140    /// Server-side tool call.
141    #[serde(default)]
142    pub tool_call: Option<ServerToolCall>,
143    /// Server-side tool response (opaque).
144    #[serde(default)]
145    pub tool_response: Option<JsonValue>,
146}
147
148/// A function call part.
149#[derive(Debug, Clone, Default, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct FunctionCall {
152    /// Call id.
153    #[serde(default)]
154    pub id: Option<String>,
155    /// Function name.
156    #[serde(default)]
157    pub name: Option<String>,
158    /// Complete arguments.
159    #[serde(default)]
160    pub args: Option<JsonValue>,
161    /// Streamed argument fragments.
162    #[serde(default)]
163    pub partial_args: Option<Vec<PartialArg>>,
164    /// Whether more fragments of this call follow.
165    #[serde(default)]
166    pub will_continue: Option<bool>,
167}
168
169impl FunctionCall {
170    /// A fragment of a streamed call (partial arguments, or a name announced
171    /// with `willContinue`).
172    #[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    /// The `{}` fragment that terminates a streamed call.
178    #[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    /// Whether this fragment completes the streamed call.
187    #[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/// Inline binary data (`{mimeType, data}` with base64 data).
198#[derive(Debug, Clone, Default, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct InlineData {
201    /// Media type.
202    #[serde(default)]
203    pub mime_type: String,
204    /// Base64 data.
205    #[serde(default)]
206    pub data: String,
207}
208
209/// Code generated by the code execution tool.
210#[derive(Debug, Clone, Default, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct ExecutableCode {
213    /// Language (`PYTHON`).
214    #[serde(default)]
215    pub language: Option<String>,
216    /// Code.
217    #[serde(default)]
218    pub code: Option<String>,
219}
220
221/// Result of the code execution tool.
222#[derive(Debug, Clone, Default, Deserialize)]
223#[serde(rename_all = "camelCase")]
224pub struct CodeExecutionResult {
225    /// Outcome (`OUTCOME_OK`, ...).
226    #[serde(default)]
227    pub outcome: Option<String>,
228    /// Output.
229    #[serde(default)]
230    pub output: Option<String>,
231}
232
233/// A server-side tool call (`{toolType, args, id}`).
234#[derive(Debug, Clone, Default, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct ServerToolCall {
237    /// Tool type.
238    #[serde(default)]
239    pub tool_type: Option<String>,
240    /// Arguments.
241    #[serde(default)]
242    pub args: Option<JsonValue>,
243    /// Call id.
244    #[serde(default)]
245    pub id: Option<String>,
246}
247
248/// Token usage (`usageMetadata`).
249#[derive(Debug, Clone, Default, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct UsageMetadata {
252    /// Prompt tokens.
253    #[serde(default)]
254    pub prompt_token_count: Option<u64>,
255    /// Candidate (text output) tokens.
256    #[serde(default)]
257    pub candidates_token_count: Option<u64>,
258    /// Cached prompt tokens.
259    #[serde(default)]
260    pub cached_content_token_count: Option<u64>,
261    /// Thinking tokens.
262    #[serde(default)]
263    pub thoughts_token_count: Option<u64>,
264    /// Total tokens.
265    #[serde(default)]
266    pub total_token_count: Option<u64>,
267    /// Service tier that served the request.
268    #[serde(default)]
269    pub service_tier: Option<String>,
270}
271
272/// A grounding chunk.
273#[derive(Debug, Clone, Default, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct GroundingChunk {
276    /// Web result.
277    #[serde(default)]
278    pub web: Option<WebChunk>,
279    /// Image result.
280    #[serde(default)]
281    pub image: Option<ImageChunk>,
282    /// Retrieved context (file search, RAG).
283    #[serde(default)]
284    pub retrieved_context: Option<RetrievedContextChunk>,
285    /// Maps result.
286    #[serde(default)]
287    pub maps: Option<WebChunk>,
288}
289
290/// A web (or maps) grounding chunk.
291#[derive(Debug, Clone, Default, Deserialize)]
292#[serde(rename_all = "camelCase")]
293pub struct WebChunk {
294    /// URI.
295    #[serde(default)]
296    pub uri: Option<String>,
297    /// Title.
298    #[serde(default)]
299    pub title: Option<String>,
300}
301
302/// An image grounding chunk.
303#[derive(Debug, Clone, Default, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct ImageChunk {
306    /// Page the image was found on.
307    #[serde(default)]
308    pub source_uri: Option<String>,
309    /// Image URI.
310    #[serde(default)]
311    pub image_uri: Option<String>,
312    /// Title.
313    #[serde(default)]
314    pub title: Option<String>,
315}
316
317/// A retrieved-context grounding chunk.
318#[derive(Debug, Clone, Default, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct RetrievedContextChunk {
321    /// Document URI.
322    #[serde(default)]
323    pub uri: Option<String>,
324    /// Title.
325    #[serde(default)]
326    pub title: Option<String>,
327    /// Retrieved text.
328    #[serde(default)]
329    pub text: Option<String>,
330    /// File search store name.
331    #[serde(default)]
332    pub file_search_store: Option<String>,
333}
334
335/// `google.rpc.Status` as embedded in operations and batch result lines.
336#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
337pub struct RpcStatus {
338    /// Canonical error code.
339    #[serde(default)]
340    pub code: Option<i64>,
341    /// Message.
342    #[serde(default)]
343    pub message: Option<String>,
344    /// Status name (`CANCELLED`, `INVALID_ARGUMENT`, ...).
345    #[serde(default)]
346    pub status: Option<String>,
347}
348
349/// Deserializes a count that the API renders either as a number or as a
350/// decimal string (`"42"`); anything else becomes `None`.
351///
352/// # Errors
353///
354/// Returns the deserializer's error when the value is not valid JSON.
355pub 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}