Skip to content

Latest commit

 

History

History
822 lines (649 loc) · 27.5 KB

function-tools.md

File metadata and controls

822 lines (649 loc) · 27.5 KB

Function Tools

Extend agent capabilities with custom Rust functions.


What are Function Tools?

Function tools let you give agents abilities beyond conversation - calling APIs, performing calculations, accessing databases, or any custom logic. The LLM decides when to use a tool based on the user's request.

Key highlights:

  • 🚀 #[tool] macro - zero-boilerplate tool registration (recommended)
  • 🔧 FunctionTool::new() - wrap any async function manually
  • 📝 JSON parameters - flexible input/output
  • 🎯 Type-safe schemas - automatic JSON Schema from types via schemars
  • 🔗 Context access - session state, artifacts, memory

Tool Execution Pipeline

sequenceDiagram
    participant LLM as Model Provider
    participant Agent as LlmAgent
    participant Tool as Tool::execute()
    participant Fn as Your function

    LLM->>Agent: Response with tool_call(name, args_json)
    Agent->>Tool: Resolve tool by name, execute(ctx, args)

    alt #[tool]: args match parameters_schema
        Tool->>Fn: Deserialize args and call
        Fn-->>Tool: Result (JSON Value)
        Tool-->>Agent: Result
        Agent->>LLM: Send tool_result back
    else args break the schema
        Tool-->>Agent: tool.invalid_arguments error (the function does not run)
        Agent->>LLM: Error message as tool_result, so the model can correct the call
    end
Loading

A #[tool] tool, and a FunctionTool or StatefulTool given a schema with with_parameters_schema, checks its arguments itself; a hand-written Tool can check them with ToolArgumentValidator.


Recommended: #[tool] Macro

The fastest way to create tools. The macro reads your doc comment as the description and derives the JSON schema from your args type:

use adk_tool::tool;
use adk_core::AdkError;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{json, Value};

#[derive(Deserialize, JsonSchema)]
struct WeatherArgs {
    /// The city to look up
    city: String,
    /// Temperature unit (celsius or fahrenheit)
    unit: Option<String>,
}

/// Get the current weather for a city.
#[tool]
async fn get_weather(args: WeatherArgs) -> Result<Value, AdkError> {
    Ok(json!({ "temp": 22, "city": args.city }))
}

// Generated: pub struct GetWeather; — implements adk_core::Tool
// Use it: agent_builder.tool(Arc::new(GetWeather))

If your tool needs session context, add Arc<dyn ToolContext> as the first parameter:

use adk_core::ToolContext;
use std::sync::Arc;

/// Search the user's saved documents.
#[tool]
async fn search_docs(
    ctx: Arc<dyn ToolContext>,
    args: SearchArgs,
) -> Result<Value, AdkError> {
    let user_id = ctx.user_id();
    // ... use context for scoped access
}

Tool Metadata Attributes

Mark tools as read-only, idempotent, concurrency-safe, or long-running directly in the macro:

/// Look up cached data — no side effects, safe for parallel dispatch.
#[tool(read_only, concurrency_safe)]
async fn cache_lookup(args: LookupArgs) -> Result<Value, AdkError> {
    Ok(json!({"result": "cached"}))
}

/// Start a long-running background report.
#[tool(long_running)]
async fn generate_report(args: ReportArgs) -> Result<Value, AdkError> {
    Ok(json!({"task_id": "abc123", "status": "processing"}))
}

Available attributes (all optional, combine freely):

Attribute Effect
read_only is_read_only() → true and effect() → ToolEffect::ReadOnly — one of two signals required for concurrent Auto dispatch
idempotent effect() → ToolEffect::Idempotent — a retry budget may retry the tool after a retryable error; ignored alongside read_only
concurrency_safe is_concurrency_safe() → true — one of two signals required for concurrent Auto dispatch
long_running is_long_running() → true — prevents LLM from re-calling a pending tool
lenient_args Turns off argument validation: arguments are deserialized with serde alone, unknown keys are dropped, and schema bounds are advisory

Plain #[tool] without attributes keeps the defaults (all false, effect NonIdempotent). See Tool Effects for how the effect governs retries.

Argument Validation

