use serde_json::{json, Map, Value};
use crate::canonical::{
CanonicalError, CanonicalRequest, ErrorKind, OutputFormat, Tool, ToolChoice,
};
use crate::protocol::json::finish_body;
use crate::protocol::{ProviderCtx, WireRequest};
mod contents;
mod count;
pub(super) use count::count as count_body;
pub(super) fn request_path(ctx: &ProviderCtx, stream: bool) -> String {
let verb = if stream {
"streamGenerateContent?alt=sse"
} else {
"generateContent"
};
format!("/v1beta/models/{}:{}", ctx.model, verb)
}
pub(super) fn encode(
req: &CanonicalRequest,
ctx: &ProviderCtx,
) -> Result<WireRequest, CanonicalError> {
let url = format!(
"{}{}",
ctx.base_url,
request_path(ctx, req.stream.unwrap_or(false))
);
Ok(finish_body(body_map(req)?, url))
}
fn body_map(req: &CanonicalRequest) -> Result<Map<String, Value>, CanonicalError> {
let mut body = Map::new();
if let Some(si) = contents::system_instruction(req)? {
body.insert("systemInstruction".into(), si);
}
body.insert("contents".into(), contents::contents_value(req)?);
if !req.tools.is_empty() {
body.insert(
"tools".into(),
json!([{ "functionDeclarations": fn_decls(&req.tools)? }]),
);
}
if let Some(tc) = tool_config(&req.tool_choice) {
body.insert("toolConfig".into(), tc);
}
let gen = generation_config(req);
if !gen.is_empty() {
body.insert("generationConfig".into(), Value::Object(gen));
}
for (k, v) in &req.extra {
body.entry(k.clone()).or_insert_with(|| v.clone()); }
Ok(body)
}
fn fn_decls(tools: &[Tool]) -> Result<Value, CanonicalError> {
let mut decls = Vec::new();
for t in tools {
let Tool::Custom {
name,
description,
input_schema,
.. } = t
else {
return Err(CanonicalError {
kind: ErrorKind::ParseInput,
message: "provider-typed tools are not projected for this dialect".into(),
provider_detail: None,
retry_after_seconds: None,
});
};
let mut d = json!({ "name": name, "parameters": input_schema });
if let Some(desc) = description {
d["description"] = json!(desc);
}
decls.push(d);
}
Ok(Value::Array(decls))
}
fn tool_config(tc: &ToolChoice) -> Option<Value> {
let cfg = match tc {
ToolChoice::Auto => return None,
ToolChoice::Any => json!({ "mode": "ANY" }),
ToolChoice::None => json!({ "mode": "NONE" }),
ToolChoice::Tool { name } => {
json!({ "mode": "ANY", "allowedFunctionNames": [name] })
}
};
Some(json!({ "functionCallingConfig": cfg }))
}
fn generation_config(req: &CanonicalRequest) -> Map<String, Value> {
let mut gen = Map::new();
if let Some(n) = req.max_tokens {
gen.insert("maxOutputTokens".into(), json!(n));
}
if let Some(t) = req.temperature {
gen.insert("temperature".into(), json!(t));
}
if let Some(p) = req.top_p {
gen.insert("topP".into(), json!(p));
}
if let Some(r) = req.reasoning {
gen.insert(
"thinkingConfig".into(),
json!({"thinkingBudget": r.budget(), "includeThoughts": true}),
);
}
if !req.stop.is_empty() {
gen.insert("stopSequences".into(), json!(req.stop)); }
match &req.output {
None => {}
Some(OutputFormat::Json) => {
gen.insert("responseMimeType".into(), json!("application/json"));
}
Some(OutputFormat::JsonSchema { schema, .. }) => {
gen.insert("responseMimeType".into(), json!("application/json"));
gen.insert("responseSchema".into(), schema.clone());
}
}
gen
}