Download crates/forge_app/src/dto/google/request.rs from SaylorTwift/forgecode: direct link, hf CLI and curl.
- Browser
- Download file 33.1 kB
-
https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_app/src/dto/google/request.rs
- Command line
-
hf download hf://SaylorTwift/forgecode/crates/forge_app/src/dto/google/request.rs
-
curl -L -o request.rs https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_app/src/dto/google/request.rs
33.1 kB
| use derive_setters::Setters; | |
| use forge_domain::{Context, ContextMessage}; | |
| use serde::Serialize; | |
| pub struct Request { | |
| pub system_instruction: Option<Content>, | |
| pub contents: Vec<Content>, | |
| pub generation_config: Option<GenerationConfig>, | |
| pub tools: Option<Vec<Tool>>, | |
| pub tool_config: Option<ToolConfig>, | |
| pub safety_settings: Option<Vec<SafetySetting>>, | |
| pub cached_content: Option<String>, | |
| pub labels: Option<serde_json::Value>, | |
| } | |
| pub struct GenerationConfig { | |
| pub thinking_config: Option<ThinkingConfig>, | |
| pub stop_sequences: Option<Vec<String>>, | |
| pub temperature: Option<f64>, | |
| pub top_p: Option<f64>, | |
| pub top_k: Option<i32>, | |
| pub response_mime_type: Option<String>, | |
| pub response_schema: Option<serde_json::Value>, | |
| pub max_output_tokens: Option<i32>, | |
| pub frequency_penalty: Option<f64>, | |
| pub presence_penalty: Option<f64>, | |
| pub seed: Option<i32>, | |
| pub response_modalities: Option<Vec<String>>, | |
| pub audio_timestamp: Option<bool>, | |
| pub media_resolution: Option<String>, | |
| pub image_config: Option<serde_json::Value>, | |
| } | |
| pub struct ThinkingConfig { | |
| pub thinking_level: Option<Level>, | |
| pub thinking_budget: Option<i32>, | |
| pub include_thoughts: Option<bool>, | |
| } | |
| pub enum Level { | |
| Minimal, | |
| Low, | |
| Medium, | |
| High, | |
| } | |
| pub struct Content { | |
| pub role: Option<Role>, | |
| pub parts: Vec<Part>, | |
| } | |
| pub enum Part { | |
| Text { | |
| text: String, | |
| thought: Option<bool>, | |
| thought_signature: Option<String>, | |
| cache_control: Option<CacheControl>, | |
| }, | |
| Image { | |
| inline_data: ImageSource, | |
| cache_control: Option<CacheControl>, | |
| }, | |
| FunctionCall { | |
| function_call: FunctionCallData, | |
| thought_signature: Option<String>, | |
| }, | |
| FunctionResponse { | |
| function_response: FunctionResponseData, | |
| }, | |
| FileData { | |
| file_data: FileDataInfo, | |
| }, | |
| } | |
| pub struct ImageSource { | |
| pub mime_type: Option<String>, | |
| pub data: Option<String>, | |
| } | |
| pub struct FunctionCallData { | |
| pub name: String, | |
| pub args: serde_json::Value, | |
| } | |
| pub struct FunctionResponseData { | |
| pub name: String, | |
| pub response: serde_json::Value, | |
| } | |
| pub struct FileDataInfo { | |
| pub mime_type: String, | |
| pub file_uri: String, | |
| } | |
| pub enum CacheControl { | |
| Ephemeral, | |
| } | |
| pub enum Role { | |
| User, | |
| Model, | |
| } | |
| pub enum Tool { | |
| FunctionDeclarations { | |
| function_declarations: Option<Vec<FunctionDeclaration>>, | |
| }, | |
| GoogleSearch { | |
| google_search: GoogleSearchTool, | |
| }, | |
| GoogleSearchRetrieval { | |
| google_search_retrieval: GoogleSearchRetrievalTool, | |
| }, | |
| EnterpriseWebSearch { | |
| enterprise_web_search: serde_json::Value, | |
| }, | |
| UrlContext { | |
| url_context: serde_json::Value, | |
| }, | |
| CodeExecution { | |
| code_execution: serde_json::Value, | |
| }, | |
| FileSearch { | |
| file_search: FileSearchTool, | |
| }, | |
| GoogleMaps { | |
| google_maps: serde_json::Value, | |
| }, | |
| Retrieval { | |
| retrieval: RetrievalTool, | |
| }, | |
| } | |
| pub struct GoogleSearchTool { | |
| // Empty object for Gemini 2.0+ | |
| } | |
| pub struct GoogleSearchRetrievalTool { | |
| pub dynamic_retrieval_config: Option<DynamicRetrievalConfig>, | |
| } | |
| pub struct DynamicRetrievalConfig { | |
| pub mode: Option<String>, | |
| pub dynamic_threshold: Option<f64>, | |
| } | |
| pub struct FileSearchTool { | |
| pub file_search_store_names: Vec<String>, | |
| pub top_k: Option<i32>, | |
| pub metadata_filter: Option<String>, | |
| } | |
| pub struct RetrievalTool { | |
| pub vertex_rag_store: VertexRagStore, | |
| } | |
| pub struct VertexRagStore { | |
| pub rag_resources: RagResources, | |
| pub similarity_top_k: Option<i32>, | |
| } | |
| pub struct RagResources { | |
| pub rag_corpus: String, | |
| } | |
| pub struct FunctionDeclaration { | |
| pub name: String, | |
| pub description: Option<String>, | |
| pub parameters: serde_json::Value, | |
| } | |
| pub struct ToolConfig { | |
| pub function_calling_config: Option<FunctionCallingConfig>, | |
| pub retrieval_config: Option<serde_json::Value>, | |
| } | |
| pub struct FunctionCallingConfig { | |
| pub mode: Option<FunctionCallingMode>, | |
| pub allowed_function_names: Option<Vec<String>>, | |
| } | |
| pub enum FunctionCallingMode { | |
| ModeUnspecified, | |
| Auto, | |
| Any, | |
| None, | |
| } | |
| pub struct SafetySetting { | |
| pub category: String, | |
| pub threshold: String, | |
| } | |
| impl From<Context> for Request { | |
| fn from(context: Context) -> Self { | |
| // Extract system instruction from ALL system messages | |
| let system_parts: Vec<Part> = context | |
| .messages | |
| .iter() | |
| .filter(|msg| msg.has_role(forge_domain::Role::System)) | |
| .filter_map(|msg| { | |
| msg.content().map(|content| Part::Text { | |
| text: content.to_string(), | |
| thought: None, | |
| thought_signature: None, | |
| cache_control: None, | |
| }) | |
| }) | |
| .collect(); | |
| let system_instruction = if !system_parts.is_empty() { | |
| Some(Content { role: None, parts: system_parts }) | |
| } else { | |
| None | |
| }; | |
| // Convert messages (excluding system messages) | |
| // Group consecutive tool results into single Content objects to match Google's | |
| // API requirements | |
| let mut contents: Vec<Content> = Vec::new(); | |
| let mut pending_tool_parts: Vec<Part> = Vec::new(); | |
| for msg in context | |
| .messages | |
| .into_iter() | |
| .filter(|msg| !msg.has_role(forge_domain::Role::System)) | |
| { | |
| match msg.message { | |
| ContextMessage::Tool(tool_result) => { | |
| // Collect tool result parts to be grouped together | |
| pending_tool_parts.push(Part::from(tool_result)); | |
| } | |
| other => { | |
| // Flush any pending tool results first | |
| if !pending_tool_parts.is_empty() { | |
| contents.push(Content { | |
| role: Some(Role::User), | |
| parts: std::mem::take(&mut pending_tool_parts), | |
| }); | |
| } | |
| // Add the current non-tool message | |
| let content = Content::from(other); | |
| if !content.parts.is_empty() { | |
| contents.push(content); | |
| } | |
| } | |
| } | |
| } | |
| // Flush any remaining tool results | |
| if !pending_tool_parts.is_empty() { | |
| contents.push(Content { role: Some(Role::User), parts: pending_tool_parts }); | |
| } | |
| // Convert tools | |
| let tools = if !context.tools.is_empty() { | |
| Some(vec![Tool::FunctionDeclarations { | |
| function_declarations: Some( | |
| context | |
| .tools | |
| .into_iter() | |
| .map(FunctionDeclaration::from) | |
| .collect(), | |
| ), | |
| }]) | |
| } else { | |
| None | |
| }; | |
| // Build generation config | |
| let generation_config = Some(GenerationConfig { | |
| max_output_tokens: context.max_tokens.map(|t| t as i32), | |
| temperature: context.temperature.map(|t| t.value() as f64), | |
| top_p: context.top_p.map(|t| t.value() as f64), | |
| top_k: context.top_k.map(|t| t.value() as i32), | |
| response_mime_type: context.response_format.as_ref().and_then(|rf| match rf { | |
| forge_domain::ResponseFormat::JsonSchema(_) => Some("application/json".to_string()), | |
| _ => None, | |
| }), | |
| response_schema: context.response_format.and_then(|rf| match rf { | |
| forge_domain::ResponseFormat::JsonSchema(schema) => { | |
| let mut schema_value = serde_json::to_value(*schema).ok()?; | |
| // Sanitize schema for Gemini API compatibility | |
| crate::utils::sanitize_gemini_schema(&mut schema_value); | |
| Some(schema_value) | |
| } | |
| _ => None, | |
| }), | |
| thinking_config: context.reasoning.and_then(|reasoning| { | |
| reasoning.enabled.and_then(|enabled| { | |
| if enabled { | |
| Some(ThinkingConfig { | |
| thinking_level: None, | |
| thinking_budget: reasoning.max_tokens.map(|t| t as i32), | |
| include_thoughts: Some(true), | |
| }) | |
| } else { | |
| None | |
| } | |
| }) | |
| }), | |
| ..Default::default() | |
| }); | |
| // Build tool config for tool choice | |
| // Only set tool_config if there's an explicit tool choice | |
| let tool_config = context.tool_choice.map(ToolConfig::from); | |
| Request { | |
| system_instruction, | |
| contents, | |
| generation_config, | |
| tools, | |
| tool_config, | |
| safety_settings: None, | |
| cached_content: None, | |
| labels: None, | |
| } | |
| } | |
| } | |
| impl From<forge_domain::ToolChoice> for ToolConfig { | |
| fn from(choice: forge_domain::ToolChoice) -> Self { | |
| let (mode, allowed_function_names) = match choice { | |
| forge_domain::ToolChoice::Auto => (FunctionCallingMode::Auto, None), | |
| forge_domain::ToolChoice::None => (FunctionCallingMode::None, None), | |
| forge_domain::ToolChoice::Required => (FunctionCallingMode::Any, None), | |
| forge_domain::ToolChoice::Call(name) => { | |
| (FunctionCallingMode::Any, Some(vec![name.to_string()])) | |
| } | |
| }; | |
| ToolConfig { | |
| function_calling_config: Some(FunctionCallingConfig { | |
| mode: Some(mode), | |
| allowed_function_names, | |
| }), | |
| retrieval_config: None, | |
| } | |
| } | |
| } | |
| impl From<forge_domain::ToolDefinition> for FunctionDeclaration { | |
| fn from(tool: forge_domain::ToolDefinition) -> Self { | |
| let mut parameters = | |
| serde_json::to_value(tool.input_schema).unwrap_or(serde_json::json!({})); | |
| // Sanitize schema for Gemini API compatibility (strips $schema, | |
| // removes additionalProperties, converts integer enums, ensures | |
| // arrays have items, removes properties from non-objects, etc.) | |
| crate::utils::sanitize_gemini_schema(&mut parameters); | |
| FunctionDeclaration { | |
| name: tool.name.to_string(), | |
| description: Some(tool.description), | |
| parameters, | |
| } | |
| } | |
| } | |
| impl From<ContextMessage> for Content { | |
| fn from(message: ContextMessage) -> Self { | |
| match message { | |
| ContextMessage::Text(text_message) => Content::from(text_message), | |
| ContextMessage::Tool(tool_result) => Content::from(tool_result), | |
| ContextMessage::Image(image) => Content::from(image), | |
| } | |
| } | |
| } | |
| impl From<forge_domain::TextMessage> for Content { | |
| fn from(text_message: forge_domain::TextMessage) -> Self { | |
| let role = match text_message.role { | |
| forge_domain::Role::User => Some(Role::User), | |
| forge_domain::Role::Assistant => Some(Role::Model), | |
| forge_domain::Role::System => None, // System messages are handled separately | |
| }; | |
| let mut parts = Vec::new(); | |
| // Add text part if content is not empty | |
| if !text_message.content.is_empty() { | |
| parts.push(Part::Text { | |
| text: text_message.content, | |
| thought: None, | |
| thought_signature: text_message.thought_signature.clone(), | |
| cache_control: None, | |
| }); | |
| } | |
| // Add function calls if present | |
| if let Some(tool_calls) = text_message.tool_calls { | |
| parts.extend(tool_calls.into_iter().map(Part::from)); | |
| } | |
| Content { role, parts } | |
| } | |
| } | |
| impl From<forge_domain::ToolResult> for Content { | |
| fn from(tool_result: forge_domain::ToolResult) -> Self { | |
| Content { | |
| role: Some(Role::User), // Tool results come back as user messages in Google's API | |
| parts: vec![Part::from(tool_result)], | |
| } | |
| } | |
| } | |
| impl From<forge_domain::Image> for Content { | |
| fn from(image: forge_domain::Image) -> Self { | |
| Content { role: Some(Role::User), parts: vec![Part::from(image)] } | |
| } | |
| } | |
| impl From<forge_domain::ToolCallFull> for Part { | |
| fn from(tool_call: forge_domain::ToolCallFull) -> Self { | |
| Part::FunctionCall { | |
| function_call: FunctionCallData { | |
| name: tool_call.name.to_string(), | |
| args: tool_call.arguments.parse().unwrap_or(serde_json::json!({})), | |
| }, | |
| thought_signature: tool_call.thought_signature, | |
| } | |
| } | |
| } | |
| impl From<forge_domain::ToolResult> for Part { | |
| fn from(tool_result: forge_domain::ToolResult) -> Self { | |
| Part::FunctionResponse { | |
| function_response: FunctionResponseData { | |
| name: tool_result.name.to_string(), | |
| response: serde_json::to_value(&tool_result.output) | |
| .unwrap_or(serde_json::json!({})), | |
| }, | |
| } | |
| } | |
| } | |
| impl From<forge_domain::Image> for Part { | |
| fn from(image: forge_domain::Image) -> Self { | |
| Part::Image { | |
| inline_data: ImageSource { | |
| mime_type: Some(image.mime_type().to_string()), | |
| data: Some(image.data().to_string()), | |
| }, | |
| cache_control: None, | |
| } | |
| } | |
| } | |
| mod tests { | |
| use forge_domain::{ToolCallArguments, ToolCallFull, ToolCallId, ToolName, ToolResult}; | |
| use super::*; | |
| fn test_tool_call_args_serialization() { | |
| // Create a ToolCallFull with Unparsed JSON arguments (as it would come from | |
| // API) | |
| let tool_call = ToolCallFull { | |
| name: ToolName::new("patch"), | |
| call_id: None, | |
| arguments: ToolCallArguments::from_json( | |
| r#"{"file_path":"test.rs","old_string":"foo","new_string":"bar"}"#, | |
| ), | |
| thought_signature: None, | |
| }; | |
| // Convert to Google Part | |
| let part = Part::from(tool_call); | |
| // Verify it serializes correctly | |
| let serialized = serde_json::to_value(&part).unwrap(); | |
| // The Part enum serializes using snake_case for the variant name | |
| if let Some(function_call) = serialized.get("function_call") { | |
| if let Some(args) = function_call.get("args") { | |
| // Args should be a proper JSON object, not a string | |
| assert!( | |
| args.is_object(), | |
| "Args should be deserialized as JSON object, not string: {:?}", | |
| args | |
| ); | |
| // Verify fields are accessible | |
| assert_eq!(args["file_path"], "test.rs"); | |
| assert_eq!(args["old_string"], "foo"); | |
| assert_eq!(args["new_string"], "bar"); | |
| } else { | |
| panic!("Expected function_call.args to exist"); | |
| } | |
| } else { | |
| panic!("Expected function_call variant"); | |
| } | |
| } | |
| fn test_multiple_tool_calls_serialization() { | |
| // Simulate multiple tool calls with different args | |
| let tool_calls = vec![ | |
| ToolCallFull { | |
| name: ToolName::new("remove"), | |
| call_id: None, | |
| arguments: ToolCallArguments::from_json(r#"{"path":"file1.rs"}"#), | |
| thought_signature: None, | |
| }, | |
| ToolCallFull { | |
| name: ToolName::new("remove"), | |
| call_id: None, | |
| arguments: ToolCallArguments::from_json(r#"{"path":"file2.rs"}"#), | |
| thought_signature: None, | |
| }, | |
| ]; | |
| // Convert each to Part and serialize | |
| for (i, tool_call) in tool_calls.into_iter().enumerate() { | |
| let part = Part::from(tool_call); | |
| let serialized = serde_json::to_value(&part).unwrap(); | |
| if let Some(function_call) = serialized.get("function_call") { | |
| if let Some(args) = function_call.get("args") { | |
| assert!( | |
| args.is_object(), | |
| "Tool call {} args should be object, got: {:?}", | |
| i, | |
| args | |
| ); | |
| let expected_path = format!("file{}.rs", i + 1); | |
| assert_eq!(args["path"], expected_path, "Tool call {} path mismatch", i); | |
| } else { | |
| panic!("Expected function_call.args for tool call {}", i); | |
| } | |
| } else { | |
| panic!("Expected function_call variant for tool call {}", i); | |
| } | |
| } | |
| } | |
| fn test_consecutive_tool_results_grouped() { | |
| use forge_domain::{Context, ContextMessage, ModelId}; | |
| // Create a context with multiple consecutive tool results (simulating 13 read | |
| // calls) | |
| let mut context = Context::default(); | |
| // Add initial user message | |
| context = context.add_message(ContextMessage::user( | |
| "Read these files", | |
| Some(ModelId::new("test")), | |
| )); | |
| // Add assistant message with tool calls | |
| context = context.add_message(ContextMessage::assistant("", None, None, None)); | |
| // Add 13 consecutive tool results (like in the dump) | |
| for i in 1..=13 { | |
| let tool_result = ToolResult::new("read") | |
| .call_id(ToolCallId::new(format!("call_{}", i))) | |
| .success(format!("Content of file {}", i)); | |
| context = context.add_message(ContextMessage::tool_result(tool_result)); | |
| } | |
| // Convert to Google Request | |
| let request = Request::from(context); | |
| // Verify structure: | |
| // 1. First content: user message | |
| // 2. Second content: assistant message (might be empty and filtered out) | |
| // 3. Third content: ALL 13 tool results grouped together | |
| // Find the content with tool results | |
| let tool_result_content = request | |
| .contents | |
| .iter() | |
| .find(|c| { | |
| c.parts | |
| .iter() | |
| .any(|p| matches!(p, Part::FunctionResponse { .. })) | |
| }) | |
| .expect("Should have a content with function responses"); | |
| // Verify all 13 tool results are in ONE Content object | |
| let function_response_count = tool_result_content | |
| .parts | |
| .iter() | |
| .filter(|p| matches!(p, Part::FunctionResponse { .. })) | |
| .count(); | |
| assert_eq!( | |
| function_response_count, 13, | |
| "All 13 tool results should be grouped into a single Content with 13 FunctionResponse parts" | |
| ); | |
| // Verify the role is User | |
| assert_eq!( | |
| tool_result_content.role, | |
| Some(Role::User), | |
| "Tool results should have User role" | |
| ); | |
| } | |
| fn test_non_consecutive_tool_results_not_grouped() { | |
| use forge_domain::{Context, ContextMessage, ModelId}; | |
| // Create a context with tool results separated by other messages | |
| let mut context = Context::default(); | |
| // User message | |
| context = context.add_message(ContextMessage::user( | |
| "First task", | |
| Some(ModelId::new("test")), | |
| )); | |
| // Tool result #1 | |
| let tool_result_1 = ToolResult::new("read") | |
| .call_id(ToolCallId::new("call_1")) | |
| .success("Result 1"); | |
| context = context.add_message(ContextMessage::tool_result(tool_result_1)); | |
| // Assistant message (breaks the sequence) | |
| context = context.add_message(ContextMessage::assistant( | |
| "Let me do more", | |
| None, | |
| None, | |
| None, | |
| )); | |
| // Tool result #2 (should be in a separate Content) | |
| let tool_result_2 = ToolResult::new("read") | |
| .call_id(ToolCallId::new("call_2")) | |
| .success("Result 2"); | |
| context = context.add_message(ContextMessage::tool_result(tool_result_2)); | |
| // Convert to Google Request | |
| let request = Request::from(context); | |
| // Count how many Content objects have FunctionResponse parts | |
| let contents_with_tool_results: Vec<_> = request | |
| .contents | |
| .iter() | |
| .filter(|c| { | |
| c.parts | |
| .iter() | |
| .any(|p| matches!(p, Part::FunctionResponse { .. })) | |
| }) | |
| .collect(); | |
| // Should have 2 separate Content objects for the 2 non-consecutive tool results | |
| assert_eq!( | |
| contents_with_tool_results.len(), | |
| 2, | |
| "Non-consecutive tool results should be in separate Content objects" | |
| ); | |
| // Each should have exactly 1 FunctionResponse | |
| for content in contents_with_tool_results { | |
| let count = content | |
| .parts | |
| .iter() | |
| .filter(|p| matches!(p, Part::FunctionResponse { .. })) | |
| .count(); | |
| assert_eq!( | |
| count, 1, | |
| "Each separate tool result group should have 1 FunctionResponse" | |
| ); | |
| } | |
| } | |
| fn test_tool_choice_conversion() { | |
| use forge_domain::ToolChoice; | |
| // Test Auto | |
| let config = ToolConfig::from(ToolChoice::Auto); | |
| let fc_config = config.function_calling_config.unwrap(); | |
| assert!(matches!(fc_config.mode, Some(FunctionCallingMode::Auto))); | |
| assert!(fc_config.allowed_function_names.is_none()); | |
| // Test None | |
| let config = ToolConfig::from(ToolChoice::None); | |
| let fc_config = config.function_calling_config.unwrap(); | |
| assert!(matches!(fc_config.mode, Some(FunctionCallingMode::None))); | |
| assert!(fc_config.allowed_function_names.is_none()); | |
| // Test Required | |
| let config = ToolConfig::from(ToolChoice::Required); | |
| let fc_config = config.function_calling_config.unwrap(); | |
| assert!(matches!(fc_config.mode, Some(FunctionCallingMode::Any))); | |
| assert!(fc_config.allowed_function_names.is_none()); | |
| // Test Call | |
| let config = ToolConfig::from(ToolChoice::Call(ToolName::new("my_tool"))); | |
| let fc_config = config.function_calling_config.unwrap(); | |
| assert!(matches!(fc_config.mode, Some(FunctionCallingMode::Any))); | |
| assert_eq!(fc_config.allowed_function_names.unwrap(), vec!["my_tool"]); | |
| } | |
| fn test_tool_definition_conversion() { | |
| use forge_domain::ToolDefinition; | |
| use schemars::schema_for; | |
| struct Args { | |
| _arg1: String, | |
| } | |
| let tool_def = ToolDefinition { | |
| name: ToolName::new("test_tool"), | |
| description: "A test tool".to_string(), | |
| input_schema: schema_for!(Args), | |
| }; | |
| let decl = FunctionDeclaration::from(tool_def); | |
| assert_eq!(decl.name, "test_tool"); | |
| assert_eq!(decl.description.unwrap(), "A test tool"); | |
| // Check Gemini schema sanitization | |
| let params = decl.parameters; | |
| assert!(params.is_object()); | |
| assert!(!params.as_object().unwrap().contains_key("$schema")); | |
| assert!(params.as_object().unwrap().contains_key("type")); | |
| // additionalProperties should be removed by Gemini sanitization | |
| assert!( | |
| !params | |
| .as_object() | |
| .unwrap() | |
| .contains_key("additionalProperties") | |
| ); | |
| } | |
| fn test_text_message_conversion() { | |
| use forge_domain::{Role, TextMessage}; | |
| // User message | |
| let msg = TextMessage::new(Role::User, "Hello"); | |
| let content = Content::from(msg); | |
| assert_eq!(content.role, Some(self::Role::User)); | |
| assert_eq!(content.parts.len(), 1); | |
| match &content.parts[0] { | |
| Part::Text { text, .. } => assert_eq!(text, "Hello"), | |
| _ => panic!("Expected Text part"), | |
| } | |
| // Assistant message with thought signature | |
| let msg = TextMessage::assistant("Hi", None, None).thought_signature("sig"); | |
| let content = Content::from(msg); | |
| assert_eq!(content.role, Some(self::Role::Model)); | |
| match &content.parts[0] { | |
| Part::Text { text, thought_signature, .. } => { | |
| assert_eq!(text, "Hi"); | |
| assert_eq!(thought_signature.as_deref(), Some("sig")); | |
| } | |
| _ => panic!("Expected Text part"), | |
| } | |
| } | |
| fn test_image_conversion() { | |
| use forge_domain::Image; | |
| let image = Image::new_base64("base64data".to_string(), "image/png"); | |
| let content = Content::from(image.clone()); | |
| assert_eq!(content.role, Some(self::Role::User)); | |
| assert_eq!(content.parts.len(), 1); | |
| match &content.parts[0] { | |
| Part::Image { inline_data, .. } => { | |
| assert_eq!(inline_data.mime_type.as_deref(), Some("image/png")); | |
| assert_eq!(inline_data.data.as_deref(), Some("base64data")); | |
| } | |
| _ => panic!("Expected Image part"), | |
| } | |
| // Test direct Part conversion | |
| let part = Part::from(image); | |
| match part { | |
| Part::Image { inline_data, .. } => { | |
| assert_eq!(inline_data.mime_type.as_deref(), Some("image/png")); | |
| assert_eq!(inline_data.data.as_deref(), Some("base64data")); | |
| } | |
| _ => panic!("Expected Image part"), | |
| } | |
| } | |
| fn test_response_schema_strips_dollar_schema() { | |
| use forge_domain::{Context, ResponseFormat}; | |
| use schemars::schema_for; | |
| struct TestResponse { | |
| result: String, | |
| } | |
| // Create a context with a JSON schema response format | |
| let schema = schema_for!(TestResponse); | |
| let context = | |
| Context::default().response_format(ResponseFormat::JsonSchema(Box::new(schema))); | |
| // Convert to Google Request | |
| let request = Request::from(context); | |
| // Verify generation_config has response_schema | |
| let generation_config = request | |
| .generation_config | |
| .expect("Should have generation_config"); | |
| let response_schema = generation_config | |
| .response_schema | |
| .expect("Should have response_schema"); | |
| // Verify $schema field is removed | |
| if let Some(obj) = response_schema.as_object() { | |
| assert!( | |
| !obj.contains_key("$schema"), | |
| "$schema field should be removed from response_schema" | |
| ); | |
| // Verify other schema properties are still present | |
| assert!( | |
| obj.contains_key("type") | |
| || obj.contains_key("properties") | |
| || obj.contains_key("title"), | |
| "Schema should still contain other properties" | |
| ); | |
| // Verify additionalProperties is also removed by Gemini sanitization | |
| assert!( | |
| !obj.contains_key("additionalProperties"), | |
| "additionalProperties should be removed by Gemini sanitization" | |
| ); | |
| } else { | |
| panic!("response_schema should be an object"); | |
| } | |
| // Verify response_mime_type is set to application/json | |
| assert_eq!( | |
| generation_config.response_mime_type, | |
| Some("application/json".to_string()), | |
| "response_mime_type should be set for JSON schema" | |
| ); | |
| } | |
| fn test_tool_result_part_conversion() { | |
| use forge_domain::{ToolCallId, ToolName, ToolResult}; | |
| let result = ToolResult::new(ToolName::new("test")) | |
| .call_id(ToolCallId::new("call_1")) | |
| .success("output"); | |
| let part = Part::from(result); | |
| match part { | |
| Part::FunctionResponse { function_response } => { | |
| assert_eq!(function_response.name, "test"); | |
| // The response should be wrapped in a JSON value | |
| let expected = serde_json::json!({ | |
| "is_error": false, | |
| "values": [{"text": "output"}] | |
| }); | |
| assert_eq!(function_response.response, expected); | |
| } | |
| _ => panic!("Expected FunctionResponse part"), | |
| } | |
| } | |
| } | |