A #[tool] tool checks each call's arguments against its schema before your function runs. The schema is the one schemars derives from the args type — the one parameters_schema() returns — with additionalProperties: false set on every object that declares all of its fields. The closure applies to the check only: Gemini's function declarations reject additionalProperties, so the declared schema leaves it out.

The model sends Result
A key the schema does not name, at any depth Rejected, except in an object whose fields come partly from a flattened enum, which stays open
A value outside a #[schemars(range(...))], length(...) or pattern(...) bound Rejected
A missing required field, or a value of the wrong type Rejected
null for an Option field Accepted
An integral float such as 3.0 for an integer field Accepted, and read as 3 (also with lenient_args)

A rejected call fails with an ErrorCategory::InvalidInput error with code TOOL_INVALID_ARGUMENTS_CODE (tool.invalid_arguments) that lists each violation by JSON pointer. LlmAgent always shows that message to the model, so it can fix the arguments and call again; the function does not run.

use adk_core::AdkError;
use adk_tool::tool;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::{Value, json};

#[derive(Deserialize, JsonSchema)]
struct BookArgs {
    /// Room code such as `A1`.
    #[schemars(length(min = 2, max = 4), pattern(r"^[A-Z][0-9]{1,3}$"))]
    room: String,
    /// Slots to book, from 1 to 10.
    #[schemars(range(min = 1, max = 10))]
    count: u32,
}

/// Book meeting-room slots.
#[tool]
async fn book_slots(args: BookArgs) -> Result<Value, AdkError> {
    Ok(json!({ "booked": args.count, "room": args.room }))
}

// {"room": "A1", "count": 50, "dry_run": true} reaches the model as:
// tool.invalid_input: invalid arguments for 'book_slots': /count: 50 is greater than the
// maximum of 10; arguments: Additional properties are not allowed ('dry_run' was unexpected).
// Correct the arguments to match the tool's parameters schema and call it again.

A schema narrower than what the type deserializes from rejects calls serde would accept, such as a #[serde(alias = "...")] key or a field with a custom deserialize_with. Keep one struct's keys open with #[schemars(extend("additionalProperties" = true))], or opt the whole tool out with #[tool(lenient_args)].

FunctionTool and StatefulTool apply the same check from the type given to with_parameters_schema::<T>(); .with_lenient_args(true) turns it off. Hand-written tools use the check through adk_tool::ToolArgumentValidator, with adk_tool::close_object_schemas to reject undeclared keys and adk_tool::coerce_integral_floats to read 3.0 as 3:

use adk_core::AdkError;
use adk_tool::{ToolArgumentValidator, close_object_schemas};
use serde_json::json;

fn weather_validator() -> Result<ToolArgumentValidator, AdkError> {
    let mut schema = json!({
        "type": "object",
        "properties": { "city": { "type": "string", "maxLength": 40 } },
        "required": ["city"]
    });
    close_object_schemas(&mut schema);
    ToolArgumentValidator::new(&schema)
}

// In `Tool::execute`, before acting on `args`:
// weather_validator()?.validate(self.name(), &args)?;

Alternative: FunctionTool::new()

For dynamic tools or when you prefer explicit registration:

Create a tool with FunctionTool::new() and always add a schema so the LLM knows what parameters to pass:

use adk_rust::prelude::*;
use adk_rust::Launcher;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;

#[derive(JsonSchema, Serialize, Deserialize)]
struct WeatherParams {
    /// The city or location to get weather for
    location: String,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenvy::dotenv().ok();
    let api_key = std::env::var("GOOGLE_API_KEY")?;
    let model = GeminiModel::new(&api_key, "gemini-3.7-flash")?;

    // Weather tool with proper schema
    let weather_tool = FunctionTool::new(
        "get_weather",
        "Get current weather for a location",
        |_ctx, args| async move {
            let location = args.get("location")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");
            Ok(json!({
                "location": location,
                "temperature": "22°C",
                "conditions": "sunny"
            }))
        },
    )
    .with_parameters_schema::<WeatherParams>(); // Required for LLM to call correctly!

    let agent = LlmAgentBuilder::new("weather_agent")
        .instruction("You help users check the weather. Always use the get_weather tool.")
        .model(Arc::new(model))
        .tool(Arc::new(weather_tool))
        .build()?;

