├── examples ├── models.rs ├── ask.rs ├── test.rs ├── streaming.rs ├── json.rs ├── function_calling.rs └── function_call.rs ├── src ├── error.rs ├── lib.rs ├── chat.rs ├── stream.rs ├── client.rs └── types.rs ├── README.md ├── .gitignore ├── Cargo.toml ├── LICENSE └── .github └── workflows └── rust.yml /examples/models.rs: -------------------------------------------------------------------------------- 1 | #[tokio::main] 2 | async fn main() -> Result<(), Box> { 3 | let client = gemini_rs::Client::instance(); 4 | let models = client.models().await?; 5 | println!("{models:#?}"); 6 | Ok(()) 7 | } 8 | -------------------------------------------------------------------------------- /examples/ask.rs: -------------------------------------------------------------------------------- 1 | #[tokio::main] 2 | async fn main() -> Result<(), Box> { 3 | println!( 4 | "{}", 5 | gemini_rs::chat("gemini-2.0-flash") 6 | .send_message("Explain how AI works") 7 | .await? 8 | ); 9 | Ok(()) 10 | } 11 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(thiserror::Error, Debug)] 2 | pub enum Error { 3 | #[error("serde: {0}")] 4 | Serde(#[from] serde_json::Error), 5 | #[error("http: {0}")] 6 | Http(#[from] reqwest::Error), 7 | #[error("gemini: {0:?}")] 8 | Gemini(crate::types::ErrorDetail), 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STILL A WIP 2 | 3 | A library to use Google Gemini's API directly in Rust! 4 | Made because the current options weren't very capable and didn't support 100% of the official API. 5 | 6 | ## Example 7 | 8 | ```rust 9 | #[tokio::main] 10 | async fn main() -> Result<(), Box> { 11 | println!( 12 | "{}", 13 | gemini_rs::chat("gemini-2.0-flash") 14 | .send_message("Explain how AI works") 15 | .await? 16 | ); 17 | Ok(()) 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | 16 | # IDEs 17 | .idea/ 18 | .idx/ 19 | .vscode/ 20 | 21 | # MAIN.RS 22 | # Only here because this is a library, and I use main.rs for testing purposes. 23 | src/main.rs 24 | Testing/ 25 | -------------------------------------------------------------------------------- /examples/test.rs: -------------------------------------------------------------------------------- 1 | use gemini_rs::types::{CodeExecutionTool, Tools}; 2 | 3 | #[tokio::main] 4 | async fn main() -> Result<(), Box> { 5 | let tools = vec![Tools { 6 | function_declarations: None, 7 | google_search: None, 8 | code_execution: Some(CodeExecutionTool {}), 9 | }]; 10 | 11 | let client = gemini_rs::Client::instance(); 12 | let mut req = client.generate_content("gemini-2.0-flash"); 13 | req.body.tools = tools; 14 | req.message("Write a function to calculate the factorial of a number."); 15 | let response = req.await?; 16 | 17 | println!("Response: {:?}", response.candidates[0].content.parts); 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gemini-rs" 3 | version = "2.0.0" 4 | edition = "2024" 5 | license = "MIT" 6 | authors = [ 7 | "gvozdvmozgu ", 8 | "Shuflduf ", 9 | ] 10 | description = "A library to interact with the Google Gemini API" 11 | homepage = "https://github.com/Shuflduf/gemini-rs" 12 | repository = "https://github.com/Shuflduf/gemini-rs" 13 | readme = "README.md" 14 | keywords = ["ai", "google", "gemini"] 15 | 16 | [dependencies] 17 | bytes = "1.10.1" 18 | futures = "0.3" 19 | reqwest = { version = "0.12.19", features = ["stream", "json", "rustls-tls"] } 20 | secrecy = "0.10" 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_json = "1.0" 23 | thiserror = "2.0" 24 | tokio = { version = "1.43", features = ["full"] } 25 | 26 | [dev-dependencies] 27 | tokio = { version = "1.43", default-features = false, features = [ 28 | "macros", 29 | "rt-multi-thread", 30 | ] } 31 | -------------------------------------------------------------------------------- /examples/streaming.rs: -------------------------------------------------------------------------------- 1 | use futures::StreamExt as _; 2 | use gemini_rs::types::{CodeExecutionTool, Tools}; 3 | 4 | #[tokio::main] 5 | async fn main() -> Result<(), Box> { 6 | let client = gemini_rs::Client::instance(); 7 | let tools = vec![Tools { 8 | function_declarations: None, 9 | google_search: None, 10 | code_execution: Some(CodeExecutionTool {}), 11 | }]; 12 | let mut req = client.stream_generate_content("gemini-2.5-flash-preview-05-20"); 13 | req.message("what's the sum of the prime numbers between 1 and 100?"); 14 | req.tools(tools); 15 | 16 | let stream = req.stream().await?; 17 | println!("Stream started..."); 18 | 19 | stream 20 | .for_each(|chunk| async move { 21 | match chunk { 22 | Ok(chunk) => println!("Chunk: {:?}", chunk.candidates), 23 | Err(e) => eprintln!("Error in stream: {:?}", e), 24 | } 25 | }) 26 | .await; 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Andrey Nikolaev, Shuflduf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/json.rs: -------------------------------------------------------------------------------- 1 | use gemini_rs::types::{Schema, Type}; 2 | 3 | #[tokio::main] 4 | async fn main() -> Result<(), Box> { 5 | let person = Schema { 6 | schema_type: Some(Type::Object), 7 | properties: Some( 8 | [ 9 | ( 10 | "name".to_string(), 11 | Schema { 12 | schema_type: Some(Type::String), 13 | ..Default::default() 14 | }, 15 | ), 16 | ( 17 | "age".to_string(), 18 | Schema { 19 | schema_type: Some(Type::Integer), 20 | ..Default::default() 21 | }, 22 | ), 23 | ( 24 | "election_percentage".to_string(), 25 | Schema { 26 | schema_type: Some(Type::Number), 27 | ..Default::default() 28 | }, 29 | ), 30 | ] 31 | .into_iter() 32 | .collect(), 33 | ), 34 | ..Default::default() 35 | }; 36 | 37 | let list_person = Schema { 38 | schema_type: Some(Type::Array), 39 | items: Some(Box::new(person)), 40 | ..Default::default() 41 | }; 42 | 43 | let response = gemini_rs::chat("gemini-2.0-flash") 44 | .to_json() 45 | .response_schema(list_person) 46 | .json::("List some US presidents") 47 | .await?; 48 | 49 | println!("{:#?}", response); 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | on: 3 | push: 4 | branches: ["main"] 5 | merge_group: 6 | pull_request: 7 | workflow_dispatch: 8 | env: 9 | CARGO_TERM_COLOR: always 10 | CARGO_INCREMENTAL: 0 11 | CARGO_NET_RETRY: 10 12 | RUST_BACKTRACE: short 13 | RUSTFLAGS: "-D warnings" 14 | RUSTUP_MAX_RETRIES: 10 15 | jobs: 16 | rust: 17 | runs-on: ubuntu-latest 18 | env: 19 | RUST_CHANNEL: stable 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | - name: Install Rust toolchain 24 | run: | 25 | rustup update --no-self-update ${{ env.RUST_CHANNEL }} 26 | rustup default ${{ env.RUST_CHANNEL }} 27 | rustup component add --toolchain ${{ env.RUST_CHANNEL }} rustfmt clippy 28 | - name: Cache Dependencies 29 | uses: Swatinem/rust-cache@v2 30 | with: 31 | key: ${{ env.RUST_CHANNEL }} 32 | - name: cargo build 33 | run: cargo build --quiet 34 | - name: cargo clippy 35 | run: cargo clippy --quiet 36 | - name: cargo test 37 | run: cargo test -- --nocapture --quiet 38 | - name: cargo fmt --check 39 | run: cargo fmt --check 40 | unused_dependencies: 41 | runs-on: ubuntu-latest 42 | env: 43 | RUST_CHANNEL: nightly 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Install Rust toolchain 47 | run: | 48 | rustup update --no-self-update ${{ env.RUST_CHANNEL }} 49 | rustup default ${{ env.RUST_CHANNEL }} 50 | - name: install cargo-udeps 51 | uses: taiki-e/install-action@cargo-udeps 52 | - name: cargo udeps 53 | run: cargo udeps 54 | -------------------------------------------------------------------------------- /examples/function_calling.rs: -------------------------------------------------------------------------------- 1 | // function calling example 2 | use gemini_rs::types::{FunctionDeclaration, Tools}; 3 | use serde_json::json; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | // 1. Build the function declaration 8 | let function_decs = vec![FunctionDeclaration { 9 | name: "set_theme".to_string(), 10 | description: "Set the theme of the application".to_string(), 11 | parameters: json!({ 12 | "type": "object", 13 | "properties": { 14 | "theme": { 15 | "type": "string", 16 | "description": "The theme to set", 17 | "enum": ["light", "dark"] 18 | } 19 | }, 20 | "required": ["theme"] 21 | }), 22 | }]; 23 | 24 | // 2. Wrap in Tools 25 | let tools = vec![Tools { 26 | function_declarations: Some(function_decs), 27 | google_search: None, 28 | code_execution: None, 29 | }]; 30 | 31 | // 3. Get the client and builder 32 | let client = gemini_rs::Client::instance(); 33 | let mut req = client.generate_content("gemini-2.0-flash"); 34 | 35 | // 4. Use builder methods to set up the request 36 | req.tools(tools); 37 | req.message("Set the theme to dark."); 38 | 39 | // 5. Await the request to send it 40 | let response = req.await; 41 | 42 | // 6. Process response accordingly 43 | match response { 44 | Ok(resp) => match &resp.candidates[0].content.parts[0].function_call { 45 | Some(func) => set_theme(func.args["theme"].as_str().unwrap()), 46 | None => eprint!("Function not called"), 47 | }, 48 | Err(e) => eprintln!("Error: {e}"), 49 | } 50 | } 51 | 52 | fn set_theme(theme: &str) { 53 | println!("Setting application theme to {}", theme); 54 | } 55 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(unreachable_pub, unused_qualifications)] 2 | 3 | //! *A Rust client library for Google's Gemini AI models.* 4 | //! 5 | //! # Overview 6 | //! 7 | //! This library provides a fully-featured client for interacting with Google's Gemini AI models, 8 | //! supporting all major API features including: 9 | //! 10 | //! - Text generation and chat conversations 11 | //! - JSON-structured outputs 12 | //! - Function calling 13 | //! - Safety settings and content filtering 14 | //! - System instructions 15 | //! - Model configuration (temperature, tokens, etc.) 16 | //! 17 | //! # Authentication 18 | //! 19 | //! The client requires a Gemini API key which can be provided in two ways: 20 | //! - Environment variable: `GEMINI_API_KEY` 21 | //! - Programmatically: `Client::new(api_key)` 22 | //! 23 | //! # Basic Usage 24 | //! 25 | //! ```rust,no_run 26 | //! #[tokio::main] 27 | //! async fn main() -> gemini_rs::Result<()> { 28 | //! // Simple chat interaction 29 | //! let response = gemini_rs::chat("gemini-2.0-flash") 30 | //! .send_message("What is Rust's ownership model?") 31 | //! .await?; 32 | //! println!("{}", response); 33 | //! 34 | //! Ok(()) 35 | //! } 36 | //! ``` 37 | //! 38 | //! # Advanced Features 39 | //! 40 | //! The library supports advanced Gemini features through the `Client` and `Chat` types: 41 | //! 42 | //! - Model management (`client().models()`) 43 | //! - Custom generation settings (`chat.config_mut()`) 44 | //! - Safety settings (`chat.safety_settings()`) 45 | //! - System instructions (`chat.system_instruction()`) 46 | //! - Conversation history management (`chat.history_mut()`) 47 | 48 | mod chat; 49 | mod client; 50 | mod error; 51 | mod stream; 52 | pub mod types; 53 | 54 | pub type Result = std::result::Result; 55 | 56 | pub use chat::Chat; 57 | pub use client::Client; 58 | pub use error::Error; 59 | pub use stream::{RouteStream, StreamGenerateContent}; 60 | 61 | /// Creates a new Gemini client instance using the default configuration. 62 | pub fn client() -> Client { 63 | Client::instance() 64 | } 65 | 66 | /// Creates a new chat session with the specified Gemini model. 67 | pub fn chat(model: &str) -> Chat { 68 | client().chat(model) 69 | } 70 | -------------------------------------------------------------------------------- /examples/function_call.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use gemini_rs::types::{ 4 | FunctionCallingConfig, FunctionCallingMode, FunctionDeclaration, Schema, ToolConfig, Tools, 5 | Type, 6 | }; 7 | use serde_json::json; 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<(), Box> { 11 | let client = gemini_rs::Client::instance(); 12 | 13 | let mut content = client.generate_content("gemini-2.5-flash-preview-04-17"); 14 | content.tool_config(ToolConfig { 15 | function_calling_config: Some(FunctionCallingConfig { 16 | mode: Some(FunctionCallingMode::Any), 17 | allowed_function_names: Some(vec!["set_alarm".to_string()]), 18 | }), 19 | }); 20 | 21 | let mut properties = BTreeMap::new(); 22 | properties.insert( 23 | "time".to_string(), 24 | Schema { 25 | schema_type: Some(Type::String), 26 | description: Some("The time to set the alarm for.".to_string()), 27 | ..Default::default() 28 | }, 29 | ); 30 | 31 | let parameters = json!({ 32 | "type": "object", 33 | "properties": { 34 | "theme": { 35 | "type": "string", 36 | "description": "The theme to set", 37 | "enum": ["light", "dark"] 38 | } 39 | }, 40 | "required": ["theme"] 41 | }); 42 | 43 | let function_declaration = FunctionDeclaration { 44 | name: "set_alarm".to_string(), 45 | description: "Set an alarm for a specific time.".to_string(), 46 | parameters, 47 | }; 48 | 49 | content.body.tools = vec![Tools { 50 | //Set alarm 7:00 AM 51 | function_declarations: Some(vec![function_declaration]), 52 | google_search: None, 53 | //Search : What is the time in New York 54 | //google_search: Some(GoogleSearchTool{}), 55 | 56 | //What is the sum of the first 50 prime numbers? Generate and run code for the calculation, and make sure you get all 50. 57 | code_execution: None, 58 | //Some(CodeExecutionTool{}), 59 | }]; 60 | content.system_instruction("You are a helpful assistant. You can set alarms for the user."); 61 | content.message("What is the sum of the first 50 prime numbers? Generate and run code for the calculation, and make sure you get all 50."); 62 | println!("Request: {:#?}", content.body); 63 | let response = content.await?; 64 | println!("{:#?}", response); 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /src/chat.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{ 4 | Client, Result, 5 | types::{self, Response}, 6 | }; 7 | 8 | /// Simplest way to use gemini-rs, and covers 80% of use cases 9 | pub struct Chat { 10 | model: Box, 11 | client: Client, 12 | system_instruction: Option>, 13 | safety_settings: Vec, 14 | history: Vec, 15 | config: Option, 16 | phantom: PhantomData, 17 | } 18 | 19 | impl Chat { 20 | pub fn new(client: &Client, model: &str) -> Self { 21 | Self { 22 | model: model.into(), 23 | client: client.clone(), 24 | system_instruction: None, 25 | safety_settings: Vec::new(), 26 | history: Vec::new(), 27 | config: None, 28 | phantom: PhantomData, 29 | } 30 | } 31 | 32 | pub fn config(&mut self) -> &types::GenerationConfig { 33 | self.config.get_or_insert_default() 34 | } 35 | 36 | pub fn to_json(mut self) -> Chat { 37 | self.config_mut().response_mime_type = Some("application/json".into()); 38 | Chat { 39 | model: self.model, 40 | client: self.client, 41 | system_instruction: self.system_instruction, 42 | safety_settings: self.safety_settings, 43 | history: self.history, 44 | config: self.config, 45 | phantom: PhantomData, 46 | } 47 | } 48 | 49 | pub fn config_mut(&mut self) -> &mut types::GenerationConfig { 50 | self.config.get_or_insert_default() 51 | } 52 | 53 | pub fn history(&self) -> &[types::Content] { 54 | &self.history 55 | } 56 | 57 | pub fn history_mut(&mut self) -> &mut Vec { 58 | &mut self.history 59 | } 60 | 61 | pub fn safety_settings(&mut self, safety_settings: Vec) { 62 | self.safety_settings = safety_settings; 63 | } 64 | 65 | pub fn system_instruction(mut self, instruction: &str) -> Self { 66 | self.system_instruction = Some(Box::from(instruction)); 67 | self 68 | } 69 | 70 | pub async fn generate_content(&mut self) -> Result { 71 | let mut generate_content = self.client.generate_content(&self.model); 72 | 73 | if let Some(system_instruction) = &self.system_instruction { 74 | generate_content.system_instruction(system_instruction); 75 | } 76 | 77 | if let Some(config) = &self.config { 78 | generate_content.config(config.clone()); 79 | } 80 | 81 | generate_content.contents(self.history.clone()); 82 | generate_content.safety_settings(self.safety_settings.clone()); 83 | generate_content.await 84 | } 85 | 86 | pub async fn send_message(&mut self, message: &str) -> Result { 87 | self.history.push(types::Content { 88 | role: types::Role::User, 89 | parts: vec![types::Part::text(message)], 90 | }); 91 | 92 | self.generate_content().await 93 | } 94 | } 95 | 96 | impl Chat { 97 | pub fn response_schema(mut self, schema: types::Schema) -> Self { 98 | self.config_mut().response_schema = Some(schema); 99 | self 100 | } 101 | 102 | pub async fn json(&mut self, message: &str) -> Result { 103 | let response = self.send_message(message).await?; 104 | let json = format!("{response}"); 105 | serde_json::from_str(&json).map_err(Into::into) 106 | } 107 | } 108 | 109 | pub struct Text {} 110 | 111 | pub struct Json {} 112 | -------------------------------------------------------------------------------- /src/stream.rs: -------------------------------------------------------------------------------- 1 | use crate::client::{Formatter, Request}; 2 | use std::{ 3 | ops::{Deref, DerefMut}, 4 | pin::Pin, 5 | task::Poll, 6 | }; 7 | 8 | use bytes::Bytes; 9 | use futures::Stream; 10 | use reqwest::Method; 11 | use serde::ser::Error as _; 12 | 13 | use crate::{ 14 | Error, Result, 15 | client::{BASE_URI, GenerateContent, Route}, 16 | types, 17 | }; 18 | 19 | pub struct StreamGenerateContent(pub(crate) GenerateContent); 20 | 21 | impl StreamGenerateContent { 22 | pub fn new(model: &str) -> Self { 23 | Self(GenerateContent::new(model.into())) 24 | } 25 | } 26 | 27 | impl Deref for Route { 28 | type Target = GenerateContent; 29 | 30 | fn deref(&self) -> &Self::Target { 31 | &self.kind.0 32 | } 33 | } 34 | 35 | impl DerefMut for Route { 36 | fn deref_mut(&mut self) -> &mut Self::Target { 37 | &mut self.kind.0 38 | } 39 | } 40 | 41 | impl Route { 42 | pub async fn stream(self) -> std::result::Result, String> { 43 | let url = format!("{BASE_URI}/{}", self); 44 | let body = self.kind.body().clone(); 45 | let mut request = self 46 | .client 47 | .reqwest 48 | .request(StreamGenerateContent::METHOD, url); 49 | 50 | if let Some(body) = body { 51 | request = request.json(&body); 52 | } 53 | 54 | let response = request.send().await.map_err(|e| e.to_string())?; 55 | let stream = response.bytes_stream(); 56 | 57 | Ok(RouteStream { 58 | phantom: std::marker::PhantomData, 59 | stream: Box::pin(stream), 60 | buffer: Vec::new(), 61 | pos: 0, 62 | state: ParseState::CannotAdvance, 63 | }) 64 | } 65 | } 66 | 67 | pub struct RouteStream { 68 | phantom: std::marker::PhantomData, 69 | stream: Pin> + Send>>, 70 | buffer: Vec, 71 | pos: usize, // A cursor into the buffer. 72 | state: ParseState, 73 | } 74 | 75 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 76 | enum ParseState { 77 | CannotAdvance, 78 | ReadingChars, 79 | ReadingValue, 80 | Finished, 81 | } 82 | 83 | #[derive(Debug)] 84 | enum ParseOutcome { 85 | Ok(Option), 86 | Err(serde_json::Error), 87 | Eof, 88 | } 89 | 90 | impl RouteStream { 91 | fn next_char_pos(&self) -> Option { 92 | self.buffer[self.pos..] 93 | .iter() 94 | .position(|&b| !b.is_ascii_whitespace()) 95 | .map(|p| self.pos + p) 96 | } 97 | 98 | fn advance_next_char(&mut self) -> Option { 99 | self.pos = self.next_char_pos().unwrap_or(self.buffer.len()); 100 | self.buffer.get(self.pos).copied() 101 | } 102 | 103 | fn current_char(&self) -> Option { 104 | self.buffer.get(self.pos).copied() 105 | } 106 | 107 | fn is_bridge_char(&self) -> bool { 108 | matches!(self.current_char(), Some(b'[') | Some(b',')) 109 | } 110 | 111 | fn parse_chunk(&mut self) -> ParseOutcome { 112 | let mut de = serde_json::Deserializer::from_slice(&self.buffer[self.pos..]) 113 | .into_iter::(); 114 | match de.next() { 115 | Some(Ok(value)) => { 116 | self.pos += de.byte_offset(); 117 | ParseOutcome::Ok(Some(value)) 118 | } 119 | Some(Err(e)) if e.is_eof() => ParseOutcome::Eof, 120 | Some(Err(e)) => ParseOutcome::Err(e), 121 | None => ParseOutcome::Ok(None), // No more objects to read. 122 | } 123 | } 124 | 125 | fn try_parse_next(&mut self) -> Option { 126 | match self.state { 127 | ParseState::CannotAdvance => None, // nothing to read 128 | ParseState::ReadingChars => { 129 | self.advance_next_char(); 130 | if self.is_bridge_char() { 131 | self.pos += 1; // Move past this '[' or ',' 132 | self.state = ParseState::ReadingValue; 133 | None 134 | } else if let Some(b']') = self.current_char() { 135 | // If we hit a ']', we can finish reading. 136 | self.state = ParseState::Finished; 137 | Some(ParseOutcome::Ok(None)) 138 | } else { 139 | None 140 | } 141 | } 142 | ParseState::ReadingValue => { 143 | self.advance_next_char(); 144 | // Deserialize one object from our current position. 145 | let outcome = self.parse_chunk(); 146 | match &outcome { 147 | ParseOutcome::Ok(Some(_)) => { 148 | self.state = ParseState::ReadingChars; 149 | } 150 | ParseOutcome::Ok(None) | ParseOutcome::Err(_) => { 151 | self.state = ParseState::Finished; 152 | } 153 | ParseOutcome::Eof => {} 154 | }; 155 | Some(outcome) 156 | } 157 | ParseState::Finished => None, 158 | } 159 | } 160 | } 161 | 162 | impl Stream for RouteStream { 163 | type Item = Result; 164 | 165 | fn poll_next( 166 | mut self: Pin<&mut Self>, 167 | cx: &mut std::task::Context<'_>, 168 | ) -> Poll> { 169 | loop { 170 | // Housekeeping: drain the buffer if we've processed a lot. 171 | if self.pos > 2048 { 172 | let this_pos = self.pos; 173 | self.buffer.drain(..this_pos); 174 | self.pos = 0; 175 | } 176 | 177 | if let Some(outcome) = self.try_parse_next() { 178 | match outcome { 179 | ParseOutcome::Ok(Some(response)) => return Poll::Ready(Some(Ok(response))), 180 | ParseOutcome::Ok(None) if self.state == ParseState::Finished => { 181 | return Poll::Ready(None); 182 | } 183 | ParseOutcome::Err(error) => return Poll::Ready(Some(Err(Error::Serde(error)))), 184 | ParseOutcome::Eof => {} // Continue to read more data. 185 | _ => {} 186 | } 187 | }; 188 | 189 | // If we fell through, we need more data. Poll the underlying stream. 190 | match self.stream.as_mut().poll_next(cx) { 191 | Poll::Ready(Some(Ok(bytes))) => { 192 | if self.buffer.is_empty() && !bytes.is_empty() { 193 | self.state = ParseState::ReadingChars; 194 | } 195 | self.buffer.extend_from_slice(&bytes); 196 | continue; // Loop again to process new data. 197 | } 198 | Poll::Pending => return Poll::Pending, 199 | Poll::Ready(Some(Err(e))) => { 200 | self.state = ParseState::Finished; 201 | return Poll::Ready(Some(Err(Error::Http(e)))); 202 | } 203 | Poll::Ready(None) => { 204 | // Underlying stream ended. Check if we're in a clean state. 205 | if self.state != ParseState::Finished && self.pos < self.buffer.len() { 206 | let msg = 207 | format!("stream ended with unparsed data in state {:?}", self.state); 208 | return Poll::Ready(Some(Err(serde_json::Error::custom(msg).into()))); 209 | } 210 | self.state = ParseState::Finished; 211 | return Poll::Ready(None); 212 | } 213 | } 214 | } 215 | } 216 | } 217 | 218 | impl Request for StreamGenerateContent { 219 | type Model = types::Response; 220 | type Body = types::GenerateContent; 221 | 222 | const METHOD: Method = Method::POST; 223 | 224 | fn format_uri(&self, fmt: &mut Formatter<'_, '_>) -> std::fmt::Result { 225 | fmt.write_str("v1beta/")?; 226 | fmt.write_str("models/")?; 227 | fmt.write_str(&self.0.model)?; 228 | fmt.write_str(":streamGenerateContent") 229 | } 230 | 231 | fn body(&self) -> Option { 232 | Some(self.0.body.clone()) 233 | } 234 | } 235 | 236 | impl std::fmt::Display for StreamGenerateContent { 237 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 238 | let mut fmt = Formatter::new(fmt); 239 | self.format_uri(&mut fmt)?; 240 | fmt.write_query_param("key", &self.0.model) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Write as _, 3 | ops::{Deref, DerefMut}, 4 | sync::{Arc, LazyLock}, 5 | }; 6 | 7 | use futures::FutureExt as _; 8 | use reqwest::Method; 9 | use secrecy::{ExposeSecret as _, SecretString}; 10 | 11 | use crate::{Chat, Error, Result, StreamGenerateContent, chat, types}; 12 | 13 | pub(crate) const BASE_URI: &str = "https://generativelanguage.googleapis.com"; 14 | 15 | pub struct Route { 16 | pub(crate) client: Client, 17 | pub(crate) kind: T, 18 | } 19 | 20 | impl Route { 21 | fn new(client: &Client, kind: T) -> Self { 22 | Self { 23 | client: client.clone(), 24 | kind, 25 | } 26 | } 27 | } 28 | 29 | impl IntoFuture for Route { 30 | type Output = Result; 31 | type IntoFuture = futures::future::BoxFuture<'static, Self::Output>; 32 | 33 | fn into_future(self) -> Self::IntoFuture { 34 | async move { 35 | let mut request = self 36 | .client 37 | .reqwest 38 | .request(T::METHOD, format!("{BASE_URI}/{self}")); 39 | 40 | if let Some(body) = self.kind.body() { 41 | request = request.json(&body); 42 | }; 43 | 44 | let response = request.send().await?; 45 | let raw_json = response.text().await?; 46 | 47 | match serde_json::from_str::>(&raw_json)? { 48 | types::ApiResponse::Ok(response) => Ok(response), 49 | types::ApiResponse::Err(api_error) => Err(Error::Gemini(api_error.error)), 50 | } 51 | } 52 | .boxed() 53 | } 54 | } 55 | 56 | impl Deref for Route { 57 | type Target = GenerateContent; 58 | 59 | fn deref(&self) -> &Self::Target { 60 | &self.kind 61 | } 62 | } 63 | 64 | impl DerefMut for Route { 65 | fn deref_mut(&mut self) -> &mut Self::Target { 66 | &mut self.kind 67 | } 68 | } 69 | 70 | impl std::fmt::Display for Route { 71 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 72 | let mut fmt = Formatter::new(fmt); 73 | self.kind.format_uri(&mut fmt)?; 74 | fmt.write_query_param("key", &self.client.key.expose_secret()) 75 | } 76 | } 77 | 78 | /// Covers the 20% of use cases that [Chat] doesn't 79 | #[derive(Clone)] 80 | pub struct Client { 81 | pub(crate) inner: Arc, 82 | } 83 | 84 | impl Deref for Client { 85 | type Target = ClientInner; 86 | 87 | fn deref(&self) -> &Self::Target { 88 | &self.inner 89 | } 90 | } 91 | 92 | impl Default for Client { 93 | fn default() -> Self { 94 | Self { 95 | inner: ClientInner::new(None), 96 | } 97 | } 98 | } 99 | 100 | impl Client { 101 | pub fn new(key: impl Into) -> Self { 102 | Self { 103 | inner: ClientInner::new(Some(key.into())), 104 | } 105 | } 106 | 107 | pub fn chat(&self, model: &str) -> Chat { 108 | Chat::new(self, model) 109 | } 110 | 111 | pub fn models(&self) -> Route { 112 | Route::new(self, Models::default()) 113 | } 114 | 115 | pub fn generate_content(&self, model: &str) -> Route { 116 | Route::new(self, GenerateContent::new(model.into())) 117 | } 118 | 119 | pub fn stream_generate_content(&self, model: &str) -> Route { 120 | Route::new(self, StreamGenerateContent::new(model)) 121 | } 122 | 123 | pub fn instance() -> Client { 124 | static STATIC_INSTANCE: LazyLock = LazyLock::new(Client::default); 125 | STATIC_INSTANCE.clone() 126 | } 127 | } 128 | 129 | pub struct GenerateContent { 130 | pub(crate) model: Box, 131 | pub body: types::GenerateContent, 132 | } 133 | 134 | impl GenerateContent { 135 | pub fn new(model: Box) -> Self { 136 | Self { 137 | model, 138 | body: types::GenerateContent::default(), 139 | } 140 | } 141 | 142 | pub fn config(&mut self, config: types::GenerationConfig) { 143 | self.body.generation_config = Some(config); 144 | } 145 | 146 | pub fn safety_settings(&mut self, safety_settings: Vec) { 147 | self.body.safety_settings = safety_settings; 148 | } 149 | 150 | pub fn system_instruction(&mut self, instruction: &str) { 151 | self.body.system_instruction = Some(types::SystemInstructionContent { 152 | parts: vec![types::SystemInstructionPart { 153 | text: Some(instruction.into()), 154 | }], 155 | }); 156 | } 157 | pub fn tool_config(&mut self, conf: types::ToolConfig) { 158 | self.body.tool_config = Some(conf); 159 | } 160 | pub fn contents(&mut self, contents: Vec) { 161 | self.body.contents = contents; 162 | } 163 | 164 | pub fn message(&mut self, message: &str) { 165 | self.body.contents.push(types::Content { 166 | role: types::Role::User, 167 | parts: vec![types::Part::text(message)], 168 | }); 169 | } 170 | pub fn tools(&mut self, tools: Vec) { 171 | self.body.tools = tools; 172 | } 173 | } 174 | 175 | impl Request for GenerateContent { 176 | type Model = types::Response; 177 | type Body = types::GenerateContent; 178 | 179 | const METHOD: Method = Method::POST; 180 | 181 | fn format_uri(&self, fmt: &mut Formatter<'_, '_>) -> std::fmt::Result { 182 | fmt.write_str("v1beta/")?; 183 | fmt.write_str("models/")?; 184 | fmt.write_str(&self.model)?; 185 | fmt.write_str(":generateContent") 186 | } 187 | 188 | fn body(&self) -> Option { 189 | Some(self.body.clone()) 190 | } 191 | } 192 | 193 | #[derive(Default)] 194 | pub struct Models { 195 | page_size: Option, 196 | page_token: Option>, 197 | } 198 | 199 | impl Models { 200 | pub fn page_size(&mut self, size: usize) { 201 | self.page_size = size.into(); 202 | } 203 | 204 | pub fn page_token(&mut self, token: &str) { 205 | self.page_token = Some(Box::from(token)); 206 | } 207 | } 208 | 209 | impl Request for Models { 210 | type Model = types::Models; 211 | type Body = (); 212 | 213 | const METHOD: Method = Method::GET; 214 | 215 | fn format_uri(&self, fmt: &mut Formatter<'_, '_>) -> std::fmt::Result { 216 | fmt.write_str("v1beta/")?; 217 | fmt.write_str("models")?; 218 | fmt.write_optional_query_param("page_size", self.page_size.as_ref())?; 219 | fmt.write_optional_query_param("page_token", self.page_token.as_ref()) 220 | } 221 | } 222 | 223 | pub struct Formatter<'me, 'buffer> { 224 | formatter: &'me mut std::fmt::Formatter<'buffer>, 225 | is_first: bool, 226 | } 227 | 228 | impl<'buffer> Deref for Formatter<'_, 'buffer> { 229 | type Target = std::fmt::Formatter<'buffer>; 230 | 231 | fn deref(&self) -> &Self::Target { 232 | self.formatter 233 | } 234 | } 235 | 236 | impl DerefMut for Formatter<'_, '_> { 237 | fn deref_mut(&mut self) -> &mut Self::Target { 238 | self.formatter 239 | } 240 | } 241 | 242 | impl<'me, 'buffer> Formatter<'me, 'buffer> { 243 | pub(crate) fn new(formatter: &'me mut std::fmt::Formatter<'buffer>) -> Self { 244 | Self { 245 | formatter, 246 | is_first: true, 247 | } 248 | } 249 | 250 | pub(crate) fn write_query_param( 251 | &mut self, 252 | key: &str, 253 | value: &impl std::fmt::Display, 254 | ) -> std::fmt::Result { 255 | if self.is_first { 256 | self.formatter.write_char('?')?; 257 | self.is_first = false; 258 | } else { 259 | self.formatter.write_char('&')?; 260 | } 261 | 262 | self.formatter.write_str(key)?; 263 | self.formatter.write_char('=')?; 264 | std::fmt::Display::fmt(value, self.formatter) 265 | } 266 | 267 | fn write_optional_query_param( 268 | &mut self, 269 | key: &str, 270 | value: Option<&impl std::fmt::Display>, 271 | ) -> std::fmt::Result { 272 | if let Some(value) = value { 273 | self.write_query_param(key, value) 274 | } else { 275 | Ok(()) 276 | } 277 | } 278 | } 279 | 280 | pub struct ClientInner { 281 | pub(crate) reqwest: reqwest::Client, 282 | key: SecretString, 283 | } 284 | 285 | impl ClientInner { 286 | fn new(key: Option) -> Arc { 287 | Self { 288 | reqwest: reqwest::Client::new(), 289 | key: key 290 | .or_else(|| std::env::var("GEMINI_API_KEY").ok().map(Into::into)) 291 | .expect("API key must be set either via argument or GEMINI_API_KEY environment variable"), 292 | } 293 | .into() 294 | } 295 | } 296 | 297 | pub trait Request: Send + Sized + 'static { 298 | type Model: serde::de::DeserializeOwned + Send + 'static; 299 | type Body: serde::ser::Serialize; 300 | 301 | const METHOD: Method; 302 | 303 | fn format_uri(&self, fmt: &mut Formatter<'_, '_>) -> std::fmt::Result; 304 | 305 | fn body(&self) -> Option { 306 | None 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | //! Contains every type used in the library 2 | 3 | use std::collections::BTreeMap; 4 | 5 | use serde::{Deserialize, Serialize}; 6 | use serde_json::Value; 7 | 8 | /// The producer of the content 9 | #[derive(Debug, Deserialize, Serialize, Clone, Copy)] 10 | #[serde(rename_all = "lowercase")] 11 | pub enum Role { 12 | User, 13 | Model, 14 | } 15 | 16 | #[derive(Debug, Deserialize)] 17 | #[serde(untagged)] 18 | pub enum ApiResponse { 19 | Ok(T), 20 | Err(ApiError), 21 | } 22 | 23 | #[derive(Debug, Deserialize)] 24 | pub struct ApiError { 25 | pub error: ErrorDetail, 26 | } 27 | 28 | #[derive(Debug, Deserialize)] 29 | pub struct ErrorDetail { 30 | pub code: u16, 31 | pub message: String, 32 | pub status: Status, 33 | #[serde(default)] 34 | pub details: Vec, 35 | } 36 | 37 | #[derive(Debug, Deserialize)] 38 | pub struct ErrorInfo { 39 | #[serde(rename = "@type")] 40 | pub r#type: String, 41 | #[serde(default)] 42 | pub reason: Option, 43 | #[serde(default)] 44 | pub domain: Option, 45 | #[serde(default)] 46 | pub metadata: Option>, 47 | } 48 | 49 | /// Common backend error codes you may encounter 50 | /// 51 | /// Use the [API Reference](https://ai.google.dev/gemini-api/docs/troubleshooting#error-codes) for 52 | /// troubleshooting steps 53 | #[derive(Debug, Deserialize)] 54 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 55 | pub enum Status { 56 | /// The request body is malformed 57 | InvalidArgument, 58 | /// Gemini API free tier is not available in your country. Please enable billing on your project in Google AI Studio. 59 | FailedPrecondition, 60 | /// Your API key doesn't have the required permissions. 61 | PermissionDenied, 62 | /// The requested resource wasn't found. 63 | NotFound, 64 | /// You've exceeded the rate limit. 65 | ResourceExhausted, 66 | /// An unexpected error occurred on Google's side. 67 | Internal, 68 | /// The service may be temporarily overloaded or down. 69 | Unavailable, 70 | /// The service is unable to finish processing within the deadline. 71 | DeadlineExceeded, 72 | } 73 | 74 | /// Response from [crate::Client::models] containing a paginated list of Models 75 | /// 76 | /// [API Reference](https://ai.google.dev/api/models#response-body_1) 77 | #[derive(Deserialize, Debug)] 78 | #[serde(rename_all = "camelCase")] 79 | pub struct Models { 80 | pub models: Vec, 81 | pub next_page_token: Option, 82 | } 83 | 84 | /// Information about a Generative Language Model 85 | /// 86 | /// [API Reference](https://ai.google.dev/api/models#Model) 87 | #[derive(Debug, Default, Deserialize)] 88 | #[serde(rename_all = "camelCase")] 89 | pub struct Model { 90 | pub name: String, 91 | pub version: String, 92 | pub display_name: String, 93 | pub description: String, 94 | pub input_token_limit: i32, 95 | pub output_token_limit: i32, 96 | pub supported_generation_methods: Vec, 97 | #[serde(skip_serializing_if = "Option::is_none")] 98 | pub temperature: Option, 99 | #[serde(skip_serializing_if = "Option::is_none")] 100 | pub top_p: Option, 101 | #[serde(skip_serializing_if = "Option::is_none")] 102 | pub top_k: Option, 103 | } 104 | 105 | /// Response from the model supporting multiple candidate responses 106 | /// 107 | /// [API Reference](https://ai.google.dev/api/generate-content#generatecontentresponse) 108 | #[derive(Debug, Deserialize)] 109 | #[serde(rename_all = "camelCase")] 110 | pub struct Response { 111 | pub candidates: Vec, 112 | pub prompt_feedback: Option, 113 | pub usage_metadata: Option, 114 | } 115 | 116 | impl std::fmt::Display for Response { 117 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 118 | f.write_str( 119 | self.candidates[0].content.parts[0] 120 | .text 121 | .as_deref() 122 | .unwrap_or_default(), 123 | ) 124 | } 125 | } 126 | 127 | /// Metadata on the generation request's token usage 128 | /// 129 | /// [API Reference](https://ai.google.dev/api/generate-content#UsageMetadata) 130 | #[derive(Debug, Deserialize)] 131 | #[serde(rename_all = "camelCase")] 132 | pub struct UsageMetadata { 133 | pub prompt_token_count: u64, 134 | pub candidates_token_count: u64, 135 | } 136 | 137 | /// A response candidate generated from the model 138 | /// 139 | /// [API Reference](https://ai.google.dev/api/generate-content#candidate) 140 | #[derive(Debug, Deserialize)] 141 | #[serde(rename_all = "camelCase")] 142 | pub struct Candidate { 143 | pub content: Content, 144 | pub finish_reason: Option, 145 | pub index: Option, 146 | #[serde(default)] 147 | pub safety_ratings: Vec, 148 | } 149 | 150 | /// A set of the feedback metadata the prompt specified in [GenerateContentRequest.content]. 151 | /// 152 | /// [API Reference](https://ai.google.dev/api/generate-content#PromptFeedback) 153 | #[derive(Debug, Deserialize)] 154 | pub struct PromptFeedback { 155 | #[serde(rename = "safetyRatings")] 156 | pub safety_ratings: Vec, 157 | } 158 | 159 | /// Safety rating for a piece of content 160 | /// 161 | /// The safety rating contains the category of harm and the harm probability level in that category for a piece of content. 162 | /// Content is classified for safety across a number of harm categories and the probability of the harm classification is included here. 163 | /// 164 | /// [API Reference](https://ai.google.dev/api/generate-content#safetyrating) 165 | #[derive(Debug, Deserialize)] 166 | pub struct SafetyRating { 167 | pub category: HarmCategory, 168 | pub probability: HarmProbability, 169 | #[serde(default)] 170 | pub blocked: bool, 171 | } 172 | 173 | #[derive(Debug, Serialize, Deserialize, Clone)] 174 | pub struct FunctionCall { 175 | #[serde(rename = "id", skip_serializing_if = "Option::is_none")] 176 | pub id: Option, 177 | pub name: String, 178 | pub args: Value, 179 | } 180 | 181 | /// The base structured datatype containing multi-part content of a message 182 | /// 183 | /// [API Reference](https://ai.google.dev/api/caching#Content) 184 | #[derive(Debug, Deserialize, Serialize, Clone)] 185 | pub struct Content { 186 | pub role: Role, 187 | #[serde(default)] 188 | pub parts: Vec, 189 | } 190 | 191 | /// A datatype containing media that is part of a multi-part Content message 192 | /// 193 | /// [API Reference](https://ai.google.dev/api/caching#Part) 194 | #[derive(Debug, Default, Deserialize, Serialize, Clone)] 195 | #[serde(rename_all = "camelCase")] 196 | pub struct Part { 197 | #[serde(skip_serializing_if = "Option::is_none")] 198 | pub text: Option, 199 | #[serde(skip_serializing_if = "Option::is_none")] 200 | pub inline_data: Option, 201 | #[serde(skip_serializing_if = "Option::is_none")] 202 | pub file_data: Option, 203 | #[serde(skip_serializing_if = "Option::is_none")] 204 | pub video_metadata: Option, 205 | #[serde(skip_serializing_if = "Option::is_none")] 206 | pub executable_code: Option, 207 | #[serde(skip_serializing_if = "Option::is_none")] 208 | pub code_execution_result: Option, 209 | #[serde(rename = "functionCall", skip_serializing_if = "Option::is_none")] 210 | pub function_call: Option, 211 | } 212 | 213 | impl Part { 214 | pub fn text(text: &str) -> Self { 215 | Self { 216 | text: Some(text.into()), 217 | ..Default::default() 218 | } 219 | } 220 | } 221 | 222 | /// Metadata for a video File 223 | /// 224 | /// [API Reference](https://ai.google.dev/api/files#VideoFileMetadata) 225 | #[derive(Debug, Deserialize, Serialize, Clone)] 226 | #[serde(rename_all = "camelCase")] 227 | pub struct VideoMetadata { 228 | pub start_offset: StartOffset, 229 | pub end_offset: EndOffset, 230 | } 231 | 232 | #[derive(Debug, Deserialize, Serialize, Clone)] 233 | pub struct EndOffset { 234 | pub seconds: i32, 235 | pub nanos: i32, 236 | } 237 | 238 | #[derive(Debug, Deserialize, Serialize, Clone)] 239 | pub struct StartOffset { 240 | pub seconds: i32, 241 | pub nanos: i32, 242 | } 243 | 244 | /// URI based data 245 | /// 246 | /// [API Reference](https://ai.google.dev/api/caching#FileData) 247 | #[derive(Debug, Deserialize, Serialize, Clone)] 248 | pub struct FileData { 249 | pub mime_type: String, 250 | pub file_uri: String, 251 | } 252 | 253 | /// Inline media bytes (stored as a base64 string for some reason) //todo 254 | /// 255 | /// [API Reference](https://ai.google.dev/api/caching#Blob) 256 | #[derive(Debug, Deserialize, Serialize, Clone)] 257 | pub struct InlineData { 258 | pub mime_type: String, 259 | pub data: String, 260 | } 261 | 262 | /// Defines the reason why the model stopped generating tokens 263 | /// 264 | /// [API Reference](https://ai.google.dev/api/generate-content#FinishReason) 265 | #[derive(Debug, Serialize, Deserialize)] 266 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 267 | pub enum FinishReason { 268 | /// Default value. This value is unused. 269 | FinishReasonUnspecified, 270 | /// Natural stop point of the model or provided stop sequence 271 | Stop, 272 | /// The maximum number of tokens as specified in the request was reached 273 | MaxTokens, 274 | /// The response candidate content was flagged for safety reasons 275 | Safety, 276 | /// The response candidate content was flagged for recitation reasons 277 | Recitation, 278 | /// The response candidate content was flagged for using an unsupported language 279 | Language, 280 | /// Unknown reason 281 | Other, 282 | /// Token generation stopped because the content contains forbidden terms 283 | Blocklist, 284 | /// Token generation stopped for potentially containing prohibited content 285 | ProhibitedContent, 286 | /// Token generation stopped because the content potentially contains Sensitive Personally Identifiable Information (SPII) 287 | Spii, 288 | /// The function call generated by the model is invalid 289 | MalformedFunctionCall, 290 | /// Token generation stopped because generated images contain safety violations 291 | ImageSafety, 292 | } 293 | 294 | /// The category of a rating 295 | /// 296 | /// These categories cover various kinds of harms that developers may wish to adjust 297 | /// 298 | /// [API Reference](https://ai.google.dev/api/generate-content#harmcategory) 299 | #[derive(Debug, Serialize, Deserialize, Clone)] 300 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 301 | pub enum HarmCategory { 302 | /// Category is unspecified 303 | HarmCategoryUnspecified, 304 | /// PaLM - Negative or harmful comments targeting identity and/or protected attribute 305 | HarmCategoryDerogatory, 306 | /// PaLM - Content that is rude, disrespectful, or profane 307 | HarmCategoryToxicity, 308 | /// PaLM - Describes scenarios depicting violence against an individual or group, or general descriptions of gore 309 | HarmCategoryViolence, 310 | /// PaLM - Describes scenarios depicting violence against an individual or group, or general descriptions of gore 311 | HarmCategorySexual, 312 | /// PaLM - Promotes unchecked medical advice 313 | HarmCategoryMedical, 314 | /// PaLM - Dangerous content that promotes, facilitates, or encourages harmful acts 315 | HarmCategoryDangerous, 316 | /// Gemini - Harassment content 317 | HarmCategoryHarassment, 318 | /// Gemini - Hate speech and content 319 | HarmCategoryHateSpeech, 320 | /// Gemini - Sexually explicit content 321 | HarmCategorySexuallyExplicit, 322 | /// Gemini - Dangerous content 323 | HarmCategoryDangerousContent, 324 | /// Gemini - Content that may be used to harm civic integrity 325 | HarmCategoryCivicIntegrity, 326 | } 327 | 328 | /// Block at and beyond a specified harm probability 329 | /// 330 | /// [API Reference](https://ai.google.dev/api/generate-content#HarmBlockThreshold) 331 | #[derive(Debug, Deserialize, Serialize, Clone)] 332 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 333 | pub enum HarmBlockThreshold { 334 | /// Threshold is unspecified 335 | HarmBlockThresholdUnspecified, 336 | /// Content with [HarmProbability::Negligible] will be allowed 337 | BlockLowAndAbove, 338 | /// Content with [HarmProbability::Negligible] and [HarmProbability::Low] will be allowed 339 | BlockMedAndAbove, 340 | /// Content with [HarmProbability::Negligible], [HarmProbability::Low], and 341 | /// [HarmProbability::Medium] will be allowed 342 | BlockOnlyHigh, 343 | /// All content will be allowed 344 | BlockNone, 345 | /// Turn off the safety filter 346 | OFF, 347 | } 348 | 349 | /// The probability that a piece of content is harmful 350 | /// 351 | /// The classification system gives the probability of the content being unsafe. This does not 352 | /// indicate the severity of harm for a piece of content. 353 | /// 354 | /// [API Reference](https://ai.google.dev/api/generate-content#HarmProbability) 355 | #[derive(Debug, Deserialize, Serialize)] 356 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 357 | pub enum HarmProbability { 358 | /// Probability is unspecified 359 | HarmProbabilityUnspecified, 360 | /// Content has a negligible chance of being unsafe 361 | Negligible, 362 | /// Content has a low chance of being unsafe 363 | Low, 364 | /// Content has a medium chance of being unsafe 365 | Medium, 366 | /// Content has a high chance of being unsafe 367 | High, 368 | } 369 | 370 | /// GoogleSearch tool type. 371 | /// 372 | /// Tool to support Google Search in Model. Powered by Google. 373 | /// 374 | /// [API Reference](https://ai.google.dev/api/caching#GoogleSearch) 375 | #[derive(Debug, Clone, Deserialize, Serialize)] 376 | pub struct GoogleSearchTool {} 377 | 378 | /// Tool that executes code generated by the model, and automatically returns the result to the model 379 | /// 380 | /// See also [ExecutableCode] and [CodeExecutionResult] which are only generated when using this tool 381 | /// 382 | /// [API Reference](https://ai.google.dev/api/caching#CodeExecution) 383 | #[derive(Debug, Clone, Deserialize, Serialize)] 384 | pub struct CodeExecutionTool {} 385 | 386 | /// Tool details that the model may use to generate response 387 | /// 388 | /// A `Tool` is a piece of code that enables the system to interact with external systems to perform 389 | /// an action, or set of actions, outside of knowledge and scope of the model. 390 | /// 391 | /// [API Reference](https://ai.google.dev/api/caching#Tool) 392 | #[derive(Debug, Clone, Serialize)] 393 | pub struct Tools { 394 | #[serde(skip_serializing_if = "Option::is_none")] 395 | #[serde(rename = "functionDeclarations")] 396 | pub function_declarations: Option>, 397 | #[serde(skip_serializing_if = "Option::is_none")] 398 | pub google_search: Option, 399 | #[serde(skip_serializing_if = "Option::is_none")] 400 | pub code_execution: Option, 401 | } 402 | 403 | /// Structured representation of a function declaration 404 | /// 405 | /// Defined by the OpenAPI 3.03 specification. 406 | /// `FunctionDeclaration` is a representation of a block of code that can be used in [Tools] by the model and executed by the client. 407 | /// 408 | /// [API Reference](https://ai.google.dev/api/caching#FunctionDeclaration) 409 | #[derive(Debug, Clone, Deserialize, Serialize)] 410 | pub struct FunctionDeclaration { 411 | pub name: String, 412 | pub description: String, 413 | /// Defines the input parameters the function expects 414 | /// 415 | /// Use the [API Reference](https://ai.google.dev/gemini-api/docs/function-calling#function_declarations) 416 | /// to see how to structure the parameters. 417 | pub parameters: Value, 418 | } 419 | 420 | /// Request to generate content from the model 421 | /// 422 | /// [API Reference](https://ai.google.dev/api/generate-content#request-body) 423 | #[derive(Debug, Default, Clone, Serialize)] 424 | pub struct GenerateContent { 425 | pub contents: Vec, 426 | #[serde(skip_serializing_if = "Vec::is_empty")] 427 | pub tools: Vec, 428 | #[serde( 429 | default, 430 | rename = "toolConfig", 431 | skip_serializing_if = "Option::is_none" 432 | )] 433 | pub tool_config: Option, 434 | #[serde(skip_serializing_if = "Vec::is_empty")] 435 | #[serde(default, rename = "safetySettings")] 436 | pub safety_settings: Vec, 437 | #[serde(skip_serializing_if = "Option::is_none")] 438 | #[serde(default, rename = "system_instruction")] 439 | pub system_instruction: Option, 440 | #[serde(skip_serializing_if = "Option::is_none")] 441 | #[serde(default, rename = "generationConfig")] 442 | pub generation_config: Option, 443 | } 444 | 445 | /// System instructions are used to provide the model with additional context or instructions 446 | /// 447 | /// Similar to the [Content] struct, but specifically for system instructions. 448 | #[derive(Debug, Clone, Deserialize, Serialize)] 449 | pub struct SystemInstructionContent { 450 | #[serde(default)] 451 | pub parts: Vec, 452 | } 453 | 454 | /// A part of the system instruction content 455 | #[derive(Debug, Clone, Deserialize, Serialize)] 456 | #[serde(rename_all = "camelCase")] 457 | pub struct SystemInstructionPart { 458 | #[serde(skip_serializing_if = "Option::is_none")] 459 | pub text: Option, 460 | } 461 | 462 | /// Configuration options for model generation and outputs. 463 | /// 464 | /// Not all parameters are configurable for every model. 465 | /// 466 | /// [API Reference](https://ai.google.dev/api/generate-content#v1beta.GenerationConfig) 467 | #[derive(Debug, Deserialize, Serialize, Default, Clone)] 468 | #[serde(rename_all = "camelCase")] 469 | pub struct GenerationConfig { 470 | pub temperature: Option, 471 | pub top_p: Option, 472 | pub top_k: Option, 473 | pub candidate_count: Option, 474 | pub max_output_tokens: Option, 475 | pub stop_sequences: Option>, 476 | pub response_mime_type: Option, 477 | pub response_schema: Option, 478 | #[serde(rename = "thinkingConfig", skip_serializing_if = "Option::is_none")] 479 | pub thinking_config: Option, 480 | } 481 | 482 | /// Config for thinking features 483 | /// 484 | /// [API Reference](https://ai.google.dev/api/generate-content#ThinkingConfig) 485 | #[derive(Debug, Default, Serialize, Deserialize, Clone)] 486 | #[serde(rename_all = "camelCase")] 487 | pub struct ThinkingConfig { 488 | #[serde(rename = "thinkingBudget", skip_serializing_if = "Option::is_none")] 489 | pub thinking_budget: Option, // 0~24576 490 | #[serde(rename = "includeThoughts", skip_serializing_if = "Option::is_none")] 491 | pub include_thoughts: Option, 492 | } 493 | 494 | /// Safety setting, affecting the safety-blocking behavior 495 | /// 496 | /// Passing a safety setting for a category changes the allowed probability that content is blocked. 497 | /// 498 | /// [API Reference](https://ai.google.dev/api/generate-content#safetysetting) 499 | #[derive(Debug, Deserialize, Serialize, Clone)] 500 | pub struct SafetySettings { 501 | pub category: HarmCategory, 502 | pub threshold: HarmBlockThreshold, 503 | } 504 | 505 | /// The Schema object allows the definition of input and output data types. 506 | /// 507 | /// These types can be objects, but also primitives and arrays. 508 | /// Represents a select subset of an [OpenAPI 3.0 schema 509 | /// object](https://spec.openapis.org/oas/v3.0.3#schema). 510 | /// 511 | /// [API Reference](https://ai.google.dev/api/caching#Schema) 512 | #[derive(Debug, Serialize, Deserialize, Clone, Default)] 513 | #[serde(rename_all = "camelCase")] 514 | pub struct Schema { 515 | #[serde(rename = "type")] 516 | pub schema_type: Option, 517 | pub format: Option, 518 | pub title: Option, 519 | pub description: Option, 520 | pub nullable: Option, 521 | #[serde(rename = "enum")] 522 | pub enum_values: Option>, 523 | #[serde(rename = "maxItems")] 524 | pub max_items: Option, 525 | #[serde(rename = "minItems")] 526 | pub min_items: Option, 527 | pub properties: Option>, 528 | pub required: Option>, 529 | #[serde(rename = "propertyOrdering")] 530 | pub property_ordering: Option>, 531 | pub items: Option>, 532 | } 533 | 534 | /// The Tool configuration containing parameters for specifying [Tools] use in the request 535 | /// 536 | /// [API Reference](https://ai.google.dev/api/caching#ToolConfig) 537 | #[derive(Debug, Clone, Deserialize, Serialize)] 538 | #[serde(rename_all = "camelCase")] 539 | pub struct ToolConfig { 540 | #[serde(skip_serializing_if = "Option::is_none")] 541 | pub function_calling_config: Option, 542 | } 543 | 544 | /// Configuration for specifying function calling behavior 545 | /// 546 | /// [API Reference](https://ai.google.dev/api/caching#FunctionCallingConfig) 547 | #[derive(Debug, Clone, Deserialize, Serialize)] 548 | #[serde(rename_all = "camelCase")] 549 | pub struct FunctionCallingConfig { 550 | #[serde(skip_serializing_if = "Option::is_none")] 551 | pub mode: Option, 552 | #[serde(skip_serializing_if = "Option::is_none")] 553 | pub allowed_function_names: Option>, 554 | } 555 | 556 | /// Defines the execution behavior for function calling by defining the execution mode 557 | /// 558 | /// [API Reference](https://ai.google.dev/api/caching#Mode_1) 559 | #[derive(Debug, Clone, Deserialize, Serialize)] 560 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 561 | pub enum FunctionCallingMode { 562 | /// Unspecified function calling mode. This value should not be used. 563 | ModeUnspecified, 564 | /// Default model behavior, model decides to predict either a function call or a natural language response. 565 | Auto, 566 | /// Model is constrained to always predicting a function call only. If "allowedFunctionNames" are set, the predicted function call will be limited to any one of "allowedFunctionNames", else the predicted function call will be any one of the provided "functionDeclarations". 567 | Any, 568 | /// Model will not predict any function call. Model behavior is same as when not passing any function declarations. 569 | None, 570 | /// Model decides to predict either a function call or a natural language response, but will validate function calls with constrained decoding. 571 | Validated, 572 | } 573 | 574 | /// Code generated by the model that is meant to be executed, and the result returned to the model 575 | /// 576 | /// Only generated when using the [CodeExecutionTool] tool, in which the code will be automatically executed, and a corresponding [CodeExecutionResult] will also be generated. 577 | #[derive(Debug, Deserialize, Serialize, Clone)] 578 | pub struct ExecutableCode { 579 | #[serde(rename = "language")] 580 | pub language: ProgrammingLanguage, 581 | #[serde(rename = "code")] 582 | pub code: String, 583 | } 584 | 585 | /// Supported programming languages for the generated code 586 | /// 587 | /// [API Reference](https://ai.google.dev/api/caching#Language) 588 | #[derive(Debug, Deserialize, Serialize, Clone)] 589 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 590 | pub enum ProgrammingLanguage { 591 | /// Unspecified language. This value should not be used. 592 | LanguageUnspecified, 593 | /// Python >= 3.10, with numpy and simpy available. 594 | Python, 595 | } 596 | 597 | /// Enumeration of possible outcomes of the [CodeExecutionTool] 598 | /// 599 | /// [API Reference](https://ai.google.dev/api/caching#Outcome) 600 | #[derive(Debug, Deserialize, Serialize, Clone)] 601 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 602 | pub enum Outcome { 603 | /// Unspecified status. This value should not be used. 604 | OutcomeUnspecified, 605 | /// Code execution completed successfully. 606 | OutcomeOk, 607 | /// Code execution finished but with a failure. stderr should contain the reason. 608 | OutcomeError, 609 | /// Code execution ran for too long, and was cancelled. There may or may not be a partial output present. 610 | OutcomeDeadlineExceeded, 611 | } 612 | 613 | /// The result output from a [FunctionCall] 614 | /// 615 | /// [API Reference](https://ai.google.dev/api/caching#FunctionResponse) 616 | #[derive(Debug, Deserialize, Serialize, Clone)] 617 | pub struct FunctionResponse { 618 | #[serde(skip_serializing_if = "Option::is_none")] 619 | pub id: Option, 620 | pub name: String, 621 | #[serde(skip_serializing_if = "Option::is_none")] 622 | //Optional. The function parameters and values in JSON object format. 623 | pub args: Option, 624 | } 625 | 626 | /// Result of executing the [ExecutableCode] 627 | /// 628 | /// [API Reference](https://ai.google.dev/api/caching#CodeExecutionResult) 629 | #[derive(Debug, Deserialize, Serialize, Clone)] 630 | pub struct CodeExecutionResult { 631 | pub outcome: Outcome, 632 | #[serde(skip_serializing_if = "Option::is_none")] 633 | pub output: Option, 634 | } 635 | 636 | /// Definitions of the types of data that can be used in [Schema] 637 | /// 638 | /// Copied from [serde_json](https://docs.rs/serde_json/1.0.140/serde_json/value/enum.Value.html) 639 | #[derive(Debug, Serialize, Deserialize, Clone, Copy)] 640 | #[serde(rename_all = "lowercase")] 641 | pub enum Type { 642 | Object, 643 | Array, 644 | String, 645 | Integer, 646 | Number, 647 | Boolean, 648 | } 649 | --------------------------------------------------------------------------------