use std::collections::HashMap;
use serde_json::Value;
use super::request_parts::{content_to_part, plain_text_part};
use super::wire::{
GeminiContent, GeminiEmpty, GeminiFunctionCallingConfig, GeminiFunctionDeclaration,
GeminiFunctionResponse, GeminiGenerationConfig, GeminiPart, GeminiRequest,
GeminiSystemInstruction, GeminiTool, GeminiToolConfig,
};
use crate::schema::SchemaSanitizer;
use crate::services::WireProtocol;
use crate::services::ai::ModelLimits;
use crate::wire::canonical::{
CanonicalContent, CanonicalMessage, CanonicalRequest, CanonicalToolChoice, ResponseFormat, Role,
};
#[must_use]
pub fn build_request_body(request: &CanonicalRequest, limits: Option<ModelLimits>) -> Value {
let body = GeminiRequest {
contents: contents(request),
system_instruction: request.system_text().map(|s| GeminiSystemInstruction {
parts: vec![plain_text_part(s)],
}),
generation_config: Some(generation_config(request, limits)),
tools: tools(request),
tool_config: request.tool_choice.as_ref().map(tool_config),
};
serde_json::to_value(&body).unwrap_or(Value::Null)
}
fn generation_config(
request: &CanonicalRequest,
limits: Option<ModelLimits>,
) -> GeminiGenerationConfig {
let (response_mime_type, response_schema) = match &request.response_format {
Some(ResponseFormat::JsonSchema { schema, .. }) => {
let sanitizer = SchemaSanitizer::new(WireProtocol::Gemini.schema_capabilities());
(
Some("application/json".to_owned()),
Some(sanitizer.sanitize(schema.clone())),
)
},
Some(ResponseFormat::JsonObject) => (Some("application/json".to_owned()), None),
None => (None, None),
};
let (thinking_config, max_output_tokens) = super::thinking::thinking_config(request, limits);
GeminiGenerationConfig {
temperature: request.temperature,
top_p: request.top_p,
top_k: request.top_k,
max_output_tokens: Some(max_output_tokens),
stop_sequences: if request.stop_sequences.is_empty() {
None
} else {
Some(request.stop_sequences.clone())
},
response_mime_type,
response_schema,
thinking_config,
}
}
fn tools(request: &CanonicalRequest) -> Option<Vec<GeminiTool>> {
let mut tools: Vec<GeminiTool> = Vec::new();
if !request.tools.is_empty() {
let sanitizer = SchemaSanitizer::new(WireProtocol::Gemini.schema_capabilities());
let declarations = request
.tools
.iter()
.map(|t| GeminiFunctionDeclaration {
name: t.name.clone(),
description: t.description.clone(),
parameters: sanitizer.sanitize(t.input_schema.clone()),
})
.collect();
tools.push(GeminiTool::Functions {
function_declarations: declarations,
});
}
if let Some(search) = &request.search {
tools.push(GeminiTool::GoogleSearch {
google_search: GeminiEmpty {},
});
if !search.urls.is_empty() {
tools.push(GeminiTool::UrlContext {
url_context: GeminiEmpty {},
});
}
}
if request.code_execution {
tools.push(GeminiTool::CodeExecution {
code_execution: GeminiEmpty {},
});
}
(!tools.is_empty()).then_some(tools)
}
fn tool_config(choice: &CanonicalToolChoice) -> GeminiToolConfig {
let (mode, allowed) = match choice {
CanonicalToolChoice::Auto => ("AUTO", None),
CanonicalToolChoice::None => ("NONE", None),
CanonicalToolChoice::Any | CanonicalToolChoice::Required => ("ANY", None),
CanonicalToolChoice::Tool(name) => ("ANY", Some(vec![name.clone()])),
};
GeminiToolConfig {
function_calling_config: GeminiFunctionCallingConfig {
mode,
allowed_function_names: allowed,
},
}
}
fn contents(request: &CanonicalRequest) -> Vec<GeminiContent> {
let call_names = tool_call_names(request);
let mut contents: Vec<GeminiContent> = Vec::new();
for msg in &request.messages {
let Some(content) = message_to_content(msg, &call_names) else {
continue;
};
match contents.last_mut() {
Some(last) if last.role == "user" && content.role == "user" => {
last.parts.extend(content.parts);
},
_ => contents.push(content),
}
}
for content in &mut contents {
if content.role == "user" {
fold_text_into_function_responses(&mut content.parts);
} else {
share_turn_signature(&mut content.parts);
}
}
contents
}
fn share_turn_signature(parts: &mut [GeminiPart]) {
let turn_signature = parts
.iter()
.find_map(|part| match part {
GeminiPart::FunctionCall {
thought_signature: Some(signature),
..
} => Some(signature.clone()),
_ => None,
})
.or_else(|| {
parts.iter().find_map(|part| match part {
GeminiPart::Text {
thought: Some(true),
thought_signature: Some(signature),
..
} => Some(signature.clone()),
_ => None,
})
});
let Some(signature) = turn_signature else {
return;
};
for part in parts.iter_mut() {
if let GeminiPart::FunctionCall {
thought_signature: slot @ None,
..
} = part
{
*slot = Some(signature.clone());
}
}
}
fn fold_text_into_function_responses(parts: &mut Vec<GeminiPart>) {
let has_response = parts
.iter()
.any(|part| matches!(part, GeminiPart::FunctionResponse { .. }));
if !has_response {
return;
}
let mut texts: Vec<String> = Vec::new();
let mut kept: Vec<GeminiPart> = Vec::with_capacity(parts.len());
for part in parts.drain(..) {
match part {
GeminiPart::Text {
text,
thought: None | Some(false),
..
} => texts.push(text),
other => kept.push(other),
}
}
let mut responses: Vec<&mut GeminiFunctionResponse> = kept
.iter_mut()
.filter_map(|part| match part {
GeminiPart::FunctionResponse { function_response } => Some(function_response),
_ => None,
})
.collect();
let last = responses.len() - 1;
for (i, text) in texts.into_iter().enumerate() {
append_context(responses[i.min(last)], &text);
}
*parts = kept;
}
fn append_context(response: &mut GeminiFunctionResponse, text: &str) {
if let Some(Value::String(result)) = response.response.get_mut("result") {
result.push_str("\n\n");
result.push_str(text);
return;
}
if let Some(map) = response.response.as_object_mut() {
match map.get_mut("context") {
Some(Value::String(existing)) => {
existing.push_str("\n\n");
existing.push_str(text);
},
_ => {
map.insert("context".to_owned(), Value::String(text.to_owned()));
},
}
}
}
fn tool_call_names(request: &CanonicalRequest) -> HashMap<&str, &str> {
let mut names = HashMap::new();
for msg in &request.messages {
for part in &msg.content {
if let CanonicalContent::ToolUse { id, name, .. } = part {
names.insert(id.as_str(), name.as_str());
}
}
}
names
}
fn message_to_content(
msg: &CanonicalMessage,
call_names: &HashMap<&str, &str>,
) -> Option<GeminiContent> {
let role = match msg.role {
Role::Assistant => "model",
Role::User | Role::Tool | Role::System => "user",
};
let parts: Vec<GeminiPart> = msg
.content
.iter()
.map(|part| content_to_part(part, call_names))
.collect();
if parts.is_empty() {
return None;
}
Some(GeminiContent {
role: role.to_owned(),
parts,
})
}