    Launcher::new(Arc::new(agent)).run().await?;
    Ok(())
}

⚠️ Important: Always use .with_parameters_schema<T>() - without it, the LLM won't know what parameters to pass and may not call the tool.

How it works:

  1. User asks: "What's the weather in Tokyo?"
  2. LLM decides to call get_weather with {"location": "Tokyo"}
  3. Tool returns {"location": "Tokyo", "temperature": "22°C", "conditions": "sunny"}
  4. LLM formats response: "The weather in Tokyo is sunny at 22°C."

Step 2: Parameter Handling

Extract parameters from the JSON args:

let order_tool = FunctionTool::new(
    "process_order",
    "Process an order. Parameters: product_id (required), quantity (required), priority (optional)",
    |_ctx, args| async move {
        // Required parameters - return error if missing
        let product_id = args.get("product_id")
            .and_then(|v| v.as_str())
            .ok_or_else(|| adk_core::AdkError::tool("product_id is required"))?;
        
        let quantity = args.get("quantity")
            .and_then(|v| v.as_i64())
            .ok_or_else(|| adk_core::AdkError::tool("quantity is required"))?;
        
        // Optional parameter with default
        let priority = args.get("priority")
            .and_then(|v| v.as_str())
            .unwrap_or("normal");
        
        Ok(json!({
            "order_id": "ORD-12345",
            "product_id": product_id,
            "quantity": quantity,
            "priority": priority,
            "status": "confirmed"
        }))
    },
);

Step 3: Typed Parameters with Schema

For complex tools, use typed structs with JSON Schema:

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(JsonSchema, Serialize, Deserialize)]
struct CalculatorParams {
    /// The arithmetic operation to perform
    operation: Operation,
    /// First operand
    a: f64,
    /// Second operand
    b: f64,
}

#[derive(JsonSchema, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Operation {
    Add,
    Subtract,
    Multiply,
    Divide,
}

let calculator = FunctionTool::new(
    "calculator",
    "Perform arithmetic operations",
    |_ctx, args| async move {
        let params: CalculatorParams = serde_json::from_value(args)?;
        let result = match params.operation {
            Operation::Add => params.a + params.b,
            Operation::Subtract => params.a - params.b,
            Operation::Multiply => params.a * params.b,
            Operation::Divide if params.b != 0.0 => params.a / params.b,
            Operation::Divide => return Err(adk_core::AdkError::tool("Cannot divide by zero")),
        };
        Ok(json!({ "result": result }))
    },
)
.with_parameters_schema::<CalculatorParams>();

The schema is auto-generated from Rust types using schemars.


Step 4: Multi-Tool Agent

Add multiple tools to one agent:

let agent = LlmAgentBuilder::new("assistant")
    .instruction("Help with calculations, conversions, and weather.")
    .model(Arc::new(model))
    .tool(Arc::new(calc_tool))
    .tool(Arc::new(convert_tool))
    .tool(Arc::new(weather_tool))
    .build()?;

The LLM automatically chooses the right tool based on the user's request.


Error Handling

Return errors with the Tool component for tool-specific failures:

use adk_core::{AdkError, ErrorComponent, ErrorCategory};

let divide_tool = FunctionTool::new(
    "divide",
    "Divide two numbers",
    |_ctx, args| async move {
        let a = args.get("a").and_then(|v| v.as_f64())
            .ok_or_else(|| AdkError::new(
                ErrorComponent::Tool,
                ErrorCategory::InvalidInput,
                "tool.divide.missing_param",
                "Parameter 'a' is required",
            ))?;
        let b = args.get("b").and_then(|v| v.as_f64())
            .ok_or_else(|| AdkError::new(
                ErrorComponent::Tool,
                ErrorCategory::InvalidInput,
                "tool.divide.missing_param",
                "Parameter 'b' is required",
            ))?;
        
        if b == 0.0 {
            return Err(AdkError::new(
                ErrorComponent::Tool,
                ErrorCategory::InvalidInput,
                "tool.divide.division_by_zero",
                "Cannot divide by zero",
            ));
        }
        
        Ok(json!({ "result": a / b }))
    },
);

For quick migration, the backward-compatible shorthand also works:

Err(AdkError::tool("Parameter 'a' is required"))

