Skip to content

toGemini — emit Gemini tool schema

Rust · crate toolnexus · SPEC §4

pub fn to_gemini(tools: &[Tool]) -> Vec<Value>
impl Toolkit { pub fn to_gemini(&self) -> Vec<Value> }

Render the toolkit as Gemini function-declaration schema.

When you call the provider yourself and need the tool schema in its shape: [\{functionDeclarations:[...]\}]. Schema only — the input schema is passed through verbatim.

Use the adapter rather than hand-writing the JSON: names and schemas then always match what Toolkit::execute routes.

1. One tool, and the wrapper that surprises everyone

Section titled “1. One tool, and the wrapper that surprises everyone”
use serde_json::json;
use toolnexus::{define_tool, to_gemini};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let weather = define_tool(
"get_weather",
"Current weather for a city",
Some(json!({"type":"object","properties":{"city":{"type":"string"}},"required":["city"]})),
|args, _ctx| async move {
let city = args["city"].as_str().ok_or("city is required")?;
Ok::<_, String>(format!("{city}: 21C, clear"))
},
);
let now = define_tool("now", "Current time", None, |_a, _c| async { Ok::<_, String>("12:00".to_string()) });
let schema = to_gemini(&[weather, now]);
// ONE element wrapping ALL declarations — not one element per tool.
assert_eq!(schema.len(), 1);
let decls = schema[0]["functionDeclarations"].as_array().unwrap();
assert_eq!(decls.len(), 2);
assert_eq!(decls[0]["name"], "get_weather");
assert_eq!(decls[0]["parameters"]["required"][0], "city");
println!("{}", serde_json::to_string_pretty(&schema)?);
println!("ok: 1 wrapper, {} declarations", decls.len());
Ok(())
}

2. Feeding it straight into a generateContent body

Section titled “2. Feeding it straight into a generateContent body”
use serde_json::json;
use toolnexus::{BuiltinsConfig, ToolkitOptions, create_toolkit, define_tool};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let now = define_tool("now", "Current time", None, |_a, _c| async { Ok::<_, String>("12:00".to_string()) });
let tk = create_toolkit(ToolkitOptions {
extra_tools: vec![now],
builtins: Some(BuiltinsConfig::off()),
..Default::default()
})
.await?;
let body = json!({
"contents": [{"role": "user", "parts": [{"text": "what time is it?"}]}],
"tools": tk.to_gemini(),
});
assert_eq!(body["tools"][0]["functionDeclarations"][0]["name"], "now");
println!("ok: {}", body["tools"][0]["functionDeclarations"][0]["name"]);
Ok(())
}

3. Round-tripping a functionCall part back to the tool

Section titled “3. Round-tripping a functionCall part back to the tool”

Gemini returns args as an object and has no call id — the reply functionResponse is matched by name.

use serde_json::json;
use toolnexus::{BuiltinsConfig, ToolContext, ToolkitOptions, create_toolkit, define_tool};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let weather = define_tool(
"get_weather",
"Current weather for a city",
Some(json!({"type":"object","properties":{"city":{"type":"string"}},"required":["city"]})),
|args, _ctx| async move {
let city = args["city"].as_str().ok_or("city is required")?;
Ok::<_, String>(format!("{city}: 21C, clear"))
},
);
let tk = create_toolkit(ToolkitOptions {
extra_tools: vec![weather],
builtins: Some(BuiltinsConfig::off()),
..Default::default()
})
.await?;
// candidates[0].content.parts[0] of a generateContent response.
let part = json!({"functionCall": {"name": "get_weather", "args": {"city": "Chennai"}}});
let call = &part["functionCall"];
let r = tk
.execute(call["name"].as_str().unwrap(), call["args"].clone(), ToolContext::default())
.await;
let reply = json!({"role": "user", "parts": [{"functionResponse": {
"name": call["name"], "response": {"output": r.output},
}}]});
assert_eq!(reply["parts"][0]["functionResponse"]["response"]["output"], "Chennai: 21C, clear");
println!("ok: {}", reply["parts"][0]["functionResponse"]["name"]);
Ok(())
}
  • to_openai — Render the toolkit as OpenAI function-calling schema.
  • to_anthropic — Render the toolkit as Anthropic tool-use schema.