gemini_genai_rs/generate/
mod.rs

1//! generateContent and streamGenerateContent REST API.
2//!
3//! This module provides typed request/response types and a client for the
4//! Gemini generateContent REST API. Feature-gated behind `generate`.
5//!
6//! # Usage
7//!
8//! ```no_run
9//! use gemini_genai_rs::{Client, prelude::ModelId};
10//!
11//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
12//! let client = Client::from_api_key("your-key").model(ModelId::FLASH_LATEST);
13//!
14//! let response = client.generate_content("What is Rust?").await?;
15//! println!("{}", response.text().unwrap_or_default());
16//! # Ok(())
17//! # }
18//! ```
19
20mod config;
21mod response;
22
23pub use config::GenerateContentConfig;
24pub use response::{BlockReason, Candidate, GenerateContentResponse, PromptFeedback};
25
26use crate::client::Client;
27use crate::client::http::HttpError;
28use crate::protocol::types::ModelId;
29use crate::transport::auth::ServiceEndpoint;
30
31impl Client {
32    /// Generate content from a text prompt using the default model.
33    pub async fn generate_content(
34        &self,
35        prompt: impl Into<String>,
36    ) -> Result<GenerateContentResponse, GenerateError> {
37        let config = GenerateContentConfig::from_text(prompt);
38        self.generate_content_with(config, None).await
39    }
40
41    /// Generate content with full configuration and optional model override.
42    pub async fn generate_content_with(
43        &self,
44        config: GenerateContentConfig,
45        model: Option<&ModelId>,
46    ) -> Result<GenerateContentResponse, GenerateError> {
47        let model = model.unwrap_or(self.default_model());
48        let url = self.rest_url_for(ServiceEndpoint::GenerateContent, model);
49        let headers = self.auth_headers().await?;
50
51        let body = config.to_request_body();
52        let json = self
53            .http_client()
54            .post_json(&url, headers, &body)
55            .await
56            .map_err(GenerateError::from)?;
57
58        let response: GenerateContentResponse = serde_json::from_value(json)?;
59        Ok(response)
60    }
61}
62
63/// Errors specific to the Generate API.
64#[derive(Debug, thiserror::Error)]
65pub enum GenerateError {
66    /// HTTP transport error.
67    #[error(transparent)]
68    Http(#[from] HttpError),
69
70    /// JSON deserialization error.
71    #[error("Failed to parse response: {0}")]
72    Parse(#[from] serde_json::Error),
73
74    /// Authentication error.
75    #[error("Auth error: {0}")]
76    Auth(#[from] crate::session::AuthError),
77
78    /// Content was blocked by safety filters.
79    #[error("Content blocked: {reason:?}")]
80    SafetyBlocked {
81        /// The reason the content was blocked.
82        reason: BlockReason,
83    },
84
85    /// Prompt was rejected.
86    #[error("Prompt blocked: {reason:?}")]
87    PromptBlocked {
88        /// The reason the prompt was blocked.
89        reason: BlockReason,
90    },
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn generate_error_display() {
99        let err = GenerateError::SafetyBlocked {
100            reason: BlockReason::Safety,
101        };
102        assert!(err.to_string().contains("blocked"));
103    }
104
105    #[test]
106    fn generate_content_config_from_text() {
107        let config = GenerateContentConfig::from_text("Hello");
108        let body = config.to_request_body();
109        let contents = body.get("contents").unwrap();
110        assert!(contents.is_array());
111        let parts = contents[0].get("parts").unwrap();
112        assert!(parts[0].get("text").unwrap().as_str().unwrap() == "Hello");
113    }
114
115    #[test]
116    fn generate_content_config_with_system() {
117        let config = GenerateContentConfig::from_text("Hello")
118            .system_instruction("You are a helpful assistant");
119        let body = config.to_request_body();
120        assert!(body.get("systemInstruction").is_some());
121    }
122
123    #[test]
124    fn parse_generate_response() {
125        let json = serde_json::json!({
126            "candidates": [{
127                "content": {
128                    "parts": [{"text": "Hello world!"}],
129                    "role": "model"
130                },
131                "finishReason": "STOP",
132                "safetyRatings": [{
133                    "category": "HARM_CATEGORY_HARASSMENT",
134                    "probability": "NEGLIGIBLE"
135                }]
136            }],
137            "usageMetadata": {
138                "promptTokenCount": 5,
139                "candidatesTokenCount": 10,
140                "totalTokenCount": 15
141            }
142        });
143
144        let resp: GenerateContentResponse = serde_json::from_value(json).unwrap();
145        assert_eq!(resp.candidates.len(), 1);
146        assert_eq!(resp.text().unwrap(), "Hello world!");
147        assert!(resp.usage_metadata.is_some());
148    }
149}