Error messages are passed to the LLM, which can retry or ask for different input.


Tool Context

Access session info via ToolContext:

#[derive(JsonSchema, Serialize, Deserialize)]
struct GreetParams {
    #[serde(default)]
    message: Option<String>,
}

let greet_tool = FunctionTool::new(
    "greet",
    "Greet the user with session info",
    |ctx, _args| async move {
        let user_id = ctx.user_id();
        let session_id = ctx.session_id();
        let agent_name = ctx.agent_name();
        Ok(json!({
            "greeting": format!("Hello, user {}!", user_id),
            "session": session_id,
            "served_by": agent_name
        }))
    },
)
.with_parameters_schema::<GreetParams>();

Available context:

  • ctx.user_id() - Current user ID
  • ctx.session_id() - Current session ID
  • ctx.agent_name() - Name of the agent
  • ctx.artifacts() - Access to artifact storage
  • ctx.search_memory(query) - Search memory service

Long-Running Tools

For operations that take significant time (data processing, external APIs), use the non-blocking pattern:

  1. Start tool returns immediately with a task_id
  2. Background work runs asynchronously
  3. Status tool lets users check progress
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;

#[derive(JsonSchema, Serialize, Deserialize)]
struct ReportParams {
    topic: String,
}

#[derive(JsonSchema, Serialize, Deserialize)]
struct StatusParams {
    task_id: String,
}

// Shared task store
let tasks: Arc<RwLock<HashMap<String, TaskState>>> = Arc::new(RwLock::new(HashMap::new()));
let tasks1 = tasks.clone();
let tasks2 = tasks.clone();

// Tool 1: Start (returns immediately)
let start_tool = FunctionTool::new(
    "generate_report",
    "Start generating a report. Returns task_id immediately.",
    move |_ctx, args| {
        let tasks = tasks1.clone();
        async move {
            let topic = args.get("topic").and_then(|v| v.as_str()).unwrap_or("general").to_string();
            let task_id = format!("task_{}", rand::random::<u32>());
            
            // Store initial state
            tasks.write().await.insert(task_id.clone(), TaskState {
                status: "processing".to_string(),
                progress: 0,
                result: None,
            });

            // Spawn background work (non-blocking!)
            let tasks_bg = tasks.clone();
            let tid = task_id.clone();
            tokio::spawn(async move {
                // Simulate work...
                tokio::time::sleep(tokio::time::Duration::from_secs(10)).await;
                if let Some(t) = tasks_bg.write().await.get_mut(&tid) {
                    t.status = "completed".to_string();
                    t.result = Some("Report complete".to_string());
                }
            });

            // Return immediately with task_id
            Ok(json!({"task_id": task_id, "status": "processing"}))
        }
    },
)
.with_parameters_schema::<ReportParams>()
.with_long_running(true);  // Mark as long-running

// Tool 2: Check status
let status_tool = FunctionTool::new(
    "check_report_status",
    "Check report generation status",
    move |_ctx, args| {
        let tasks = tasks2.clone();
        async move {
            let task_id = args.get("task_id").and_then(|v| v.as_str()).unwrap_or("");
            if let Some(t) = tasks.read().await.get(task_id) {
                Ok(json!({"status": t.status, "result": t.result}))
            } else {
                Ok(json!({"error": "Task not found"}))
            }
        }
    },
)
.with_parameters_schema::<StatusParams>();

Key points:

  • .with_long_running(true) tells the agent this tool returns a pending status
  • The tool spawns work with tokio::spawn() and returns immediately
  • Provide a status check tool so users can poll progress

This adds a note to prevent the LLM from calling the tool repeatedly.


Streaming Progress from a Tool

Long-running tools can push intermediate output to the UI while still executing, so the user sees a shell command's stdout, a build's logs, or a download's bytes live instead of waiting for the final result. Call ToolContext::emit_progress as output arrives:

use adk_core::{Result, Tool, ToolContext};
use std::sync::Arc;

#[async_trait::async_trait]
impl Tool for BuildTool {
    // ... name(), description(), parameters_schema() ...

