Skip to main content

ferrin_google/
output.rs

1//! Mapping of `generateContent` responses to specification content, usage,
2//! finish reasons and provider metadata.
3
4use base64::Engine;
5use bytes::Bytes;
6use ferrin_provider_util::tool_name_mapping::ToolNameMapping;
7use ferrin_spec::Content;
8use ferrin_spec::FileData;
9use ferrin_spec::FinishReason;
10use ferrin_spec::FinishReasonKind;
11use ferrin_spec::JsonObject;
12use ferrin_spec::JsonValue;
13use ferrin_spec::MediaType;
14use ferrin_spec::ProviderMetadata;
15use ferrin_spec::ToolCall;
16use ferrin_spec::Usage;
17use ferrin_spec::error::InvalidResponseDataError;
18use ferrin_spec::error::ProviderError;
19use ferrin_spec::language_model::InputTokens;
20use ferrin_spec::language_model::OutputTokens;
21use ferrin_spec::language_model::ProviderToolResult;
22use ferrin_spec::language_model::Source;
23use serde_json::json;
24
25use crate::api_types::Candidate;
26use crate::api_types::GenerateContentResponse;
27use crate::api_types::GroundingChunk;
28use crate::api_types::Part;
29use crate::api_types::UsageMetadata;
30use crate::config::CANONICAL_OPTIONS_KEY;
31use crate::config::SharedConfig;
32use crate::prepare_tools::CODE_EXECUTION_TOOL_NAME;
33
34/// Maps a Gemini finish reason.
35#[must_use]
36pub fn map_finish_reason(reason: Option<&str>, has_tool_calls: bool) -> FinishReason {
37    let unified = match reason {
38        Some("STOP") if has_tool_calls => FinishReasonKind::ToolCalls,
39        Some("STOP") => FinishReasonKind::Stop,
40        Some("MAX_TOKENS") => FinishReasonKind::Length,
41        Some(
42            "IMAGE_SAFETY" | "RECITATION" | "SAFETY" | "BLOCKLIST" | "PROHIBITED_CONTENT" | "SPII",
43        ) => FinishReasonKind::ContentFilter,
44        Some("MALFORMED_FUNCTION_CALL") => FinishReasonKind::Error,
45        _ => FinishReasonKind::Other,
46    };
47    match reason {
48        Some(raw) => FinishReason::with_raw(unified, raw),
49        None => FinishReason::new(unified),
50    }
51}
52
53/// Converts `usageMetadata`; `raw` is the original object.
54#[must_use]
55pub fn convert_usage(usage: Option<&UsageMetadata>, raw: Option<JsonObject>) -> Usage {
56    let Some(usage) = usage else {
57        return Usage::default();
58    };
59    let prompt = usage.prompt_token_count.unwrap_or_default();
60    let candidates = usage.candidates_token_count.unwrap_or_default();
61    let cached = usage.cached_content_token_count.unwrap_or_default();
62    let thoughts = usage.thoughts_token_count.unwrap_or_default();
63    Usage {
64        input: InputTokens {
65            total: Some(prompt),
66            no_cache: Some(prompt.saturating_sub(cached)),
67            cache_read: Some(cached),
68            cache_write: None,
69        },
70        output: OutputTokens {
71            total: Some(candidates + thoughts),
72            text: Some(candidates),
73            reasoning: Some(thoughts),
74        },
75        raw,
76    }
77}
78
79/// Whether `content` contains a tool call executed by the client.
80#[must_use]
81pub fn has_client_tool_calls(content: &[Content]) -> bool {
82    content
83        .iter()
84        .any(|part| matches!(part, Content::ToolCall(call) if !call.provider_executed))
85}
86
87/// Media type of a retrieved document by file extension.
88#[must_use]
89pub fn document_media_type(uri: &str) -> &'static str {
90    let lower = uri.to_ascii_lowercase();
91    if lower.ends_with(".pdf") {
92        "application/pdf"
93    } else if lower.ends_with(".txt") {
94        "text/plain"
95    } else if lower.ends_with(".docx") {
96        "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
97    } else if lower.ends_with(".doc") {
98        "application/msword"
99    } else if lower.ends_with(".md") || lower.ends_with(".markdown") {
100        "text/markdown"
101    } else {
102        "application/octet-stream"
103    }
104}
105
106fn last_segment(path: &str) -> Option<String> {
107    path.rsplit('/')
108        .next()
109        .filter(|segment| !segment.is_empty())
110        .map(str::to_owned)
111}
112
113fn decode_base64(data: &str) -> Result<Bytes, ProviderError> {
114    base64::engine::general_purpose::STANDARD
115        .decode(data)
116        .map(Bytes::from)
117        .map_err(|error| {
118            ProviderError::InvalidResponseData(Box::new(InvalidResponseDataError::new(
119                format!("invalid base64 inline data: {error}"),
120                JsonValue::Null,
121            )))
122        })
123}
124
125/// Maps response parts to content and builds provider metadata.
126#[derive(Debug, Clone)]
127pub struct OutputMapper {
128    config: SharedConfig,
129    mapping: ToolNameMapping,
130    last_code_execution_id: Option<String>,
131    last_server_tool_call_id: Option<String>,
132}
133
134impl OutputMapper {
135    /// Creates a mapper.
136    #[must_use]
137    pub fn new(config: SharedConfig, mapping: ToolNameMapping) -> Self {
138        Self {
139            config,
140            mapping,
141            last_code_execution_id: None,
142            last_server_tool_call_id: None,
143        }
144    }
145
146    /// Wraps `object` under the `google` key (and the configured name when it
147    /// differs).
148    #[must_use]
149    pub fn metadata(&self, object: JsonObject) -> ProviderMetadata {
150        let mut metadata = ProviderMetadata::new();
151        if self.config.options_key() != CANONICAL_OPTIONS_KEY {
152            metadata.insert(self.config.options_key().to_owned(), object.clone());
153        }
154        metadata.insert(CANONICAL_OPTIONS_KEY.to_owned(), object);
155        metadata
156    }
157
158    /// Like [`Self::metadata`], `None` for an empty object.
159    #[must_use]
160    pub fn metadata_opt(&self, object: JsonObject) -> Option<ProviderMetadata> {
161        (!object.is_empty()).then(|| self.metadata(object))
162    }
163
164    /// Metadata carrying only a thought signature.
165    #[must_use]
166    pub fn signature_metadata(&self, signature: Option<&str>) -> Option<ProviderMetadata> {
167        signature.map(|signature| {
168            let mut object = JsonObject::new();
169            object.insert("thoughtSignature".to_owned(), JsonValue::from(signature));
170            self.metadata(object)
171        })
172    }
173
174    /// Custom name of the code execution tool.
175    #[must_use]
176    pub fn code_execution_name(&self) -> String {
177        self.mapping
178            .to_custom_tool_name(CODE_EXECUTION_TOOL_NAME)
179            .to_owned()
180    }
181
182    /// Custom name of a function.
183    #[must_use]
184    pub fn custom_tool_name(&self, provider_name: &str) -> String {
185        self.mapping.to_custom_tool_name(provider_name).to_owned()
186    }
187
188    /// Generates an id.
189    #[must_use]
190    pub fn generate_id(&self) -> String {
191        self.config.generate_id()
192    }
193
194    /// Maps a code execution part to a provider-executed tool call.
195    #[must_use]
196    pub fn code_execution_call(&mut self, language: Option<&str>, code: Option<&str>) -> ToolCall {
197        self.code_execution_call_with_signature(language, code, None)
198    }
199
200    pub(crate) fn code_execution_call_with_signature(
201        &mut self,
202        language: Option<&str>,
203        code: Option<&str>,
204        signature: Option<&str>,
205    ) -> ToolCall {
206        let id = self.generate_id();
207        self.last_code_execution_id = Some(id.clone());
208        let mut call = ToolCall::new(
209            id,
210            self.code_execution_name(),
211            json!({"language": language, "code": code}).to_string(),
212        );
213        call.provider_executed = true;
214        call.provider_metadata = Some(self.code_execution_metadata(signature));
215        call
216    }
217
218    /// Maps a code execution result.
219    #[must_use]
220    pub fn code_execution_result(
221        &mut self,
222        outcome: Option<&str>,
223        output: Option<&str>,
224    ) -> ProviderToolResult {
225        self.code_execution_result_with_signature(outcome, output, None)
226    }
227
228    pub(crate) fn code_execution_result_with_signature(
229        &mut self,
230        outcome: Option<&str>,
231        output: Option<&str>,
232        signature: Option<&str>,
233    ) -> ProviderToolResult {
234        let id = self
235            .last_code_execution_id
236            .take()
237            .unwrap_or_else(|| self.generate_id());
238        ProviderToolResult {
239            tool_call_id: id.into(),
240            tool_name: self.code_execution_name().into(),
241            result: json!({"outcome": outcome, "output": output.unwrap_or_default()}),
242            is_error: false,
243            preliminary: false,
244            dynamic: false,
245            provider_metadata: Some(self.code_execution_metadata(signature)),
246        }
247    }
248
249    fn code_execution_metadata(&self, signature: Option<&str>) -> ProviderMetadata {
250        let mut object = JsonObject::new();
251        object.insert(
252            "serverToolType".to_owned(),
253            JsonValue::from("code_execution"),
254        );
255        if let Some(signature) = signature {
256            object.insert("thoughtSignature".to_owned(), JsonValue::from(signature));
257        }
258        self.metadata(object)
259    }
260
261    /// Maps a function call part.
262    #[must_use]
263    pub fn function_call(
264        &self,
265        id: Option<&str>,
266        name: &str,
267        args: Option<&JsonValue>,
268        signature: Option<&str>,
269    ) -> ToolCall {
270        let id = id
271            .filter(|id| !id.is_empty())
272            .map_or_else(|| self.generate_id(), str::to_owned);
273        let input = args
274            .cloned()
275            .unwrap_or_else(|| JsonValue::Object(JsonObject::new()))
276            .to_string();
277        let mut call = ToolCall::new(id, self.custom_tool_name(name), input);
278        call.provider_metadata = self.signature_metadata(signature);
279        call
280    }
281
282    /// Maps a server-side tool call part.
283    #[must_use]
284    pub fn server_tool_call(
285        &mut self,
286        tool_type: Option<&str>,
287        args: Option<&JsonValue>,
288        id: Option<&str>,
289        signature: Option<&str>,
290    ) -> ToolCall {
291        let tool_type = tool_type.unwrap_or("unknown");
292        let id = id
293            .filter(|id| !id.is_empty())
294            .map_or_else(|| self.generate_id(), str::to_owned);
295        self.last_server_tool_call_id = Some(id.clone());
296        let mut call = ToolCall::new(
297            id.clone(),
298            format!("server:{tool_type}"),
299            args.cloned()
300                .unwrap_or_else(|| JsonValue::Object(JsonObject::new()))
301                .to_string(),
302        );
303        call.provider_executed = true;
304        call.dynamic = true;
305        let mut object = JsonObject::new();
306        if let Some(signature) = signature {
307            object.insert("thoughtSignature".to_owned(), JsonValue::from(signature));
308        }
309        object.insert("serverToolCallId".to_owned(), JsonValue::from(id));
310        object.insert("serverToolType".to_owned(), JsonValue::from(tool_type));
311        call.provider_metadata = Some(self.metadata(object));
312        call
313    }
314
315    /// Maps a server-side tool response part.
316    #[must_use]
317    pub fn server_tool_response(&mut self, response: &JsonValue) -> ProviderToolResult {
318        let tool_type = response
319            .get("toolType")
320            .and_then(JsonValue::as_str)
321            .unwrap_or("unknown");
322        let id = self
323            .last_server_tool_call_id
324            .take()
325            .or_else(|| {
326                response
327                    .get("id")
328                    .and_then(JsonValue::as_str)
329                    .map(str::to_owned)
330            })
331            .unwrap_or_else(|| self.generate_id());
332        ProviderToolResult {
333            tool_call_id: id.into(),
334            tool_name: format!("server:{tool_type}").into(),
335            result: response.clone(),
336            is_error: false,
337            preliminary: false,
338            dynamic: true,
339            provider_metadata: None,
340        }
341    }
342
343    /// Decodes an inline data part into a file or reasoning file.
344    ///
345    /// # Errors
346    ///
347    /// Returns [`ProviderError::InvalidResponseData`] for invalid base64.
348    pub fn inline_file(
349        &self,
350        mime_type: &str,
351        data: &str,
352        thought: bool,
353        signature: Option<&str>,
354    ) -> Result<Content, ProviderError> {
355        let bytes = decode_base64(data)?;
356        let media_type = MediaType::new(mime_type);
357        Ok(if thought {
358            Content::ReasoningFile {
359                data: FileData::Bytes { data: bytes },
360                media_type,
361                provider_metadata: self.signature_metadata(signature),
362            }
363        } else {
364            Content::File {
365                data: FileData::Bytes { data: bytes },
366                media_type,
367                filename: None,
368                provider_metadata: self.signature_metadata(signature),
369            }
370        })
371    }
372
373    /// Maps the parts of a non-streaming candidate.
374    ///
375    /// # Errors
376    ///
377    /// Returns [`ProviderError::InvalidResponseData`] for invalid inline data.
378    pub fn map_parts(&mut self, parts: &[Part]) -> Result<Vec<Content>, ProviderError> {
379        let mut content: Vec<Content> = Vec::new();
380        for part in parts {
381            let signature = part.thought_signature.as_deref();
382            if let Some(code) = &part.executable_code {
383                content.push(Content::ToolCall(self.code_execution_call_with_signature(
384                    code.language.as_deref(),
385                    code.code.as_deref(),
386                    signature,
387                )));
388            }
389            if let Some(result) = &part.code_execution_result {
390                content.push(Content::ToolResult(
391                    self.code_execution_result_with_signature(
392                        result.outcome.as_deref(),
393                        result.output.as_deref(),
394                        signature,
395                    ),
396                ));
397            }
398            if let Some(text) = &part.text {
399                if text.is_empty() {
400                    if let Some(signature) = signature {
401                        attach_signature(&mut content, self.signature_metadata(Some(signature)));
402                    }
403                } else if part.thought == Some(true) {
404                    content.push(Content::Reasoning {
405                        text: text.clone(),
406                        provider_metadata: self.signature_metadata(signature),
407                    });
408                } else {
409                    content.push(Content::Text {
410                        text: text.clone(),
411                        provider_metadata: self.signature_metadata(signature),
412                    });
413                }
414            }
415            if let Some(call) = &part.function_call
416                && let Some(name) = &call.name
417            {
418                content.push(Content::ToolCall(self.function_call(
419                    call.id.as_deref(),
420                    name,
421                    call.args.as_ref(),
422                    signature,
423                )));
424            }
425            if let Some(inline) = &part.inline_data {
426                content.push(self.inline_file(
427                    &inline.mime_type,
428                    &inline.data,
429                    part.thought == Some(true),
430                    signature,
431                )?);
432            }
433            if let Some(call) = &part.tool_call {
434                content.push(Content::ToolCall(self.server_tool_call(
435                    call.tool_type.as_deref(),
436                    call.args.as_ref(),
437                    call.id.as_deref(),
438                    signature,
439                )));
440            }
441            if let Some(response) = &part.tool_response {
442                content.push(Content::ToolResult(self.server_tool_response(response)));
443            }
444        }
445        Ok(content)
446    }
447
448    /// Maps grounding chunks to sources.
449    #[must_use]
450    pub fn sources(&self, chunks: &[GroundingChunk]) -> Vec<Source> {
451        let mut sources = Vec::new();
452        for chunk in chunks {
453            if let Some(web) = &chunk.web
454                && let Some(uri) = &web.uri
455            {
456                sources.push(Source::Url {
457                    id: self.generate_id(),
458                    url: uri.clone(),
459                    title: web.title.clone(),
460                    provider_metadata: None,
461                });
462            }
463            if let Some(image) = &chunk.image
464                && let Some(uri) = image.source_uri.as_ref().or(image.image_uri.as_ref())
465            {
466                sources.push(Source::Url {
467                    id: self.generate_id(),
468                    url: uri.clone(),
469                    title: image.title.clone(),
470                    provider_metadata: None,
471                });
472            }
473            if let Some(context) = &chunk.retrieved_context {
474                if let Some(uri) = &context.uri {
475                    if uri.starts_with("http://") || uri.starts_with("https://") {
476                        sources.push(Source::Url {
477                            id: self.generate_id(),
478                            url: uri.clone(),
479                            title: context.title.clone(),
480                            provider_metadata: None,
481                        });
482                    } else {
483                        sources.push(Source::Document {
484                            id: self.generate_id(),
485                            media_type: MediaType::new(document_media_type(uri)),
486                            title: context
487                                .title
488                                .clone()
489                                .unwrap_or_else(|| "Unknown Document".to_owned()),
490                            filename: last_segment(uri),
491                            provider_metadata: None,
492                        });
493                    }
494                } else if let Some(store) = &context.file_search_store {
495                    sources.push(Source::Document {
496                        id: self.generate_id(),
497                        media_type: MediaType::new("application/octet-stream"),
498                        title: context
499                            .title
500                            .clone()
501                            .unwrap_or_else(|| "Unknown Document".to_owned()),
502                        filename: last_segment(store),
503                        provider_metadata: None,
504                    });
505                }
506            }
507            if let Some(maps) = &chunk.maps
508                && let Some(uri) = &maps.uri
509            {
510                sources.push(Source::Url {
511                    id: self.generate_id(),
512                    url: uri.clone(),
513                    title: maps.title.clone(),
514                    provider_metadata: None,
515                });
516            }
517        }
518        sources
519    }
520
521    /// Result-level provider metadata.
522    #[must_use]
523    pub fn response_metadata(
524        &self,
525        response: &GenerateContentResponse,
526        candidate: Option<&Candidate>,
527        raw_usage: Option<&JsonValue>,
528    ) -> ProviderMetadata {
529        let mut object = JsonObject::new();
530        object.insert(
531            "promptFeedback".to_owned(),
532            response.prompt_feedback.clone().unwrap_or(JsonValue::Null),
533        );
534        object.insert(
535            "groundingMetadata".to_owned(),
536            candidate
537                .and_then(|candidate| candidate.grounding_metadata.clone())
538                .unwrap_or(JsonValue::Null),
539        );
540        object.insert(
541            "urlContextMetadata".to_owned(),
542            candidate
543                .and_then(|candidate| candidate.url_context_metadata.clone())
544                .unwrap_or(JsonValue::Null),
545        );
546        object.insert(
547            "safetyRatings".to_owned(),
548            candidate
549                .and_then(|candidate| candidate.safety_ratings.clone())
550                .unwrap_or(JsonValue::Null),
551        );
552        object.insert(
553            "usageMetadata".to_owned(),
554            raw_usage.cloned().unwrap_or(JsonValue::Null),
555        );
556        object.insert(
557            "finishMessage".to_owned(),
558            candidate
559                .and_then(|candidate| candidate.finish_message.clone())
560                .map_or(JsonValue::Null, JsonValue::from),
561        );
562        object.insert(
563            "serviceTier".to_owned(),
564            response
565                .usage_metadata
566                .as_ref()
567                .and_then(|usage| usage.service_tier.clone())
568                .map_or(JsonValue::Null, JsonValue::from),
569        );
570        self.metadata(object)
571    }
572}
573
574/// Attaches `metadata` to the last content part that can carry it.
575fn attach_signature(content: &mut [Content], metadata: Option<ProviderMetadata>) {
576    let Some(last) = content.last_mut() else {
577        return;
578    };
579    match last {
580        Content::Text {
581            provider_metadata, ..
582        }
583        | Content::Reasoning {
584            provider_metadata, ..
585        }
586        | Content::File {
587            provider_metadata, ..
588        }
589        | Content::ReasoningFile {
590            provider_metadata, ..
591        } => merge_metadata(provider_metadata, metadata),
592        Content::ToolCall(call) => merge_metadata(&mut call.provider_metadata, metadata),
593        _ => {}
594    }
595}
596
597fn merge_metadata(target: &mut Option<ProviderMetadata>, extra: Option<ProviderMetadata>) {
598    let Some(extra) = extra else {
599        return;
600    };
601    match target {
602        Some(existing) => {
603            for (key, object) in extra {
604                existing.entry(key).or_default().extend(object);
605            }
606        }
607        None => *target = Some(extra),
608    }
609}
610
611/// `usageMetadata` object of a raw response.
612#[must_use]
613pub fn raw_usage(raw: Option<&JsonValue>) -> Option<JsonObject> {
614    raw?.get("usageMetadata")?.as_object().cloned()
615}