    async fn execute(&self, ctx: Arc<dyn ToolContext>, args: serde_json::Value) -> Result<serde_json::Value> {
        // Emit chunks as they arrive — each becomes a partial Event on the
        // agent's EventStream, the SAME stream the model's reply travels on.
        ctx.emit_progress("stdout", "Compiling project...\n").await;
        ctx.emit_progress("stdout", "Build finished in 4.2s\n").await;
        ctx.emit_progress("stderr", "warning: unused variable `x`\n").await;

        // The final return value is still the complete result the model consumes.
        Ok(serde_json::json!({ "status": "ok", "warnings": 1 }))
    }
}

The signature:

async fn emit_progress(&self, stream: &str, chunk: &str)
  • stream — a label for the chunk: "stdout", "stderr", or any custom channel.
  • chunk — the text to emit (emit per line for terminal-style output).

How it reaches the UI. The framework forwards each chunk as a partial Event on the agent's EventStream. A consumer detects it with event.tool_progress_stream() and renders it live. There is no second channel and no log scraping — progress, model text, and the final tool result all arrive on one ordered stream.

Backward compatible. The default emit_progress is a no-op, so existing tools and runners that don't stream are unaffected. Only tools that opt in emit progress, and only consumers that check tool_progress_stream() observe it.

Progress is bounded and lossy. A tool can produce output faster than a client consumes it — a compiler log, a shell command, a runaway loop — so the framework caps what it will hold and forward rather than growing without limit:

Limit Value On exceeding it
Queue depth per tool batch 256 events The tool waits up to 100 ms for space, then the chunk is dropped
Bytes per chunk 8 KiB The chunk is truncated on a character boundary
Bytes per tool call 1 MiB Remaining progress is not forwarded

When output is dropped for any of these reasons, exactly one progress event carrying the text [adk: tool progress truncated] is emitted for that call, so a gap is always visible rather than silent. A slow consumer therefore slows the tool briefly but can never stall it indefinitely or exhaust memory.

These limits apply only to progress. A tool's final result is not affected, so truncate large results inside the tool if that matters to you.

See the streaming_bash example for a complete web UI that renders live bash output and one-shot tool results (read_file, grep, glob) from a single event feed. The streaming bash tool itself lives in adk-devtools.


Run Examples

cargo adk new tool_agent --template tools
cd tool_agent
cargo run

Best Practices

  1. Clear descriptions - Help the LLM understand when to use the tool
  2. Validate inputs - Return helpful error messages for missing parameters
  3. Return structured JSON - Use clear field names
  4. Keep tools focused - Each tool should do one thing well
  5. Use schemas - For complex tools, define parameter schemas
  6. Mark safe read-only tools - Set both .with_read_only(true) and .with_concurrency_safe(true) so Auto dispatch can include them in its concurrent subset

Tool Metadata: Read-Only and Concurrency

Mark tools as read-only or concurrency-safe to enable smarter dispatch:

// A lookup tool that performs no side effects
let lookup = FunctionTool::new("lookup", "Look up data", |_ctx, args| async move {
    Ok(json!({"result": "cached data"}))
})
.with_read_only(true)
.with_concurrency_safe(true); // Auto mode requires both signals

// A mutation tool (defaults: read_only=false, concurrency_safe=false)
let update = FunctionTool::new("update", "Update record", |_ctx, args| async move {
    Ok(json!({"updated": true}))
});

When ToolExecutionStrategy::Auto is active, the dispatch loop first runs calls concurrently when their selected tools return true from both is_read_only() and is_concurrency_safe(). It then executes all remaining calls sequentially. ToolExecutionStrategy::Parallel is an explicit override that bypasses these signals, so its caller owns concurrency safety.

.with_effect(ToolEffect::Idempotent) declares a mutation tool safe to repeat, so a retry budget may retry it. Without it, a read-only tool is ReadOnly and every other tool is NonIdempotent and never retried. See Tool Effects.


SimpleToolContext: Using Tools Outside the Agent Loop

When you need to call a tool outside the agent loop (testing, MCP server mode, sub-agent delegation), use SimpleToolContext instead of implementing the full ToolContext trait hierarchy:

use adk_tool::SimpleToolContext;
use adk_core::ToolContext;
use std::sync::Arc;

// Construct with just a caller name — all other fields get sensible defaults
let ctx = SimpleToolContext::new("my-test-harness");

// Optionally override the function call ID
let ctx = SimpleToolContext::new("my-mcp-server")
    .with_function_call_id("custom-call-id");

// Bind a session ID so session-aware tools (and MCP servers that key state by
// session) see a stable identifier instead of the empty default.
let ctx = SimpleToolContext::new("my-mcp-server")
    .with_session_id("session-42");

// Use it to execute any tool
let tool_ctx: Arc<dyn ToolContext> = Arc::new(ctx);
let result = my_tool.execute(tool_ctx, json!({"key": "value"})).await?;

Defaults: user_id() → "anonymous", session_id() / branch() → "", artifacts() → None, search_memory() → empty vec. Both invocation_id and function_call_id are auto-generated UUIDs. Set a real session with with_session_id(...) when a tool routes or persists state per session.


StatefulTool: Shared State Across Invocations

For tools that need to maintain state between calls (counters, caches, connection pools), use StatefulTool<S>:

use adk_tool::StatefulTool;
use adk_core::ToolContext;
use std::sync::Arc;
use tokio::sync::RwLock;

struct AppCache {
    entries: RwLock<HashMap<String, String>>,
}

let cache = Arc::new(AppCache {
    entries: RwLock::new(HashMap::new()),
});

let cache_tool = StatefulTool::new(
    "cache_lookup",
    "Look up a value in the application cache",
    cache.clone(),
    |state, _ctx, args| async move {
        let key = args["key"].as_str().unwrap_or("");
        let entries = state.entries.read().await;
        let value = entries.get(key).cloned().unwrap_or_default();
        Ok(json!({"key": key, "value": value}))
    },
)
.with_read_only(true)
.with_concurrency_safe(true);

StatefulTool clones the Arc<S> on each invocation (cheap reference count bump), so all executions share the same underlying state. It supports the same builder methods as FunctionTool: with_long_running, with_parameters_schema (which also validates arguments), with_lenient_args, with_response_schema, with_scopes, with_read_only, and with_concurrency_safe.


Related


Multimodal Function Responses

Gemini 3 models support receiving images, audio, PDFs, and file references in function responses — not just JSON. Tools can return multimodal data by including inline_data and/or file_data arrays in their JSON return value:

/// Tool that returns a chart image alongside JSON metadata.
async fn generate_chart(
    _ctx: Arc<dyn ToolContext>,
    args: serde_json::Value,
) -> Result<serde_json::Value> {
    let png_bytes: Vec<u8> = render_chart(&args);

    // Include inline_data in the return value — the framework extracts it automatically
    Ok(json!({
        "response": {
            "title": "Q4 Sales",
            "chart_type": "bar"
        },
        "inline_data": [{
            "mime_type": "image/png",
            "data": png_bytes
        }]
    }))
}

The framework automatically:

  1. Detects inline_data/file_data via FunctionResponseData::from_tool_result()
  2. Base64-encodes inline binary data
  3. Nests the parts inside the functionResponse wire object (matching the Gemini 3 API format)

File References

For large files stored externally, use file_data with a URI instead of embedding bytes:

Ok(json!({
    "response": { "document_id": "report-2024", "pages": 12 },
    "file_data": [{
        "mime_type": "application/pdf",
        "file_uri": "gs://my-bucket/reports/report-2024.pdf"
    }]
}))

Direct Construction

For framework-level code (custom agents, conversion layers), construct FunctionResponseData directly:

use adk_core::{FunctionResponseData, InlineDataPart, FileDataPart};

// JSON + inline image
let frd = FunctionResponseData::with_inline_data(
    "chart_tool",
    json!({"title": "Q4 Chart"}),
    vec![InlineDataPart { mime_type: "image/png".into(), data: png_bytes }],
);

// JSON + file reference
let frd = FunctionResponseData::with_file_data(
    "doc_tool",
    json!({"status": "ok"}),
    vec![FileDataPart { mime_type: "application/pdf".into(), file_uri: "gs://bucket/file.pdf".into() }],
);

// JSON + both
let frd = FunctionResponseData::with_multimodal("tool", json, inline_parts, file_parts);

Note: Multimodal function responses require Gemini 3 series models such as gemini-3.7-flash. Earlier models return a 400 error.

See examples/multimodal_function_response/ for a complete working example.


Previous: ← mistral.rs | Next: Built-in Tools →